diff --git a/cmd/bot/channels.go b/cmd/bot/channels.go index 1bb89b5..2524bec 100644 --- a/cmd/bot/channels.go +++ b/cmd/bot/channels.go @@ -15,7 +15,7 @@ import ( ) // loadChannels fetches configured channels from the database, sets default values and message queue for each of them -func loadChannels(bgctx context.Context, mongoConn *mongo.Connection, twitchIRC *twitch.Client) map[string]*bot.Channel { +func loadChannels(bgctx context.Context, mongoConn *mongo.Connection, twitchWrite *twitch.Client) map[string]*bot.Channel { channels := make(map[string]*bot.Channel) ctx, cancel := context.WithTimeout(bgctx, 10*time.Second) @@ -29,7 +29,6 @@ func loadChannels(bgctx context.Context, mongoConn *mongo.Connection, twitchIRC }) if err != nil { log.Fatalln("[Mongo] Error querying channels:", err) - return channels } for cur.Next(ctx) { @@ -43,7 +42,7 @@ func loadChannels(bgctx context.Context, mongoConn *mongo.Connection, twitchIRC // Initialize default values channel.QueueChannel = make(chan *bot.QueueMessage) - go channel.StartMessageQueue(twitchIRC) + go channel.StartMessageQueue(twitchWrite) channels[channel.ID] = &channel } @@ -107,7 +106,7 @@ func handleChannelsChunk(tcb *bot.Bot, chunk []string) { channel.CurrentTitle = respChannel.Title // JOIN the channel - tcb.TwitchIRC.Join(channel.Login) + tcb.TwitchRead.Join(channel.Login) // Create all EventSub subscriptions parallelly for _, subscription := range channelSubscriptions { diff --git a/cmd/bot/events.go b/cmd/bot/events.go index 188b955..191b55e 100644 --- a/cmd/bot/events.go +++ b/cmd/bot/events.go @@ -11,17 +11,43 @@ import ( "github.com/zneix/tcb2/internal/bot" ) +// handlerOnNoticeMessage logic for NOTICE twitch IRC messages +// It's taken out of registerEvents since it's used for both read and write conns +func handlerOnNoticeMessage(tcb *bot.Bot, message *twitch.NoticeMessage, connType string) { + channelID, ok := tcb.Logins[message.Channel] + if !ok { + // tcb.Logins map didn't have current channel's ID + // Note: this should realistically never occur though, but early exit to prevent panic + return + } + channel := tcb.Channels[channelID] + + log.Printf("[TwitchIRC:%s] NOTICE %s in %s: %s\n", connType, message.MsgID, channel, message.Message) + + switch message.MsgID { + case "msg_banned", "msg_channel_suspended": + err := channel.ChangeMode(tcb.Mongo, bot.ChannelModeInactive) + if err != nil { + log.Printf("Failed to change mode in %s: %s\n", channel, err) + } + default: + } +} + func registerEvents(tcb *bot.Bot) { // Twitch IRC events // Authenticated with IRC - tcb.TwitchIRC.OnConnect(func() { - log.Println("[TwitchIRC] connected") + tcb.TwitchRead.OnConnect(func() { + log.Println("[TwitchIRC:read] connected, joining channels") joinChannels(tcb) }) + tcb.TwitchWrite.OnConnect(func() { + log.Println("[TwitchIRC:write] connected") + }) // PRIVMSG - tcb.TwitchIRC.OnPrivateMessage(func(message twitch.PrivateMessage) { + tcb.TwitchRead.OnPrivateMessage(func(message twitch.PrivateMessage) { // Early out in case message does not start with command prefix - meaning it's not a command if !strings.HasPrefix(message.Message, tcb.Commands.Prefix) { // Handle non-commands @@ -64,7 +90,9 @@ func registerEvents(tcb *bot.Bot) { }) // USERSTATE - tcb.TwitchIRC.OnUserStateMessage(func(message twitch.UserStateMessage) { + // These will be triggered whenever a message is written to a channel - so react to those on write connection + // They might also be received upon JOINing on authed connection, however we don't do that + tcb.TwitchWrite.OnUserStateMessage(func(message twitch.UserStateMessage) { channelID, ok := tcb.Logins[message.Channel] if !ok { // tcb.Logins map didn't have current channel's ID @@ -74,30 +102,16 @@ func registerEvents(tcb *bot.Bot) { channel := tcb.Channels[channelID] - // Check if Channel.Mode changed by comparing bot's state - newMode := bot.ChannelModeNormal - // Bot will always have elevated permissions in its own chat, saving some time with the early-out if channel.Login == tcb.Self.Login { return } - userType, ok := message.Tags["user-type"] - switch { - case !ok: - log.Println("[TwitchIRC:USERSTATE] user-type tag was not found in the IRC message, either no capabilities or Twitch removed this tag xd") + // Check if Channel.Mode changed to see if we now have privileged write limits - by being either a moderator or vip + newMode := bot.ChannelModeNormal - case userType == "mod": + if message.User.IsMod || message.User.IsVip { newMode = bot.ChannelModeModerator - - default: - // Since user-type does not care about VIP status, we need to check badges - for key := range message.User.Badges { - if key == "vip" || key == "moderator" { - newMode = bot.ChannelModeModerator - break - } - } } // Update ChannelMode in the current channel if it differs @@ -110,25 +124,14 @@ func registerEvents(tcb *bot.Bot) { }) // NOTICE - tcb.TwitchIRC.OnNoticeMessage(func(message twitch.NoticeMessage) { - channelID, ok := tcb.Logins[message.Channel] - if !ok { - // tcb.Logins map didn't have current channel's ID - // Note: this should realistically never occur though, but early exit to prevent panic - return - } - channel := tcb.Channels[channelID] - - log.Printf("[TwitchIRC:NOTICE] %s in %s\n", message.MsgID, channel) - - switch message.MsgID { - case "msg_banned", "msg_channel_suspended": - err := channel.ChangeMode(tcb.Mongo, bot.ChannelModeInactive) - if err != nil { - log.Printf("Failed to change mode in %s: %s\n", channel, err) - } - default: - } + // This might be relevant for both read and write connections: + // on Read: a channel might be suspended + // on Write: the bot user might be banned from channel it attempts to send a message in + tcb.TwitchRead.OnNoticeMessage(func(message twitch.NoticeMessage) { + handlerOnNoticeMessage(tcb, &message, "read") + }) + tcb.TwitchWrite.OnNoticeMessage(func(message twitch.NoticeMessage) { + handlerOnNoticeMessage(tcb, &message, "write") }) // Twitch EventSub events diff --git a/cmd/bot/main.go b/cmd/bot/main.go index 16fd422..b641b86 100644 --- a/cmd/bot/main.go +++ b/cmd/bot/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "fmt" "log" "time" @@ -29,8 +30,12 @@ func main() { mongoConnection := mongo.NewMongoConnection(ctx, cfg) mongoConnection.Connect(ctx) - twitchIRC := twitch.NewClient(cfg.TwitchLogin, "oauth:"+cfg.TwitchOAuth) - twitchIRC.SetJoinRateLimiter(twitch.CreateVerifiedRateLimiter()) + // twitch read conn + twitchRead := twitch.NewAnonymousClient() + twitchRead.SetJoinRateLimiter(twitch.CreateVerifiedRateLimiter()) + + // twitch write conn + twitchWrite := twitch.NewClient(cfg.TwitchLogin, "oauth:"+cfg.TwitchOAuth) helixClient, err := helixclient.New(cfg) if err != nil { @@ -41,20 +46,19 @@ func main() { esub := eventsub.New(cfg, apiServer) - self := &bot.Self{ - Login: cfg.TwitchLogin, - OAuth: cfg.TwitchOAuth, - } - tcb := &bot.Bot{ - TwitchIRC: twitchIRC, - Mongo: mongoConnection, - Helix: helixClient, - EventSub: esub, - Logins: make(map[string]string), - Channels: loadChannels(ctx, mongoConnection, twitchIRC), - Commands: bot.NewCommandController(cfg.CommandPrefix), - Self: self, + TwitchRead: twitchRead, + TwitchWrite: twitchWrite, + Mongo: mongoConnection, + Helix: helixClient, + EventSub: esub, + Logins: make(map[string]string), + Channels: loadChannels(ctx, mongoConnection, twitchWrite), + Commands: bot.NewCommandController(cfg.CommandPrefix), + Self: &bot.Self{ + Login: cfg.TwitchLogin, + OAuth: cfg.TwitchOAuth, + }, StartTime: time.Now(), } @@ -70,8 +74,19 @@ func main() { supinic := supinicapi.New(cfg.SupinicAPIKey) go supinic.UpdateAliveStatus() - err = tcb.TwitchIRC.Connect() + // Connect twitch connections + // TODO: Use proper waiting for both connections - maybe use channels waiting for closure + // Check twitch connection's .Connect for a good example + // For now as a scuffed fix read connection will be blocking + go func() { + err = tcb.TwitchWrite.Connect() + if err != nil { + log.Fatalln(fmt.Errorf("twitch write connection errored: %w", err)) + } + }() + + err = tcb.TwitchRead.Connect() if err != nil { - log.Fatalln(err) + log.Fatalln(fmt.Errorf("twitch read connection errored: %w", err)) } } diff --git a/internal/bot/channel.go b/internal/bot/channel.go index 57297a1..a5bcca1 100644 --- a/internal/bot/channel.go +++ b/internal/bot/channel.go @@ -35,13 +35,13 @@ func (channel *Channel) String() string { return fmt.Sprintf("#%s(%s)", channel.Login, channel.ID) } -func (channel *Channel) StartMessageQueue(twitchIRC *twitch.Client) { +func (channel *Channel) StartMessageQueue(twitchWrite *twitch.Client) { // log.Println("Starting message queue for", channel) defer log.Println("[Channel] Message queue suddenly quit(?) for", channel) for message := range channel.QueueChannel { // Actually send the message to the chat - twitchIRC.Say(channel.Login, message.Message) + twitchWrite.Say(channel.Login, message.Message) // Update last sent message channel.LastMsg = message.Message diff --git a/internal/bot/types.go b/internal/bot/types.go index 7b00922..e42ebc4 100644 --- a/internal/bot/types.go +++ b/internal/bot/types.go @@ -21,10 +21,11 @@ type Self struct { } type Bot struct { - TwitchIRC *twitch.Client - Mongo *mongo.Connection - Helix *helix.Client - EventSub *eventsub.EventSub + TwitchRead *twitch.Client + TwitchWrite *twitch.Client + Mongo *mongo.Connection + Helix *helix.Client + EventSub *eventsub.EventSub loginsMu sync.Mutex Logins map[string]string