bot: Added channel handler and the actual logging.

This commit is contained in:
2018-11-22 22:24:33 +01:00
parent 7336ea2b47
commit c08bd6c05a
9 changed files with 630 additions and 6 deletions
+23 -1
View File
@@ -19,6 +19,7 @@ type Bot struct {
ctxCancel context.CancelFunc
loopCtx context.Context
loopCancel context.CancelFunc
channels map[string]*Channel
}
// New creates a new Bot.
@@ -33,7 +34,8 @@ func New(ctx context.Context, nick string, alternatives []string, user string, r
})
bot := &Bot{
client: client,
client: client,
channels: make(map[string]*Channel),
}
client.SetValue(botKey, bot)
@@ -71,6 +73,26 @@ func (bot *Bot) Connect(server string, ssl bool, maxRetries int) (err error) {
return err
}
func (bot *Bot) addChannel(channelName string) *Channel {
if bot.channels[channelName] != nil {
return bot.channels[channelName]
}
channel := newChannel(bot.ctx, 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) loop() {
bot.loopCtx, bot.loopCancel = context.WithCancel(bot.ctx)
+267
View File
@@ -0,0 +1,267 @@
package bot
import (
"context"
"log"
"strings"
"time"
"git.aiterp.net/gisle/irc"
"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
client *irc.Client
}
func newChannel(parentCtx context.Context, name string, client *irc.Client) *Channel {
ctx, cancel := context.WithCancel(context.Background())
return &Channel{
name: name,
parentCtx: parentCtx,
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()
session, err := logs.FindOpen(channel.parentCtx, channel.name)
if err == nil {
minsUntilDeadline := 1 + int(((time.Hour * 2) - time.Since(session.LatestTime())).Minutes())
channel.client.Sayf(channel.name, "Session https://aiterp.net/logs/%s will be closed in %d minutes if there are no new posts.", session.ID, minsUntilDeadline)
channel.lastPostSessionID = session.ID
channel.lastPostTime = session.LatestTime()
}
for {
select {
case post := <-channel.ch:
{
log.Printf("Received %s post from %s", post.Kind, post.Nick)
// 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)
}
}
}
} else {
queue = append(queue, post)
if post.Time.After(channel.lastPostTime) {
channel.lastPostTime = post.Time
}
}
// 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 due to 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
}
+39 -5
View File
@@ -34,17 +34,13 @@ func handler(event *irc.Event, client *irc.Client) {
case "packet.join":
{
if client.Nick() == event.Nick {
// TODO: Open channel handler
bot.addChannel(event.Arg(0))
}
log.Println("(JOIN)", event.Nick, "joined", event.Arg(0))
}
case "packet.part":
{
if client.Nick() == event.Nick {
// TODO: Close channel handler
}
log.Println("(PART)", event.Nick, "left", event.Arg(0))
}
case "packet.quit":
@@ -52,6 +48,44 @@ func handler(event *irc.Event, client *irc.Client) {
log.Println("(QUIT)", event.Nick, "quit")
}
case "ctcp.action", "packet.privmsg":
{
if event.Nick == bot.client.Nick() {
break
}
channel := event.ChannelTarget()
if channel == nil {
break
}
account := ""
if user, ok := bot.client.FindUser(event.Nick); ok {
account = user.Account
}
kind := ""
if event.Verb() == "action" {
kind = "action"
} else {
if strings.HasPrefix(event.Nick, "=") {
kind = "scene"
} else {
kind = "text"
}
}
post := ChannelPost{
Account: account,
Kind: kind,
Nick: event.Nick,
Time: event.Time,
Text: event.Text,
}
bot.handlePost(channel.Name(), post)
}
// Log initial numerics for debugging's sake
case "packet.001", "packet.002", "packet.003", "packet.251", "packet.255", "packet.265", "packet.266", "packet.250", "packet.375", "packet.372", "packet.376":
{