You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
312 lines
8.5 KiB
312 lines
8.5 KiB
package bot
|
|
|
|
import (
|
|
"context"
|
|
"log"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.aiterp.net/gisle/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(context.Background())
|
|
|
|
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 require operator privileges to serve this request.")
|
|
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.parentCtx.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
|
|
}
|