justlog/bot/main.go

255 lines
5.6 KiB
Go

package bot
import (
"math/rand"
"strings"
"sync"
"time"
"github.com/gempir/justlog/config"
"github.com/gempir/justlog/filelog"
expiremap "github.com/nursik/go-expire-map"
twitch "github.com/gempir/go-twitch-irc/v3"
"github.com/gempir/justlog/helix"
log "github.com/sirupsen/logrus"
)
// Bot basic logging bot
type Bot struct {
startTime time.Time
cfg *config.Config
helixClient helix.TwitchApiClient
logger filelog.Logger
worker []*worker
channels map[string]helix.UserData
clearchats sync.Map
OptoutCodes sync.Map
msgMap *expiremap.ExpireMap
}
type worker struct {
client *twitch.Client
joinedChannels map[string]bool
}
func newWorker(client *twitch.Client) *worker {
return &worker{
client: client,
joinedChannels: map[string]bool{},
}
}
// NewBot create new bot instance
func NewBot(cfg *config.Config, helixClient helix.TwitchApiClient, fileLogger filelog.Logger) *Bot {
channels, err := helixClient.GetUsersByUserIds(cfg.Channels)
if err != nil {
log.Fatalf("[bot] failed to load configured channels %s", err.Error())
}
return &Bot{
cfg: cfg,
helixClient: helixClient,
logger: fileLogger,
channels: channels,
worker: []*worker{},
OptoutCodes: sync.Map{},
msgMap: expiremap.New(),
}
}
func (b *Bot) Say(channel, text string) {
randomIndex := rand.Intn(len(b.worker))
b.worker[randomIndex].client.Say(channel, text)
}
// Connect startup the logger and bot
func (b *Bot) Connect() {
b.startTime = time.Now()
client := b.newClient()
go b.startJoinLoop()
if strings.HasPrefix(b.cfg.Username, "justinfan") {
log.Info("[bot] joining as anonymous user")
} else {
log.Info("[bot] joining as user " + b.cfg.Username)
}
defer b.msgMap.Close()
log.Fatal(client.Connect())
}
// constantly join channels to rejoin some channels that got unbanned over time
func (b *Bot) startJoinLoop() {
for {
for _, channel := range b.channels {
b.Join(channel.Login)
}
time.Sleep(time.Hour * 1)
log.Info("[bot] running hourly join loop")
}
}
func (b *Bot) Part(channelNames ...string) {
for _, channelName := range channelNames {
log.Info("[bot] leaving " + channelName)
for _, worker := range b.worker {
worker.client.Depart(channelName)
}
}
}
func (b *Bot) Join(channelNames ...string) {
for _, channel := range channelNames {
channel = strings.ToLower(channel)
joined := false
for _, worker := range b.worker {
if _, ok := worker.joinedChannels[channel]; ok {
// already joined but join again in case it was a temporary ban
worker.client.Join(channel)
joined = true
}
}
for _, worker := range b.worker {
if len(worker.joinedChannels) < 50 {
log.Info("[bot] joining " + channel)
worker.client.Join(channel)
worker.joinedChannels[channel] = true
joined = true
break
}
}
if !joined {
client := b.newClient()
go client.Connect()
b.Join(channel)
}
}
}
func (b *Bot) newClient() *twitch.Client {
client := twitch.NewClient(b.cfg.Username, "oauth:"+b.cfg.OAuth)
if b.cfg.BotVerified {
client.SetJoinRateLimiter(twitch.CreateVerifiedRateLimiter())
}
b.worker = append(b.worker, newWorker(client))
log.Infof("[bot] creating new twitch connection, new total: %d", len(b.worker))
client.OnPrivateMessage(b.handlePrivateMessage)
client.OnUserNoticeMessage(b.handleUserNotice)
client.OnClearChatMessage(b.handleClearChat)
return client
}
func (b *Bot) handlePrivateMessage(message twitch.PrivateMessage) {
if _, ok := b.msgMap.Get(message.ID); ok {
return
}
b.msgMap.Set(message.ID, true, time.Second*3)
b.handlePrivateMessageCommands(message)
if b.cfg.IsOptedOut(message.User.ID) || b.cfg.IsOptedOut(message.RoomID) {
return
}
go func() {
err := b.logger.LogPrivateMessageForUser(message.User, message)
if err != nil {
log.Error(err.Error())
}
}()
go func() {
err := b.logger.LogPrivateMessageForChannel(message)
if err != nil {
log.Error(err.Error())
}
}()
}
func (b *Bot) handleUserNotice(message twitch.UserNoticeMessage) {
if _, ok := b.msgMap.Get(message.ID); ok {
return
}
b.msgMap.Set(message.ID, true, time.Second*3)
if b.cfg.IsOptedOut(message.User.ID) || b.cfg.IsOptedOut(message.RoomID) {
return
}
go func() {
err := b.logger.LogUserNoticeMessageForUser(message.User.ID, message)
if err != nil {
log.Error(err.Error())
}
}()
if _, ok := message.Tags["msg-param-recipient-id"]; ok {
go func() {
err := b.logger.LogUserNoticeMessageForUser(message.Tags["msg-param-recipient-id"], message)
if err != nil {
log.Error(err.Error())
}
}()
}
go func() {
err := b.logger.LogUserNoticeMessageForChannel(message)
if err != nil {
log.Error(err.Error())
}
}()
}
func (b *Bot) handleClearChat(message twitch.ClearChatMessage) {
if b.cfg.IsOptedOut(message.TargetUserID) || b.cfg.IsOptedOut(message.RoomID) {
return
}
if message.BanDuration == 0 {
count, ok := b.clearchats.Load(message.RoomID)
if !ok {
count = 0
}
newCount := count.(int) + 1
b.clearchats.Store(message.RoomID, newCount)
go func() {
time.Sleep(time.Second * 1)
count, ok := b.clearchats.Load(message.RoomID)
if ok {
b.clearchats.Store(message.RoomID, count.(int)-1)
}
}()
if newCount > 50 {
if newCount == 51 {
log.Infof("Stopped recording CLEARCHAT permabans in: %s", message.Channel)
}
return
}
}
go func() {
err := b.logger.LogClearchatMessageForUser(message.TargetUserID, message)
if err != nil {
log.Error(err.Error())
}
}()
go func() {
err := b.logger.LogClearchatMessageForChannel(message)
if err != nil {
log.Error(err.Error())
}
}()
}