aboutsummaryrefslogtreecommitdiff
path: root/commands
diff options
context:
space:
mode:
authorWilliam Hergès <william@herges.fr>2025-09-04 16:30:54 +0200
committerWilliam Hergès <william@herges.fr>2025-09-04 16:30:54 +0200
commitfa9b7767ab81471d15c29d77a85968e505b3b433 (patch)
tree481f2ac867a7709e263802e842b9e12835f6ac0c /commands
parent7508627d86a4f2ef9b3caebd88d92fe8be854816 (diff)
parent30ecd60b041398390f11fccdf46444fa28690bd8 (diff)
Merge branch 'main' into feat/xp-boost
Diffstat (limited to 'commands')
-rw-r--r--commands/config.go397
-rw-r--r--commands/credits.go39
-rw-r--r--commands/rank.go24
-rw-r--r--commands/reset.go23
-rw-r--r--commands/stats.go224
-rw-r--r--commands/top.go24
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())
}
}()
}