diff --git a/bot/async.go b/bot/async.go index 7097013..595db49 100644 --- a/bot/async.go +++ b/bot/async.go @@ -1,9 +1,12 @@ package bot import ( + "fmt" "log" "time" + "github.com/diamondburned/arikawa/v3/api" + "github.com/diamondburned/arikawa/v3/discord" "github.com/jxsl13/twstatus-bot/model" ) @@ -64,7 +67,116 @@ loop: log.Printf("goroutine %d: closed async goroutine for message updates", id) } -func (b *Bot) cacheCleanup() { +func (b *Bot) notificationUpdater(id int) { + log.Printf("goroutine %d starting async goroutine for channel notifications", id) + +loop: + for { + select { + case <-b.ctx.Done(): + break loop + case notification, ok := <-b.n: + if !ok { + break loop + } + err := b.updateChannelNotification(notification) + if err != nil { + b.l.Errorf("goroutine %0d: failed to update channel notification %v: %v", id, notification, err) + } + + } + } + + log.Printf("goroutine %d: closed async goroutine for channel notifications", id) +} + +func (b *Bot) updateChannelNotification(n model.PlayerCountNotificationMessage) (err error) { + dao, closer, err := b.TxDAO(b.ctx) + if err != nil { + return fmt.Errorf("failed to get transaction queries for channel notification: %w", err) + } + defer func() { + err = closer(err) + }() + + // remove previous notification message if exists + if n.PrevMessageID != 0 { + // check if message still exists + err := b.state.DeleteMessage( + n.ChannelTarget.ChannelID, + n.PrevMessageID, + api.AuditLogReason("removing previous channel notification message"), + ) + if err != nil && !ErrIsNotFound(err) { + b.l.Errorf("failed to delete previous notification message %s: %v", n.MessageTarget(n.PrevMessageID), err) + err = nil + } + + // cleanup database if notification was deleted by some user/admin + err = dao.RemovePlayerCountNotificationMessage(b.ctx, n.ChannelTarget.ChannelID, n.PrevMessageID) + if err != nil { + return fmt.Errorf("failed to remove previous channel notification message from database: %w", err) + } + } + + // remove all requests from database + for _, umt := range n.RemoveUserMessageReactions { + err = dao.RemovePlayerCountNotificationRequest(b.ctx, model.PlayerCountNotificationRequest{ + MessageUserTarget: model.MessageUserTarget{ + UserID: umt.UserID, + MessageTarget: model.MessageTarget{ + ChannelTarget: model.ChannelTarget{ + GuildID: n.ChannelTarget.GuildID, + ChannelID: n.ChannelTarget.ChannelID, + }, + MessageID: umt.MessageID, + }, + }, + Threshold: umt.Threshold, + }) + if err != nil { + return fmt.Errorf("failed to remove player count notification request from database: %w", err) + } + } + + // delete all reactions from specified messages + for _, mt := range n.RemoveMessageReactions { + err = b.state.DeleteReactions(n.ChannelID, mt.MessageID, mt.Reaction()) + if err != nil && !ErrIsNotFound(err) { + b.l.Errorf("failed to delete reaction %s from message %s: %v", mt.Reaction(), n.MessageTarget(mt.MessageID), err) + err = nil + } + } + + mentionUsers := n.UserIDs + if len(n.UserIDs) > 100 { + // we do not expect more than 100 users to be mentioned anyway + mentionUsers = n.UserIDs[:100] + } + + // send new message + msg, err := b.state.SendMessageComplex(n.ChannelTarget.ChannelID, api.SendMessageData{ + Content: n.Format(), + Flags: discord.SuppressEmbeds, + AllowedMentions: &api.AllowedMentions{ + Users: mentionUsers, + }, + }) + if err != nil { + return err + } + + // update database to contain latest notification message for the current channel + err = dao.AddPlayerCountNotificationMessage(b.ctx, msg.ChannelID, msg.ID) + if err != nil { + return err + } + + return nil +} + +func (b *Bot) cacheCleanup(id int) { + log.Printf("goroutine %d starting async goroutine for cache cleanup", id) var ( cleanupInterval = 20 * b.pollingInterval timer = time.NewTimer(cleanupInterval) @@ -96,7 +208,7 @@ func (b *Bot) cacheCleanup() { log.Printf("cache contains %d entries after cleanup at %s", b.conflictMap.Size(), now) case <-b.ctx.Done(): - log.Println("closed async goroutine for cache cleanup") + log.Printf("goroutine %d: closed async goroutine for cache cleanup", id) return } } diff --git a/bot/bot.go b/bot/bot.go index 4e4f772..2abe256 100644 --- a/bot/bot.go +++ b/bot/bot.go @@ -29,33 +29,6 @@ const ( channelOptionName = "channel" ) -var ( - reactionPlayerCountNotificationMap = map[discord.APIEmoji]int{ - discord.NewAPIEmoji(0, "1️⃣"): 1, - discord.NewAPIEmoji(0, "2️⃣"): 2, - discord.NewAPIEmoji(0, "3️⃣"): 3, - discord.NewAPIEmoji(0, "4️⃣"): 4, - discord.NewAPIEmoji(0, "5️⃣"): 5, - discord.NewAPIEmoji(0, "6️⃣"): 6, - discord.NewAPIEmoji(0, "7️⃣"): 7, - discord.NewAPIEmoji(0, "8️⃣"): 8, - discord.NewAPIEmoji(0, "9️⃣"): 9, - discord.NewAPIEmoji(0, "🔟"): 10, - } - reactionPlayerCountNotificationReverseMap = map[int]discord.APIEmoji{ - 1: discord.NewAPIEmoji(0, "1️⃣"), - 2: discord.NewAPIEmoji(0, "2️⃣"), - 3: discord.NewAPIEmoji(0, "3️⃣"), - 4: discord.NewAPIEmoji(0, "4️⃣"), - 5: discord.NewAPIEmoji(0, "5️⃣"), - 6: discord.NewAPIEmoji(0, "6️⃣"), - 7: discord.NewAPIEmoji(0, "7️⃣"), - 8: discord.NewAPIEmoji(0, "8️⃣"), - 9: discord.NewAPIEmoji(0, "9️⃣"), - 10: discord.NewAPIEmoji(0, "🔟"), - } -) - var ownerCommandList = []api.CreateCommandData{ { Name: "list-guilds", @@ -287,6 +260,7 @@ type Bot struct { channelID discord.ChannelID userID discord.UserID c chan model.ChangedServerStatus + n chan model.PlayerCountNotificationMessage pollingInterval time.Duration conflictMap *xsync.MapOf[model.MessageTarget, Backoff] l *logging.Logger @@ -318,6 +292,7 @@ func New( superAdmins: superAdmins, useEmbeds: !legacyMessageFormat, c: make(chan model.ChangedServerStatus, 1024), + n: make(chan model.PlayerCountNotificationMessage, 1024), conflictMap: xsync.NewMapOf[model.MessageTarget, Backoff](), pollingInterval: pollingInterval, guildID: guildID, @@ -357,11 +332,19 @@ func New( log.Fatalf("failed to synchronize database with discord state: %v", err) } + routines := 1 + // start polling - go bot.cacheCleanup() + go bot.cacheCleanup(routines) go bot.serverUpdater(pollingInterval) for i := 0; i < max(2*runtime.NumCPU(), 5); i++ { - go bot.messageUpdater(i + 1) + routines++ + go bot.messageUpdater(routines) + } + + for i := 0; i < max(runtime.NumCPU(), 3); i++ { + routines++ + go bot.notificationUpdater(routines) } }) }) @@ -452,7 +435,7 @@ func (b *Bot) TxDAO(ctx context.Context) (d *dao.DAO, closer func(error) error, if err != nil { return nil, nil, err } - return dao.NewDAO(sqlc.New(tx)), closer, nil + return dao.NewDAO(sqlc.New(tx), b.l), closer, nil } func (b *Bot) ConnDAO(ctx context.Context) (d *dao.DAO, closer func(), err error) { @@ -460,7 +443,7 @@ func (b *Bot) ConnDAO(ctx context.Context) (d *dao.DAO, closer func(), err error if err != nil { return nil, nil, err } - return dao.NewDAO(sqlc.New(c)), f, nil + return dao.NewDAO(sqlc.New(c), b.l), f, nil } func (b *Bot) syncDatabaseState(ctx context.Context) (err error) { @@ -469,7 +452,7 @@ func (b *Bot) syncDatabaseState(ctx context.Context) (err error) { err = closer(err) }() - err = dao.RemovePlayerCountNotifications(ctx) + err = dao.RemovePlayerCountNotificationRequests(ctx) if err != nil { return err } @@ -479,8 +462,7 @@ func (b *Bot) syncDatabaseState(ctx context.Context) (err error) { return err } - //msgs := make([]*discord.Message, 0, len(trackings)) - notifications := make(map[model.MessageUserTarget]model.PlayerCountNotification) + notifications := make(map[model.MessageUserTarget]model.PlayerCountNotificationRequest) for _, t := range trackings { log.Printf("fetching message %s for notification tracking", t.MessageTarget) @@ -500,7 +482,7 @@ func (b *Bot) syncDatabaseState(ctx context.Context) (err error) { // iterate over all message reactions for _, reaction := range m.Reactions { emoji := reaction.Emoji.APIString() - if _, ok := reactionPlayerCountNotificationMap[emoji]; !ok { + if _, ok := model.ReactionPlayerCountNotificationMap[emoji]; !ok { // none of the ones that we want to look at continue } @@ -512,7 +494,7 @@ func (b *Bot) syncDatabaseState(ctx context.Context) (err error) { } return err } - val := reactionPlayerCountNotificationMap[emoji] + val := model.ReactionPlayerCountNotificationMap[emoji] log.Printf("found %d users for emoji %s of message %s", len(users), emoji, t.MessageTarget) for _, user := range users { @@ -528,7 +510,7 @@ func (b *Bot) syncDatabaseState(ctx context.Context) (err error) { n.ChannelID, n.MessageID, n.UserID, - reactionPlayerCountNotificationReverseMap[n.Threshold], + model.ReactionPlayerCountNotificationReverseMap[n.Threshold], ) if err != nil { return err @@ -543,14 +525,14 @@ func (b *Bot) syncDatabaseState(ctx context.Context) (err error) { n.ChannelID, n.MessageID, n.UserID, - reactionPlayerCountNotificationReverseMap[val], + model.ReactionPlayerCountNotificationReverseMap[val], ) if err != nil { return err } } } else { - notifications[userTarget] = model.PlayerCountNotification{ + notifications[userTarget] = model.PlayerCountNotificationRequest{ MessageUserTarget: userTarget, Threshold: val, } @@ -560,9 +542,9 @@ func (b *Bot) syncDatabaseState(ctx context.Context) (err error) { } values := utils.Values(notifications) - sort.Sort(model.ByPlayerCountNotificationIDs(values)) + sort.Sort(model.ByPlayerCountNotificationRequestIDs(values)) - err = dao.SetPlayerCountNotificationList(ctx, values) + err = dao.SetPlayerCountNotificationRequestList(ctx, values) if err != nil { return err } diff --git a/bot/log.go b/bot/log.go index 6d2d2bd..a657ebb 100644 --- a/bot/log.go +++ b/bot/log.go @@ -37,7 +37,7 @@ func (b *Bot) logWriter() { Flags: discord.SuppressEmbeds, }) if err != nil { - b.l.Errorf("failed to send log message: %v", err) + b.l.Errorf("failed to send log message to %s: %v", b.channelID, err) continue } case <-b.ctx.Done(): diff --git a/bot/message_deleter.go b/bot/message_deleter.go index 92e84fe..6d408dd 100644 --- a/bot/message_deleter.go +++ b/bot/message_deleter.go @@ -5,16 +5,22 @@ import ( ) func (b *Bot) handleMessageDeletion(e *gateway.MessageDeleteEvent) { - dao, closer, err := b.ConnDAO(b.ctx) + dao, closer, err := b.TxDAO(b.ctx) if err != nil { - b.l.Errorf("failed to get connection queries for message deletion: %v", err) + b.l.Errorf("failed to get transaction dao for message deletion: %v", err) return } - defer closer() + defer func() { + err = closer(err) + if err != nil { + b.l.Errorf("failed to close transaction dao for message deletion: %v", err) + } + }() // delete tracking messages from db in case someone deletes any message err = dao.RemoveTrackingByMessageID(b.ctx, e.GuildID, e.ID) if err != nil { b.l.Errorf("failed to remove tracking of guild %s and message id: %s: %v", e.GuildID, e.ID, err) } + } diff --git a/bot/player_count_notifications.go b/bot/player_count_notification_request.go similarity index 77% rename from bot/player_count_notifications.go rename to bot/player_count_notification_request.go index afc1ff5..eb8b344 100644 --- a/bot/player_count_notifications.go +++ b/bot/player_count_notification_request.go @@ -9,8 +9,8 @@ import ( "github.com/jxsl13/twstatus-bot/model" ) -func (b *Bot) handleAddPlayerCountNotifications(e *gateway.MessageReactionAddEvent) { - val, found := reactionPlayerCountNotificationMap[e.Emoji.APIString()] +func (b *Bot) handleAddPlayerCountNotificationRequest(e *gateway.MessageReactionAddEvent) { + val, found := model.ReactionPlayerCountNotificationMap[e.Emoji.APIString()] if !found { return } @@ -26,7 +26,7 @@ func (b *Bot) handleAddPlayerCountNotifications(e *gateway.MessageReactionAddEve }, } - n := model.PlayerCountNotification{ + n := model.PlayerCountNotificationRequest{ MessageUserTarget: userTarget, Threshold: val, } @@ -43,11 +43,11 @@ func (b *Bot) handleAddPlayerCountNotifications(e *gateway.MessageReactionAddEve } }() - pcn, err := dao.GetPlayerCountNotification(b.ctx, userTarget) + pcn, err := dao.GetPlayerCountNotificationRequest(b.ctx, userTarget) if err != nil { // not found, just insert if errors.Is(err, d.ErrNotFound) { - err = dao.SetPlayerCountNotification(b.ctx, n) + err = dao.SetPlayerCountNotificationRequest(b.ctx, n) if err != nil { b.l.Errorf("failed to set player count notification(%s -> %s): %v", n.MessageTarget, n.UserID, err) return @@ -65,7 +65,7 @@ func (b *Bot) handleAddPlayerCountNotifications(e *gateway.MessageReactionAddEve return } - prevEmoji, ok := reactionPlayerCountNotificationReverseMap[pcn.Threshold] + prevEmoji, ok := model.ReactionPlayerCountNotificationReverseMap[pcn.Threshold] if !ok { panic("failed to get emoji for player count notification: map must contain value to emoji mapping") } @@ -76,7 +76,7 @@ func (b *Bot) handleAddPlayerCountNotifications(e *gateway.MessageReactionAddEve return } - err = dao.SetPlayerCountNotification(b.ctx, n) + err = dao.SetPlayerCountNotificationRequest(b.ctx, n) if err != nil { b.l.Errorf("failed to set player count notification(%s -> %s): %v", n.MessageTarget, n.UserID, err) return @@ -85,8 +85,8 @@ func (b *Bot) handleAddPlayerCountNotifications(e *gateway.MessageReactionAddEve log.Printf("added player count notification for user %s and message %s", e.UserID, n.MessageTarget) } -func (b *Bot) handleRemovePlayerCountNotifications(e *gateway.MessageReactionRemoveEvent) { - val, found := reactionPlayerCountNotificationMap[e.Emoji.APIString()] +func (b *Bot) handleRemovePlayerCountNotificationRequest(e *gateway.MessageReactionRemoveEvent) { + val, found := model.ReactionPlayerCountNotificationMap[e.Emoji.APIString()] if !found || b.userID == e.UserID { return } @@ -102,7 +102,7 @@ func (b *Bot) handleRemovePlayerCountNotifications(e *gateway.MessageReactionRem }, } - n := model.PlayerCountNotification{ + n := model.PlayerCountNotificationRequest{ MessageUserTarget: userTarget, Threshold: val, } @@ -114,7 +114,7 @@ func (b *Bot) handleRemovePlayerCountNotifications(e *gateway.MessageReactionRem } defer closer() - err = dao.RemovePlayerCountNotification(b.ctx, n) + err = dao.RemovePlayerCountNotificationRequest(b.ctx, n) if err != nil { b.l.Errorf("failed to remove player count notification(%s -> %s): %v", n.MessageTarget, n.UserID, err) return diff --git a/bot/reactions.go b/bot/reactions.go index 3a023e7..3e7ab59 100644 --- a/bot/reactions.go +++ b/bot/reactions.go @@ -10,7 +10,7 @@ func (b *Bot) handleAddReactions(e *gateway.MessageReactionAddEvent) { // if so, add the reaction to the message // if not, ignore the reaction - b.handleAddPlayerCountNotifications(e) + b.handleAddPlayerCountNotificationRequest(e) } func (b *Bot) handleRemoveReactions(e *gateway.MessageReactionRemoveEvent) { @@ -19,5 +19,5 @@ func (b *Bot) handleRemoveReactions(e *gateway.MessageReactionRemoveEvent) { // and if the user that removed the reaction is the bot itself // if so, remove the reaction from the message // if not, ignore the reaction - b.handleRemovePlayerCountNotifications(e) + b.handleRemovePlayerCountNotificationRequest(e) } diff --git a/bot/update.go b/bot/update.go index 9ef5063..0be81c2 100644 --- a/bot/update.go +++ b/bot/update.go @@ -74,19 +74,30 @@ db transaction took %s return src, dst, nil } +// returns a list of active changed addresses for notification purposes func (b *Bot) changedServers() error { - var producer chan<- model.ChangedServerStatus = b.c + var updateProducer chan<- model.ChangedServerStatus = b.c - servers, err := func() (map[model.MessageTarget]model.ChangedServerStatus, error) { + servers, notifications, err := func() (map[model.MessageTarget]model.ChangedServerStatus, []model.PlayerCountNotificationMessage, error) { dao, closer, err := b.TxDAO(b.ctx) if err != nil { - return nil, err + return nil, nil, err } defer func() { err = closer(err) }() - return dao.ChangedServers(b.ctx) + servers, addresses, err := dao.ChangedServers(b.ctx) + if err != nil { + return nil, nil, err + } + + pcnm, err := dao.GetPlayerCountNotificationMessages(b.ctx, addresses) + if err != nil { + return nil, nil, err + } + + return servers, pcnm, nil }() if err != nil { return err @@ -95,12 +106,23 @@ func (b *Bot) changedServers() error { log.Printf("%d server messages require an update", len(servers)) for _, server := range servers { select { - case producer <- server: + case updateProducer <- server: continue case <-b.ctx.Done(): return b.ctx.Err() } } + + for _, v := range notifications { + select { + case b.n <- v: + continue + case <-b.ctx.Done(): + return b.ctx.Err() + } + + } + return nil } diff --git a/dao/active_servers.go.go b/dao/active_servers.go.go index c205e14..567b6a4 100644 --- a/dao/active_servers.go.go +++ b/dao/active_servers.go.go @@ -6,21 +6,21 @@ import ( "github.com/diamondburned/arikawa/v3/discord" "github.com/jxsl13/twstatus-bot/model" + "github.com/jxsl13/twstatus-bot/utils" ) -func (dao *DAO) ChangedServers(ctx context.Context) (_ map[model.MessageTarget]model.ChangedServerStatus, err error) { +func (dao *DAO) ChangedServers(ctx context.Context) (_ map[model.MessageTarget]model.ChangedServerStatus, changedActiveAddresses []string, err error) { previousServers, err := dao.PrevActiveServers(ctx) if err != nil { - return nil, err + return nil, nil, fmt.Errorf("failed to get previous active servers: %w", err) } currentServers, err := dao.ActiveServers(ctx) if err != nil { - return nil, err + return nil, nil, fmt.Errorf("failed to get current active servers: %w", err) } changedServers := make(map[model.MessageTarget]model.ChangedServerStatus, 64) - // removed servers for target := range previousServers { if empty, ok := currentServers[target]; !ok { @@ -33,6 +33,7 @@ func (dao *DAO) ChangedServers(ctx context.Context) (_ map[model.MessageTarget]m } } + changedActiveServers := make(map[string]struct{}, 64) // to add added := make(map[model.MessageTarget]model.ServerStatus, 64) for target, server := range currentServers { @@ -45,6 +46,7 @@ func (dao *DAO) ChangedServers(ctx context.Context) (_ map[model.MessageTarget]m Curr: server, } added[target] = server + changedActiveServers[server.Address] = struct{}{} } } else { // not found in prev -> new server @@ -63,30 +65,25 @@ func (dao *DAO) ChangedServers(ctx context.Context) (_ map[model.MessageTarget]m } err = dao.removePrevActiveServers(ctx, messageIDs) if err != nil { - return nil, err + return nil, nil, fmt.Errorf("failed to remove previous active servers: %w", err) } err = dao.removePrevActiveClients(ctx, messageIDs) if err != nil { - return nil, err + return nil, nil, fmt.Errorf("failed to remove previous active clients: %w", err) } err = dao.addPrevActiveServers(ctx, added) if err != nil { - return nil, err + return nil, nil, fmt.Errorf("failed to add previous active servers: %w", err) } err = dao.addPrevActiveClients(ctx, added) if err != nil { - return nil, err - } - - changedServers, err = dao.GetTargetListNotifications(ctx, changedServers) - if err != nil { - return nil, err + return nil, nil, fmt.Errorf("failed to add previous active clients: %w", err) } - return changedServers, nil + return changedServers, utils.SortedMapKeys(changedActiveServers), nil } func (dao *DAO) ActiveServers(ctx context.Context) (servers map[model.MessageTarget]model.ServerStatus, err error) { diff --git a/dao/dao.go b/dao/dao.go index 56c2006..30233e9 100644 --- a/dao/dao.go +++ b/dao/dao.go @@ -25,9 +25,10 @@ func IsUniqueConstraintErr(err error) bool { return false } -func NewDAO(q *sqlc.Queries) *DAO { +func NewDAO(q *sqlc.Queries, l *logging.Logger) *DAO { return &DAO{ q: q, + l: l, } } diff --git a/dao/player_count_notification.go b/dao/player_count_notification.go deleted file mode 100644 index be51f32..0000000 --- a/dao/player_count_notification.go +++ /dev/null @@ -1,147 +0,0 @@ -package dao - -import ( - "context" - "fmt" - - "github.com/diamondburned/arikawa/v3/discord" - "github.com/jxsl13/twstatus-bot/model" - "github.com/jxsl13/twstatus-bot/sqlc" -) - -func (dao *DAO) ListAllPlayerCountNotifications(ctx context.Context) (notifications []model.PlayerCountNotification, err error) { - pcn, err := dao.q.ListPlayerCountNotifications(ctx) - if err != nil { - return nil, fmt.Errorf("failed to get player count notifications: %w", err) - } - result := make([]model.PlayerCountNotification, 0, len(pcn)) - for _, n := range pcn { - result = append(result, model.PlayerCountNotification{ - MessageUserTarget: model.MessageUserTarget{ - UserID: discord.UserID(n.UserID), - MessageTarget: model.MessageTarget{ - ChannelTarget: model.ChannelTarget{ - GuildID: discord.GuildID(n.GuildID), - ChannelID: discord.ChannelID(n.ChannelID), - }, - MessageID: discord.MessageID(n.MessageID), - }, - }, - Threshold: int(n.Threshold), - }) - } - return result, nil -} - -func (dao *DAO) GetTargetListNotifications( - ctx context.Context, - servers map[model.MessageTarget]model.ChangedServerStatus) ( - _ map[model.MessageTarget]model.ChangedServerStatus, - err error, -) { - - for t, server := range servers { - - messageNotifications, err := dao.q.GetMessageTargetNotifications(ctx, - sqlc.GetMessageTargetNotificationsParams{ - GuildID: int64(t.GuildID), - ChannelID: int64(t.ChannelID), - MessageID: int64(t.MessageID), - }) - if err != nil { - return nil, fmt.Errorf("failed to get message target notifications: %w", err) - } - - for _, n := range messageNotifications { - notification := model.PlayerCountNotification{ - Threshold: int(n.Threshold), - MessageUserTarget: model.MessageUserTarget{ - UserID: discord.UserID(n.UserID), - MessageTarget: model.MessageTarget{ - ChannelTarget: model.ChannelTarget{ - GuildID: t.GuildID, - ChannelID: t.ChannelID, - }, - MessageID: t.MessageID, - }, - }, - } - - if notification.Notify(&server) { - server.UserNotifications = append(server.UserNotifications, discord.UserID(n.UserID)) - servers[t] = server - } - } - - } - return servers, nil -} - -func (dao *DAO) GetPlayerCountNotification( - ctx context.Context, - t model.MessageUserTarget, -) ( - notification model.PlayerCountNotification, - err error, -) { - - ns, err := dao.q.GetPlayerCountNotification(ctx, - sqlc.GetPlayerCountNotificationParams{ - GuildID: int64(t.GuildID), - ChannelID: int64(t.ChannelID), - MessageID: int64(t.MessageID), - UserID: int64(t.UserID), - }, - ) - if err != nil { - return model.PlayerCountNotification{}, err - } - if len(ns) == 0 { - return model.PlayerCountNotification{}, fmt.Errorf("%w: player count notification", ErrNotFound) - } - n := ns[0] - return model.PlayerCountNotification{ - MessageUserTarget: model.MessageUserTarget{ - UserID: discord.UserID(n.UserID), - MessageTarget: model.MessageTarget{ - ChannelTarget: model.ChannelTarget{ - GuildID: t.GuildID, - ChannelID: t.ChannelID, - }, - MessageID: t.MessageID, - }, - }, - Threshold: int(n.Threshold), - }, nil - -} - -func (dao *DAO) SetPlayerCountNotificationList(ctx context.Context, notifications []model.PlayerCountNotification) (err error) { - - for _, n := range notifications { - err = dao.q.SetPlayerCountNotification(ctx, sqlc.SetPlayerCountNotificationParams{ - GuildID: int64(n.GuildID), - ChannelID: int64(n.ChannelID), - MessageID: int64(n.MessageID), - UserID: int64(n.UserID), - }) - if err != nil { - return err - } - } - return nil -} - -func (dao *DAO) SetPlayerCountNotification(ctx context.Context, n model.PlayerCountNotification) (err error) { - return dao.q.SetPlayerCountNotification(ctx, n.ToSetSQLC()) - -} - -func (dao *DAO) RemovePlayerCountNotifications(ctx context.Context) (err error) { - return dao.q.RemovePlayerCountNotifications(ctx) -} - -func (dao *DAO) RemovePlayerCountNotification(ctx context.Context, n model.PlayerCountNotification) (err error) { - return dao.q.RemovePlayerCountNotification(ctx, n.ToRemoveSQLC()) - -} diff --git a/dao/player_count_notification_messages.go b/dao/player_count_notification_messages.go new file mode 100644 index 0000000..bdf7427 --- /dev/null +++ b/dao/player_count_notification_messages.go @@ -0,0 +1,34 @@ +package dao + +import ( + "context" + "fmt" + + "github.com/diamondburned/arikawa/v3/discord" + "github.com/jxsl13/twstatus-bot/model" + "github.com/jxsl13/twstatus-bot/sqlc" +) + +func (dao *DAO) AddPlayerCountNotificationMessage(ctx context.Context, channelID discord.ChannelID, messageID discord.MessageID) error { + return dao.q.AddPlayerCountNotificationMessage(ctx, sqlc.AddPlayerCountNotificationMessageParams{ + ChannelID: int64(channelID), + MessageID: int64(messageID), + }) +} + +func (dao *DAO) RemovePlayerCountNotificationMessage(ctx context.Context, channelID discord.ChannelID, messageID discord.MessageID) error { + return dao.q.RemovePlayerCountNotificationMessage(ctx, sqlc.RemovePlayerCountNotificationMessageParams{ + ChannelID: int64(channelID), + MessageID: int64(messageID), + }) +} + +func (dao *DAO) GetPlayerCountNotificationMessages(ctx context.Context, addresses []string) ([]model.PlayerCountNotificationMessage, error) { + + gpcnmr, err := dao.q.GetPlayerCountNotificationMessages(ctx, addresses) + if err != nil { + return nil, fmt.Errorf("failed to query player count notification messages: %w", err) + } + + return model.NewPlayerCountNotificationMessages(gpcnmr), nil +} diff --git a/dao/player_count_notification_request.go b/dao/player_count_notification_request.go new file mode 100644 index 0000000..7601441 --- /dev/null +++ b/dao/player_count_notification_request.go @@ -0,0 +1,80 @@ +package dao + +import ( + "context" + "fmt" + + "github.com/diamondburned/arikawa/v3/discord" + "github.com/jxsl13/twstatus-bot/model" + "github.com/jxsl13/twstatus-bot/sqlc" +) + +func (dao *DAO) GetPlayerCountNotificationRequest( + ctx context.Context, + t model.MessageUserTarget, +) ( + notification model.PlayerCountNotificationRequest, + err error, +) { + + ns, err := dao.q.GetPlayerCountNotificationRequest(ctx, + sqlc.GetPlayerCountNotificationRequestParams{ + GuildID: int64(t.GuildID), + ChannelID: int64(t.ChannelID), + MessageID: int64(t.MessageID), + UserID: int64(t.UserID), + }, + ) + if err != nil { + return model.PlayerCountNotificationRequest{}, err + } + if len(ns) == 0 { + return model.PlayerCountNotificationRequest{}, fmt.Errorf("%w: player count notification", ErrNotFound) + } + n := ns[0] + return model.PlayerCountNotificationRequest{ + MessageUserTarget: model.MessageUserTarget{ + UserID: discord.UserID(n.UserID), + MessageTarget: model.MessageTarget{ + ChannelTarget: model.ChannelTarget{ + GuildID: t.GuildID, + ChannelID: t.ChannelID, + }, + MessageID: t.MessageID, + }, + }, + Threshold: int(n.Threshold), + }, nil + +} + +func (dao *DAO) SetPlayerCountNotificationRequestList(ctx context.Context, notifications []model.PlayerCountNotificationRequest) (err error) { + + for _, n := range notifications { + err = dao.q.SetPlayerCountNotificationRequest(ctx, sqlc.SetPlayerCountNotificationRequestParams{ + GuildID: int64(n.GuildID), + ChannelID: int64(n.ChannelID), + MessageID: int64(n.MessageID), + UserID: int64(n.UserID), + Threshold: int16(n.Threshold), + }) + if err != nil { + return err + } + } + return nil +} + +func (dao *DAO) SetPlayerCountNotificationRequest(ctx context.Context, n model.PlayerCountNotificationRequest) (err error) { + return dao.q.SetPlayerCountNotificationRequest(ctx, n.ToSetSQLC()) + +} + +func (dao *DAO) RemovePlayerCountNotificationRequests(ctx context.Context) (err error) { + return dao.q.RemovePlayerCountNotificationRequests(ctx) +} + +func (dao *DAO) RemovePlayerCountNotificationRequest(ctx context.Context, n model.PlayerCountNotificationRequest) (err error) { + return dao.q.RemovePlayerCountNotificationRequest(ctx, n.ToRemoveSQLC()) + +} diff --git a/dao/prev_mentions.go b/dao/prev_mentions.go deleted file mode 100644 index 0050217..0000000 --- a/dao/prev_mentions.go +++ /dev/null @@ -1,68 +0,0 @@ -package dao - -import ( - "context" - "database/sql" - "fmt" - - "github.com/diamondburned/arikawa/v3/discord" - "github.com/jxsl13/twstatus-bot/model" - "github.com/jxsl13/twstatus-bot/sqlc" -) - -// TODO: continue here -func (dao *DAO) ChangedMessageMentions( - ctx context.Context, - tx *sql.Tx, - currentMentions model.MessageMentions, -) ( - messageMentions model.MessageMentions, - err error, -) { - - // removed mentions - // changed mentions - // unchanged mentions - - return messageMentions, nil -} - -func (dao *DAO) ListPrevMessageMentions(ctx context.Context, q *sqlc.Queries) (messageMentions model.MessageMentions, err error) { - pmm, err := q.ListPreviousMessageMentions(ctx) - if err != nil { - return nil, fmt.Errorf("failed to query previous message mentions: %w", err) - } - messageMentions = make(model.MessageMentions, 128) - var ( - mt model.MessageTarget - u discord.UserID - ) - for _, pm := range pmm { - mt = model.MessageTarget{ - ChannelTarget: model.ChannelTarget{ - GuildID: discord.GuildID(pm.GuildID), - ChannelID: discord.ChannelID(pm.ChannelID), - }, - MessageID: discord.MessageID(pm.MessageID), - } - u = discord.UserID(pm.UserID) - messageMentions[mt] = append(messageMentions[mt], u) - } - - return messageMentions, nil -} - -func (dao *DAO) RemoveMessageMentions(ctx context.Context, q *sqlc.Queries, mts []model.MessageTarget) (err error) { - for _, mt := range mts { - err = q.RemoveMessageMentions(ctx, sqlc.RemoveMessageMentionsParams{ - GuildID: int64(mt.GuildID), - ChannelID: int64(mt.ChannelID), - MessageID: int64(mt.MessageID), - }) - if err != nil { - return fmt.Errorf("failed to remove message mentions: %w", err) - } - - } - return nil -} diff --git a/migrations/003_schema.sql b/migrations/003_schema.sql new file mode 100644 index 0000000..e6aa453 --- /dev/null +++ b/migrations/003_schema.sql @@ -0,0 +1,12 @@ + +CREATE TABLE IF NOT EXISTS player_count_notification_messages ( + channel_id BIGINT NOT NULL PRIMARY KEY + references channels(channel_id) + ON DELETE CASCADE, + message_id BIGINT NOT NULL +); + + +---- create above / drop below ---- + +DROP TABLE IF EXISTS player_count_notification_messages; \ No newline at end of file diff --git a/migrations/004_schema.sql b/migrations/004_schema.sql new file mode 100644 index 0000000..6632957 --- /dev/null +++ b/migrations/004_schema.sql @@ -0,0 +1,25 @@ + + +DROP TABLE IF EXISTS prev_message_mentions; + +ALTER TABLE IF EXISTS player_count_notifications +RENAME TO player_count_notification_requests; + + +---- create above / drop below ---- + + +CREATE TABLE IF NOT EXISTS prev_message_mentions ( + guild_id BIGINT NOT NULL + REFERENCES guilds(guild_id) + ON DELETE CASCADE, + channel_id BIGINT NOT NULL + REFERENCES channels(channel_id) + ON DELETE CASCADE, + message_id BIGINT NOT NULL, + user_id BIGINT NOT NULL, + PRIMARY KEY (guild_id, channel_id, message_id, user_id) +); + +ALTER TABLE IF EXISTS player_count_notification_requests +RENAME TO player_count_notifications; \ No newline at end of file diff --git a/model/changed_server.go b/model/changed_server.go index e6bc9df..6d2cd4d 100644 --- a/model/changed_server.go +++ b/model/changed_server.go @@ -2,18 +2,14 @@ package model import ( "fmt" - "strings" - - "github.com/diamondburned/arikawa/v3/discord" ) type ChangedServerStatus struct { Target MessageTarget - Prev ServerStatus - Curr ServerStatus - Offline bool - UserNotifications []discord.UserID + Prev ServerStatus + Curr ServerStatus + Offline bool } func (c *ChangedServerStatus) Content() string { @@ -22,23 +18,5 @@ func (c *ChangedServerStatus) Content() string { } header := c.Curr.Header() - - if len(c.UserNotifications) == 0 { - return header - } - - const limit = 2000 - sb := strings.Builder{} - sb.Grow(limit) - sb.WriteString(header) - sb.WriteString("\n") - - for _, user := range c.UserNotifications { - mention := user.Mention() - if sb.Len()+len(mention) > limit { - break - } - sb.WriteString(mention) - } - return sb.String() + return header } diff --git a/model/mentions.go b/model/mentions.go deleted file mode 100644 index 270630a..0000000 --- a/model/mentions.go +++ /dev/null @@ -1,21 +0,0 @@ -package model - -import "github.com/diamondburned/arikawa/v3/discord" - -type MessageMentions map[MessageTarget]Mentions - -type Mentions []discord.UserID - -func (m Mentions) Equals(other Mentions) bool { - if len(m) != len(other) { - return false - } - - for i := range m { - if m[i] != other[i] { - return false - } - } - - return true -} diff --git a/model/player_count_notification.go b/model/player_count_notification.go deleted file mode 100644 index 4b76549..0000000 --- a/model/player_count_notification.go +++ /dev/null @@ -1,49 +0,0 @@ -package model - -import ( - "github.com/diamondburned/arikawa/v3/discord" - "github.com/jxsl13/twstatus-bot/sqlc" -) - -type MessageUserTarget struct { - MessageTarget - UserID discord.UserID -} - -type PlayerCountNotification struct { - MessageUserTarget - Threshold int -} - -func (p *PlayerCountNotification) ToSetSQLC() sqlc.SetPlayerCountNotificationParams { - return sqlc.SetPlayerCountNotificationParams{ - GuildID: int64(p.GuildID), - ChannelID: int64(p.ChannelID), - MessageID: int64(p.MessageID), - UserID: int64(p.UserID), - Threshold: int16(p.Threshold), - } -} - -func (p *PlayerCountNotification) ToRemoveSQLC() sqlc.RemovePlayerCountNotificationParams { - return sqlc.RemovePlayerCountNotificationParams{ - GuildID: int64(p.GuildID), - ChannelID: int64(p.ChannelID), - MessageID: int64(p.MessageID), - UserID: int64(p.UserID), - } -} - -func (p PlayerCountNotification) Notify(change *ChangedServerStatus) bool { - return len(change.Curr.Clients) >= p.Threshold -} - -type PlayerCountNotifications []PlayerCountNotification - -type ByPlayerCountNotificationIDs []PlayerCountNotification - -func (a ByPlayerCountNotificationIDs) Len() int { return len(a) } -func (a ByPlayerCountNotificationIDs) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (a ByPlayerCountNotificationIDs) Less(i, j int) bool { - return a[i].MessageTarget.Less(a[j].MessageTarget) -} diff --git a/model/player_count_notification_message.go b/model/player_count_notification_message.go new file mode 100644 index 0000000..ef0adda --- /dev/null +++ b/model/player_count_notification_message.go @@ -0,0 +1,121 @@ +package model + +import ( + "strings" + + "github.com/diamondburned/arikawa/v3/discord" + "github.com/jxsl13/twstatus-bot/sqlc" + "github.com/jxsl13/twstatus-bot/utils" +) + +func NewPlayerCountNotificationMessages(rows []sqlc.GetPlayerCountNotificationMessagesRow) []PlayerCountNotificationMessage { + + resultMap := make(map[ChannelTarget]PlayerCountNotificationMessage, len(rows)/10) + for _, row := range rows { + target := ChannelTarget{ + GuildID: discord.GuildID(row.GuildID), + ChannelID: discord.ChannelID(row.ChannelID), + } + + usm := UserMessageThreshold{ + MessageThreshold: MessageThreshold{ + MessageID: discord.MessageID(row.ReqMessageID), + Threshold: int(row.Threshold), + }, + UserID: discord.UserID(row.UserID), + } + + mt := MessageThreshold{ + MessageID: discord.MessageID(row.ReqMessageID), + Threshold: int(row.Threshold), + } + + if n, ok := resultMap[target]; ok { + n.UserIDs = append(n.UserIDs, discord.UserID(row.UserID)) + n.RemoveMessageReactions = append(n.RemoveMessageReactions, mt) + n.RemoveUserMessageReactions = append(n.RemoveUserMessageReactions, usm) + resultMap[target] = n + } else { + resultMap[target] = PlayerCountNotificationMessage{ + ChannelTarget: target, + PrevMessageID: discord.MessageID(row.PrevMessageID), + UserIDs: []discord.UserID{discord.UserID(row.UserID)}, + RemoveMessageReactions: []MessageThreshold{mt}, + RemoveUserMessageReactions: []UserMessageThreshold{usm}, + } + } + } + + result := make([]PlayerCountNotificationMessage, 0, len(resultMap)) + for _, v := range resultMap { + v.UserIDs = utils.Unique(v.UserIDs) + v.RemoveMessageReactions = utils.Unique(v.RemoveMessageReactions) + result = append(result, v) + } + + return result + +} + +type MessageUserID struct { + MessageID discord.MessageID + UserID discord.UserID +} + +type MessageThreshold struct { + MessageID discord.MessageID + // corresponding emoji must be removed + // because we do not want to spam the channel + Threshold int +} + +func (m MessageThreshold) Reaction() discord.APIEmoji { + return ReactionPlayerCountNotificationReverseMap[m.Threshold] +} + +type UserMessageThreshold struct { + MessageThreshold + UserID discord.UserID +} + +type PlayerCountNotificationMessage struct { + // message is supposed to be sent into that channel + ChannelTarget + + // needs to be removed from that channel if it exists + // is 0 if no message was sent yet + PrevMessageID discord.MessageID + + // mention these users for the current channel + UserIDs []discord.UserID + + // for removing reactions from messages + RemoveMessageReactions []MessageThreshold + // for removing from database + RemoveUserMessageReactions []UserMessageThreshold +} + +func (p *PlayerCountNotificationMessage) MessageTarget(messageID discord.MessageID) MessageTarget { + return MessageTarget{ + ChannelTarget: p.ChannelTarget, + MessageID: messageID, + } +} + +// format header +func (p *PlayerCountNotificationMessage) Format() string { + + const limit = 2000 + sb := strings.Builder{} + sb.Grow(limit) + + for _, user := range p.UserIDs { + mention := user.Mention() + if sb.Len()+len(mention) > limit { + break + } + sb.WriteString(mention) + sb.WriteString(" ") + } + return sb.String() +} diff --git a/model/player_count_notification_request.go b/model/player_count_notification_request.go new file mode 100644 index 0000000..1812466 --- /dev/null +++ b/model/player_count_notification_request.go @@ -0,0 +1,46 @@ +package model + +import ( + "github.com/diamondburned/arikawa/v3/discord" + "github.com/jxsl13/twstatus-bot/sqlc" +) + +type MessageUserTarget struct { + MessageTarget + UserID discord.UserID +} + +type PlayerCountNotificationRequest struct { + MessageUserTarget + Threshold int +} + +func (p *PlayerCountNotificationRequest) ToSetSQLC() sqlc.SetPlayerCountNotificationRequestParams { + return sqlc.SetPlayerCountNotificationRequestParams{ + GuildID: int64(p.GuildID), + ChannelID: int64(p.ChannelID), + MessageID: int64(p.MessageID), + UserID: int64(p.UserID), + Threshold: int16(p.Threshold), + } +} + +func (p *PlayerCountNotificationRequest) ToRemoveSQLC() sqlc.RemovePlayerCountNotificationRequestParams { + return sqlc.RemovePlayerCountNotificationRequestParams{ + GuildID: int64(p.GuildID), + ChannelID: int64(p.ChannelID), + MessageID: int64(p.MessageID), + UserID: int64(p.UserID), + Threshold: int16(p.Threshold), + } +} + +type PlayerCountNotificationRequests []PlayerCountNotificationRequest + +type ByPlayerCountNotificationRequestIDs []PlayerCountNotificationRequest + +func (a ByPlayerCountNotificationRequestIDs) Len() int { return len(a) } +func (a ByPlayerCountNotificationRequestIDs) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a ByPlayerCountNotificationRequestIDs) Less(i, j int) bool { + return a[i].MessageTarget.Less(a[j].MessageTarget) +} diff --git a/model/reactions.go b/model/reactions.go new file mode 100644 index 0000000..b6fac3c --- /dev/null +++ b/model/reactions.go @@ -0,0 +1,30 @@ +package model + +import "github.com/diamondburned/arikawa/v3/discord" + +var ( + ReactionPlayerCountNotificationMap = map[discord.APIEmoji]int{ + discord.NewAPIEmoji(0, "1️⃣"): 1, + discord.NewAPIEmoji(0, "2️⃣"): 2, + discord.NewAPIEmoji(0, "3️⃣"): 3, + discord.NewAPIEmoji(0, "4️⃣"): 4, + discord.NewAPIEmoji(0, "5️⃣"): 5, + discord.NewAPIEmoji(0, "6️⃣"): 6, + discord.NewAPIEmoji(0, "7️⃣"): 7, + discord.NewAPIEmoji(0, "8️⃣"): 8, + discord.NewAPIEmoji(0, "9️⃣"): 9, + discord.NewAPIEmoji(0, "🔟"): 10, + } + ReactionPlayerCountNotificationReverseMap = map[int]discord.APIEmoji{ + 1: discord.NewAPIEmoji(0, "1️⃣"), + 2: discord.NewAPIEmoji(0, "2️⃣"), + 3: discord.NewAPIEmoji(0, "3️⃣"), + 4: discord.NewAPIEmoji(0, "4️⃣"), + 5: discord.NewAPIEmoji(0, "5️⃣"), + 6: discord.NewAPIEmoji(0, "6️⃣"), + 7: discord.NewAPIEmoji(0, "7️⃣"), + 8: discord.NewAPIEmoji(0, "8️⃣"), + 9: discord.NewAPIEmoji(0, "9️⃣"), + 10: discord.NewAPIEmoji(0, "🔟"), + } +) diff --git a/queries/player_count_notification.sql b/queries/player_count_notification.sql deleted file mode 100644 index 47d9ee9..0000000 --- a/queries/player_count_notification.sql +++ /dev/null @@ -1,66 +0,0 @@ - - --- name: ListPlayerCountNotifications :many -SELECT - guild_id, - channel_id, - message_id, - user_id, - threshold -FROM player_count_notifications -ORDER BY - guild_id ASC, - channel_id ASC, - message_id ASC, - user_id ASC; - - --- name: GetMessageTargetNotifications :many -SELECT - user_id, - threshold -FROM player_count_notifications -WHERE guild_id = $1 -AND channel_id = $2 -AND message_id = $3 -ORDER BY user_id ASC; - - --- name: GetPlayerCountNotification :many -SELECT - guild_id, - channel_id, - message_id, - user_id, - threshold -FROM player_count_notifications -WHERE guild_id = $1 -AND channel_id = $2 -AND message_id = $3 -AND user_id = $4 -LIMIT 1; - - --- name: SetPlayerCountNotification :exec -INSERT INTO player_count_notifications ( - guild_id, - channel_id, - message_id, - user_id, - threshold -) VALUES ($1, $2, $3, $4, $5) -ON CONFLICT (guild_id, channel_id, message_id, user_id) -DO UPDATE SET threshold = $5; - - --- name: RemovePlayerCountNotifications :exec -DELETE FROM player_count_notifications; - - --- name: RemovePlayerCountNotification :exec -DELETE FROM player_count_notifications -WHERE guild_id = $1 -AND channel_id = $2 -AND message_id = $3 -AND user_id = $4 -AND threshold = $5; diff --git a/queries/player_count_notification_messages.sql b/queries/player_count_notification_messages.sql new file mode 100644 index 0000000..ee1d08a --- /dev/null +++ b/queries/player_count_notification_messages.sql @@ -0,0 +1,54 @@ + + +-- name: GetPlayerCountNotificationMessages :many + + +SELECT + t.guild_id, + t.channel_id, + pcr.message_id AS req_message_id, + COALESCE(pcm.message_id, 0)::bigint AS prev_message_id, + pcr.user_id, + MIN(pcr.threshold)::smallint AS threshold, + MAX(COALESCE(np.num_players, 0))::smallint AS num_players +FROM channels c +JOIN tracking t ON c.channel_id = t.channel_id +LEFT JOIN ( + SELECT ac.address, count(*) AS num_players + FROM active_server_clients ac + WHERE ac.address = ANY($1::TEXT[]) + GROUP BY ac.address + ORDER BY ac.address +) np ON np.address = t.address +JOIN player_count_notification_requests pcr +ON ( + t.guild_id = pcr.guild_id AND + t.channel_id = pcr.channel_id AND + t.message_id = pcr.message_id AND + num_players >= pcr.threshold +) +LEFT JOIN player_count_notification_messages pcm +ON (t.channel_id = pcm.channel_id) +WHERE c.running = TRUE +GROUP BY + t.guild_id, + t.channel_id, + pcm.message_id, + pcr.message_id, + pcr.user_id, + num_players +ORDER BY t.guild_id, t.channel_id, pcm.message_id, num_players, pcr.user_id; + + + +-- name: AddPlayerCountNotificationMessage :exec +INSERT INTO player_count_notification_messages (channel_id, message_id) +VALUES ($1, $2) +ON CONFLICT (channel_id) +DO UPDATE SET + message_id = EXCLUDED.message_id; + +-- name: RemovePlayerCountNotificationMessage :exec +DELETE FROM player_count_notification_messages +WHERE channel_id = $1 +AND message_id = $2; \ No newline at end of file diff --git a/queries/player_count_notification_request.sql b/queries/player_count_notification_request.sql new file mode 100644 index 0000000..fba1d1d --- /dev/null +++ b/queries/player_count_notification_request.sql @@ -0,0 +1,40 @@ + + +-- name: GetPlayerCountNotificationRequest :many +SELECT + guild_id, + channel_id, + message_id, + user_id, + threshold +FROM player_count_notification_requests +WHERE guild_id = $1 +AND channel_id = $2 +AND message_id = $3 +AND user_id = $4 +LIMIT 1; + + +-- name: SetPlayerCountNotificationRequest :exec +INSERT INTO player_count_notification_requests ( + guild_id, + channel_id, + message_id, + user_id, + threshold +) VALUES ($1, $2, $3, $4, $5) +ON CONFLICT (guild_id, channel_id, message_id, user_id) +DO UPDATE SET threshold = $5; + + +-- name: RemovePlayerCountNotificationRequests :exec +DELETE FROM player_count_notification_requests; + + +-- name: RemovePlayerCountNotificationRequest :exec +DELETE FROM player_count_notification_requests +WHERE guild_id = $1 +AND channel_id = $2 +AND message_id = $3 +AND user_id = $4 +AND threshold = $5; diff --git a/sqlc.yaml b/sqlc.yaml index 925a038..ff3164d 100644 --- a/sqlc.yaml +++ b/sqlc.yaml @@ -7,13 +7,15 @@ sql: "queries/flag_mappings.sql", "queries/flags.sql", "queries/guild.sql", - "queries/player_count_notification.sql", + "queries/player_count_notification_messages.sql", + "queries/player_count_notification_request.sql", "queries/prev_active_servers.sql", - "queries/prev_mentions.sql", "queries/tracking.sql" ] schema: [ "migrations/001_schema.sql", + "migrations/003_schema.sql", + "migrations/004_schema.sql", ] gen: go: diff --git a/sqlc/models.go b/sqlc/models.go index e24e4d1..6096ca9 100644 --- a/sqlc/models.go +++ b/sqlc/models.go @@ -59,7 +59,12 @@ type Guild struct { Description string `db:"description"` } -type PlayerCountNotification struct { +type PlayerCountNotificationMessage struct { + ChannelID int64 `db:"channel_id"` + MessageID int64 `db:"message_id"` +} + +type PlayerCountNotificationRequest struct { GuildID int64 `db:"guild_id"` ChannelID int64 `db:"channel_id"` MessageID int64 `db:"message_id"` @@ -101,13 +106,6 @@ type PrevActiveServerClient struct { FlagEmoji string `db:"flag_emoji"` } -type PrevMessageMention struct { - GuildID int64 `db:"guild_id"` - ChannelID int64 `db:"channel_id"` - MessageID int64 `db:"message_id"` - UserID int64 `db:"user_id"` -} - type Tracking struct { ID *int64 `db:"id"` MessageID int64 `db:"message_id"` diff --git a/sqlc/player_count_notification.sql.go b/sqlc/player_count_notification.sql.go deleted file mode 100644 index 0da9d28..0000000 --- a/sqlc/player_count_notification.sql.go +++ /dev/null @@ -1,214 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.25.0 -// source: player_count_notification.sql - -package sqlc - -import ( - "context" -) - -const getMessageTargetNotifications = `-- name: GetMessageTargetNotifications :many -SELECT - user_id, - threshold -FROM player_count_notifications -WHERE guild_id = $1 -AND channel_id = $2 -AND message_id = $3 -ORDER BY user_id ASC -` - -type GetMessageTargetNotificationsParams struct { - GuildID int64 `db:"guild_id"` - ChannelID int64 `db:"channel_id"` - MessageID int64 `db:"message_id"` -} - -type GetMessageTargetNotificationsRow struct { - UserID int64 `db:"user_id"` - Threshold int16 `db:"threshold"` -} - -func (q *Queries) GetMessageTargetNotifications(ctx context.Context, arg GetMessageTargetNotificationsParams) ([]GetMessageTargetNotificationsRow, error) { - rows, err := q.db.Query(ctx, getMessageTargetNotifications, arg.GuildID, arg.ChannelID, arg.MessageID) - if err != nil { - return nil, err - } - defer rows.Close() - items := []GetMessageTargetNotificationsRow{} - for rows.Next() { - var i GetMessageTargetNotificationsRow - if err := rows.Scan(&i.UserID, &i.Threshold); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const getPlayerCountNotification = `-- name: GetPlayerCountNotification :many -SELECT - guild_id, - channel_id, - message_id, - user_id, - threshold -FROM player_count_notifications -WHERE guild_id = $1 -AND channel_id = $2 -AND message_id = $3 -AND user_id = $4 -LIMIT 1 -` - -type GetPlayerCountNotificationParams struct { - GuildID int64 `db:"guild_id"` - ChannelID int64 `db:"channel_id"` - MessageID int64 `db:"message_id"` - UserID int64 `db:"user_id"` -} - -func (q *Queries) GetPlayerCountNotification(ctx context.Context, arg GetPlayerCountNotificationParams) ([]PlayerCountNotification, error) { - rows, err := q.db.Query(ctx, getPlayerCountNotification, - arg.GuildID, - arg.ChannelID, - arg.MessageID, - arg.UserID, - ) - if err != nil { - return nil, err - } - defer rows.Close() - items := []PlayerCountNotification{} - for rows.Next() { - var i PlayerCountNotification - if err := rows.Scan( - &i.GuildID, - &i.ChannelID, - &i.MessageID, - &i.UserID, - &i.Threshold, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const listPlayerCountNotifications = `-- name: ListPlayerCountNotifications :many -SELECT - guild_id, - channel_id, - message_id, - user_id, - threshold -FROM player_count_notifications -ORDER BY - guild_id ASC, - channel_id ASC, - message_id ASC, - user_id ASC -` - -func (q *Queries) ListPlayerCountNotifications(ctx context.Context) ([]PlayerCountNotification, error) { - rows, err := q.db.Query(ctx, listPlayerCountNotifications) - if err != nil { - return nil, err - } - defer rows.Close() - items := []PlayerCountNotification{} - for rows.Next() { - var i PlayerCountNotification - if err := rows.Scan( - &i.GuildID, - &i.ChannelID, - &i.MessageID, - &i.UserID, - &i.Threshold, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const removePlayerCountNotification = `-- name: RemovePlayerCountNotification :exec -DELETE FROM player_count_notifications -WHERE guild_id = $1 -AND channel_id = $2 -AND message_id = $3 -AND user_id = $4 -AND threshold = $5 -` - -type RemovePlayerCountNotificationParams struct { - GuildID int64 `db:"guild_id"` - ChannelID int64 `db:"channel_id"` - MessageID int64 `db:"message_id"` - UserID int64 `db:"user_id"` - Threshold int16 `db:"threshold"` -} - -func (q *Queries) RemovePlayerCountNotification(ctx context.Context, arg RemovePlayerCountNotificationParams) error { - _, err := q.db.Exec(ctx, removePlayerCountNotification, - arg.GuildID, - arg.ChannelID, - arg.MessageID, - arg.UserID, - arg.Threshold, - ) - return err -} - -const removePlayerCountNotifications = `-- name: RemovePlayerCountNotifications :exec -DELETE FROM player_count_notifications -` - -func (q *Queries) RemovePlayerCountNotifications(ctx context.Context) error { - _, err := q.db.Exec(ctx, removePlayerCountNotifications) - return err -} - -const setPlayerCountNotification = `-- name: SetPlayerCountNotification :exec -INSERT INTO player_count_notifications ( - guild_id, - channel_id, - message_id, - user_id, - threshold -) VALUES ($1, $2, $3, $4, $5) -ON CONFLICT (guild_id, channel_id, message_id, user_id) -DO UPDATE SET threshold = $5 -` - -type SetPlayerCountNotificationParams struct { - GuildID int64 `db:"guild_id"` - ChannelID int64 `db:"channel_id"` - MessageID int64 `db:"message_id"` - UserID int64 `db:"user_id"` - Threshold int16 `db:"threshold"` -} - -func (q *Queries) SetPlayerCountNotification(ctx context.Context, arg SetPlayerCountNotificationParams) error { - _, err := q.db.Exec(ctx, setPlayerCountNotification, - arg.GuildID, - arg.ChannelID, - arg.MessageID, - arg.UserID, - arg.Threshold, - ) - return err -} diff --git a/sqlc/player_count_notification_messages.sql.go b/sqlc/player_count_notification_messages.sql.go new file mode 100644 index 0000000..f0863a3 --- /dev/null +++ b/sqlc/player_count_notification_messages.sql.go @@ -0,0 +1,122 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.25.0 +// source: player_count_notification_messages.sql + +package sqlc + +import ( + "context" +) + +const addPlayerCountNotificationMessage = `-- name: AddPlayerCountNotificationMessage :exec +INSERT INTO player_count_notification_messages (channel_id, message_id) +VALUES ($1, $2) +ON CONFLICT (channel_id) +DO UPDATE SET + message_id = EXCLUDED.message_id +` + +type AddPlayerCountNotificationMessageParams struct { + ChannelID int64 `db:"channel_id"` + MessageID int64 `db:"message_id"` +} + +func (q *Queries) AddPlayerCountNotificationMessage(ctx context.Context, arg AddPlayerCountNotificationMessageParams) error { + _, err := q.db.Exec(ctx, addPlayerCountNotificationMessage, arg.ChannelID, arg.MessageID) + return err +} + +const getPlayerCountNotificationMessages = `-- name: GetPlayerCountNotificationMessages :many + + +SELECT + t.guild_id, + t.channel_id, + pcr.message_id AS req_message_id, + COALESCE(pcm.message_id, 0)::bigint AS prev_message_id, + pcr.user_id, + MIN(pcr.threshold)::smallint AS threshold, + MAX(COALESCE(np.num_players, 0))::smallint AS num_players +FROM channels c +JOIN tracking t ON c.channel_id = t.channel_id +LEFT JOIN ( + SELECT ac.address, count(*) AS num_players + FROM active_server_clients ac + WHERE ac.address = ANY($1::TEXT[]) + GROUP BY ac.address + ORDER BY ac.address +) np ON np.address = t.address +JOIN player_count_notification_requests pcr +ON ( + t.guild_id = pcr.guild_id AND + t.channel_id = pcr.channel_id AND + t.message_id = pcr.message_id AND + num_players >= pcr.threshold +) +LEFT JOIN player_count_notification_messages pcm +ON (t.channel_id = pcm.channel_id) +WHERE c.running = TRUE +GROUP BY + t.guild_id, + t.channel_id, + pcm.message_id, + pcr.message_id, + pcr.user_id, + num_players +ORDER BY t.guild_id, t.channel_id, pcm.message_id, num_players, pcr.user_id +` + +type GetPlayerCountNotificationMessagesRow struct { + GuildID int64 `db:"guild_id"` + ChannelID int64 `db:"channel_id"` + ReqMessageID int64 `db:"req_message_id"` + PrevMessageID int64 `db:"prev_message_id"` + UserID int64 `db:"user_id"` + Threshold int16 `db:"threshold"` + NumPlayers int16 `db:"num_players"` +} + +func (q *Queries) GetPlayerCountNotificationMessages(ctx context.Context, dollar_1 []string) ([]GetPlayerCountNotificationMessagesRow, error) { + rows, err := q.db.Query(ctx, getPlayerCountNotificationMessages, dollar_1) + if err != nil { + return nil, err + } + defer rows.Close() + items := []GetPlayerCountNotificationMessagesRow{} + for rows.Next() { + var i GetPlayerCountNotificationMessagesRow + if err := rows.Scan( + &i.GuildID, + &i.ChannelID, + &i.ReqMessageID, + &i.PrevMessageID, + &i.UserID, + &i.Threshold, + &i.NumPlayers, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const removePlayerCountNotificationMessage = `-- name: RemovePlayerCountNotificationMessage :exec +DELETE FROM player_count_notification_messages +WHERE channel_id = $1 +AND message_id = $2 +` + +type RemovePlayerCountNotificationMessageParams struct { + ChannelID int64 `db:"channel_id"` + MessageID int64 `db:"message_id"` +} + +func (q *Queries) RemovePlayerCountNotificationMessage(ctx context.Context, arg RemovePlayerCountNotificationMessageParams) error { + _, err := q.db.Exec(ctx, removePlayerCountNotificationMessage, arg.ChannelID, arg.MessageID) + return err +} diff --git a/sqlc/player_count_notification_request.sql.go b/sqlc/player_count_notification_request.sql.go new file mode 100644 index 0000000..fa1d5a6 --- /dev/null +++ b/sqlc/player_count_notification_request.sql.go @@ -0,0 +1,131 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.25.0 +// source: player_count_notification_request.sql + +package sqlc + +import ( + "context" +) + +const getPlayerCountNotificationRequest = `-- name: GetPlayerCountNotificationRequest :many +SELECT + guild_id, + channel_id, + message_id, + user_id, + threshold +FROM player_count_notification_requests +WHERE guild_id = $1 +AND channel_id = $2 +AND message_id = $3 +AND user_id = $4 +LIMIT 1 +` + +type GetPlayerCountNotificationRequestParams struct { + GuildID int64 `db:"guild_id"` + ChannelID int64 `db:"channel_id"` + MessageID int64 `db:"message_id"` + UserID int64 `db:"user_id"` +} + +func (q *Queries) GetPlayerCountNotificationRequest(ctx context.Context, arg GetPlayerCountNotificationRequestParams) ([]PlayerCountNotificationRequest, error) { + rows, err := q.db.Query(ctx, getPlayerCountNotificationRequest, + arg.GuildID, + arg.ChannelID, + arg.MessageID, + arg.UserID, + ) + if err != nil { + return nil, err + } + defer rows.Close() + items := []PlayerCountNotificationRequest{} + for rows.Next() { + var i PlayerCountNotificationRequest + if err := rows.Scan( + &i.GuildID, + &i.ChannelID, + &i.MessageID, + &i.UserID, + &i.Threshold, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const removePlayerCountNotificationRequest = `-- name: RemovePlayerCountNotificationRequest :exec +DELETE FROM player_count_notification_requests +WHERE guild_id = $1 +AND channel_id = $2 +AND message_id = $3 +AND user_id = $4 +AND threshold = $5 +` + +type RemovePlayerCountNotificationRequestParams struct { + GuildID int64 `db:"guild_id"` + ChannelID int64 `db:"channel_id"` + MessageID int64 `db:"message_id"` + UserID int64 `db:"user_id"` + Threshold int16 `db:"threshold"` +} + +func (q *Queries) RemovePlayerCountNotificationRequest(ctx context.Context, arg RemovePlayerCountNotificationRequestParams) error { + _, err := q.db.Exec(ctx, removePlayerCountNotificationRequest, + arg.GuildID, + arg.ChannelID, + arg.MessageID, + arg.UserID, + arg.Threshold, + ) + return err +} + +const removePlayerCountNotificationRequests = `-- name: RemovePlayerCountNotificationRequests :exec +DELETE FROM player_count_notification_requests +` + +func (q *Queries) RemovePlayerCountNotificationRequests(ctx context.Context) error { + _, err := q.db.Exec(ctx, removePlayerCountNotificationRequests) + return err +} + +const setPlayerCountNotificationRequest = `-- name: SetPlayerCountNotificationRequest :exec +INSERT INTO player_count_notification_requests ( + guild_id, + channel_id, + message_id, + user_id, + threshold +) VALUES ($1, $2, $3, $4, $5) +ON CONFLICT (guild_id, channel_id, message_id, user_id) +DO UPDATE SET threshold = $5 +` + +type SetPlayerCountNotificationRequestParams struct { + GuildID int64 `db:"guild_id"` + ChannelID int64 `db:"channel_id"` + MessageID int64 `db:"message_id"` + UserID int64 `db:"user_id"` + Threshold int16 `db:"threshold"` +} + +func (q *Queries) SetPlayerCountNotificationRequest(ctx context.Context, arg SetPlayerCountNotificationRequestParams) error { + _, err := q.db.Exec(ctx, setPlayerCountNotificationRequest, + arg.GuildID, + arg.ChannelID, + arg.MessageID, + arg.UserID, + arg.Threshold, + ) + return err +} diff --git a/sqlc/prev_mentions.sql.go b/sqlc/prev_mentions.sql.go deleted file mode 100644 index 5dc333c..0000000 --- a/sqlc/prev_mentions.sql.go +++ /dev/null @@ -1,63 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.25.0 -// source: prev_mentions.sql - -package sqlc - -import ( - "context" -) - -const listPreviousMessageMentions = `-- name: ListPreviousMessageMentions :many -SELECT - guild_id, - channel_id, - message_id, - user_id -FROM prev_message_mentions -ORDER BY guild_id ASC, channel_id ASC, message_id ASC, user_id ASC -` - -func (q *Queries) ListPreviousMessageMentions(ctx context.Context) ([]PrevMessageMention, error) { - rows, err := q.db.Query(ctx, listPreviousMessageMentions) - if err != nil { - return nil, err - } - defer rows.Close() - items := []PrevMessageMention{} - for rows.Next() { - var i PrevMessageMention - if err := rows.Scan( - &i.GuildID, - &i.ChannelID, - &i.MessageID, - &i.UserID, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const removeMessageMentions = `-- name: RemoveMessageMentions :exec -DELETE FROM prev_message_mentions -WHERE guild_id = $1 -AND channel_id = $2 -AND message_id = $3 -` - -type RemoveMessageMentionsParams struct { - GuildID int64 `db:"guild_id"` - ChannelID int64 `db:"channel_id"` - MessageID int64 `db:"message_id"` -} - -func (q *Queries) RemoveMessageMentions(ctx context.Context, arg RemoveMessageMentionsParams) error { - _, err := q.db.Exec(ctx, removeMessageMentions, arg.GuildID, arg.ChannelID, arg.MessageID) - return err -}