Add functionality to download each table separately, bump utils, adjust redis keys

This commit is contained in:
denverquane
2022-12-29 15:15:02 -05:00
parent cc368d3027
commit b2ae30b538
6 changed files with 240 additions and 88 deletions

View File

@ -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 {

View File

@ -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,
}

View File

@ -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],
}),
},
}

View File

@ -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

6
go.mod
View File

@ -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
)

4
go.sum
View File

@ -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=