bot: Added channel handler and the actual logging.
This commit is contained in:
+23
-1
@@ -19,6 +19,7 @@ type Bot struct {
|
|||||||
ctxCancel context.CancelFunc
|
ctxCancel context.CancelFunc
|
||||||
loopCtx context.Context
|
loopCtx context.Context
|
||||||
loopCancel context.CancelFunc
|
loopCancel context.CancelFunc
|
||||||
|
channels map[string]*Channel
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new Bot.
|
// New creates a new Bot.
|
||||||
@@ -33,7 +34,8 @@ func New(ctx context.Context, nick string, alternatives []string, user string, r
|
|||||||
})
|
})
|
||||||
|
|
||||||
bot := &Bot{
|
bot := &Bot{
|
||||||
client: client,
|
client: client,
|
||||||
|
channels: make(map[string]*Channel),
|
||||||
}
|
}
|
||||||
|
|
||||||
client.SetValue(botKey, bot)
|
client.SetValue(botKey, bot)
|
||||||
@@ -71,6 +73,26 @@ func (bot *Bot) Connect(server string, ssl bool, maxRetries int) (err error) {
|
|||||||
return err
|
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() {
|
func (bot *Bot) loop() {
|
||||||
bot.loopCtx, bot.loopCancel = context.WithCancel(bot.ctx)
|
bot.loopCtx, bot.loopCancel = context.WithCancel(bot.ctx)
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -34,17 +34,13 @@ func handler(event *irc.Event, client *irc.Client) {
|
|||||||
case "packet.join":
|
case "packet.join":
|
||||||
{
|
{
|
||||||
if client.Nick() == event.Nick {
|
if client.Nick() == event.Nick {
|
||||||
// TODO: Open channel handler
|
bot.addChannel(event.Arg(0))
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("(JOIN)", event.Nick, "joined", event.Arg(0))
|
log.Println("(JOIN)", event.Nick, "joined", event.Arg(0))
|
||||||
}
|
}
|
||||||
case "packet.part":
|
case "packet.part":
|
||||||
{
|
{
|
||||||
if client.Nick() == event.Nick {
|
|
||||||
// TODO: Close channel handler
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("(PART)", event.Nick, "left", event.Arg(0))
|
log.Println("(PART)", event.Nick, "left", event.Arg(0))
|
||||||
}
|
}
|
||||||
case "packet.quit":
|
case "packet.quit":
|
||||||
@@ -52,6 +48,44 @@ func handler(event *irc.Event, client *irc.Client) {
|
|||||||
log.Println("(QUIT)", event.Nick, "quit")
|
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
|
// 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":
|
case "packet.001", "packet.002", "packet.003", "packet.251", "packet.255", "packet.265", "packet.266", "packet.250", "packet.375", "packet.372", "packet.376":
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// A Log represents a log session.
|
||||||
|
type Log struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Date time.Time `json:"date"`
|
||||||
|
ChannelName string `json:"channelName"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
EventName string `json:"eventName"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Open bool `json:"open"`
|
||||||
|
Posts []Post `json:"posts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LatestTime gets the latest timestamp in the log.
|
||||||
|
func (log *Log) LatestTime() time.Time {
|
||||||
|
if len(log.Posts) == 0 {
|
||||||
|
return log.Date
|
||||||
|
}
|
||||||
|
|
||||||
|
latest := log.Date
|
||||||
|
for _, post := range log.Posts {
|
||||||
|
if post.Time.After(latest) {
|
||||||
|
latest = post.Time
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return latest
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package logs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.aiterp.net/rpdata/logbot3/internal/api"
|
||||||
|
"git.aiterp.net/rpdata/logbot3/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Add adds a log file.
|
||||||
|
func Add(ctx context.Context, channelName string, date time.Time, open bool, event string) (models.Log, error) {
|
||||||
|
input := addInput{
|
||||||
|
Date: date,
|
||||||
|
Channel: channelName,
|
||||||
|
Open: &open,
|
||||||
|
Event: &event,
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := api.Global().Query(ctx, addGQL, map[string]interface{}{"input": input}, []string{"log.add"})
|
||||||
|
if err != nil {
|
||||||
|
return models.Log{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res := addResult{}
|
||||||
|
err = json.Unmarshal(data, &res)
|
||||||
|
if err != nil {
|
||||||
|
return models.Log{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.Log, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type addResult struct {
|
||||||
|
Log models.Log `json:"addLog"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type addInput struct {
|
||||||
|
Date time.Time `json:"date"`
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
Title *string `json:"title"`
|
||||||
|
Open *bool `json:"open"`
|
||||||
|
Event *string `json:"event"`
|
||||||
|
Description *string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const addGQL = `
|
||||||
|
mutation AddLog($input:LogAddInput!) {
|
||||||
|
addLog(input:$input) {
|
||||||
|
id
|
||||||
|
date
|
||||||
|
channelName
|
||||||
|
title
|
||||||
|
eventName
|
||||||
|
description
|
||||||
|
open
|
||||||
|
posts {
|
||||||
|
id
|
||||||
|
time
|
||||||
|
kind
|
||||||
|
nick
|
||||||
|
text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package logs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"git.aiterp.net/rpdata/logbot3/internal/api"
|
||||||
|
"git.aiterp.net/rpdata/logbot3/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SetOpen changes the open state of a log.
|
||||||
|
func SetOpen(ctx context.Context, log models.Log, open bool) (models.Log, error) {
|
||||||
|
return edit(ctx, editInput{ID: log.ID, Open: &open})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetEventName changes the event name of a log.
|
||||||
|
func SetEventName(ctx context.Context, log models.Log, event string) (models.Log, error) {
|
||||||
|
return edit(ctx, editInput{ID: log.ID, Event: &event})
|
||||||
|
}
|
||||||
|
|
||||||
|
func edit(ctx context.Context, input editInput) (models.Log, error) {
|
||||||
|
data, err := api.Global().Query(ctx, editGQL, map[string]interface{}{"input": input}, []string{"log.edit"})
|
||||||
|
if err != nil {
|
||||||
|
return models.Log{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res := editResult{}
|
||||||
|
err = json.Unmarshal(data, &res)
|
||||||
|
if err != nil {
|
||||||
|
return models.Log{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.Log, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type editResult struct {
|
||||||
|
Log models.Log `json:"editLog"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type editInput struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Title *string `json:"title"`
|
||||||
|
Event *string `json:"event"`
|
||||||
|
Description *string `json:"description"`
|
||||||
|
Open *bool `json:"open"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const editGQL = `
|
||||||
|
mutation EditLog($input:LogEditInput!) {
|
||||||
|
editLog(input:$input) {
|
||||||
|
id
|
||||||
|
date
|
||||||
|
channelName
|
||||||
|
title
|
||||||
|
eventName
|
||||||
|
description
|
||||||
|
open
|
||||||
|
posts {
|
||||||
|
id
|
||||||
|
time
|
||||||
|
kind
|
||||||
|
nick
|
||||||
|
text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package logs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"git.aiterp.net/rpdata/logbot3/internal/api"
|
||||||
|
"git.aiterp.net/rpdata/logbot3/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrNoneOpen is returned by FindOpen if no logs are open, but the query did succeed.
|
||||||
|
var ErrNoneOpen = errors.New("No open logs")
|
||||||
|
|
||||||
|
// FindOpen lists all changes according to the filter.
|
||||||
|
func FindOpen(ctx context.Context, channelName string) (models.Log, error) {
|
||||||
|
data, err := api.Global().Query(ctx, findOpenGQL, map[string]interface{}{"channel": channelName}, nil)
|
||||||
|
if err != nil {
|
||||||
|
return models.Log{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res := findOpenResult{}
|
||||||
|
err = json.Unmarshal(data, &res)
|
||||||
|
if err != nil {
|
||||||
|
return models.Log{}, err
|
||||||
|
}
|
||||||
|
if len(res.Logs) == 0 {
|
||||||
|
return models.Log{}, ErrNoneOpen
|
||||||
|
}
|
||||||
|
|
||||||
|
// This shouldn't happen, but if there are more than one
|
||||||
|
// open logs for a channel, select the most recent one.
|
||||||
|
selected := res.Logs[0]
|
||||||
|
for _, log := range res.Logs[1:] {
|
||||||
|
if log.Date.After(selected.Date) {
|
||||||
|
selected = log
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return selected, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type findOpenResult struct {
|
||||||
|
Logs []models.Log `json:"logs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var findOpenGQL = `
|
||||||
|
query FindOpen($channel:String!) {
|
||||||
|
logs(filter:{open:true, channels:[$channel]}) {
|
||||||
|
id
|
||||||
|
date
|
||||||
|
channelName
|
||||||
|
title
|
||||||
|
eventName
|
||||||
|
description
|
||||||
|
open
|
||||||
|
posts {
|
||||||
|
id
|
||||||
|
time
|
||||||
|
kind
|
||||||
|
nick
|
||||||
|
text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// A Post is a post.
|
||||||
|
type Post struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Time time.Time `json:"time"`
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
Nick string `json:"nick"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package posts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.aiterp.net/rpdata/logbot3/internal/api"
|
||||||
|
"git.aiterp.net/rpdata/logbot3/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Add adds a log file.
|
||||||
|
func Add(ctx context.Context, log models.Log, time time.Time, kind, nick, text string) (models.Post, error) {
|
||||||
|
input := addInput{
|
||||||
|
LogID: log.ID,
|
||||||
|
Time: time,
|
||||||
|
Kind: kind,
|
||||||
|
Nick: nick,
|
||||||
|
Text: text,
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := api.Global().Query(ctx, addGQL, map[string]interface{}{"input": input}, []string{"post.add"})
|
||||||
|
if err != nil {
|
||||||
|
return models.Post{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res := addResult{}
|
||||||
|
err = json.Unmarshal(data, &res)
|
||||||
|
if err != nil {
|
||||||
|
return models.Post{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.Post, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type addResult struct {
|
||||||
|
Post models.Post `json:"addPost"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type addInput struct {
|
||||||
|
LogID string `json:"logId"`
|
||||||
|
Time time.Time `json:"time"`
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
Nick string `json:"nick"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const addGQL = `
|
||||||
|
mutation AddPost($input:PostAddInput!) {
|
||||||
|
addPost(input:$input) {
|
||||||
|
id
|
||||||
|
time
|
||||||
|
kind
|
||||||
|
nick
|
||||||
|
text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
Reference in New Issue
Block a user