Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions cmd/bot/channels.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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) {
Expand All @@ -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
}
Expand Down Expand Up @@ -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 {
Expand Down
83 changes: 43 additions & 40 deletions cmd/bot/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
49 changes: 32 additions & 17 deletions cmd/bot/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"context"
"fmt"
"log"
"time"

Expand Down Expand Up @@ -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 {
Expand All @@ -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(),
}

Expand All @@ -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))
}
}
4 changes: 2 additions & 2 deletions internal/bot/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 5 additions & 4 deletions internal/bot/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading