package bot import ( "context" "log" "strings" "time" "github.com/gissleh/irc" "git.aiterp.net/rpdata/logbot3/internal/models" "git.aiterp.net/rpdata/logbot3/internal/models/channels" ) var botKey = "git.aiterp.net/rpdata/logbot.Bot.key" // The Bot is the IRC client. type Bot struct { client *irc.Client ctx context.Context ctxCancel context.CancelFunc loopCtx context.Context loopCancel context.CancelFunc channels map[string]*Channel } // New creates a new Bot. func New(ctx context.Context, nick string, alternatives []string, user string, realName string) *Bot { client := irc.New(ctx, irc.Config{ Nick: nick, User: user, RealName: realName, Alternatives: alternatives, SendRate: 2, SkipSSLVerification: false, }) client.AddHandler(handler) bot := &Bot{ client: client, channels: make(map[string]*Channel), } client.SetValue(botKey, bot) return bot } // Connect connects the bot to the IRC server. This will disconnect already // established connections. func (bot *Bot) Connect(server string, ssl bool, maxRetries int) (err error) { if bot.ctxCancel != nil { bot.ctxCancel() } bot.ctx, bot.ctxCancel = context.WithCancel(bot.client.Context()) retries := 0 for maxRetries == 0 || retries < maxRetries { err = bot.client.Connect(server, ssl) if err != nil { log.Println("Connect failed:", err.Error()) if maxRetries > 0 && retries < maxRetries { retries++ log.Printf("Retrying in 5s (Retry %d/%d)", retries, maxRetries) } else { log.Println("Retrying in 5s (No retry limit)") } time.Sleep(time.Second * 10) continue } return nil } return err } func (bot *Bot) ClientState() irc.ClientState { return bot.client.State() } func (bot *Bot) addChannel(channelName string) *Channel { if bot.channels[channelName] != nil { return bot.channels[channelName] } channel := newChannel(bot, channelName, bot.client) go channel.loop() bot.channels[channelName] = channel return channel } func (bot *Bot) handlePost(channelName string, post ChannelPost) { channel := bot.channels[channelName] if channel == nil { channel = bot.addChannel(channelName) } channel.ch <- post } func (bot *Bot) runCommands(commands []string, target irc.Target, replacers map[string]string) { if replacers == nil { replacers = make(map[string]string) } replacers["me"] = bot.client.Nick() for _, command := range commands { for from, to := range replacers { command = strings.Replace(command, "%"+from, to, -1) } bot.client.EmitInput(command, target) } } func (bot *Bot) loop() { bot.loopCtx, bot.loopCancel = context.WithCancel(bot.ctx) channelChanges, err := channels.SubscribeLogged(bot.ctx) if err != nil { log.Println("Failed to get channel changes:", err) return } for { select { case channel := <-channelChanges: { channels := []models.Channel{channel} deadline := time.After(time.Second * 1) buffering := true for buffering { select { case channel := <-channelChanges: channels = append(channels, channel) case <-deadline: buffering = false } } decisions := make(map[string]bool) for _, channel := range channels { decisions[channel.Name] = channel.Logged } joins := make([]string, 0, len(decisions)) parts := make([]string, 0, len(decisions)) for channelName, logged := range decisions { if logged { if bot.client.Channel(channelName) != nil { continue } joins = append(joins, channelName) } else { if bot.client.Channel(channelName) == nil { continue } parts = append(parts, channelName) } } if len(parts) > 0 { log.Println("Leaving", strings.Join(parts, ", ")) bot.client.Part(parts...) } if len(joins) > 0 { log.Println("Joining", strings.Join(joins, ", ")) bot.client.Join(joins...) } } case <-bot.loopCtx.Done(): { log.Println("Spinning down bot loop.") return } } } } func (bot *Bot) stopLoop() { if bot.loopCancel != nil { bot.loopCancel() } }