|
|
package bot
import ( "context" "log" "strings" "time"
"github.com/gissleh/irc" "git.aiterp.net/rpdata/logbot3/internal/config" "git.aiterp.net/rpdata/logbot3/internal/models/channels" "git.aiterp.net/rpdata/logbot3/internal/models/logs" "git.aiterp.net/rpdata/logbot3/internal/models/posts" )
// A Channel represents a handler for a single channel that takes care of the logging and such.
type Channel struct { ctx context.Context cancel context.CancelFunc ch chan ChannelPost
lastPostSessionID string lastPostTime time.Time
name string parentCtx context.Context parent *Bot client *irc.Client }
func newChannel(parent *Bot, name string, client *irc.Client) *Channel { ctx, cancel := context.WithCancel(parent.ctx)
return &Channel{ name: name, parent: parent, parentCtx: parent.ctx, client: client, ch: make(chan ChannelPost, 8),
ctx: ctx, cancel: cancel, } }
func (channel *Channel) loop() { queue := make([]ChannelPost, 0, 8) minutely := time.NewTicker(time.Minute)
defer channel.cancel() defer minutely.Stop()
// In case of a crash, it should take ownership of sessions when rejoining.
session, err := logs.FindOpen(channel.parentCtx, channel.name) if err == nil { minsUntilDeadline := 1 + int(((time.Hour * 2) - time.Since(session.LatestTime())).Minutes()) if minsUntilDeadline < 1 { minsUntilDeadline = 1 }
channel.client.Sayf(channel.name, "Session https://aiterp.net/logs/%s will be closed in %d min if there are no new posts.", session.ID, minsUntilDeadline)
channel.lastPostSessionID = session.ID channel.lastPostTime = session.LatestTime() }
// If the bot is op, run the commands from the configuration.
go func() { time.Sleep(time.Second)
commands := config.Get().Commands.OnJoinOp target := channel.client.Channel(channel.name)
if len(commands) > 0 && target != nil { me, ok := target.UserList().User(channel.client.Nick()) if ok && strings.ContainsRune(me.Modes, 'o') { channel.parent.runCommands(commands, target, map[string]string{ "chan": channel.name, }) } } }()
for { select { case post := <-channel.ch: { // Handle bot commands, or add to queue otherwise.
if cmd := post.botCommand(); cmd != nil { switch cmd.Verb { case "end", "ends", "endsession": { session, err := logs.FindOpen(channel.parentCtx, channel.name) if err == logs.ErrNoneOpen { channel.client.Say(channel.name, "No open session found. Mission accomplished, I guess?") break } else if err != nil { log.Println("Could not find session:", err) break }
_, err = logs.SetOpen(channel.parentCtx, session, false) if err != nil { channel.client.Say(channel.name, "Something went wrong when closing the log, please use the website instead.") log.Println("Could not set open:", err) } } case "tag", "event": { if cmd.Text == "" { channel.client.Say(channel.name, "Usage: !tag <Event Name>") }
session, err := logs.FindOpen(channel.parentCtx, channel.name) if err == logs.ErrNoneOpen { channel.client.Say(channel.name, "No open session found. You can edit closed logs on the website.") break } else if err != nil { log.Println("Could not find session:", err) break }
_, err = logs.SetEventName(channel.parentCtx, session, cmd.Text) if err != nil { channel.client.Say(channel.name, "Something went wrong when setting the event name, please use the website instead.") log.Println("Could not set event name:", err) } } case "register": { target := channel.client.Channel(channel.name) if target != nil { me, ok := target.UserList().User(channel.client.Nick()) if !ok || !strings.ContainsRune(me.Modes, 'o') { channel.client.Say(channel.name, "This unit has insufficient privileges.") break }
chanServNick := config.Get().Names.ChanServ if chanServNick == "" { channel.client.Say(channel.name, "This function is not enabled.") break }
channel.client.Sayf(chanServNick, "REGISTER %s", target.Name()) } } } } else { queue = append(queue, post)
if post.Time.After(channel.lastPostTime) { channel.lastPostTime = post.Time } }
// Posts after here aren't going to be bot commands, so log its reception.
log.Printf("Received %s post from %s", post.Kind, post.Nick)
// Stop here if there's nothing to post.
if len(queue) == 0 { break }
// Buffer up posts close to one another.
deadline := time.After(time.Second * 3) buffering := true for buffering { select { case post := <-channel.ch: { queue = append(queue, post) } case <-deadline: { buffering = false } } }
// Select session.
session, err := logs.FindOpen(channel.parentCtx, channel.name) if err == logs.ErrNoneOpen { eventName := "" channelData, err := channels.Find(channel.parentCtx, channel.name) if err == nil { eventName = channelData.EventName }
session, err = logs.Add(channel.parentCtx, channel.name, queue[0].Time, true, eventName) if err != nil { channel.client.Say(channel.name, "This unit failed to open session: "+err.Error()) log.Println("Failed to open session:", err) } } else if err != nil { channel.client.Say(channel.name, "This unit is unable to check active sessions: "+err.Error()) break } log.Println("Selected session:", session.ID)
// Remember which session was last posted to.
channel.lastPostSessionID = session.ID
// Post posts
lastSuccess := -1 for i, channelPost := range queue { post, err := posts.Add(channel.parentCtx, session, channelPost.Time, channelPost.Kind, channelPost.Nick, channelPost.Text) if err != nil { log.Println("Failed to post:", err) break }
summary := "" for _, ru := range post.Text { summary += string(ru) if len(summary) > 30 { summary += "..." break } }
log.Printf("Posted (id=%s, kind=%s, nick=%s, delay=%s): %s", post.ID, post.Kind, post.Nick, time.Since(post.Time), summary) lastSuccess = i } if lastSuccess >= 0 { copy(queue, queue[lastSuccess:]) queue = queue[:len(queue)-(lastSuccess+1)] } }
case now := <-minutely.C: if !channel.lastPostTime.IsZero() && now.Sub(channel.lastPostTime) > (time.Hour*2) { session, err := logs.FindOpen(channel.parentCtx, channel.name) if err == logs.ErrNoneOpen { log.Println(channel.name, "Log already closed.") channel.lastPostTime = time.Time{} channel.lastPostSessionID = "" break } else if err != nil { log.Println(channel.name, "Could not find session:", err) break }
if session.ID != channel.lastPostSessionID { log.Println("Aborted auto-close in", channel.name, "due to session change.") channel.lastPostTime = time.Time{} channel.lastPostSessionID = "" break }
if now.Sub(session.LatestTime()) < (time.Hour * 2) { log.Println("Aborted auto-close in", channel.name, "due to session being more recent.") channel.lastPostTime = session.LatestTime() break }
_, err = logs.SetOpen(channel.parentCtx, session, false) if err != nil { log.Println("Could not set open:", err) break }
channel.client.Sayf(channel.name, "Log session closed after 2+ hours of inactivity. See log at https://aiterp.net/logs/%s", channel.lastPostSessionID)
channel.lastPostTime = time.Time{} channel.lastPostSessionID = "" }
case <-channel.ctx.Done(): { // Time to pack up shop.
return } } } }
func (channel *Channel) wait(ctx context.Context) error { select { case <-ctx.Done(): return ctx.Err() case <-channel.ctx.Done(): return nil } }
// A ChannelPost is a post made to a channel.
type ChannelPost struct { Kind string Time time.Time Nick string Text string Account string }
func (post *ChannelPost) botCommand() *botCommand { if !strings.HasPrefix(post.Text, "!") { return nil }
split := strings.SplitN(post.Text, " ", 2)
if len(split) == 1 { return &botCommand{Verb: strings.ToLower(split[0][1:]), Text: ""} } return &botCommand{Verb: strings.ToLower(split[0][1:]), Text: split[1]} }
type botCommand struct { Verb string Text string }
|