From b2ae30b5384286c9304d9373ec90eeb0f24323a2 Mon Sep 17 00:00:00 2001 From: denverquane Date: Thu, 29 Dec 2022 15:15:02 -0500 Subject: [PATCH] Add functionality to download each table separately, bump utils, adjust redis keys --- common/redis.go | 12 +- discord/command/commands.go | 26 ++-- discord/command/download.go | 37 ++++-- discord/slash_commands.go | 243 ++++++++++++++++++++++++++++-------- go.mod | 6 +- go.sum | 4 +- 6 files changed, 240 insertions(+), 88 deletions(-) diff --git a/common/redis.go b/common/redis.go index 1758874..bedae6c 100644 --- a/common/redis.go +++ b/common/redis.go @@ -39,8 +39,8 @@ func UserSoftbanCountKey(userID string) string { return "automuteus:ratelimit:softban:count:user:" + userID } -func GuildDownloadCooldownKey(guildID string) string { - return "automuteus:ratelimit:download:guild:" + guildID +func GuildDownloadCategoryCooldownKey(guildID, category string) string { + return "automuteus:ratelimit:download:guild:" + guildID + ":category:" + category } func MarkUserRateLimit(client *redis.Client, userID, cmdType string, ttl time.Duration) { @@ -120,15 +120,15 @@ func IsUserRateLimitedSpecific(client *redis.Client, userID string, cmdType stri return v == 1 // =1 means the user is present, and thus rate-limited } -func MarkGuildDownloadCooldown(client *redis.Client, guildID string) { - err := client.Set(context.Background(), GuildDownloadCooldownKey(guildID), "", GuildDownloadCooldown).Err() +func MarkDownloadCategoryCooldown(client *redis.Client, guildID, category string) { + err := client.Set(context.Background(), GuildDownloadCategoryCooldownKey(guildID, category), "", GuildDownloadCooldown).Err() if err != nil { log.Println(err) } } -func GetGuildDownloadCooldown(client *redis.Client, guildID string) (time.Duration, error) { - v, err := client.TTL(context.Background(), GuildDownloadCooldownKey(guildID)).Result() +func GetDownloadCategoryCooldown(client *redis.Client, guildID, category string) (time.Duration, error) { + v, err := client.TTL(context.Background(), GuildDownloadCategoryCooldownKey(guildID, category)).Result() if err == redis.Nil { return 0, nil } else if err != nil { diff --git a/discord/command/commands.go b/discord/command/commands.go index 840e3b9..1fe62be 100644 --- a/discord/command/commands.go +++ b/discord/command/commands.go @@ -20,19 +20,19 @@ const ( // All is all slash commands for the bot, ordered to match the README var All = []*discordgo.ApplicationCommand{ &Help, - //&New, - //&Refresh, - //&Pause, - //&End, - //&Link, - //&Unlink, - //&Settings, - //&Privacy, - //&Info, - //&Map, - //&Stats, - //&Premium, - //&Debug, + &New, + &Refresh, + &Pause, + &End, + &Link, + &Unlink, + &Settings, + &Privacy, + &Info, + &Map, + &Stats, + &Premium, + &Debug, &Download, } diff --git a/discord/command/download.go b/discord/command/download.go index bffcba7..4da02ee 100644 --- a/discord/command/download.go +++ b/discord/command/download.go @@ -8,7 +8,11 @@ import ( ) const ( - Category = "category" + Category = "category" + Users = "users" + UsersGames = "users_games" + Games = "games" + GameEvents = "game_events" ) var Download = discordgo.ApplicationCommand{ @@ -20,11 +24,26 @@ var Download = discordgo.ApplicationCommand{ Description: "Data to download", Type: discordgo.ApplicationCommandOptionString, Choices: []*discordgo.ApplicationCommandOptionChoice{ - &discordgo.ApplicationCommandOptionChoice{ + { Name: Guild, Value: Guild, }, - // TODO add option to download individual user data? + { + Name: Users, + Value: Users, + }, + { + Name: UsersGames, + Value: UsersGames, + }, + { + Name: Games, + Value: Games, + }, + { + Name: GameEvents, + Value: GameEvents, + }, }, Required: true, }, @@ -35,17 +54,21 @@ func GetDownloadParams(options []*discordgo.ApplicationCommandInteractionDataOpt return options[0].StringValue() } -func DownloadGuildOnCooldownResponse(sett *settings.GuildSettings, duration time.Duration) *discordgo.InteractionResponse { +func DownloadCooldownResponse(sett *settings.GuildSettings, category string, duration time.Duration) *discordgo.InteractionResponse { + // report with minute-level precision + durationStr := duration.Truncate(time.Minute).String() return &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ Flags: 1 << 6, Content: sett.LocalizeMessage(&i18n.Message{ - ID: "commands.download.guild.cooldown", - Other: "Sorry, guild stats can only downloaded once every 24 hours!\n\n" + + ID: "commands.download.cooldown", + Other: "Sorry, `{{.Category}}` data can only downloaded once every 24 hours!\n\n" + "Please wait {{.Duration}} and then try again", }, map[string]interface{}{ - "Duration": duration.Truncate(time.Hour).String(), + "Category": category, + // strip the "0s" off the end + "Duration": durationStr[:len(durationStr)-2], }), }, } diff --git a/discord/slash_commands.go b/discord/slash_commands.go index b939eb4..90c8f77 100644 --- a/discord/slash_commands.go +++ b/discord/slash_commands.go @@ -3,6 +3,7 @@ package discord import ( "encoding/json" "fmt" + "github.com/automuteus/utils/pkg/storage" "log" "regexp" "strconv" @@ -37,12 +38,16 @@ var VoicePermissions = []int64{ } const ( - resetUserConfirmedID = "reset-user-confirmed" - resetUserCanceledID = "reset-user-canceled" - resetGuildConfirmedID = "reset-guild-confirmed" - resetGuildCanceledID = "reset-guild-canceled" - downloadGuildConfirmedID = "download-guild-confirmed" - downloadGuildCanceledID = "download-guild-canceled" + resetUserConfirmedID = "reset-user-confirmed" + resetUserCanceledID = "reset-user-canceled" + resetGuildConfirmedID = "reset-guild-confirmed" + resetGuildCanceledID = "reset-guild-canceled" + downloadGuildConfirmedID = "download-guild-confirmed" + downloadUsersConfirmedID = "download-users-confirmed" + downloadUsersGamesConfirmedID = "download-users-games-confirmed" + downloadGamesConfirmedID = "download-games-confirmed" + downloadGameEventsConfirmedID = "download-game-events-confirmed" + downloadCanceledID = "download-canceled" ) func (bot *Bot) handleInteractionCreate(s *discordgo.Session, i *discordgo.InteractionCreate) { @@ -512,6 +517,9 @@ func (bot *Bot) slashCommandHandler(s *discordgo.Session, i *discordgo.Interacti return command.PrivateResponse(ThumbsUp) } case command.Download.Name: + if !isAdmin { + return command.InsufficientPermissionsResponse(sett) + } // don't send the userid because downloading is restricted to Gold members premStatus, days, err := bot.PostgresInterface.GetGuildOrUserPremiumStatus(bot.official, bot.TopGGClient, i.GuildID, "") if err != nil { @@ -527,31 +535,45 @@ func (bot *Bot) slashCommandHandler(s *discordgo.Session, i *discordgo.Interacti if missingPerms > 0 { return command.ReinviteMeResponse(missingPerms, i.ChannelID, sett) } + category := command.GetDownloadParams(i.ApplicationCommandData().Options) + + d, err := redis_common.GetDownloadCategoryCooldown(bot.RedisInterface.client, i.GuildID, category) + if err != nil { + return command.PrivateErrorResponse("/download guild", err, sett) + } + if d > 0 { + return command.DownloadCooldownResponse(sett, category, d) + } + var content string + var components []discordgo.MessageComponent + content = sett.LocalizeMessage(&i18n.Message{ + ID: "commands.download.guild.confirmation", + Other: "⚠️**Are you sure?**⚠️\nIf you download the `{{.Category}}` data now, it will not be downloadable again for 24 hours!", + }, map[string]interface{}{ + "Category": category, + }) + var downloadConfirmedID string switch category { case command.Guild: - d, err := redis_common.GetGuildDownloadCooldown(bot.RedisInterface.client, i.GuildID) - if err != nil { - return command.PrivateErrorResponse("/download guild", err, sett) - } - if d > 0 { - return command.DownloadGuildOnCooldownResponse(sett, d) - } - var content string - var components []discordgo.MessageComponent - content = sett.LocalizeMessage(&i18n.Message{ - ID: "commands.download.guild.confirmation", - Other: "⚠️**Are you sure?**⚠️\nIf you download the guild's stats now, they will not be downloadable again for 24 hours", - }) - components = confirmationComponents(downloadGuildConfirmedID, downloadGuildCanceledID, sett) - return &discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseChannelMessageWithSource, - Data: &discordgo.InteractionResponseData{ - Flags: 1 << 6, //private message - Content: content, - Components: components, - }, - } + downloadConfirmedID = downloadGuildConfirmedID + case command.Users: + downloadConfirmedID = downloadUsersConfirmedID + case command.UsersGames: + downloadConfirmedID = downloadUsersGamesConfirmedID + case command.Games: + downloadConfirmedID = downloadGamesConfirmedID + case command.GameEvents: + downloadConfirmedID = downloadGameEventsConfirmedID + } + components = confirmationComponents(downloadConfirmedID, downloadCanceledID, sett) + return &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Flags: 1 << 6, //private message + Content: content, + Components: components, + }, } } @@ -562,6 +584,11 @@ func (bot *Bot) slashCommandHandler(s *discordgo.Session, i *discordgo.Interacti } redis_common.MarkUserRateLimit(bot.RedisInterface.client, i.Member.User.ID, i.MessageComponentData().CustomID, redis_common.GlobalUserRateLimitDuration) + gid, err := strconv.ParseUint(i.GuildID, 10, 64) + if err != nil { + log.Println(err) + // TODO report this properly + } switch i.MessageComponentData().CustomID { case colorSelectID: if len(i.MessageComponentData().Values) > 0 { @@ -662,46 +689,137 @@ func (bot *Bot) slashCommandHandler(s *discordgo.Session, i *discordgo.Interacti }, } case downloadGuildConfirmedID: - // TODO fetch the basic information about the guild (to populate the guild table) - - gid, err := strconv.ParseUint(i.GuildID, 10, 64) + guild, err := bot.PostgresInterface.GetGuildForDownload(gid) if err != nil { - log.Println(err) - // TODO report this properly + log.Println("Error downloading guild data:", err) + return downloadErrorResponse(sett, err) } else { - games, err := bot.PostgresInterface.GetGamesForGuild(gid) - if err != nil { - log.Println(err) - } - log.Println("Games played", len(games)) - events, err := bot.PostgresInterface.GetGamesEventsForGuild(gid) - if err != nil { - log.Println(err) - } - log.Println("Events", events) - // TODO get the relevant users as well? To view opt info? - - // TODO prepend the relevant headers to each csv - - // redis_common.MarkGuildDownloadCooldown(bot.RedisInterface.client, i.GuildID) - + redis_common.MarkDownloadCategoryCooldown(bot.RedisInterface.client, i.GuildID, command.Guild) return &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseUpdateMessage, Data: &discordgo.InteractionResponseData{ - Flags: 1 << 6, //private message - Content: "Here are your files!", + Flags: 1 << 6, //private message + Content: sett.LocalizeMessage(&i18n.Message{ + ID: "commands.download.file.success", + Other: "Here's that file for you!", + }), Components: []discordgo.MessageComponent{}, Files: []*discordgo.File{ - &discordgo.File{ - Name: "test.csv", + { + Name: "guilds.csv", ContentType: "text/csv", - Reader: strings.NewReader("testfile"), + Reader: strings.NewReader(guild.ToCSV()), }, }, }, } } - case downloadGuildCanceledID: + + case downloadUsersConfirmedID: + users, err := bot.PostgresInterface.GetUsersForGuild(gid) + if err != nil { + log.Println("Error downloading users data:", err) + return downloadErrorResponse(sett, err) + } else { + redis_common.MarkDownloadCategoryCooldown(bot.RedisInterface.client, i.GuildID, command.Users) + return &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseUpdateMessage, + Data: &discordgo.InteractionResponseData{ + Flags: 1 << 6, //private message + Content: sett.LocalizeMessage(&i18n.Message{ + ID: "commands.download.file.success", + Other: "Here's that file for you!", + }), + Components: []discordgo.MessageComponent{}, + Files: []*discordgo.File{ + { + Name: "users.csv", + ContentType: "text/csv", + Reader: strings.NewReader(storage.UsersToCSV(users)), + }, + }, + }, + } + } + case downloadUsersGamesConfirmedID: + usersGames, err := bot.PostgresInterface.GetUsersGamesForGuild(gid) + if err != nil { + log.Println("Error downloading users_games data:", err) + return downloadErrorResponse(sett, err) + } else { + redis_common.MarkDownloadCategoryCooldown(bot.RedisInterface.client, i.GuildID, command.UsersGames) + return &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseUpdateMessage, + Data: &discordgo.InteractionResponseData{ + Flags: 1 << 6, //private message + Content: sett.LocalizeMessage(&i18n.Message{ + ID: "commands.download.file.success", + Other: "Here's that file for you!", + }), + Components: []discordgo.MessageComponent{}, + Files: []*discordgo.File{ + { + Name: "users_games.csv", + ContentType: "text/csv", + Reader: strings.NewReader(storage.UsersGamesToCSV(usersGames)), + }, + }, + }, + } + } + case downloadGamesConfirmedID: + games, err := bot.PostgresInterface.GetGamesForGuild(gid) + if err != nil { + log.Println("Error downloading game data:", err) + return downloadErrorResponse(sett, err) + } else { + redis_common.MarkDownloadCategoryCooldown(bot.RedisInterface.client, i.GuildID, command.Games) + return &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseUpdateMessage, + Data: &discordgo.InteractionResponseData{ + Flags: 1 << 6, //private message + Content: sett.LocalizeMessage(&i18n.Message{ + ID: "commands.download.file.success", + Other: "Here's that file for you!", + }), + Components: []discordgo.MessageComponent{}, + Files: []*discordgo.File{ + { + Name: "games.csv", + ContentType: "text/csv", + Reader: strings.NewReader(storage.GamesToCSV(games)), + }, + }, + }, + } + } + case downloadGameEventsConfirmedID: + events, err := bot.PostgresInterface.GetGamesEventsForGuild(gid) + if err != nil { + log.Println("Error downloading game events data:", err) + return downloadErrorResponse(sett, err) + } else { + redis_common.MarkDownloadCategoryCooldown(bot.RedisInterface.client, i.GuildID, command.GameEvents) + return &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseUpdateMessage, + Data: &discordgo.InteractionResponseData{ + Flags: 1 << 6, //private message + Content: sett.LocalizeMessage(&i18n.Message{ + ID: "commands.download.file.success", + Other: "Here's that file for you!", + }), + Components: []discordgo.MessageComponent{}, + Files: []*discordgo.File{ + { + Name: "events.csv", + ContentType: "text/csv", + Reader: strings.NewReader(storage.EventsToCSV(events)), + }, + }, + }, + } + } + case downloadCanceledID: fallthrough case resetUserCanceledID: fallthrough @@ -717,6 +835,21 @@ func (bot *Bot) slashCommandHandler(s *discordgo.Session, i *discordgo.Interacti return nil } +func downloadErrorResponse(sett *settings.GuildSettings, err error) *discordgo.InteractionResponse { + return &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseUpdateMessage, + Data: &discordgo.InteractionResponseData{ + Flags: 1 << 6, //private message + Content: sett.LocalizeMessage(&i18n.Message{ + ID: "commands.download.guild.error", + Other: "I encountered an error fetching your stats for download: {{.Error}}", + }, map[string]interface{}{ + "Error": err.Error(), + }), + }, + } +} + func (bot *Bot) linkOrUnlinkAndRespond(dgs *GameState, userID, testValue string, sett *settings.GuildSettings) (*discordgo.InteractionResponse, bool) { if testValue != "" { // don't care if it's successful, just always unlink before linking diff --git a/go.mod b/go.mod index 4164362..f88c730 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/automuteus/automuteus go 1.18 require ( - github.com/automuteus/utils v0.3.2 + github.com/automuteus/utils v0.4.1 github.com/bsm/redislock v0.7.1 github.com/bwmarrin/discordgo v0.24.0 github.com/go-redis/redis/v8 v8.8.0 @@ -50,7 +50,3 @@ require ( gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect ) - -replace ( - github.com/automuteus/utils v0.3.2 => ../utils -) diff --git a/go.sum b/go.sum index a34e388..2479655 100644 --- a/go.sum +++ b/go.sum @@ -23,8 +23,8 @@ github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hC github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= -github.com/automuteus/utils v0.3.2 h1:+r598HkMEYXmz9RjDSRamVgObxVJE9EayhSf7XdBulc= -github.com/automuteus/utils v0.3.2/go.mod h1:A8zWmP3FEG1Aanz/9msstVMswIfkx1nxY59gNfRztXw= +github.com/automuteus/utils v0.4.1 h1:wrEqLaQa0xZ8Ka4J6eXGf0OIx84DgwD3XF4AkSnP6lo= +github.com/automuteus/utils v0.4.1/go.mod h1:A8zWmP3FEG1Aanz/9msstVMswIfkx1nxY59gNfRztXw= github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=