diff options
| author | William Hergès <william@herges.fr> | 2025-09-04 16:30:54 +0200 |
|---|---|---|
| committer | William Hergès <william@herges.fr> | 2025-09-04 16:30:54 +0200 |
| commit | fa9b7767ab81471d15c29d77a85968e505b3b433 (patch) | |
| tree | 481f2ac867a7709e263802e842b9e12835f6ac0c /commands | |
| parent | 7508627d86a4f2ef9b3caebd88d92fe8be854816 (diff) | |
| parent | 30ecd60b041398390f11fccdf46444fa28690bd8 (diff) | |
Merge branch 'main' into feat/xp-boost
Diffstat (limited to 'commands')
| -rw-r--r-- | commands/config.go | 397 | ||||
| -rw-r--r-- | commands/credits.go | 39 | ||||
| -rw-r--r-- | commands/rank.go | 24 | ||||
| -rw-r--r-- | commands/reset.go | 23 | ||||
| -rw-r--r-- | commands/stats.go | 224 | ||||
| -rw-r--r-- | commands/top.go | 24 |
6 files changed, 352 insertions, 379 deletions
diff --git a/commands/config.go b/commands/config.go index 48d7ab8..c4644bc 100644 --- a/commands/config.go +++ b/commands/config.go @@ -2,15 +2,21 @@ package commands import ( "fmt" - "github.com/anhgelus/gokord" - "github.com/anhgelus/gokord/utils" - "github.com/anhgelus/les-copaings-bot/config" - "github.com/anhgelus/les-copaings-bot/exp" - "github.com/bwmarrin/discordgo" "strings" + + "git.anhgelus.world/anhgelus/les-copaings-bot/config" + "git.anhgelus.world/anhgelus/les-copaings-bot/exp" + "github.com/anhgelus/gokord/cmd" + "github.com/anhgelus/gokord/component" + "github.com/anhgelus/gokord/logger" + discordgo "github.com/nyttikord/gokord" +) + +const ( + ConfigModify = "config_modify" ) -func ConfigShow(s *discordgo.Session, i *discordgo.InteractionCreate, optMap utils.OptionMap, resp *utils.ResponseBuilder) { +func Config(_ *discordgo.Session, i *discordgo.InteractionCreate, _ cmd.OptionMap, resp *cmd.ResponseBuilder) { cfg := config.GetGuildConfig(i.GuildID) roles := "" l := len(cfg.XpRoles) - 1 @@ -43,318 +49,73 @@ func ConfigShow(s *discordgo.Session, i *discordgo.InteractionCreate, optMap uti } else { defaultChan = fmt.Sprintf("<#%s>", cfg.FallbackChannel) } - err := resp.AddEmbed(&discordgo.MessageEmbed{ - Type: discordgo.EmbedTypeRich, - Title: "Config", - Color: utils.Success, - Fields: []*discordgo.MessageEmbedField{ - { - Name: "Salon par défaut", - Value: defaultChan, - Inline: false, - }, - { - Name: "Rôles liés aux niveaux", - Value: roles, - Inline: false, - }, - { - Name: "Salons désactivés", - Value: chans, - Inline: false, - }, - { - Name: "Jours avant la réduction", - Value: fmt.Sprintf("%d", cfg.DaysXPRemains), - Inline: false, - }, - }, - }).Send() - if err != nil { - utils.SendAlert("config/guild.go - Sending config", err.Error()) - } -} - -func ConfigXP(s *discordgo.Session, i *discordgo.InteractionCreate, optMap utils.OptionMap, resp *utils.ResponseBuilder) { - resp.IsEphemeral() - // verify every args - t, ok := optMap["type"] - if !ok { - err := resp.SetMessage("Le type d'action n'a pas été renseigné.").Send() - if err != nil { - utils.SendAlert("commands/config.go - Action type not set", err.Error()) - } - return - } - ts := t.StringValue() - lvl, ok := optMap["level"] - if !ok { - err := resp.SetMessage("Le niveau n'a pas été renseigné.").Send() - if err != nil { - utils.SendAlert("commands/config.go - Level not set", err.Error()) - } - return - } - level := lvl.IntValue() - if level < 1 { - err := resp.SetMessage("Le niveau doit forcément être supérieur à 0.").Send() - if err != nil { - utils.SendAlert("commands/config.go - Invalid level", err.Error()) - } - return - } - xp := exp.LevelXP(uint(level)) - r, ok := optMap["role"] - if !ok { - err := resp.SetMessage("Le rôle n'a pas été renseigné.").Send() - if err != nil { - utils.SendAlert("commands/config.go - Role not set", err.Error()) - } - return - } - role := r.RoleValue(s, i.GuildID) - cfg := config.GetGuildConfig(i.GuildID) - - // add or delete or edit - var err error - switch ts { - case "add": - for _, r := range cfg.XpRoles { - if r.RoleID == role.ID { - err = resp.SetMessage("Le rôle est déjà présent dans la config").Send() - if err != nil { - utils.SendAlert("commands/config.go - Role already in config", err.Error()) - } - return - } - } - cfg.XpRoles = append(cfg.XpRoles, config.XpRole{ - XP: xp, - RoleID: role.ID, - }) - err = cfg.Save() - if err != nil { - utils.SendAlert( - "commands/config.go - Saving config", - err.Error(), - "guild_id", - i.GuildID, - "role_id", - role.ID, - "type", - "add", - ) - } - case "del": - _, r := cfg.FindXpRole(role.ID) - if r == nil { - err = resp.SetMessage("Le rôle n'a pas été trouvé dans la config.").Send() - if err != nil { - utils.SendAlert("commands/config.go - Role not found (del)", err.Error()) - } - return - } - err = gokord.DB.Delete(r).Error - if err != nil { - utils.SendAlert( - "commands/config.go - Deleting entry", - err.Error(), - "guild_id", - i.GuildID, - "role_id", - role.ID, - "type", - "del", - ) - } - case "edit": - _, r := cfg.FindXpRole(role.ID) - if r == nil { - err = resp.SetMessage("Le rôle n'a pas été trouvé dans la config.").Send() - if err != nil { - utils.SendAlert("commands/config.go - Role not found (edit)", err.Error()) - } - return - } - r.XP = xp - err = gokord.DB.Save(r).Error - if err != nil { - utils.SendAlert( - "commands/config.go - Saving config", - err.Error(), - "guild_id", - i.GuildID, - "role_id", - role.ID, - "type", - "edit", - ) - } - default: - err = resp.SetMessage("Le type d'action n'est pas valide.").Send() - if err != nil { - utils.SendAlert("commands/config.go - Invalid action type", err.Error()) - } - return - } - if err != nil { - err = resp.SetMessage("Il y a eu une erreur lors de la modification de de la base de données.").Send() - } else { - err = resp.SetMessage("La configuration a bien été mise à jour.").Send() - } - if err != nil { - utils.SendAlert("commands/config.go - Config updated message", err.Error()) - } -} - -func ConfigChannel(s *discordgo.Session, i *discordgo.InteractionCreate, optMap utils.OptionMap, resp *utils.ResponseBuilder) { - resp.IsEphemeral() - // verify every args - t, ok := optMap["type"] - if !ok { - err := resp.SetMessage("Le type d'action n'a pas été renseigné.").Send() - if err != nil { - utils.SendAlert("commands/config.go - Action type not set", err.Error()) - } - return - } - ts := t.StringValue() - salon, ok := optMap["channel"] - if !ok { - err := resp.SetMessage("Le salon n'a pas été renseigné.").Send() - if err != nil { - utils.SendAlert("commands/config.go - Channel not set (disabled)", err.Error()) - } - return - } - channel := salon.ChannelValue(s) - cfg := config.GetGuildConfig(i.GuildID) - switch ts { - case "add": - if strings.Contains(cfg.DisabledChannels, channel.ID) { - err := resp.SetMessage("Le salon est déjà dans la liste des salons désactivés").Send() - if err != nil { - utils.SendAlert("commands/config.go - Channel already disabled", err.Error()) - } - return - } - cfg.DisabledChannels += channel.ID + ";" - case "del": - if !strings.Contains(cfg.DisabledChannels, channel.ID) { - err := resp.SetMessage("Le salon n'est pas désactivé").Send() - if err != nil { - utils.SendAlert("commands/config.go - Channel not disabled", err.Error()) - } - return - } - cfg.DisabledChannels = strings.ReplaceAll(cfg.DisabledChannels, channel.ID+";", "") - default: - err := resp.SetMessage("Le type d'action n'est pas valide.").Send() - if err != nil { - utils.SendAlert("commands/config.go - Invalid action type", err.Error()) - } - return - } - // save - err := cfg.Save() - if err != nil { - utils.SendAlert( - "commands/config.go - Saving config", - err.Error(), - "guild_id", - i.GuildID, - "type", - ts, - "channel_id", - channel.ID, - ) - err = resp.SetMessage("Il y a eu une erreur lors de la modification de de la base de données.").Send() - } else { - err = resp.SetMessage("Modification sauvegardé.").Send() - } - if err != nil { - utils.SendAlert("commands/config.go - Modification saved message", err.Error()) - } -} - -func ConfigFallbackChannel(s *discordgo.Session, i *discordgo.InteractionCreate, optMap utils.OptionMap, resp *utils.ResponseBuilder) { - resp.IsEphemeral() - // verify every args - salon, ok := optMap["channel"] - if !ok { - err := resp.SetMessage("Le salon n'a pas été renseigné.").Send() - if err != nil { - utils.SendAlert("commands/config.go - Channel not set (fallback)", err.Error()) - } - return - } - channel := salon.ChannelValue(s) - if channel.Type != discordgo.ChannelTypeGuildText { - err := resp.SetMessage("Le salon n'est pas un salon textuel.").Send() - if err != nil { - utils.SendAlert("commands/config.go - Invalid channel type", err.Error()) - } - return - } - cfg := config.GetGuildConfig(i.GuildID) - cfg.FallbackChannel = channel.ID - // save - err := cfg.Save() - if err != nil { - utils.SendAlert( - "commands/config.go - Saving config", - err.Error(), - "guild_id", - i.GuildID, - "channel_id", - channel.ID, - ) - err = resp.SetMessage("Il y a eu une erreur lors de la modification de de la base de données.").Send() - } else { - err = resp.SetMessage("Salon enregistré.").Send() - } - if err != nil { - utils.SendAlert("commands/config.go - Channel saved message", err.Error()) - } -} - -func ConfigPeriodBeforeReduce(s *discordgo.Session, i *discordgo.InteractionCreate, optMap utils.OptionMap, resp *utils.ResponseBuilder) { - resp.IsEphemeral() - // verify every args - days, ok := optMap["days"] - if !ok { - err := resp.SetMessage("Le nombre de jours n'a pas été renseigné.").Send() - if err != nil { - utils.SendAlert("commands/config.go - Days not set (fallback)", err.Error()) - } - return - } - d := days.IntValue() - if d < 30 { - err := resp.SetMessage("Le nombre de jours est inférieur à 30.").Send() - if err != nil { - utils.SendAlert("commands/config.go - Days < 30 (fallback)", err.Error()) - } - return - } - // save - cfg := config.GetGuildConfig(i.GuildID) - cfg.DaysXPRemains = uint(d) - err := cfg.Save() - if err != nil { - utils.SendAlert( - "commands/config.go - Saving config", - err.Error(), - "guild_id", - i.GuildID, - "days", - d, - ) - err = resp.SetMessage("Il y a eu une erreur lors de la modification de de la base de données.").Send() - } else { - err = resp.SetMessage("Nombre de jours enregistré.").Send() - } + //comp := component.New(). + // Add(component.NewTextDisplay("# Config")). + // Add(component.NewTextDisplay("**Salon par défaut**\n" + defaultChan)). + // Add(component.NewSeparator()). + // Add(component.NewTextDisplay("**Rôles liés aux niveaux**\n" + roles)). + // Add(component.NewSeparator()). + // Add(component.NewTextDisplay("**Salons désactivés**\n" + chans)). + // Add(component.NewSeparator()). + // Add(component.NewTextDisplay(fmt.Sprintf("**%s**\n%d", "Jours avant la réduction", cfg.DaysXPRemains))). + // Add(component.NewActionRow().Add(component.NewStringSelect(ConfigModify). + // SetPlaceholder("Modifier..."). + // AddOption( + // component.NewSelectOption("Rôles liés à l'XP", config.ModifyXpRole). + // SetDescription("Gère les rôles liés à l'XP"). + // SetEmoji(&discordgo.ComponentEmoji{Name: "🏅"}), + // ). + // AddOption( + // component.NewSelectOption("Salons désactivés", config.ModifyDisChannel). + // SetDescription("Gère les salons désactivés"). + // SetEmoji(&discordgo.ComponentEmoji{Name: "❌"}), + // ). + // AddOption( + // // I don't have a better idea for this... + // component.NewSelectOption("Salons par défaut", config.ModifyFallbackChannel). + // SetDescription("Spécifie le salon par défaut"). + // SetEmoji(&discordgo.ComponentEmoji{Name: "💾"}), + // ). + // AddOption( + // component.NewSelectOption("Temps avec la réduction", config.ModifyTimeReduce). + // SetDescription("Gère le temps avant la réduction d'XP"). + // SetEmoji(&discordgo.ComponentEmoji{Name: "⌛"}), + // ), + // )) + comp := component.New(). + Add(component.NewActionRow().Add(component.NewStringSelect(ConfigModify). + SetPlaceholder("Modifier..."). + AddOption( + component.NewSelectOption("Rôles liés à l'XP", config.ModifyXpRole). + SetDescription("Gère les rôles liés à l'XP"). + SetEmoji(&discordgo.ComponentEmoji{Name: "🏅"}), + ). + AddOption( + component.NewSelectOption("Salons désactivés", config.ModifyDisChannel). + SetDescription("Gère les salons désactivés"). + SetEmoji(&discordgo.ComponentEmoji{Name: "❌"}), + ). + AddOption( + // I don't have a better idea for this... + component.NewSelectOption("Salons par défaut", config.ModifyFallbackChannel). + SetDescription("Spécifie le salon par défaut"). + SetEmoji(&discordgo.ComponentEmoji{Name: "💾"}), + ). + AddOption( + component.NewSelectOption("Temps avec la réduction", config.ModifyTimeReduce). + SetDescription("Gère le temps avant la réduction d'XP"). + SetEmoji(&discordgo.ComponentEmoji{Name: "⌛"}), + ), + )) + msg := fmt.Sprintf( + "# Config\n**Salon par défaut**\n%s\n\n**Rôles liés aux niveaux**\n%s\n\n**Salons désactivés**\n%s\n\n**Jours avant la réduction**\n%d", + defaultChan, + roles, + chans, + cfg.DaysXPRemains, + ) + err := resp.SetComponents(comp).SetMessage(msg).IsEphemeral().Send() if err != nil { - utils.SendAlert("commands/config.go - Days saved message", err.Error()) + logger.Alert("config/guild.go - Sending config", err.Error()) } } diff --git a/commands/credits.go b/commands/credits.go index 0943761..f0a8c46 100644 --- a/commands/credits.go +++ b/commands/credits.go @@ -1,36 +1,19 @@ package commands import ( - "github.com/anhgelus/gokord/utils" - "github.com/bwmarrin/discordgo" + "github.com/anhgelus/gokord" + "github.com/anhgelus/gokord/cmd" + "github.com/anhgelus/gokord/logger" + discordgo "github.com/nyttikord/gokord" ) -func Credits(s *discordgo.Session, i *discordgo.InteractionCreate, optMap utils.OptionMap, resp *utils.ResponseBuilder) { - err := resp.AddEmbed(&discordgo.MessageEmbed{ - - Type: discordgo.EmbedTypeRich, - Title: "Crédits", - Description: "Auteur du bot : @anhgelus (https://github.com/anhgelus)\nLangage : Go 1.24\nLicence : AGPLv3", - Color: utils.Success, - Fields: []*discordgo.MessageEmbedField{ - { - Name: "anhgelus/gokord", - Value: "v0.10.0 - MPL 2.0", - Inline: true, - }, - { - Name: "bwmarrin/discordgo", - Value: "v0.29.0 - BSD-3-Clause", - Inline: true, - }, - { - Name: "gorm", - Value: "v1.30.0 - MIT", - Inline: true, - }, - }, - }).Send() +func Credits(_ *discordgo.Session, i *discordgo.InteractionCreate, _ cmd.OptionMap, resp *cmd.ResponseBuilder) { + msg := "**Les Copaings**, le bot gérant les serveurs privés de [anhgelus](<https://anhgelus.world/>).\n" + msg += "Code source : <https://git.anhgelus.world/anhgelus/les-copaings-bot>\n\n" + msg += "Host du bot : " + gokord.BaseCfg.GetAuthor() + ".\n\n" + msg += "Utilise :\n- [anhgelus/gokord](<https://github.com/anhgelus/gokord>)" + err := resp.SetMessage(msg).Send() if err != nil { - utils.SendAlert("commands/credits.go - Sending credits", err.Error(), "guild_id", i.GuildID) + logger.Alert("commands/credits.go - Sending credits", err.Error(), "guild_id", i.GuildID) } } diff --git a/commands/rank.go b/commands/rank.go index dd5859a..94dabf2 100644 --- a/commands/rank.go +++ b/commands/rank.go @@ -2,13 +2,15 @@ package commands import ( "fmt" - "github.com/anhgelus/gokord/utils" - "github.com/anhgelus/les-copaings-bot/exp" - "github.com/anhgelus/les-copaings-bot/user" - "github.com/bwmarrin/discordgo" + + "git.anhgelus.world/anhgelus/les-copaings-bot/exp" + "git.anhgelus.world/anhgelus/les-copaings-bot/user" + "github.com/anhgelus/gokord/cmd" + "github.com/anhgelus/gokord/logger" + discordgo "github.com/nyttikord/gokord" ) -func Rank(s *discordgo.Session, i *discordgo.InteractionCreate, optMap utils.OptionMap, resp *utils.ResponseBuilder) { +func Rank(s *discordgo.Session, i *discordgo.InteractionCreate, optMap cmd.OptionMap, resp *cmd.ResponseBuilder) { c := user.GetCopaing(i.Member.User.ID, i.GuildID) // current user = member who used /rank msg := "Votre niveau" m := i.Member @@ -18,12 +20,12 @@ func Rank(s *discordgo.Session, i *discordgo.InteractionCreate, optMap utils.Opt if u.Bot { err = resp.SetMessage("Imagine si les bots avaient un niveau :rolling_eyes:").IsEphemeral().Send() if err != nil { - utils.SendAlert("commands/rank.go - Reply error user is a bot", err.Error()) + logger.Alert("commands/rank.go - Reply error user is a bot", err.Error()) } } m, err = s.GuildMember(i.GuildID, u.ID) if err != nil { - utils.SendAlert( + logger.Alert( "commands/rank.go - Fetching guild member", err.Error(), "discord_id", @@ -33,7 +35,7 @@ func Rank(s *discordgo.Session, i *discordgo.InteractionCreate, optMap utils.Opt ) err = resp.SetMessage("Erreur : impossible de récupérer le membre").IsEphemeral().Send() if err != nil { - utils.SendAlert("commands/rank.go - Reply error fetching guild member", err.Error()) + logger.Alert("commands/rank.go - Reply error fetching guild member", err.Error()) } return } @@ -42,7 +44,7 @@ func Rank(s *discordgo.Session, i *discordgo.InteractionCreate, optMap utils.Opt } xp, err := c.GetXP() if err != nil { - utils.SendAlert( + logger.Alert( "commands/rank.go - Fetching xp", err.Error(), "discord_id", @@ -52,7 +54,7 @@ func Rank(s *discordgo.Session, i *discordgo.InteractionCreate, optMap utils.Opt ) err = resp.SetMessage("Erreur : impossible de récupérer l'XP").IsEphemeral().Send() if err != nil { - utils.SendAlert("commands/rank.go - Reply error fetching xp", err.Error()) + logger.Alert("commands/rank.go - Reply error fetching xp", err.Error()) } return } @@ -66,6 +68,6 @@ func Rank(s *discordgo.Session, i *discordgo.InteractionCreate, optMap utils.Opt nxtLvlXP-xp, )).Send() if err != nil { - utils.SendAlert("commands/rank.go - Sending rank", err.Error()) + logger.Alert("commands/rank.go - Sending rank", err.Error()) } } diff --git a/commands/reset.go b/commands/reset.go index c4275a0..07e3ba3 100644 --- a/commands/reset.go +++ b/commands/reset.go @@ -1,45 +1,46 @@ package commands import ( + "git.anhgelus.world/anhgelus/les-copaings-bot/user" "github.com/anhgelus/gokord" - "github.com/anhgelus/gokord/utils" - "github.com/anhgelus/les-copaings-bot/user" - "github.com/bwmarrin/discordgo" + "github.com/anhgelus/gokord/cmd" + "github.com/anhgelus/gokord/logger" + discordgo "github.com/nyttikord/gokord" ) -func Reset(s *discordgo.Session, i *discordgo.InteractionCreate, optMap utils.OptionMap, resp *utils.ResponseBuilder) { +func Reset(_ *discordgo.Session, i *discordgo.InteractionCreate, _ cmd.OptionMap, resp *cmd.ResponseBuilder) { var copaings []*user.Copaing gokord.DB.Where("guild_id = ?", i.GuildID).Delete(&copaings) if err := resp.IsEphemeral().SetMessage("L'XP a été reset.").Send(); err != nil { - utils.SendAlert("commands/reset.go - Sending success (all)", err.Error()) + logger.Alert("commands/reset.go - Sending success (all)", err.Error()) } } -func ResetUser(s *discordgo.Session, i *discordgo.InteractionCreate, optMap utils.OptionMap, resp *utils.ResponseBuilder) { +func ResetUser(s *discordgo.Session, i *discordgo.InteractionCreate, optMap cmd.OptionMap, resp *cmd.ResponseBuilder) { resp.IsEphemeral() v, ok := optMap["user"] if !ok { if err := resp.SetMessage("Le user n'a pas été renseigné.").Send(); err != nil { - utils.SendAlert("commands/reset.go - Copaing not set", err.Error()) + logger.Alert("commands/reset.go - Copaing not set", err.Error()) } return } m := v.UserValue(s) if m.Bot { if err := resp.SetMessage("Les bots n'ont pas de niveau :upside_down:").Send(); err != nil { - utils.SendAlert("commands/reset.go - Copaing not set", err.Error()) + logger.Alert("commands/reset.go - Copaing not set", err.Error()) } return } err := user.GetCopaing(m.ID, i.GuildID).Delete() if err != nil { - utils.SendAlert("commands/reset.go - Copaing not deleted", err.Error(), "discord_id", m.ID, "guild_id", i.GuildID) + logger.Alert("commands/reset.go - Copaing not deleted", err.Error(), "discord_id", m.ID, "guild_id", i.GuildID) err = resp.SetMessage("Erreur : impossible de reset l'utilisateur").Send() if err != nil { - utils.SendAlert("commands/reset.go - Error deleting", err.Error()) + logger.Alert("commands/reset.go - Error deleting", err.Error()) } } if err = resp.SetMessage("Le user bien été reset.").Send(); err != nil { - utils.SendAlert("commands/reset.go - Sending success (user)", err.Error()) + logger.Alert("commands/reset.go - Sending success (user)", err.Error()) } } diff --git a/commands/stats.go b/commands/stats.go new file mode 100644 index 0000000..f93f6a0 --- /dev/null +++ b/commands/stats.go @@ -0,0 +1,224 @@ +package commands + +import ( + "bytes" + "errors" + "gorm.io/gorm" + "image/color" + "io" + "math" + "math/rand/v2" + "slices" + "time" + + "git.anhgelus.world/anhgelus/les-copaings-bot/config" + "git.anhgelus.world/anhgelus/les-copaings-bot/exp" + "git.anhgelus.world/anhgelus/les-copaings-bot/user" + "github.com/anhgelus/gokord" + "github.com/anhgelus/gokord/cmd" + "github.com/anhgelus/gokord/logger" + "github.com/jackc/pgx/v5/pgtype" + discordgo "github.com/nyttikord/gokord" + "gonum.org/v1/plot" + "gonum.org/v1/plot/plotter" + "gonum.org/v1/plot/vg" +) + +type data struct { + CreatedAt time.Time + XP int + CopaingID int +} + +type dbData struct { + CreatedAt *pgtype.Date + XP int + CopaingID int +} + +func Stats(s *discordgo.Session, i *discordgo.InteractionCreate, opt cmd.OptionMap, resp *cmd.ResponseBuilder) { + cfg := config.GetGuildConfig(i.GuildID) + days := cfg.DaysXPRemains + if v, ok := opt["days"]; ok { + in := v.IntValue() + if in < 0 || uint(in) > days { + if err := resp.SetMessage("Nombre de jours invalide").IsEphemeral().Send(); err != nil { + logger.Alert("commands/stats.go - Sending invalid days", err.Error()) + } + return + } + days = uint(in) + } + err := resp.IsDeferred().Send() + if err != nil { + logger.Alert("commands/stats.go - Sending deferred", err.Error()) + return + } + go func() { + var w io.WriterTo + if v, ok := opt["user"]; ok { + w, err = statsMember(s, i, days, v.UserValue(s).ID) + } else { + w, err = statsAll(s, i, days) + } + if err != nil { + if err = resp.IsEphemeral().SetMessage("Il y a eu une erreur...").Send(); err != nil { + logger.Alert("commands/stats.go - Sending error occurred", err.Error()) + } + return + } + b := new(bytes.Buffer) + _, err = w.WriteTo(b) + if err != nil { + logger.Alert("commands/stats.go - Writing png", err.Error()) + } + err = resp.AddFile(&discordgo.File{ + Name: "plot.png", + ContentType: "image/png", + Reader: b, + }).Send() + if err != nil { + logger.Alert("commands/stats.go - Sending response", err.Error()) + } + }() +} + +func statsAll(s *discordgo.Session, i *discordgo.InteractionCreate, days uint) (io.WriterTo, error) { + return stats(s, i, days, func(before, after string) *gorm.DB { + return gokord.DB.Raw(before+"WHERE guild_id = ? and created_at > ?"+after, i.GuildID, exp.TimeStampNDaysBefore(days)) + }) +} + +func statsMember(s *discordgo.Session, i *discordgo.InteractionCreate, days uint, discordID string) (io.WriterTo, error) { + _, err := s.GuildMember(i.GuildID, discordID) + if err != nil { + return nil, err + } + return stats(s, i, days, func(before, after string) *gorm.DB { + return gokord.DB.Raw( + before+"WHERE guild_id = ? and created_at > ? and copaing_id = ?"+after, + i.GuildID, exp.TimeStampNDaysBefore(days), user.GetCopaing(discordID, i.GuildID).ID, + ) + }) +} + +func stats(s *discordgo.Session, i *discordgo.InteractionCreate, days uint, execSql func(before, after string) *gorm.DB) (io.WriterTo, error) { + var rawData []*data + if gokord.Debug { + var rawCopaingData []*user.CopaingXP + if err := execSql("SELECT * FROM copaing_xps ", "").Scan(&rawCopaingData).Error; err != nil { + logger.Alert("commands/stats.go - Fetching result", err.Error()) + return nil, err + } + rawData = make([]*data, len(rawCopaingData)) + for in, d := range rawCopaingData { + rawData[in] = &data{ + CreatedAt: d.CreatedAt, + XP: int(d.XP), + CopaingID: int(d.CopaingID), + } + } + } else { + var rawDbData []dbData + if err := execSql( + `SELECT "created_at"::date::text, sum("xp") as xp, "copaing_id" FROM copaing_xps `, ` GROUP BY "created_at"::date, "copaing_id"`, + ).Scan(&rawDbData).Error; err != nil { + logger.Alert("commands/stats.go - Fetching result", err.Error()) + return nil, err + } + rawData = make([]*data, len(rawDbData)) + for in, d := range rawDbData { + rawData[in] = &data{ + CreatedAt: d.CreatedAt.Time, + XP: d.XP, + CopaingID: d.CopaingID, + } + } + } + + copaings := map[int]*user.Copaing{} + stats := map[int][]plotter.XY{} + + for _, raw := range rawData { + _, ok := copaings[raw.CopaingID] + if !ok { + var cp user.Copaing + if err := gokord.DB.First(&cp, raw.CopaingID).Error; err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + logger.Alert("commands/stats.go - Finding copaing", err.Error(), "id", raw.CopaingID) + return nil, err + } + logger.Warn("Copaing not found, skipping entry", "old_id", raw.CopaingID) + continue + } + copaings[raw.CopaingID] = &cp + } + pts, ok := stats[raw.CopaingID] + now := time.Now().Unix() + if !ok { + pts = make([]plotter.XY, days+1) + for i := 0; i < len(pts); i++ { + pts[i] = plotter.XY{ + X: float64(i - int(days)), + Y: 0, + } + } + stats[raw.CopaingID] = pts + } + t := raw.CreatedAt.Unix() - now + if !gokord.Debug { + t = int64(math.Ceil(float64(t) / (24 * 60 * 60))) + } + pts[int64(days)+t] = plotter.XY{ // because t <= 0 + X: float64(t), + Y: float64(raw.XP), + } + } + return generatePlot(s, i, copaings, stats) +} + +func generatePlot(s *discordgo.Session, i *discordgo.InteractionCreate, copaings map[int]*user.Copaing, stats map[int][]plotter.XY) (io.WriterTo, error) { + p := plot.New() + p.Title.Text = "Évolution de l'XP" + p.X.Label.Text = "Jours" + if gokord.Debug { + p.X.Label.Text = "Secondes" + } + p.Y.Label.Text = "XP" + + p.Add(plotter.NewGrid()) + + r := rand.New(rand.NewPCG(uint64(time.Now().Unix()), uint64(time.Now().Unix()))) + for in, c := range copaings { + m, err := s.GuildMember(i.GuildID, c.DiscordID) + if err != nil { + logger.Alert("commands/stats.go - Fetching guild member", err.Error()) + return nil, err + } + slices.SortFunc(stats[in], func(a, b plotter.XY) int { + if a.X < b.X { + return -1 + } + if a.X > b.X { + return 1 + } + return 0 + }) + l, err := plotter.NewLine(plotter.XYs(stats[in])) + if err != nil { + logger.Alert("commands/stats.go - Adding line points", err.Error()) + return nil, err + } + l.LineStyle.Width = vg.Points(1) + l.LineStyle.Dashes = []vg.Length{vg.Points(5), vg.Points(5)} + l.LineStyle.Color = color.RGBA{R: uint8(r.UintN(255)), G: uint8(r.UintN(255)), B: uint8(r.UintN(255)), A: 255} + p.Add(l) + p.Legend.Add(m.DisplayName(), l) + } + w, err := p.WriterTo(8*vg.Inch, 6*vg.Inch, "png") + if err != nil { + logger.Alert("commands/stats.go - Generating png", err.Error()) + return nil, err + } + return w, nil +} diff --git a/commands/top.go b/commands/top.go index daa1ccb..ecbf6f4 100644 --- a/commands/top.go +++ b/commands/top.go @@ -2,18 +2,20 @@ package commands import ( "fmt" - "github.com/anhgelus/gokord/utils" - "github.com/anhgelus/les-copaings-bot/config" - "github.com/anhgelus/les-copaings-bot/exp" - "github.com/anhgelus/les-copaings-bot/user" - "github.com/bwmarrin/discordgo" "sync" + + "git.anhgelus.world/anhgelus/les-copaings-bot/config" + "git.anhgelus.world/anhgelus/les-copaings-bot/exp" + "git.anhgelus.world/anhgelus/les-copaings-bot/user" + "github.com/anhgelus/gokord/cmd" + "github.com/anhgelus/gokord/logger" + discordgo "github.com/nyttikord/gokord" ) -func Top(s *discordgo.Session, i *discordgo.InteractionCreate, optMap utils.OptionMap, resp *utils.ResponseBuilder) { +func Top(_ *discordgo.Session, i *discordgo.InteractionCreate, _ cmd.OptionMap, resp *cmd.ResponseBuilder) { err := resp.IsDeferred().Send() if err != nil { - utils.SendAlert("commands/top.go - Sending deferred", err.Error()) + logger.Alert("commands/top.go - Sending deferred", err.Error()) return } embeds := make([]*discordgo.MessageEmbed, 3) @@ -23,18 +25,18 @@ func Top(s *discordgo.Session, i *discordgo.InteractionCreate, optMap utils.Opti defer wg.Done() tops, err := user.GetBestXP(i.GuildID, n, d) if err != nil { - utils.SendAlert("commands/top.go - Fetching best xp", err.Error(), "n", n, "d", d, "id", id, "guild_id", i.GuildID) + logger.Alert("commands/top.go - Fetching best xp", err.Error(), "n", n, "d", d, "id", id, "guild_id", i.GuildID) embeds[id] = &discordgo.MessageEmbed{ Title: s, Description: "Erreur : impossible de récupérer la liste", - Color: utils.Error, + Color: 0x831010, } return } embeds[id] = &discordgo.MessageEmbed{ Title: s, Description: genTopsMessage(tops), - Color: utils.Success, + Color: 0x10E6AD, } } cfg := config.GetGuildConfig(i.GuildID) @@ -57,7 +59,7 @@ func Top(s *discordgo.Session, i *discordgo.InteractionCreate, optMap utils.Opti } err = resp.Send() if err != nil { - utils.SendAlert("commands/top.go - Sending response top", err.Error()) + logger.Alert("commands/top.go - Sending response top", err.Error()) } }() } |
