aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md5
-rw-r--r--assets/inter-variable.ttfbin0 -> 874708 bytes
-rw-r--r--commands/config.go216
-rw-r--r--commands/credits.go11
-rw-r--r--commands/rank.go39
-rw-r--r--commands/reset.go22
-rw-r--r--commands/stats.go118
-rw-r--r--commands/top.go27
-rw-r--r--config/channel.go118
-rw-r--r--config/guild.go36
-rw-r--r--config/xp_reduce.go79
-rw-r--r--config/xp_role.go501
-rw-r--r--events.go79
-rw-r--r--exp/functions.go24
-rw-r--r--go.mod22
-rw-r--r--go.sum32
-rw-r--r--main.go177
-rw-r--r--updates.json9
-rw-r--r--user/level.go79
-rw-r--r--user/member.go5
-rw-r--r--user/xp.go41
21 files changed, 994 insertions, 646 deletions
diff --git a/README.md b/README.md
index 2390dc4..efd2005 100644
--- a/README.md
+++ b/README.md
@@ -87,3 +87,8 @@ in `.env`)
- Go 1.24
- anhgelus/gokord
+
+## License
+
+This project uses [Inter](https://github.com/rsms/inter), an open font.
+Check its license [here](https://github.com/rsms/inter/blob/master/LICENSE.txt) (SIL OPEN FONT LICENSE Version 1.1).
diff --git a/assets/inter-variable.ttf b/assets/inter-variable.ttf
new file mode 100644
index 0000000..e31b51e
--- /dev/null
+++ b/assets/inter-variable.ttf
Binary files differ
diff --git a/commands/config.go b/commands/config.go
index c4644bc..336e66a 100644
--- a/commands/config.go
+++ b/commands/config.go
@@ -2,24 +2,32 @@ package commands
import (
"fmt"
+ "slices"
"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"
+ "github.com/nyttikord/gokord/bot"
+ "github.com/nyttikord/gokord/channel"
+ "github.com/nyttikord/gokord/component"
+ "github.com/nyttikord/gokord/discord/types"
+ "github.com/nyttikord/gokord/event"
+ "github.com/nyttikord/gokord/interaction"
)
const (
ConfigModify = "config_modify"
+ OpenConfig = "config"
)
-func Config(_ *discordgo.Session, i *discordgo.InteractionCreate, _ cmd.OptionMap, resp *cmd.ResponseBuilder) {
+func ConfigResponse(i *event.InteractionCreate) *interaction.Response {
cfg := config.GetGuildConfig(i.GuildID)
roles := ""
l := len(cfg.XpRoles) - 1
+ slices.SortFunc(cfg.XpRoles, func(xp1, xp2 config.XpRole) int {
+ return int(xp2.XP) - int(xp1.XP)
+ })
for i, r := range cfg.XpRoles {
if i == l {
roles += fmt.Sprintf("> Niveau %d - <@&%s>", exp.Level(r.XP), r.RoleID)
@@ -28,94 +36,130 @@ func Config(_ *discordgo.Session, i *discordgo.InteractionCreate, _ cmd.OptionMa
}
}
if len(roles) == 0 {
- roles = "Aucun rôle configuré :("
+ roles = "Aucun rôle configuré"
}
disChans := strings.Split(cfg.DisabledChannels, ";")
- l = len(disChans) - 1
- chans := ""
- for i, c := range disChans {
- if i == l-1 {
- chans += fmt.Sprintf("> <#%s>", c)
- } else if i != l {
- chans += fmt.Sprintf("> <#%s>\n", c)
+ var disChansDefault []component.SelectMenuDefaultValue
+ for _, c := range disChans {
+ if c != "" {
+ disChansDefault = append(disChansDefault, component.SelectMenuDefaultValue{
+ ID: c,
+ Type: types.SelectMenuDefaultValueChannel,
+ })
}
}
- if len(chans) == 0 {
- chans = "Aucun salon désactivé :)"
+ var defaultChan []component.SelectMenuDefaultValue
+ if len(cfg.FallbackChannel) > 0 {
+ defaultChan = append(defaultChan, component.SelectMenuDefaultValue{
+ ID: cfg.FallbackChannel,
+ Type: types.SelectMenuDefaultValueChannel,
+ })
}
- var defaultChan string
- if len(cfg.FallbackChannel) == 0 {
- defaultChan = "Pas de valeur"
- } else {
- defaultChan = fmt.Sprintf("<#%s>", cfg.FallbackChannel)
+ zero := 0
+ content := []component.Component{
+ &component.Container{
+ Components: []component.Message{
+ &component.TextDisplay{Content: "## Configuration"},
+ &component.Separator{},
+ &component.TextDisplay{Content: "**Salons par défaut**\n-# Les niveaux obtenue grâce à un appel sont affichés ici"},
+ &component.ActionsRow{
+ Components: []component.Message{
+ &component.SelectMenu{
+ MenuType: types.SelectMenuChannel,
+ CustomID: config.ModifyFallbackChannel,
+ Placeholder: "Pas de salon par défaut",
+ MinValues: &zero,
+ MaxValues: 1,
+ DefaultValues: defaultChan,
+ },
+ },
+ },
+ &component.TextDisplay{Content: "**Salons désactivé**\n-# Les messages ne donneront pas d'expérience dans ces salons"},
+ &component.ActionsRow{
+ Components: []component.Message{
+ &component.SelectMenu{
+ MenuType: types.SelectMenuChannel,
+ CustomID: config.ModifyDisChannel,
+ Placeholder: "Pas de salons désactivé",
+ MinValues: &zero,
+ MaxValues: 25,
+ DefaultValues: disChansDefault,
+ },
+ },
+ },
+ &component.Section{
+ Components: []component.Message{
+ &component.TextDisplay{Content: "**Rôles de niveau**\n" + roles},
+ },
+ Accessory: &component.Button{
+ Label: "Modifier",
+ Style: component.ButtonStyleSecondary,
+ CustomID: config.ModifyXpRole,
+ },
+ },
+ &component.Section{
+ Components: []component.Message{
+ &component.TextDisplay{
+ Content: fmt.Sprintf("**Jours avant la réduction**\n-# Seule l'expérience gagnée les x derniers jours est comptabilisée dans le niveau par défaut\n%d jours", cfg.DaysXPRemains),
+ },
+ },
+ Accessory: &component.Button{
+ Label: "Modifier",
+ Style: component.ButtonStyleSecondary,
+ CustomID: config.ModifyTimeReduce,
+ },
+ },
+ },
+ },
}
- //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()
+ return &interaction.Response{
+ Type: types.InteractionResponseChannelMessageWithSource,
+ Data: &interaction.ResponseData{
+ Components: content,
+ Flags: channel.MessageFlagsEphemeral | channel.MessageFlagsIsComponentsV2,
+ },
+ }
+}
+
+func ConfigCommand(
+ s bot.Session,
+ i *event.InteractionCreate,
+ _ cmd.OptionMap,
+ resp *cmd.ResponseBuilder,
+) {
+ err := s.InteractionAPI().Respond(i.Interaction, ConfigResponse(i))
+
+ if err != nil {
+ s.Logger().Error("sending config", "error", err)
+ }
+}
+
+func ConfigMessageComponent(
+ s bot.Session,
+ i *event.InteractionCreate,
+ _ *interaction.MessageComponentData,
+ _ *cmd.ResponseBuilder,
+) {
+ response := ConfigResponse(i)
+ response.Type = types.InteractionResponseUpdateMessage
+ err := s.InteractionAPI().Respond(i.Interaction, response)
+
+ if err != nil {
+ s.Logger().Error("sending config", "error", err)
+ }
+}
+
+func ConfigModal(
+ s bot.Session,
+ i *event.InteractionCreate,
+ _ *interaction.ModalSubmitData,
+ _ *cmd.ResponseBuilder,
+) {
+ response := ConfigResponse(i)
+ response.Type = types.InteractionResponseUpdateMessage
+ err := s.InteractionAPI().Respond(i.Interaction, response)
+
if err != nil {
- logger.Alert("config/guild.go - Sending config", err.Error())
+ s.Logger().Error("sending config", "error", err)
}
}
diff --git a/commands/credits.go b/commands/credits.go
index f0a8c46..340ea57 100644
--- a/commands/credits.go
+++ b/commands/credits.go
@@ -3,17 +3,18 @@ package commands
import (
"github.com/anhgelus/gokord"
"github.com/anhgelus/gokord/cmd"
- "github.com/anhgelus/gokord/logger"
- discordgo "github.com/nyttikord/gokord"
+ "github.com/nyttikord/gokord/bot"
+ "github.com/nyttikord/gokord/event"
)
-func Credits(_ *discordgo.Session, i *discordgo.InteractionCreate, _ cmd.OptionMap, resp *cmd.ResponseBuilder) {
+func Credits(s bot.Session, _ *event.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>)"
+ msg += "Utilise :\n- [anhgelus/gokord](<https://github.com/anhgelus/gokord>)\n"
+ msg += "- [Inter](<https://github.com/rsms/inter>)"
err := resp.SetMessage(msg).Send()
if err != nil {
- logger.Alert("commands/credits.go - Sending credits", err.Error(), "guild_id", i.GuildID)
+ s.Logger().Error("sending credits", "error", err)
}
}
diff --git a/commands/rank.go b/commands/rank.go
index 94dabf2..4080864 100644
--- a/commands/rank.go
+++ b/commands/rank.go
@@ -6,55 +6,42 @@ import (
"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"
+ "github.com/nyttikord/gokord/bot"
+ "github.com/nyttikord/gokord/event"
)
-func Rank(s *discordgo.Session, i *discordgo.InteractionCreate, optMap cmd.OptionMap, resp *cmd.ResponseBuilder) {
+func Rank(s bot.Session, i *event.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
var err error
if v, ok := optMap["copaing"]; ok {
- u := v.UserValue(s)
+ u := v.UserValue(s.UserAPI())
if u.Bot {
err = resp.SetMessage("Imagine si les bots avaient un niveau :rolling_eyes:").IsEphemeral().Send()
if err != nil {
- logger.Alert("commands/rank.go - Reply error user is a bot", err.Error())
+ s.Logger().Error("reply error user is a bot", "error", err)
}
+ return
}
- m, err = s.GuildMember(i.GuildID, u.ID)
+ m, err = s.GuildAPI().Member(i.GuildID, u.ID)
if err != nil {
- logger.Alert(
- "commands/rank.go - Fetching guild member",
- err.Error(),
- "discord_id",
- u.ID,
- "guild_id",
- i.GuildID,
- )
+ s.Logger().Error("fetching guild member", "error", err, "user", u.Username, "guild", i.GuildID)
err = resp.SetMessage("Erreur : impossible de récupérer le membre").IsEphemeral().Send()
if err != nil {
- logger.Alert("commands/rank.go - Reply error fetching guild member", err.Error())
+ s.Logger().Error("reply error fetching guild member", "error", err)
}
return
}
c = user.GetCopaing(u.ID, i.GuildID) // current user = member targeted by member who wrote /rank
msg = fmt.Sprintf("Le niveau de %s", m.DisplayName())
}
- xp, err := c.GetXP()
+ xp, err := c.GetXP(s.Logger())
if err != nil {
- logger.Alert(
- "commands/rank.go - Fetching xp",
- err.Error(),
- "discord_id",
- c.ID,
- "guild_id",
- i.GuildID,
- )
+ s.Logger().Error("fetching xp", "error", err, "copaing", c.ID, "guild", i.GuildID)
err = resp.SetMessage("Erreur : impossible de récupérer l'XP").IsEphemeral().Send()
if err != nil {
- logger.Alert("commands/rank.go - Reply error fetching xp", err.Error())
+ s.Logger().Error("reply error fetching xp", "error", err)
}
return
}
@@ -68,6 +55,6 @@ func Rank(s *discordgo.Session, i *discordgo.InteractionCreate, optMap cmd.Optio
nxtLvlXP-xp,
)).Send()
if err != nil {
- logger.Alert("commands/rank.go - Sending rank", err.Error())
+ s.Logger().Error("sending rank", "error", err)
}
}
diff --git a/commands/reset.go b/commands/reset.go
index 07e3ba3..21bfeb7 100644
--- a/commands/reset.go
+++ b/commands/reset.go
@@ -4,43 +4,43 @@ import (
"git.anhgelus.world/anhgelus/les-copaings-bot/user"
"github.com/anhgelus/gokord"
"github.com/anhgelus/gokord/cmd"
- "github.com/anhgelus/gokord/logger"
- discordgo "github.com/nyttikord/gokord"
+ "github.com/nyttikord/gokord/bot"
+ "github.com/nyttikord/gokord/event"
)
-func Reset(_ *discordgo.Session, i *discordgo.InteractionCreate, _ cmd.OptionMap, resp *cmd.ResponseBuilder) {
+func Reset(s bot.Session, i *event.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 {
- logger.Alert("commands/reset.go - Sending success (all)", err.Error())
+ s.Logger().Error("sending reset success", "error", err)
}
}
-func ResetUser(s *discordgo.Session, i *discordgo.InteractionCreate, optMap cmd.OptionMap, resp *cmd.ResponseBuilder) {
+func ResetUser(s bot.Session, i *event.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 {
- logger.Alert("commands/reset.go - Copaing not set", err.Error())
+ s.Logger().Error("sending error copaing not set", "error", err)
}
return
}
- m := v.UserValue(s)
+ m := v.UserValue(s.UserAPI())
if m.Bot {
if err := resp.SetMessage("Les bots n'ont pas de niveau :upside_down:").Send(); err != nil {
- logger.Alert("commands/reset.go - Copaing not set", err.Error())
+ s.Logger().Error("sending error bot does not have xp", "error", err)
}
return
}
err := user.GetCopaing(m.ID, i.GuildID).Delete()
if err != nil {
- logger.Alert("commands/reset.go - Copaing not deleted", err.Error(), "discord_id", m.ID, "guild_id", i.GuildID)
+ s.Logger().Error("deleting copaing", "error", err, "user", m.Username, "guild", i.GuildID)
err = resp.SetMessage("Erreur : impossible de reset l'utilisateur").Send()
if err != nil {
- logger.Alert("commands/reset.go - Error deleting", err.Error())
+ s.Logger().Error("sending error while deleting", "error", err)
}
}
if err = resp.SetMessage("Le user bien été reset.").Send(); err != nil {
- logger.Alert("commands/reset.go - Sending success (user)", err.Error())
+ s.Logger().Error("sending reset success", "error", err)
}
}
diff --git a/commands/stats.go b/commands/stats.go
index f93f6a0..4fc35ae 100644
--- a/commands/stats.go
+++ b/commands/stats.go
@@ -3,11 +3,10 @@ package commands
import (
"bytes"
"errors"
- "gorm.io/gorm"
+ "fmt"
"image/color"
"io"
"math"
- "math/rand/v2"
"slices"
"time"
@@ -16,12 +15,15 @@ import (
"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"
+ "github.com/nyttikord/gokord/bot"
+ "github.com/nyttikord/gokord/channel"
+ "github.com/nyttikord/gokord/event"
"gonum.org/v1/plot"
"gonum.org/v1/plot/plotter"
"gonum.org/v1/plot/vg"
+ "gonum.org/v1/plot/vg/draw"
+ "gorm.io/gorm"
)
type data struct {
@@ -36,78 +38,91 @@ type dbData struct {
CopaingID int
}
-func Stats(s *discordgo.Session, i *discordgo.InteractionCreate, opt cmd.OptionMap, resp *cmd.ResponseBuilder) {
+var colors = []color.RGBA{
+ {38, 70, 83, 255},
+ {42, 157, 143, 255},
+ {244, 162, 97, 255},
+ {231, 111, 81, 255},
+ {193, 18, 31, 255},
+}
+
+func Stats(s bot.Session, i *event.InteractionCreate, opt cmd.OptionMap, resp *cmd.ResponseBuilder) {
cfg := config.GetGuildConfig(i.GuildID)
- days := cfg.DaysXPRemains
+ days := 15
+ if gokord.Debug {
+ days = 90
+ }
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())
+ if in < 1 || uint(in) > cfg.DaysXPRemains {
+ msg := fmt.Sprintf("Nombre de jours invalide. Il doit être strictement positif et inférieur à %d", cfg.DaysXPRemains)
+ if err := resp.SetMessage(msg).IsEphemeral().Send(); err != nil {
+ s.Logger().Error("sending error invalid days", "error", err)
}
return
}
- days = uint(in)
+ days = int(in)
}
err := resp.IsDeferred().Send()
if err != nil {
- logger.Alert("commands/stats.go - Sending deferred", err.Error())
+ s.Logger().Error("sending deferred", "error", err)
return
}
go func() {
var w io.WriterTo
if v, ok := opt["user"]; ok {
- w, err = statsMember(s, i, days, v.UserValue(s).ID)
+ w, err = statsMember(s, i, days, v.UserValue(s.UserAPI()).ID)
} else {
w, err = statsAll(s, i, days)
}
if err != nil {
+ s.Logger().Error("generating stats", "error", err, "guild", i.GuildID)
if err = resp.IsEphemeral().SetMessage("Il y a eu une erreur...").Send(); err != nil {
- logger.Alert("commands/stats.go - Sending error occurred", err.Error())
+ s.Logger().Error("sending error occurred", "error", err)
}
return
}
b := new(bytes.Buffer)
_, err = w.WriteTo(b)
if err != nil {
- logger.Alert("commands/stats.go - Writing png", err.Error())
+ s.Logger().Error("writing png", "error", err)
}
- err = resp.AddFile(&discordgo.File{
+ err = resp.AddFile(&channel.File{
Name: "plot.png",
ContentType: "image/png",
Reader: b,
}).Send()
if err != nil {
- logger.Alert("commands/stats.go - Sending response", err.Error())
+ s.Logger().Error("sending stats", "error", err)
}
}()
}
-func statsAll(s *discordgo.Session, i *discordgo.InteractionCreate, days uint) (io.WriterTo, error) {
+func statsAll(s bot.Session, i *event.InteractionCreate, days int) (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))
+ return gokord.DB.Raw(before+"WHERE guild_id = ? and created_at > ?"+after, i.GuildID, exp.TimeStampNDaysBefore(uint(days)))
})
}
-func statsMember(s *discordgo.Session, i *discordgo.InteractionCreate, days uint, discordID string) (io.WriterTo, error) {
- _, err := s.GuildMember(i.GuildID, discordID)
+func statsMember(s bot.Session, i *event.InteractionCreate, days int, discordID string) (io.WriterTo, error) {
+ _, err := s.GuildAPI().Member(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,
+ i.GuildID, exp.TimeStampNDaysBefore(uint(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) {
+func stats(s bot.Session, i *event.InteractionCreate, days int, 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())
+ s.Logger().Error("fetching result", "error", err)
return nil, err
}
rawData = make([]*data, len(rawCopaingData))
@@ -123,7 +138,7 @@ func stats(s *discordgo.Session, i *discordgo.InteractionCreate, days uint, exec
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())
+ s.Logger().Error("fetching result", "error", err)
return nil, err
}
rawData = make([]*data, len(rawDbData))
@@ -145,10 +160,10 @@ func stats(s *discordgo.Session, i *discordgo.InteractionCreate, days uint, exec
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)
+ s.Logger().Error("finding copaing", "error", err, "copaing", raw.CopaingID)
return nil, err
}
- logger.Warn("Copaing not found, skipping entry", "old_id", raw.CopaingID)
+ s.Logger().Warn("copaing not found, skipping", "copaing", raw.CopaingID)
continue
}
copaings[raw.CopaingID] = &cp
@@ -159,7 +174,7 @@ func stats(s *discordgo.Session, i *discordgo.InteractionCreate, days uint, exec
pts = make([]plotter.XY, days+1)
for i := 0; i < len(pts); i++ {
pts[i] = plotter.XY{
- X: float64(i - int(days)),
+ X: float64(i - days),
Y: 0,
}
}
@@ -168,6 +183,8 @@ func stats(s *discordgo.Session, i *discordgo.InteractionCreate, days uint, exec
t := raw.CreatedAt.Unix() - now
if !gokord.Debug {
t = int64(math.Ceil(float64(t) / (24 * 60 * 60)))
+ } else {
+ t = int64(math.Ceil(float64(t) / exp.DebugFactor))
}
pts[int64(days)+t] = plotter.XY{ // because t <= 0
X: float64(t),
@@ -177,22 +194,35 @@ func stats(s *discordgo.Session, i *discordgo.InteractionCreate, days uint, exec
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) {
+func generatePlot(s bot.Session, i *event.InteractionCreate, copaings map[int]*user.Copaing, stats map[int][]plotter.XY) (io.WriterTo, error) {
p := plot.New()
- p.Title.Text = "Évolution de l'XP"
+ fontSizeTitle := vg.Length(16)
+ fontSize := vg.Length(12)
+ // set font size
+ p.Title.TextStyle.Font.Size = fontSizeTitle
+ p.X.Label.TextStyle.Font.Size = fontSizeTitle
+ p.Y.Label.TextStyle.Font.Size = fontSizeTitle
+ p.Legend.TextStyle.Font.Size = fontSize
+ // set legend style
+ p.Legend.YPosition = draw.PosTop
+ p.Legend.Top = true
+ p.Legend.Padding = vg.Points(2)
+ // set scales
+ p.Title.Text = "XP gagnées"
p.X.Label.Text = "Jours"
if gokord.Debug {
- p.X.Label.Text = "Secondes"
+ p.X.Label.Text = fmt.Sprintf("%d secondes", exp.DebugFactor)
}
p.Y.Label.Text = "XP"
+ p.Y.Scale = exp.LevelScale{}
p.Add(plotter.NewGrid())
- r := rand.New(rand.NewPCG(uint64(time.Now().Unix()), uint64(time.Now().Unix())))
+ cnt := 0
for in, c := range copaings {
- m, err := s.GuildMember(i.GuildID, c.DiscordID)
+ m, err := s.GuildAPI().Member(i.GuildID, c.DiscordID)
if err != nil {
- logger.Alert("commands/stats.go - Fetching guild member", err.Error())
+ s.Logger().Error("fetching guild member", "error", err)
return nil, err
}
slices.SortFunc(stats[in], func(a, b plotter.XY) int {
@@ -204,21 +234,21 @@ func generatePlot(s *discordgo.Session, i *discordgo.InteractionCreate, copaings
}
return 0
})
- l, err := plotter.NewLine(plotter.XYs(stats[in]))
+ l, _, err := plotter.NewLinePoints(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}
+ l.Color = colors[cnt%len(colors)]
+ if len(copaings) < 4 {
+ l.Width = vg.Points(2)
+ }
+ if cnt/len(colors) > 0 {
+ size := 7 / min(cnt/len(colors), 7)
+ l.Dashes = []vg.Length{vg.Points(float64(size)), vg.Points(float64(size))}
+ }
p.Add(l)
p.Legend.Add(m.DisplayName(), l)
+ cnt++
}
- 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
+ return p.WriterTo(12*vg.Inch, 8*vg.Inch, "png")
}
diff --git a/commands/top.go b/commands/top.go
index ecbf6f4..bb08144 100644
--- a/commands/top.go
+++ b/commands/top.go
@@ -8,33 +8,34 @@ import (
"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"
+ "github.com/nyttikord/gokord/bot"
+ "github.com/nyttikord/gokord/channel"
+ "github.com/nyttikord/gokord/event"
)
-func Top(_ *discordgo.Session, i *discordgo.InteractionCreate, _ cmd.OptionMap, resp *cmd.ResponseBuilder) {
+func Top(s bot.Session, i *event.InteractionCreate, _ cmd.OptionMap, resp *cmd.ResponseBuilder) {
err := resp.IsDeferred().Send()
if err != nil {
- logger.Alert("commands/top.go - Sending deferred", err.Error())
+ s.Logger().Error("sending deferred", "error", err)
return
}
- embeds := make([]*discordgo.MessageEmbed, 3)
+ embeds := make([]*channel.MessageEmbed, 3)
wg := sync.WaitGroup{}
- fn := func(s string, n uint, d int, id int) {
+ fn := func(str string, n uint, d int, id int) {
defer wg.Done()
- tops, err := user.GetBestXP(i.GuildID, n, d)
+ tops, err := user.GetBestXP(s.Logger(), i.GuildID, n, d)
if err != nil {
- 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,
+ s.Logger().Error("fetching best xp", "error", err, "n", n, "d", d, "id", id, "guild", i.GuildID)
+ embeds[id] = &channel.MessageEmbed{
+ Title: str,
Description: "Erreur : impossible de récupérer la liste",
Color: 0x831010,
}
return
}
- embeds[id] = &discordgo.MessageEmbed{
- Title: s,
+ embeds[id] = &channel.MessageEmbed{
+ Title: str,
Description: genTopsMessage(tops),
Color: 0x10E6AD,
}
@@ -59,7 +60,7 @@ func Top(_ *discordgo.Session, i *discordgo.InteractionCreate, _ cmd.OptionMap,
}
err = resp.Send()
if err != nil {
- logger.Alert("commands/top.go - Sending response top", err.Error())
+ s.Logger().Error("sending response top", "error", err)
}
}()
}
diff --git a/config/channel.go b/config/channel.go
index 537d586..0c25b25 100644
--- a/config/channel.go
+++ b/config/channel.go
@@ -4,14 +4,14 @@ import (
"strings"
"github.com/anhgelus/gokord/cmd"
- "github.com/anhgelus/gokord/component"
- "github.com/anhgelus/gokord/logger"
- discordgo "github.com/nyttikord/gokord"
+ "github.com/nyttikord/gokord/bot"
+ "github.com/nyttikord/gokord/event"
+
+ "github.com/nyttikord/gokord/interaction"
)
const (
ModifyFallbackChannel = "fallback_channel"
- FallbackChannelSet = "fallback_channel_set"
ModifyDisChannel = "disabled_channel"
DisChannelAdd = "disabled_channel_add"
@@ -20,108 +20,28 @@ const (
DisChannelDelSet = "disabled_channel_del_set"
)
-func HandleModifyFallbackChannel(_ *discordgo.Session, _ *discordgo.InteractionCreate, _ discordgo.MessageComponentInteractionData, resp *cmd.ResponseBuilder) {
- err := resp.IsEphemeral().SetComponents(component.New().Add(component.NewActionRow().Add(
- component.NewChannelSelect(FallbackChannelSet).AddChannelType(discordgo.ChannelTypeGuildText),
- ))).Send()
- if err != nil {
- logger.Alert("config/channel.go - Sending channel list for fallback", err.Error())
- }
-}
-
-func HandleFallbackChannelSet(_ *discordgo.Session, i *discordgo.InteractionCreate, data discordgo.MessageComponentInteractionData, resp *cmd.ResponseBuilder) {
- resp.IsEphemeral()
-
+func HandleModifyFallbackChannel(s bot.Session, i *event.InteractionCreate, data *interaction.MessageComponentData, _ *cmd.ResponseBuilder) bool {
cfg := GetGuildConfig(i.GuildID)
- channelID := data.Values[0]
-
+ var channelID string
+ if len(data.Values) > 0 {
+ channelID = data.Values[0]
+ }
cfg.FallbackChannel = channelID
err := cfg.Save()
if err != nil {
- logger.Alert("config/channel.go - Saving fallback channel", err.Error())
- if err = resp.SetMessage("Erreur lors de la sauvegarde du salon").Send(); err != nil {
- logger.Alert("config/channel.go - Sending error while saving channel", err.Error())
- }
- return
- }
- if err = resp.SetMessage("Salon sauvegardé.").Send(); err != nil {
- logger.Alert("config/channel.go - Sending channel saved", err.Error())
- }
-}
-
-func HandleModifyDisChannel(_ *discordgo.Session, _ *discordgo.InteractionCreate, _ discordgo.MessageComponentInteractionData, resp *cmd.ResponseBuilder) {
- err := resp.IsEphemeral().SetComponents(component.New().Add(component.NewActionRow().
- Add(
- component.NewButton(DisChannelAdd, discordgo.PrimaryButton).
- SetLabel("Désactiver un salon").
- SetEmoji(&discordgo.ComponentEmoji{Name: "⬇️"}),
- ).
- Add(
- component.NewButton(DisChannelDel, discordgo.DangerButton).
- SetLabel("Réactiver un salon").
- SetEmoji(&discordgo.ComponentEmoji{Name: "⬆️"}),
- ),
- )).Send()
- if err != nil {
- logger.Alert("config/channel.go - Sending action type", err.Error())
- }
-}
-
-func HandleDisChannel(_ *discordgo.Session, _ *discordgo.InteractionCreate, data discordgo.MessageComponentInteractionData, resp *cmd.ResponseBuilder) {
- resp.IsEphemeral().SetMessage("Salon à désactiver...")
- cID := DisChannelAddSet
- if data.CustomID == DisChannelDel {
- resp.SetMessage("Salon à réactiver...")
- cID = DisChannelDelSet
- }
- err := resp.SetComponents(component.New().Add(component.NewActionRow().Add(component.NewChannelSelect(cID)))).Send()
- if err != nil {
- logger.Alert("config/channel.go - Sending channel list for disable", err.Error())
- }
-}
-
-func HandleDisChannelAddSet(_ *discordgo.Session, i *discordgo.InteractionCreate, data discordgo.MessageComponentInteractionData, resp *cmd.ResponseBuilder) {
- resp.IsEphemeral()
- cfg := GetGuildConfig(i.GuildID)
- id := data.Values[0]
- if strings.Contains(cfg.DisabledChannels, id) {
- err := resp.SetMessage("Le salon est déjà dans la liste des salons désactivés").Send()
- if err != nil {
- logger.Alert("commands/config.go - Channel already disabled", err.Error())
- }
- return
- }
- cfg.DisabledChannels += id + ";"
- if err := cfg.Save(); err != nil {
- logger.Alert("commands/config.go - Saving config disable add", err.Error())
- if err = resp.SetMessage("Il y a eu une erreur lors de la modification de de la base de données.").Send(); err != nil {
- logger.Alert("config/channel.go - Sending error while saving config", err.Error())
- }
- }
- if err := resp.SetMessage("Modification sauvegardé.").Send(); err != nil {
- logger.Alert("commands/config.go - Modification saved message disable add", err.Error())
+ s.Logger().Error("saving fallback channel", "error", err)
+ return false
}
+ return true
}
-func HandleDisChannelDelSet(_ *discordgo.Session, i *discordgo.InteractionCreate, data discordgo.MessageComponentInteractionData, resp *cmd.ResponseBuilder) {
- resp.IsEphemeral()
+func HandleModifyDisChannel(s bot.Session, i *event.InteractionCreate, data *interaction.MessageComponentData, _ *cmd.ResponseBuilder) bool {
cfg := GetGuildConfig(i.GuildID)
- id := data.Values[0]
- if !strings.Contains(cfg.DisabledChannels, id) {
- err := resp.SetMessage("Le salon n'est pas désactivé").Send()
- if err != nil {
- logger.Alert("commands/config.go - Channel not disabled", err.Error())
- }
- return
- }
- cfg.DisabledChannels = strings.ReplaceAll(cfg.DisabledChannels, id+";", "")
- if err := cfg.Save(); err != nil {
- logger.Alert("commands/config.go - Saving config disable del", err.Error())
- if err = resp.SetMessage("Il y a eu une erreur lors de la modification de de la base de données.").Send(); err != nil {
- logger.Alert("config/channel.go - Sending error while saving config", err.Error())
- }
- }
- if err := resp.SetMessage("Modification sauvegardé.").Send(); err != nil {
- logger.Alert("commands/config.go - Modification saved message disable del", err.Error())
+ cfg.DisabledChannels = strings.Join(data.Values, ";")
+ err := cfg.Save()
+ if err != nil {
+ s.Logger().Error("unable to save disabled channel", "error", err)
+ return false
}
+ return true
}
diff --git a/config/guild.go b/config/guild.go
index 971470d..c2a1636 100644
--- a/config/guild.go
+++ b/config/guild.go
@@ -4,6 +4,7 @@ import (
"strings"
"github.com/anhgelus/gokord"
+ "github.com/nyttikord/gokord/bot"
)
type GuildConfig struct {
@@ -46,8 +47,30 @@ func (cfg *GuildConfig) Save() error {
return gokord.DB.Save(cfg).Error
}
-func (cfg *GuildConfig) IsDisabled(channelID string) bool {
- return strings.Contains(cfg.DisabledChannels, channelID)
+func (cfg *GuildConfig) IsDisabled(s bot.Session, channelID string) bool {
+ ok := true
+ for channelID != "" && ok {
+ ok = !strings.Contains(cfg.DisabledChannels, channelID)
+ c, err := s.ChannelAPI().State.Channel(channelID)
+ if err != nil {
+ s.Logger().Error("unable to find channel %s in state", "error", err, "channel", c)
+ c, err = s.ChannelAPI().Channel(channelID)
+ if err == nil {
+ err = s.ChannelAPI().State.ChannelAdd(c)
+ if err != nil {
+ s.Logger().Error("unable to add channel to state", "error", err, "channel", c)
+ }
+ } else {
+ s.Logger().Error("unable to fetch channel", "error", err, "channel", c)
+ return false
+ }
+ }
+ if err != nil {
+ return false
+ }
+ channelID = c.ParentID
+ }
+ return !ok
}
func (cfg *GuildConfig) FindXpRole(roleID string) (int, *XpRole) {
@@ -58,3 +81,12 @@ func (cfg *GuildConfig) FindXpRole(roleID string) (int, *XpRole) {
}
return 0, nil
}
+
+func (cfg *GuildConfig) FindXpRoleID(ID uint) (int, *XpRole) {
+ for i, r := range cfg.XpRoles {
+ if r.ID == ID {
+ return i, &r
+ }
+ }
+ return -1, nil
+}
diff --git a/config/xp_reduce.go b/config/xp_reduce.go
index defc54b..389043d 100644
--- a/config/xp_reduce.go
+++ b/config/xp_reduce.go
@@ -1,12 +1,15 @@
package config
import (
+ "fmt"
"strconv"
"github.com/anhgelus/gokord/cmd"
- "github.com/anhgelus/gokord/component"
- "github.com/anhgelus/gokord/logger"
- discordgo "github.com/nyttikord/gokord"
+ "github.com/nyttikord/gokord/bot"
+ "github.com/nyttikord/gokord/component"
+ "github.com/nyttikord/gokord/discord/types"
+ "github.com/nyttikord/gokord/event"
+ "github.com/nyttikord/gokord/interaction"
)
const (
@@ -14,47 +17,61 @@ const (
TimeReduceSet = "time_reduce_set"
)
-func HandleModifyPeriodicReduce(_ *discordgo.Session, _ *discordgo.InteractionCreate, _ discordgo.MessageComponentInteractionData, resp *cmd.ResponseBuilder) {
- err := resp.IsModal().
- SetCustomID(TimeReduceSet).
- SetComponents(component.New().ForModal().Add(component.NewActionRow().ForModal().Add(
- component.NewTextInput(TimeReduceSet, "Jours avant la réduction", discordgo.TextInputShort).
- SetMinLength(1).
- SetMaxLength(3),
- ))).Send()
+func HandleModifyPeriodicReduceCommand(s bot.Session, i *event.InteractionCreate, _ *interaction.MessageComponentData, _ *cmd.ResponseBuilder) {
+ cfg := GetGuildConfig(i.GuildID)
+ response := interaction.Response{
+ Type: types.InteractionResponseModal,
+ Data: &interaction.ResponseData{
+ CustomID: TimeReduceSet,
+ Title: "Modifier la durée de l'expérience",
+ Components: []component.Component{
+ // TODO: When gokord supports it, enable this description again
+ // &component.TextDisplay{
+ // Content: "Seul l'expérience gagnée sur cette période sera comptabilisée dans le niveau par défaut",
+ // },
+ &component.Label{
+ Label: "Durée en jours",
+ Component: &component.TextInput{
+ CustomID: TimeReduceSet,
+ MinLength: 1,
+ MaxLength: 3,
+ Style: component.TextInputShort,
+ Placeholder: "Durée en jours",
+ Value: fmt.Sprintf("%d", cfg.DaysXPRemains),
+ },
+ },
+ },
+ },
+ }
+ err := s.InteractionAPI().Respond(i.Interaction, &response)
if err != nil {
- logger.Alert("config/xp_reduce.go - Sending modal for periodic reduce", err.Error())
+ s.Logger().Error("sending xp reduce modal", "error", err)
}
}
-func HandleTimeReduceSet(_ *discordgo.Session, i *discordgo.InteractionCreate, data discordgo.ModalSubmitInteractionData, resp *cmd.ResponseBuilder) {
- resp.IsEphemeral()
- v := data.Components[0].(*discordgo.ActionsRow).Components[0].(*discordgo.TextInput).Value
+func HandleTimeReduceSet(s bot.Session, i *event.InteractionCreate, data *interaction.ModalSubmitData, resp *cmd.ResponseBuilder) bool {
+ v := data.Components[0].(*component.Label).Component.(*component.TextInput).Value
days, err := strconv.Atoi(v)
if err != nil {
- logger.Debug(err.Error())
- if err = resp.SetMessage("Nombres de jours invalides. Merci de mettre un entier.").Send(); err != nil {
- logger.Alert("config/xp_reduce.go - Sending bad input", err.Error())
+ err = resp.IsEphemeral().SetMessage(fmt.Sprintf("La valeur indiquée, `%s`, c'est pas un entier.", v)).Send()
+ if err != nil {
+ s.Logger().Error("sending bad input message", "error", err)
}
- return
+ return false
}
if days < 30 {
- err = resp.SetMessage("Le nombre de jours est inférieur à 30.").Send()
+ err = resp.IsEphemeral().SetMessage("Le nombre de jours doit être suppérieur à 30.").Send()
if err != nil {
- logger.Alert("config/xp_reduce.go - Days < 30 (fallback)", err.Error())
+ s.Logger().Error("sending less than 30 days message", "error", err)
}
- return
+ return false
}
cfg := GetGuildConfig(i.GuildID)
cfg.DaysXPRemains = uint(days)
- if err = cfg.Save(); err != nil {
- logger.Alert("config/channel.go - Saving days xp remains", err.Error())
- if err = resp.SetMessage("Erreur lors de la sauvegarde du salon").Send(); err != nil {
- logger.Alert("config/xp_reduce.go - Sending error while saving days xp remains", err.Error())
- }
- return
- }
- if err = resp.SetMessage("Modification sauvegardée.").Send(); err != nil {
- logger.Alert("config/xp_reduce.go - Sending days saved", err.Error())
+ err = cfg.Save()
+ if err != nil {
+ s.Logger().Error("saving DaysXPRemains configuration", "error", err)
+ return false
}
+ return true
}
diff --git a/config/xp_role.go b/config/xp_role.go
index 6ea89e2..8ea9d1e 100644
--- a/config/xp_role.go
+++ b/config/xp_role.go
@@ -2,208 +2,419 @@ package config
import (
"fmt"
+ "slices"
"strconv"
- "time"
"git.anhgelus.world/anhgelus/les-copaings-bot/exp"
"github.com/anhgelus/gokord"
"github.com/anhgelus/gokord/cmd"
- "github.com/anhgelus/gokord/component"
- "github.com/anhgelus/gokord/logger"
- discordgo "github.com/nyttikord/gokord"
+ "github.com/nyttikord/gokord/bot"
+ "github.com/nyttikord/gokord/channel"
+ "github.com/nyttikord/gokord/component"
+ "github.com/nyttikord/gokord/discord/types"
+ "github.com/nyttikord/gokord/event"
+ "github.com/nyttikord/gokord/interaction"
)
const (
- ModifyXpRole = "xp_role"
- XpRoleAdd = "xp_role_add"
- XpRoleAddLevel = "xp_role_add_level"
- XpRoleAddRole = "xp_role_add_role"
- XpRoleDel = "xp_role_del"
- XpRoleDelRole = "xp_role_del_role"
- XpRoleEdit = "xp_role_edit"
- XpRoleEditLevel = "xp_role_edit_level"
- XpRoleEditRole = "xp_role_edit_role"
+ ModifyXpRole = "xp_role"
+ XpRoleNew = "xp_role_add"
+ XpRoleAdd = "xp_role_add_level"
+ XpRoleEditPattern = `^xp_role_edit_(\d+)$`
+ XpRoleEditLevelPattern = `^xp_role_edit_level_(\d+)$`
+ XpRoleEditLevelStartPattern = `^xp_role_edit_level_start_(\d+)$`
+ XpRoleEditRolePattern = `^xp_role_edit_role_(\d+)$`
+ XpRoleDel = `^xp_role_del_(\d+)$`
)
-var (
- configModifyMap = map[string]uint{}
-)
+func HandleXpRole(
+ s bot.Session,
+ i *event.InteractionCreate,
+ _ *interaction.MessageComponentData,
+ _ *cmd.ResponseBuilder,
+) {
+ cfg := GetGuildConfig(i.GuildID)
+ container := component.Container{
+ Components: []component.Message{
+ &component.TextDisplay{Content: "## Configuration / Rôles de niveaux"},
+ &component.TextDisplay{Content: "Ces rôles seront donnés et retirés en fonction du niveau de chacun"},
+ &component.Separator{},
+ },
+ }
+ slices.SortFunc(cfg.XpRoles, func(xp1, xp2 XpRole) int {
+ return int(xp2.XP) - int(xp1.XP)
+ })
+ for _, r := range cfg.XpRoles {
+ container.Components = append(container.Components, &component.Section{
+ Components: []component.Message{
+ &component.TextDisplay{
+ Content: fmt.Sprintf("<@&%s> - Niveau %d", r.RoleID, exp.Level(r.XP)),
+ },
+ },
+ Accessory: &component.Button{
+ CustomID: fmt.Sprintf("xp_role_edit_%d", r.ID),
+ Style: component.ButtonStyleSecondary,
+ Label: "Modifier",
+ },
+ })
+ }
+ container.Components = append(container.Components,
+ &component.ActionsRow{
+ Components: []component.Message{
+ &component.Button{
+ CustomID: XpRoleNew,
+ Style: component.ButtonStylePrimary,
+ Label: "Nouveau rôle",
+ },
+ },
+ },
+ &component.Separator{},
+ &component.ActionsRow{
+ Components: []component.Message{
+ &component.Button{CustomID: "config", Style: component.ButtonStyleSecondary, Label: "Retour"},
+ },
+ },
+ )
-func HandleModifyXpRole(_ *discordgo.Session, _ *discordgo.InteractionCreate, _ discordgo.MessageComponentInteractionData, resp *cmd.ResponseBuilder) {
- err := resp.IsEphemeral().
- SetMessage("Action à réaliser").
- SetComponents(component.New().Add(component.NewActionRow().
- Add(component.NewButton(XpRoleAdd, discordgo.PrimaryButton).
- SetLabel("Ajouter").
- SetEmoji(&discordgo.ComponentEmoji{Name: "⬆️"}),
- ).
- Add(component.NewButton(XpRoleEdit, discordgo.SecondaryButton).
- SetLabel("Modifier").
- SetEmoji(&discordgo.ComponentEmoji{Name: "📝"}),
- ).
- Add(component.NewButton(XpRoleDel, discordgo.DangerButton).
- SetLabel("Supprimer").
- SetEmoji(&discordgo.ComponentEmoji{Name: "❌"}),
- ),
- )).Send()
+ response := &interaction.Response{
+ Type: types.InteractionResponseUpdateMessage,
+ Data: &interaction.ResponseData{
+ Components: []component.Component{&container},
+ Flags: channel.MessageFlagsIsComponentsV2,
+ },
+ }
+ err := s.InteractionAPI().Respond(i.Interaction, response)
if err != nil {
- logger.Alert("config/xp_reduce.go - Sending config", err.Error())
+ s.Logger().Error("sending config", "error", err)
}
}
-func HandleXpRoleAddEdit(_ *discordgo.Session, _ *discordgo.InteractionCreate, data discordgo.MessageComponentInteractionData, resp *cmd.ResponseBuilder) {
- cID := XpRoleAddLevel
- if data.CustomID == XpRoleEdit {
- cID = XpRoleEditLevel
- }
- err := resp.IsModal().
- SetTitle("Role").
- SetCustomID(cID).
- SetComponents(component.New().ForModal().Add(component.NewActionRow().ForModal().Add(
- component.NewTextInput(cID, "Niveau", discordgo.TextInputShort).
- SetPlaceholder("5").
- IsRequired().
- SetMinLength(0).
- SetMaxLength(5),
- ))).
- Send()
+func HandleXpRoleNew(
+ s bot.Session,
+ i *event.InteractionCreate,
+ _ *interaction.MessageComponentData,
+ _ *cmd.ResponseBuilder,
+) {
+ one := 1
+ response := &interaction.Response{
+ Type: types.InteractionResponseModal,
+ Data: &interaction.ResponseData{
+ Title: "Nouveau rôle de niveau",
+ CustomID: XpRoleAdd,
+ Components: []component.Component{
+ &component.Label{
+ Label: "Niveau",
+ Component: &component.TextInput{
+ CustomID: "level",
+ Style: component.TextInputShort,
+ Placeholder: "5",
+ MinLength: 1,
+ MaxLength: 5,
+ Required: true,
+ },
+ },
+ &component.Label{
+ Label: "Rôle",
+ Component: &component.SelectMenu{
+ MenuType: types.SelectMenuRole,
+ CustomID: "role",
+ MinValues: &one,
+ MaxValues: one,
+ },
+ },
+ },
+ },
+ }
+ err := s.InteractionAPI().Respond(i.Interaction, response)
if err != nil {
- logger.Alert("config/xp_reduce.go - Sending modal to add/edit", err.Error())
+ s.Logger().Error("sending modal to add", "error", err)
}
}
-func HandleXpRoleAddRole(_ *discordgo.Session, i *discordgo.InteractionCreate, data discordgo.MessageComponentInteractionData, resp *cmd.ResponseBuilder) {
- resp.IsEphemeral()
+func HandleXpRoleEdit(
+ s bot.Session,
+ i *event.InteractionCreate,
+ _ *interaction.MessageComponentData,
+ parameters []string, resp *cmd.ResponseBuilder,
+) {
+ config := GetGuildConfig(i.GuildID)
+ id, err := getRoleLevelID(parameters)
+ if err != nil {
+ s.Logger().Error("reading dynamic CustomID", "error", err)
+ return
+ }
+ _, role := config.FindXpRoleID(id)
+ if role == nil {
+ HandleXpRole(s, i, &interaction.MessageComponentData{}, resp)
+ return
+ }
+
+ roleSelect := &component.SelectMenu{
+ MenuType: types.SelectMenuRole,
+ CustomID: fmt.Sprintf("xp_role_edit_role_%d", id),
+ DefaultValues: []component.SelectMenuDefaultValue{
+ {ID: role.RoleID, Type: types.SelectMenuDefaultValueRole},
+ },
+ }
+
+ container := &component.Container{
+ Components: []component.Message{
+ &component.TextDisplay{Content: "## Configuration / Rôles de niveaux"},
+ &component.Separator{},
+ &component.Section{
+ Components: []component.Message{
+ &component.TextDisplay{Content: fmt.Sprintf("Niveau **%d**", exp.Level(role.XP))},
+ },
+ Accessory: &component.Button{
+ CustomID: fmt.Sprintf("xp_role_edit_level_start_%d", id),
+ Style: component.ButtonStyleSecondary,
+ Label: "Modifier",
+ },
+ },
+ &component.ActionsRow{Components: []component.Message{roleSelect}},
+ &component.ActionsRow{Components: []component.Message{
+ &component.Button{CustomID: fmt.Sprintf("xp_role_del_%d", id), Style: component.ButtonStyleDanger, Label: "Supprimer"},
+ }},
+ &component.Separator{},
+ &component.ActionsRow{Components: []component.Message{
+ &component.Button{Label: "Retour", CustomID: ModifyXpRole, Style: component.ButtonStyleSecondary},
+ }},
+ },
+ }
+
+ response := &interaction.Response{
+ Type: types.InteractionResponseUpdateMessage,
+ Data: &interaction.ResponseData{
+ Components: []component.Component{container},
+ Flags: channel.MessageFlagsIsComponentsV2,
+ },
+ }
+
+ err = s.InteractionAPI().Respond(i.Interaction, response)
+ if err != nil {
+ s.Logger().Error("sending xp_role config", "error", err)
+ }
+}
+
+func HandleXpRoleEditRole(
+ s bot.Session,
+ i *event.InteractionCreate,
+ data *interaction.MessageComponentData,
+ parameters []string, resp *cmd.ResponseBuilder,
+) {
+ id, err := getRoleLevelID(parameters)
+ if err != nil {
+ s.Logger().Error("reading dynamic CustomID", "error", err)
+ return
+ }
+ role := data.Values[0]
cfg := GetGuildConfig(i.GuildID)
- roleId := data.Values[0]
- for _, r := range cfg.XpRoles {
- if r.RoleID == roleId {
- err := resp.SetMessage("Le rôle est déjà présent dans la config").Send()
- if err != nil {
- logger.Alert("config/xp_role.go - Role already in config", err.Error())
- }
- return
+ _, xpRole := cfg.FindXpRoleID(id)
+ if xpRole == nil {
+ err = s.InteractionAPI().Respond(i.Interaction, &interaction.Response{
+ Type: types.InteractionResponseChannelMessageWithSource,
+ Data: &interaction.ResponseData{
+ Flags: channel.MessageFlagsEphemeral,
+ Content: "Impossible de modifier le rôle. Peut-être a-t-il été supprimé ?",
+ },
+ })
+ if err != nil {
+ s.Logger().Error("sending unable to get role message", "error", err)
}
+ return
}
- cfg.XpRoles = append(cfg.XpRoles, XpRole{
- XP: configModifyMap[getKeyConfigRole(i)],
- RoleID: roleId,
- })
- err := cfg.Save()
+ xpRole.RoleID = role
+ err = gokord.DB.Save(xpRole).Error
if err != nil {
- logger.Alert(
- "config/xp_role.go - Saving config",
- err.Error(),
- "guild_id", i.GuildID,
- "role_id", roleId,
- "type", "add",
- )
- }
- if err = resp.SetMessage("Rôle ajouté.").Send(); err != nil {
- logger.Alert("config/xp_role.go - Sending success", err.Error())
+ s.Logger().Error("saving config", "error", err, "guild", i.GuildID, "id", id, "type", "add")
}
+ HandleXpRoleEdit(s, i, &interaction.MessageComponentData{}, parameters, resp)
}
-func HandleXpRoleEditRole(_ *discordgo.Session, i *discordgo.InteractionCreate, data discordgo.MessageComponentInteractionData, resp *cmd.ResponseBuilder) {
- resp.IsEphemeral()
+func HandleXpRoleEditLevelStart(
+ s bot.Session,
+ i *event.InteractionCreate,
+ _ *interaction.MessageComponentData,
+ parameters []string,
+ _ *cmd.ResponseBuilder,
+) {
+ id, err := getRoleLevelID(parameters)
+ if err != nil {
+ s.Logger().Error("reading dynamic CustomID", "error", err)
+ return
+ }
cfg := GetGuildConfig(i.GuildID)
- roleId := data.Values[0]
- _, r := cfg.FindXpRole(roleId)
- if r == nil {
- err := resp.SetMessage("Le rôle n'a pas été trouvé dans la config.").Send()
+ _, xpRole := cfg.FindXpRoleID(id)
+ if xpRole == nil {
+ err = s.InteractionAPI().Respond(i.Interaction, &interaction.Response{
+ Type: types.InteractionResponseChannelMessageWithSource,
+ Data: &interaction.ResponseData{
+ Flags: channel.MessageFlagsEphemeral,
+ Content: "Impossible de trouver le rôle. Peut-être a-t-il été supprimé ?",
+ },
+ })
if err != nil {
- logger.Alert("config/xp_role.go - Role not found (edit)", err.Error())
+ s.Logger().Error("sending unable to get role message", "error", err)
}
return
}
- r.XP = configModifyMap[getKeyConfigRole(i)]
- err := gokord.DB.Save(r).Error
- if err != nil {
- logger.Alert(
- "config/xp_role.go - Saving config",
- err.Error(),
- "guild_id", i.GuildID,
- "role_id", roleId,
- "type", "edit",
- )
+ response := &interaction.Response{
+ Type: types.InteractionResponseModal,
+ Data: &interaction.ResponseData{
+ Title: "Modification du niveau lié au rôle",
+ CustomID: fmt.Sprintf("xp_role_edit_level_%d", id),
+ Components: []component.Component{
+ &component.Label{
+ Label: "Nouveau niveau",
+ Component: &component.TextInput{
+ Style: component.TextInputShort,
+ Required: true,
+ CustomID: "level",
+ MinLength: 1,
+ MaxLength: 5,
+ Placeholder: "5",
+ Value: strconv.FormatUint(uint64(exp.Level(xpRole.XP)), 10),
+ },
+ },
+ },
+ },
}
- if err = resp.SetMessage("Rôle modifié.").Send(); err != nil {
- logger.Alert("config/xp_role.go - Sending success", err.Error())
+ err = s.InteractionAPI().Respond(i.Interaction, response)
+ if err != nil {
+ s.Logger().Error("sending edit level modal", "error", err)
}
}
-func HandleXpRoleDel(_ *discordgo.Session, _ *discordgo.InteractionCreate, _ discordgo.MessageComponentInteractionData, resp *cmd.ResponseBuilder) {
- err := resp.IsEphemeral().
- SetMessage("Rôle à supprimer").
- SetComponents(component.New().Add(component.NewActionRow().Add(component.NewRoleSelect(XpRoleDelRole)))).
- Send()
+func HandleXpRoleEditLevel(
+ s bot.Session,
+ i *event.InteractionCreate,
+ data *interaction.ModalSubmitData,
+ parameters []string,
+ resp *cmd.ResponseBuilder,
+) {
+ id, err := getRoleLevelID(parameters)
if err != nil {
- logger.Alert("config/xp_reduce.go - Sending response to del", err.Error())
+ s.Logger().Error("reading dynamic CustomID", "error", err)
+ return
}
-}
-func HandleXpRoleDelRole(_ *discordgo.Session, i *discordgo.InteractionCreate, data discordgo.MessageComponentInteractionData, resp *cmd.ResponseBuilder) {
- resp.IsEphemeral()
+ fmt.Printf("Alors?... %#v", data.Components)
+ levelInput := data.Components[0].(*component.Label).Component.(*component.TextInput)
+ level, err := strconv.Atoi(levelInput.Value)
+ if err != nil || level < 0 {
+ err = resp.IsEphemeral().
+ SetMessage(
+ fmt.Sprintf("Le niveau doit être un nombre entier positif.\n-# Trouvé : %s", levelInput.Value),
+ ).
+ Send()
+ if err != nil {
+ s.Logger().Error("sending bad number warning message", "error", err)
+ }
+ return
+ }
+ xp := exp.LevelXP(uint(level))
+
cfg := GetGuildConfig(i.GuildID)
- roleId := data.Values[0]
- _, r := cfg.FindXpRole(roleId)
- if r == nil {
- err := resp.SetMessage("Le rôle n'a pas été trouvé dans la config.").Send()
+ _, xpRole := cfg.FindXpRoleID(id)
+ if xpRole == nil {
+ err = s.InteractionAPI().Respond(i.Interaction, &interaction.Response{
+ Type: types.InteractionResponseChannelMessageWithSource,
+ Data: &interaction.ResponseData{
+ Flags: channel.MessageFlagsEphemeral,
+ Content: "Impossible de modifier le rôle. Peut-être a-t-il été supprimé ?",
+ },
+ })
if err != nil {
- logger.Alert("config/xp_role.go - Sending role not found (del)", err.Error())
+ s.Logger().Error("sending unable to modify role message", "error", err)
}
return
}
- err := gokord.DB.Delete(r).Error
+ xpRole.XP = xp
+ err = gokord.DB.Save(xpRole).Error
if err != nil {
- logger.Alert(
- "config/xp_role.go - Deleting entry",
- err.Error(),
- "guild_id", i.GuildID,
- "role_id", roleId,
- "type", "del",
- )
+ s.Logger().Error("saving config", "guild", i.GuildID, "id", id, "type", "edit")
+ }
+ HandleXpRoleEdit(s, i, &interaction.MessageComponentData{}, parameters, resp)
+}
+
+func HandleXpRoleDel(
+ s bot.Session,
+ i *event.InteractionCreate,
+ _ *interaction.MessageComponentData,
+ dynamicValues []string,
+ resp *cmd.ResponseBuilder,
+) {
+ id, err := getRoleLevelID(dynamicValues)
+ if err != nil {
+ s.Logger().Error("reading dynamic CustomID", "error", err)
+ return
+ }
+ cfg := GetGuildConfig(i.GuildID)
+ _, role := cfg.FindXpRoleID(id)
+ if role == nil {
+ err = s.InteractionAPI().Respond(i.Interaction, &interaction.Response{
+ Type: types.InteractionResponseChannelMessageWithSource,
+ Data: &interaction.ResponseData{
+ Content: "Rôle introuvable. Peut-être a-t-il déjà été supprimé ?",
+ Flags: channel.MessageFlagsEphemeral,
+ },
+ })
+ if err != nil {
+ s.Logger().Error("sending role not found message", "error", err)
+ }
+ return
}
- if err = resp.SetMessage("Rôle supprimé.").Send(); err != nil {
- logger.Alert("config/xp_role.go - Sending success", err.Error())
+ err = gokord.DB.Delete(role).Error
+ if err != nil {
+ s.Logger().Error("deleting entry", "error", err, "guild", i.GuildID, "id", id, "type", "del")
}
+
+ HandleXpRole(s, i, &interaction.MessageComponentData{}, resp)
}
-func HandleXpRoleLevel(_ *discordgo.Session, i *discordgo.InteractionCreate, data discordgo.ModalSubmitInteractionData, resp *cmd.ResponseBuilder) {
- resp.IsEphemeral()
- input := data.Components[0].(*discordgo.ActionsRow).Components[0].(*discordgo.TextInput)
+func HandleXpRoleAdd(
+ s bot.Session,
+ i *event.InteractionCreate,
+ data *interaction.ModalSubmitData,
+ resp *cmd.ResponseBuilder,
+) {
+ levelInput := data.Components[0].(*component.Label).Component.(*component.TextInput)
- k := getKeyConfigRole(i)
- in, err := strconv.Atoi(input.Value)
+ in, err := strconv.Atoi(levelInput.Value)
if err != nil || in < 0 {
- if err = resp.
- SetMessage("Impossible de lire le nombre. Il doit s'agit d'un nombre entier positif.").
- Send(); err != nil {
- logger.Alert("command/config.go - Sending bad number", err.Error())
+ err = resp.IsEphemeral().
+ SetMessage(
+ fmt.Sprintf("Le niveau doit être un nombre entier positif.\n-# Trouvé : %s", levelInput.Value),
+ ).
+ Send()
+ if err != nil {
+ s.Logger().Error("sending bad number warning message", "error", err)
}
return
}
- configModifyMap[k] = exp.LevelXP(uint(in))
- go func(i *discordgo.InteractionCreate, k string) {
- time.Sleep(5 * time.Minute)
- delete(configModifyMap, k)
- }(i, k)
+ xp := exp.LevelXP(uint(in))
- cID := XpRoleAddRole
- resp.SetMessage("Rôle à ajouter")
- if data.CustomID == XpRoleEditLevel {
- cID = XpRoleEditRole
- resp.SetMessage("Rôle à modifier")
- }
+ roleId := data.Components[1].(*component.Label).Component.(*component.SelectMenu).Values[0]
- err = resp.
- SetComponents(component.New().Add(component.NewActionRow().Add(component.NewRoleSelect(cID)))).
- Send()
+ cfg := GetGuildConfig(i.GuildID)
+ cfg.XpRoles = append(cfg.XpRoles, XpRole{
+ XP: xp,
+ RoleID: roleId,
+ })
+ err = cfg.Save()
if err != nil {
- logger.Alert("config/xp_reduce.go - Sending response to add/edit", err.Error())
+ s.Logger().Error("saving config", "error", err, "role", roleId, "guild", i.GuildID)
+ return
}
+
+ HandleXpRole(s, i, &interaction.MessageComponentData{}, resp)
}
-func getKeyConfigRole(i *discordgo.InteractionCreate) string {
- return fmt.Sprintf("r:%s:%s", i.GuildID, i.Member.User.ID)
+func getRoleLevelID(dynamic []string) (uint, error) {
+ id64, err := strconv.ParseUint(dynamic[0], 10, 0)
+ if err != nil {
+ return 0, err
+ }
+
+ return uint(id64), nil
}
diff --git a/events.go b/events.go
index 6f15fd8..9a3f508 100644
--- a/events.go
+++ b/events.go
@@ -8,8 +8,9 @@ import (
"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/logger"
- discordgo "github.com/nyttikord/gokord"
+ "github.com/anhgelus/gokord"
+ "github.com/nyttikord/gokord/bot"
+ "github.com/nyttikord/gokord/event"
)
const (
@@ -22,12 +23,12 @@ var (
connectedSince = map[string]int64{}
)
-func OnMessage(s *discordgo.Session, m *discordgo.MessageCreate) {
+func OnMessage(s bot.Session, m *event.MessageCreate) {
if m.Author.Bot {
return
}
cfg := config.GetGuildConfig(m.GuildID)
- if cfg.IsDisabled(m.ChannelID) {
+ if cfg.IsDisabled(s, m.ChannelID) {
return
}
c := user.GetCopaing(m.Author.ID, m.GuildID)
@@ -37,28 +38,29 @@ func OnMessage(s *discordgo.Session, m *discordgo.MessageCreate) {
m.Member.GuildID = m.GuildID
xp := min(exp.MessageXP(uint(len(trimmed)), exp.CalcDiversity(trimmed)), MaxXpPerMessage)
c.AddXP(s, m.Member, xp, func(_ uint, _ uint) {
- if err := s.MessageReactionAdd(m.ChannelID, m.Message.ID, "⬆"); err != nil {
- logger.Alert(
- "events.go - add reaction for new level", err.Error(),
- "channel id", m.ChannelID,
- "message id", m.Message.ID,
+ if err := s.ChannelAPI().MessageReactionAdd(m.ChannelID, m.Message.ID, "⬆"); err != nil {
+ s.Logger().Error(
+ "add reaction for new level",
+ "error", err,
+ "channel", m.ChannelID,
+ "message", m.Message.ID,
)
}
})
}
-func OnVoiceUpdate(s *discordgo.Session, e *discordgo.VoiceStateUpdate) {
+func OnVoiceUpdate(s bot.Session, e *event.VoiceStateUpdate) {
if e.Member.User.Bot {
return
}
cfg := config.GetGuildConfig(e.GuildID)
- if (e.BeforeUpdate == nil || cfg.IsDisabled(e.BeforeUpdate.ChannelID)) && e.ChannelID != "" {
- if cfg.IsDisabled(e.ChannelID) {
+ if (e.BeforeUpdate == nil || cfg.IsDisabled(s, e.BeforeUpdate.ChannelID)) && e.ChannelID != "" {
+ if cfg.IsDisabled(s, e.ChannelID) {
return
}
onConnection(s, e)
- } else if e.BeforeUpdate != nil && (e.ChannelID == "" || cfg.IsDisabled(e.ChannelID)) {
- if cfg.IsDisabled(e.BeforeUpdate.ChannelID) {
+ } else if e.BeforeUpdate != nil && (e.ChannelID == "" || cfg.IsDisabled(s, e.ChannelID)) {
+ if cfg.IsDisabled(s, e.BeforeUpdate.ChannelID) {
return
}
onDisconnect(s, e)
@@ -69,64 +71,61 @@ func genMapKey(guildID string, userID string) string {
return fmt.Sprintf("%s:%s", guildID, userID)
}
-func onConnection(_ *discordgo.Session, e *discordgo.VoiceStateUpdate) {
- logger.Debug("User connected", "username", e.Member.DisplayName())
+func onConnection(s bot.Session, e *event.VoiceStateUpdate) {
+ s.Logger().Debug("user connected", "user", e.Member.DisplayName())
connectedSince[genMapKey(e.GuildID, e.UserID)] = time.Now().Unix()
}
-func onDisconnect(s *discordgo.Session, e *discordgo.VoiceStateUpdate) {
+func onDisconnect(s bot.Session, e *event.VoiceStateUpdate) {
now := time.Now().Unix()
c := user.GetCopaing(e.UserID, e.GuildID)
// check the validity of user
con, ok := connectedSince[genMapKey(e.GuildID, e.UserID)]
if !ok || con == NotConnected {
- logger.Warn(fmt.Sprintf(
- "User %s diconnect from a vocal but was registered as not connected", e.Member.DisplayName(),
- ))
+ s.Logger().Warn(
+ "user disconnect from a vocal but was registered as not connected",
+ "user", e.Member.DisplayName(),
+ )
return
}
timeInVocal := now - con
- logger.Debug("User disconnected", "username", e.Member.DisplayName(), "time in vocal", timeInVocal)
+ s.Logger().Debug("user disconnected", "user", e.Member.DisplayName(), "time in vocal", timeInVocal)
connectedSince[genMapKey(e.GuildID, e.UserID)] = NotConnected
// add exp
if timeInVocal < 0 {
- logger.Alert(
- "events.go - Calculating time spent in vocal", "the time is negative",
- "discord_id", e.UserID,
- "guild_id", e.GuildID,
- )
+ s.Logger().Warn("time spent in vocal is negative", "user", e.Member.DisplayName(), "guild", e.GuildID)
return
}
- if timeInVocal > MaxTimeInVocal {
- logger.Warn(fmt.Sprintf("User %s spent more than 6 hours in vocal", e.Member.DisplayName()))
- timeInVocal = MaxTimeInVocal
- }
+ timeInVocal = min(timeInVocal, MaxTimeInVocal)
e.Member.GuildID = e.GuildID
c.AddXP(s, e.Member, exp.VocalXP(uint(timeInVocal)), func(_ uint, newLevel uint) {
cfg := config.GetGuildConfig(e.GuildID)
if len(cfg.FallbackChannel) == 0 {
return
}
- _, err := s.ChannelMessageSend(cfg.FallbackChannel, fmt.Sprintf(
+ _, err := s.ChannelAPI().MessageSend(cfg.FallbackChannel, fmt.Sprintf(
"%s est maintenant niveau %d", e.Member.Mention(), newLevel,
))
if err != nil {
- logger.Alert("events.go - Sending new level in fallback channel", err.Error())
+ s.Logger().Error("sending new level in fallback channel", "error", err)
}
})
}
-func OnLeave(_ *discordgo.Session, e *discordgo.GuildMemberRemove) {
- logger.Debug("Leave event", "user_id", e.User.ID)
+func OnLeave(s bot.Session, e *event.GuildMemberRemove) {
+ s.Logger().Debug("leave event", "user", e.User.Username)
if e.User.Bot {
return
}
c := user.GetCopaing(e.User.ID, e.GuildID)
- if err := c.Delete(); err != nil {
- logger.Alert(
- "events.go - deleting user from db", err.Error(),
- "user_id", e.User.ID,
- "guild_id", e.GuildID,
- )
+ err := gokord.DB.
+ Where("copaing_id = ? and guild_id = ?", c.ID, e.GuildID).
+ Delete(&user.CopaingXP{}).
+ Error
+ if err != nil {
+ s.Logger().Error("deleting user xp from DB", "user", e.User.Username, "guild", e.GuildID)
+ }
+ if err = c.Delete(); err != nil {
+ s.Logger().Error("deleting user from DB", "user", e.User.Username, "guild", e.GuildID)
}
}
diff --git a/exp/functions.go b/exp/functions.go
index 2608094..681c135 100644
--- a/exp/functions.go
+++ b/exp/functions.go
@@ -11,6 +11,8 @@ import (
"github.com/anhgelus/gokord"
)
+const DebugFactor = 30
+
func MessageXP(length uint, diversity uint) uint {
return uint(math.Floor(
0.025*math.Pow(float64(length), 1.25)*math.Sqrt(float64(diversity)) + 1,
@@ -33,12 +35,26 @@ func VocalXP(time uint) uint {
))
}
+type LevelScale struct{}
+
+func (LevelScale) Normalize(min, max, x float64) float64 {
+ if min < 0 || max < 0 || x < 0 {
+ panic("Values must be positive or null for a level scale.")
+ }
+ levelMin := LevelExact(min)
+ return (LevelExact(x) - levelMin) / (LevelExact(max) - levelMin)
+}
+
// Level gives the level with the given XP.
// See LevelXP to get the XP required to get a level.
func Level(xp uint) uint {
- return uint(math.Floor(
- 0.2 * math.Sqrt(float64(xp)),
- ))
+ return uint(math.Floor(LevelExact(float64(xp))))
+}
+
+// LevelExact gives the exact level with the given XP.
+// See Level to get the floored level.
+func LevelExact(xp float64) float64 {
+ return 0.2 * math.Sqrt(xp)
}
// LevelXP gives the XP required to get this level.
@@ -53,7 +69,7 @@ func LevelXP(level uint) uint {
func TimeStampNDaysBefore(n uint) string {
var unix time.Time
if gokord.Debug {
- unix = time.Unix(time.Now().Unix()-int64(n), 0) // reduce time for debug
+ unix = time.Unix(time.Now().Unix()-int64(DebugFactor*n), 0) // reduce time for debug
} else {
unix = time.Unix(time.Now().Unix()-int64(n*24*60*60), 0)
}
diff --git a/go.mod b/go.mod
index 540e6d8..8e1ebe9 100644
--- a/go.mod
+++ b/go.mod
@@ -1,16 +1,19 @@
module git.anhgelus.world/anhgelus/les-copaings-bot
-go 1.24
+go 1.24.0
+
+toolchain go1.24.6
require (
- github.com/anhgelus/gokord v0.11.1-0.20250904142107-2d0e3c982bc3
+ github.com/anhgelus/gokord v0.12.1-0.20250927153335-e73ec7a067f2
github.com/jackc/pgx/v5 v5.7.5
github.com/joho/godotenv v1.5.1
- github.com/nyttikord/gokord v0.30.0
+ github.com/nyttikord/gokord v0.31.1-0.20250927153129-9bed7a4b26bf
github.com/pelletier/go-toml/v2 v2.2.4
+ golang.org/x/image v0.30.0
gonum.org/v1/plot v0.16.0
gorm.io/driver/postgres v1.6.0
- gorm.io/gorm v1.30.3
+ gorm.io/gorm v1.31.0
)
require (
@@ -30,10 +33,9 @@ require (
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
- github.com/redis/go-redis/v9 v9.13.0 // indirect
- golang.org/x/crypto v0.41.0 // indirect
- golang.org/x/image v0.30.0 // indirect
- golang.org/x/sync v0.16.0 // indirect
- golang.org/x/sys v0.35.0 // indirect
- golang.org/x/text v0.28.0 // indirect
+ github.com/redis/go-redis/v9 v9.15.0 // indirect
+ golang.org/x/crypto v0.42.0 // indirect
+ golang.org/x/sync v0.17.0 // indirect
+ golang.org/x/sys v0.36.0 // indirect
+ golang.org/x/text v0.29.0 // indirect
)
diff --git a/go.sum b/go.sum
index 6b5401a..80c514d 100644
--- a/go.sum
+++ b/go.sum
@@ -17,8 +17,8 @@ github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm
github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk=
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw=
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM=
-github.com/anhgelus/gokord v0.11.1-0.20250904142107-2d0e3c982bc3 h1:j7Im8+Vd7BIhBPQKsK5Hs2BsyO71Q3/XVxgZntNJa5M=
-github.com/anhgelus/gokord v0.11.1-0.20250904142107-2d0e3c982bc3/go.mod h1:Tw1djmUOTFCIRhEUI7eG2onnreJbVGA40EWYQgj20bE=
+github.com/anhgelus/gokord v0.12.1-0.20250927153335-e73ec7a067f2 h1:ousL4ksXhHtIhAiBvzu4yH1whXe+m3quo86ON8X8M/E=
+github.com/anhgelus/gokord v0.12.1-0.20250927153335-e73ec7a067f2/go.mod h1:Mk9EPnkl8xlYV+nTxO1peU+206vI4biguUGp4/4U/AU=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
@@ -51,14 +51,14 @@ github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
-github.com/nyttikord/gokord v0.30.0 h1:O4LhpCMyfWcgLqziPXQgMCopb9VO7kwcZG3phblOaTA=
-github.com/nyttikord/gokord v0.30.0/go.mod h1:Lhk268VlZ1W6Pb3kYnlU9bIuTCioaumedjHdtw1sxck=
+github.com/nyttikord/gokord v0.31.1-0.20250927153129-9bed7a4b26bf h1:d7saDF/eDtu95IgE4sfVoHdBQmqdvyJrc73mG4HoTJU=
+github.com/nyttikord/gokord v0.31.1-0.20250927153129-9bed7a4b26bf/go.mod h1:Oi0y5sfiYa+hVuV5ZSJ9UMWAGkcaLOhM7xB1TiCdX3U=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/redis/go-redis/v9 v9.13.0 h1:PpmlVykE0ODh8P43U0HqC+2NXHXwG+GUtQyz+MPKGRg=
-github.com/redis/go-redis/v9 v9.13.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
+github.com/redis/go-redis/v9 v9.15.0 h1:2jdes0xJxer4h3NUZrZ4OGSntGlXp4WbXju2nOTRXto=
+github.com/redis/go-redis/v9 v9.15.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -68,8 +68,8 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
-golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
+golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
+golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/image v0.30.0 h1:jD5RhkmVAnjqaCUXfbGBrn3lpxbknfN9w2UhHHU+5B4=
golang.org/x/image v0.30.0/go.mod h1:SAEUTxCCMWSrJcCy/4HwavEsfZZJlYxeHLc6tTiAe/c=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
@@ -78,18 +78,18 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
-golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
+golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
-golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
+golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
-golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
+golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
+golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
@@ -106,8 +106,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
-gorm.io/gorm v1.30.3 h1:QiG8upl0Sg9ba2Zatfjy0fy4It2iNBL2/eMdvEkdXNs=
-gorm.io/gorm v1.30.3/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
+gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY=
+gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las=
rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
diff --git a/main.go b/main.go
index facf243..56753e1 100644
--- a/main.go
+++ b/main.go
@@ -2,19 +2,30 @@ package main
import (
_ "embed"
+ "encoding/json"
"errors"
"flag"
+ "log/slog"
"os"
+ "regexp"
"time"
"git.anhgelus.world/anhgelus/les-copaings-bot/commands"
"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/joho/godotenv"
discordgo "github.com/nyttikord/gokord"
+ "github.com/nyttikord/gokord/bot"
+ "github.com/nyttikord/gokord/discord"
+ "github.com/nyttikord/gokord/discord/types"
+ "github.com/nyttikord/gokord/event"
+ "github.com/nyttikord/gokord/interaction"
+ "golang.org/x/image/font/opentype"
+ "gonum.org/v1/plot"
+ "gonum.org/v1/plot/font"
)
var (
@@ -30,12 +41,86 @@ var (
stopPeriodicReducer chan<- interface{}
)
+//go:embed assets/inter-variable.ttf
+var interTTF []byte
+
func init() {
err := godotenv.Load()
if err != nil && !errors.Is(err, os.ErrNotExist) {
- logger.Warn("Error while loading .env file", "error", err.Error())
+ slog.Error("error while loading .env file", "error", err)
}
flag.StringVar(&token, "token", os.Getenv("TOKEN"), "token of the bot")
+
+ // Use a nicer font
+ fontTTF, parseErr := opentype.Parse(interTTF)
+ if parseErr != nil {
+ panic(err)
+ }
+ inter := font.Font{Typeface: "Inter"}
+ font.DefaultCache.Add(
+ []font.Face{
+ {
+ Font: inter,
+ Face: fontTTF,
+ },
+ })
+ plot.DefaultFont = inter
+
+}
+
+func handleDynamicMessageComponent(
+ b *gokord.Bot,
+ handler func(
+ bot.Session,
+ *event.InteractionCreate,
+ *interaction.MessageComponentData,
+ []string, *cmd.ResponseBuilder,
+ ),
+ pattern string,
+) {
+ compiledPattern := regexp.MustCompile(pattern)
+ b.AddHandler(func(s bot.Session, i *event.InteractionCreate) {
+ if i.Type != types.InteractionMessageComponent {
+ return
+ }
+
+ data := i.MessageComponentData()
+ parameters := compiledPattern.FindStringSubmatch(data.CustomID)
+ if parameters == nil {
+ return
+ }
+ parameters = parameters[1:]
+ handler(s, i, data, parameters, cmd.NewResponseBuilder(s, i))
+ })
+}
+
+func handleDynamicModalComponent(
+ b *gokord.Bot,
+ handler func(
+ bot.Session,
+ *event.InteractionCreate,
+ *interaction.ModalSubmitData,
+ []string,
+ *cmd.ResponseBuilder,
+ ),
+ pattern string,
+) {
+ compiledPattern := regexp.MustCompile(pattern)
+ b.AddHandler(func(s bot.Session, i *event.InteractionCreate) {
+ if i.Type != types.InteractionModalSubmit {
+ return
+ }
+
+ data := i.ModalSubmitData()
+ content, _ := json.Marshal(data)
+ s.Logger().Debug(string(content))
+ parameters := compiledPattern.FindStringSubmatch(data.CustomID)
+ if parameters == nil {
+ return
+ }
+ parameters = parameters[1:]
+ handler(s, i, data, parameters, cmd.NewResponseBuilder(s, i))
+ })
}
func main() {
@@ -55,7 +140,7 @@ func main() {
rankCmd := cmd.New("rank", "Affiche le niveau d'un copaing").
AddOption(cmd.NewOption(
- discordgo.ApplicationCommandOptionUser,
+ types.CommandOptionUser,
"copaing",
"Le niveau du Copaing que vous souhaitez obtenir",
)).
@@ -63,7 +148,7 @@ func main() {
configCmd := cmd.New("config", "Modifie la config").
SetPermission(&adm).
- SetHandler(commands.Config)
+ SetHandler(commands.ConfigCommand)
topCmd := cmd.New("top", "Copaings les plus actifs").
SetHandler(commands.Top)
@@ -74,7 +159,7 @@ func main() {
resetUserCmd := cmd.New("reset-user", "Reset l'xp d'un utilisation").
AddOption(cmd.NewOption(
- discordgo.ApplicationCommandOptionUser,
+ types.CommandOptionUser,
"user",
"Copaing a reset",
).IsRequired()).
@@ -86,12 +171,12 @@ func main() {
statsCmd := cmd.New("stats", "Affiche des stats :D").
AddOption(cmd.NewOption(
- discordgo.ApplicationCommandOptionInteger,
+ types.CommandOptionInteger,
"days",
"Nombre de jours à afficher dans le graphique",
)).
AddOption(cmd.NewOption(
- discordgo.ApplicationCommandOptionUser,
+ types.CommandOptionUser,
"user",
"Utilisateur à inspecter",
)).
@@ -102,7 +187,7 @@ func main() {
panic(err)
}
- bot := gokord.Bot{
+ b := gokord.Bot{
Token: token,
Status: []*gokord.Status{
{
@@ -134,67 +219,59 @@ func main() {
AfterInit: func(dg *discordgo.Session) {
d := 24 * time.Hour
if gokord.Debug {
- d = 24 * time.Second
+ d = 3 * exp.DebugFactor * time.Second
}
user.PeriodicReducer(dg)
stopPeriodicReducer = gokord.NewTimer(d, func(stop chan<- interface{}) {
- logger.Debug("Periodic reducer")
+ dg.Logger().Debug("periodic reducer")
user.PeriodicReducer(dg)
})
},
Innovations: innovations,
Version: &Version,
- Intents: discordgo.IntentsAllWithoutPrivileged |
- discordgo.IntentsMessageContent |
- discordgo.IntentGuildMembers,
+ Intents: discord.IntentsAllWithoutPrivileged |
+ discord.IntentsMessageContent |
+ discord.IntentGuildMembers,
}
// interaction: /config
- bot.HandleMessageComponent(func(s *discordgo.Session, i *discordgo.InteractionCreate, data discordgo.MessageComponentInteractionData, resp *cmd.ResponseBuilder) {
- if len(data.Values) != 1 {
- logger.Alert("main.go - Handle config modify", "invalid data values", "values", data.Values)
- return
- }
- switch data.Values[0] {
- case config.ModifyXpRole:
- config.HandleModifyXpRole(s, i, data, resp)
- case config.ModifyFallbackChannel:
- config.HandleModifyFallbackChannel(s, i, data, resp)
- case config.ModifyDisChannel:
- config.HandleModifyDisChannel(s, i, data, resp)
- case config.ModifyTimeReduce:
- config.HandleModifyPeriodicReduce(s, i, data, resp)
- default:
- logger.Alert("main.go - Detecting value", "unkown value", "value", data.Values[0])
- return
- }
- }, commands.ConfigModify)
+ b.HandleMessageComponent(commands.ConfigMessageComponent, commands.OpenConfig)
// xp role related
- bot.HandleMessageComponent(config.HandleXpRoleAddEdit, config.XpRoleAdd)
- bot.HandleMessageComponent(config.HandleXpRoleAddEdit, config.XpRoleEdit)
- bot.HandleMessageComponent(config.HandleXpRoleAddRole, config.XpRoleAddRole)
- bot.HandleMessageComponent(config.HandleXpRoleEditRole, config.XpRoleEditRole)
- bot.HandleMessageComponent(config.HandleXpRoleDel, config.XpRoleDel)
- bot.HandleMessageComponent(config.HandleXpRoleDelRole, config.XpRoleDelRole)
- bot.HandleModal(config.HandleXpRoleLevel, config.XpRoleAddLevel)
- bot.HandleModal(config.HandleXpRoleLevel, config.XpRoleEditLevel)
+ b.HandleMessageComponent(config.HandleXpRole, config.ModifyXpRole)
+ b.HandleMessageComponent(config.HandleXpRoleNew, config.XpRoleNew)
+ b.HandleModal(config.HandleXpRoleAdd, config.XpRoleAdd)
+ handleDynamicMessageComponent(&b, config.HandleXpRoleEdit, config.XpRoleEditPattern)
+ handleDynamicMessageComponent(&b, config.HandleXpRoleEditRole, config.XpRoleEditRolePattern)
+ handleDynamicMessageComponent(&b, config.HandleXpRoleEditLevelStart, config.XpRoleEditLevelStartPattern)
+ handleDynamicModalComponent(&b, config.HandleXpRoleEditLevel, config.XpRoleEditLevelPattern)
+ handleDynamicMessageComponent(&b, config.HandleXpRoleDel, config.XpRoleDel)
// channel related
- bot.HandleMessageComponent(config.HandleFallbackChannelSet, config.FallbackChannelSet)
- bot.HandleMessageComponent(config.HandleDisChannel, config.DisChannelAdd)
- bot.HandleMessageComponent(config.HandleDisChannel, config.DisChannelDel)
- bot.HandleMessageComponent(config.HandleDisChannelAddSet, config.DisChannelAddSet)
- bot.HandleMessageComponent(config.HandleDisChannelDelSet, config.DisChannelDelSet)
+ b.HandleMessageComponent(func(s bot.Session, i *event.InteractionCreate, data *interaction.MessageComponentData, resp *cmd.ResponseBuilder) {
+ if config.HandleModifyFallbackChannel(s, i, data, resp) {
+ commands.ConfigMessageComponent(s, i, data, resp)
+ }
+ }, config.ModifyFallbackChannel)
+ b.HandleMessageComponent(func(s bot.Session, i *event.InteractionCreate, data *interaction.MessageComponentData, resp *cmd.ResponseBuilder) {
+ if config.HandleModifyDisChannel(s, i, data, resp) {
+ commands.ConfigMessageComponent(s, i, data, resp)
+ }
+ }, config.ModifyDisChannel)
// reduce related
- bot.HandleModal(config.HandleTimeReduceSet, config.TimeReduceSet)
+ b.HandleMessageComponent(config.HandleModifyPeriodicReduceCommand, config.ModifyTimeReduce)
+ b.HandleModal(func(s bot.Session, i *event.InteractionCreate, data *interaction.ModalSubmitData, resp *cmd.ResponseBuilder) {
+ if config.HandleTimeReduceSet(s, i, data, resp) {
+ commands.ConfigModal(s, i, data, resp)
+ }
+ }, config.TimeReduceSet)
// xp handlers
- bot.AddHandler(OnMessage)
- bot.AddHandler(OnVoiceUpdate)
- bot.AddHandler(OnLeave)
+ b.AddHandler(OnMessage)
+ b.AddHandler(OnVoiceUpdate)
+ b.AddHandler(OnLeave)
- bot.Start()
+ b.Start()
if stopPeriodicReducer != nil {
stopPeriodicReducer <- true
diff --git a/updates.json b/updates.json
index 452a306..81e8917 100644
--- a/updates.json
+++ b/updates.json
@@ -40,5 +40,14 @@
"config"
]
}
+ },
+ {
+ "version": "3.3.0",
+ "commands": {
+ "added": [],
+ "removed": [],
+ "updated": []
+ },
+ "changelog": "Nouvelle config très stylée par @ascpial (merci à lui) !\nAjout du role react (enfin), toujours par @ascpial (encore merci à lui) !\nAmélioration de la commande stats, par @anhgelus et @ascpial.\nRéécriture d'une bonne partie du bot pour remplacer discordgo par [gokord](<https://github.com/nyttikord/gokord>).\n\nLa prochaine version devrait arriver prochainement. Elle touchera surtout à du code en interne peu visible par les utilisateurs et les administrateurs."
}
]
diff --git a/user/level.go b/user/level.go
index be7d212..4303b47 100644
--- a/user/level.go
+++ b/user/level.go
@@ -8,71 +8,70 @@ import (
"git.anhgelus.world/anhgelus/les-copaings-bot/config"
"git.anhgelus.world/anhgelus/les-copaings-bot/exp"
"github.com/anhgelus/gokord"
- "github.com/anhgelus/gokord/logger"
discordgo "github.com/nyttikord/gokord"
+ "github.com/nyttikord/gokord/bot"
+ "github.com/nyttikord/gokord/user"
)
-func onNewLevel(dg *discordgo.Session, m *discordgo.Member, level uint) {
+func onNewLevel(s bot.Session, m *user.Member, level uint) {
cfg := config.GetGuildConfig(m.GuildID)
xpForLevel := exp.LevelXP(level)
for _, role := range cfg.XpRoles {
if role.XP <= xpForLevel && !slices.Contains(m.Roles, role.RoleID) {
- logger.Debug(
- "Add role",
- "role_id", role.RoleID,
- "user_id", m.User.ID,
- "guild_id", m.GuildID,
- )
- err := dg.GuildMemberRoleAdd(m.GuildID, m.User.ID, role.RoleID)
+ s.Logger().Debug("add role", "role", role.RoleID, "user", m.DisplayName(), "guild", m.GuildID)
+ err := s.GuildAPI().MemberRoleAdd(m.GuildID, m.User.ID, role.RoleID)
if err != nil {
- logger.Alert("user/level.go - Adding role", err.Error(), "role_id", role.RoleID)
+ s.Logger().Error(
+ "adding role",
+ "error", err,
+ "role", role.RoleID,
+ "user", m.DisplayName(),
+ "guild", m.GuildID,
+ )
}
} else if role.XP > xpForLevel && slices.Contains(m.Roles, role.RoleID) {
- logger.Debug(
- "Remove role",
- "role_id", role.RoleID,
- "user_id", m.User.ID,
- "guild_id", m.GuildID,
- )
- err := dg.GuildMemberRoleRemove(m.GuildID, m.User.ID, role.RoleID)
+ s.Logger().Debug("remove role", "role", role.RoleID, "user", m.DisplayName(), "guild", m.GuildID)
+ err := s.GuildAPI().MemberRoleRemove(m.GuildID, m.User.ID, role.RoleID)
if err != nil {
- logger.Alert("user/level.go - Removing role", err.Error(), "role_id", role.RoleID)
+ s.Logger().Error(
+ "removing role",
+ "error", err,
+ "role", role.RoleID,
+ "user", m.DisplayName(),
+ "guild", m.GuildID,
+ )
}
}
}
}
-func (c *Copaing) OnNewLevel(dg *discordgo.Session, level uint) {
- m, err := dg.GuildMember(c.GuildID, c.DiscordID)
+func (c *Copaing) OnNewLevel(s *discordgo.Session, level uint) {
+ m, err := s.GuildAPI().Member(c.GuildID, c.DiscordID)
if err != nil {
- logger.Alert(
- "user/level.go - Getting member for new level", err.Error(),
- "discord_id", c.DiscordID,
- "guild_id", c.GuildID,
- )
+ s.Logger().Error("getting member for new level", "error", err, "user", c.DiscordID, "guild", c.GuildID)
return
}
- onNewLevel(dg, m, level)
+ onNewLevel(s, m, level)
}
-func PeriodicReducer(dg *discordgo.Session) {
+func PeriodicReducer(s *discordgo.Session) {
wg := &sync.WaitGroup{}
var cs []*Copaing
if err := gokord.DB.Find(&cs).Error; err != nil {
- logger.Alert("user/level.go - Fetching all copaings", err.Error())
+ s.Logger().Error("fetching all copaings", "error", err)
return
}
cxps := make([]*cXP, len(cs))
for i, c := range cs {
- if i%10 == 9 {
+ if i%25 == 24 {
wg.Wait() // prevents spamming the DB
}
wg.Add(1)
go func() {
defer wg.Done()
- xp, err := c.GetXP()
+ xp, err := c.GetXP(s.Logger())
if err != nil {
- logger.Alert("user/level.go - Getting XP", err.Error(), "copaing_id", c.ID, "guild_id", c.GuildID)
+ s.Logger().Error("getting xp", "error", err, "copaing", c.ID, "guild", c.GuildID)
xp = 0
}
cxps[i] = &cXP{
@@ -82,7 +81,9 @@ func PeriodicReducer(dg *discordgo.Session) {
}()
}
wg.Wait()
- for _, g := range dg.State.Guilds {
+ i := 0
+ for g := range s.GuildAPI().State.Guilds() {
+ i++
wg.Add(1)
go func() {
defer wg.Done()
@@ -92,26 +93,26 @@ func PeriodicReducer(dg *discordgo.Session) {
Where("guild_id = ? and created_at < ?", g.ID, exp.TimeStampNDaysBefore(cfg.DaysXPRemains)).
Delete(&CopaingXP{})
if res.Error != nil {
- logger.Alert("user/level.go - Removing old XP", res.Error.Error(), "guild_id", g.ID)
+ s.Logger().Error("removing old xp", "error", res.Error, "guild", g.ID)
}
- logger.Debug("Guild cleaned", "guild", g.Name, "rows affected", res.RowsAffected)
+ s.Logger().Debug("guild cleaned", "guild", g.Name, "rows affected", res.RowsAffected)
}()
}
wg.Wait()
for i, c := range cxps {
if i%50 == 49 {
- logger.Debug("Sleeping...")
+ s.Logger().Debug("sleeping...")
time.Sleep(15 * time.Second) // prevents spamming the API
}
oldXp := c.GetXP()
- xp, err := c.ToCopaing().GetXP()
+ xp, err := c.ToCopaing().GetXP(s.Logger())
if err != nil {
- logger.Alert("user/level.go - Getting XP", err.Error(), "guild_id", c.ID, "discord_id", c.DiscordID)
+ s.Logger().Error("getting xp of copaing", "error", err, "copaing", c.ID, "guild", c.GuildID)
continue
}
if exp.Level(oldXp) != exp.Level(xp) {
- c.OnNewLevel(dg, exp.Level(xp))
+ c.OnNewLevel(s, exp.Level(xp))
}
}
- logger.Debug("Periodic reduce finished", "len(guilds)", len(dg.State.Guilds))
+ s.Logger().Debug("periodic reduce finished", "guilds affected", i)
}
diff --git a/user/member.go b/user/member.go
index ad8762f..9068a6f 100644
--- a/user/member.go
+++ b/user/member.go
@@ -26,11 +26,6 @@ type CopaingAccess interface {
GetXP() uint
}
-const (
- LastEvent = "last_event"
- AlreadyRemoved = "already_removed"
-)
-
func GetCopaing(discordID string, guildID string) *Copaing {
c := Copaing{DiscordID: discordID, GuildID: guildID}
if err := c.Load(); err != nil {
diff --git a/user/xp.go b/user/xp.go
index 606598c..6b0f6cc 100644
--- a/user/xp.go
+++ b/user/xp.go
@@ -1,6 +1,7 @@
package user
import (
+ "log/slog"
"math"
"slices"
"sync"
@@ -8,8 +9,8 @@ import (
"git.anhgelus.world/anhgelus/les-copaings-bot/config"
"git.anhgelus.world/anhgelus/les-copaings-bot/exp"
"github.com/anhgelus/gokord"
- "github.com/anhgelus/gokord/logger"
- discordgo "github.com/nyttikord/gokord"
+ "github.com/nyttikord/gokord/bot"
+ "github.com/nyttikord/gokord/user"
)
type cXP struct {
@@ -25,19 +26,19 @@ func (c *cXP) GetXP() uint {
return c.Cxp
}
-func (c *Copaing) AddXP(s *discordgo.Session, m *discordgo.Member, xp uint, fn func(uint, uint)) {
- old, err := c.GetXP()
+func (c *Copaing) AddXP(s bot.Session, m *user.Member, xp uint, fn func(uint, uint)) {
+ old, err := c.GetXP(s.Logger())
if err != nil {
- logger.Alert("user/xp.go - Getting xp", err.Error(), "discord_id", c.DiscordID, "guild_id", c.GuildID)
+ s.Logger().Error("getting xp", "error", err, "user", m.DisplayName(), "guild", c.GuildID)
return
}
pastLevel := exp.Level(old)
- logger.Debug("Adding xp", "member", m.DisplayName(), "old xp", old, "xp to add", xp, "old level", pastLevel)
+ s.Logger().Debug("adding xp", "member", m.DisplayName(), "old xp", old, "xp to add", xp, "old level", pastLevel)
c.CopaingXPs = append(c.CopaingXPs, CopaingXP{CopaingID: c.ID, XP: uint(math.Floor(float64(xp) * c.GetBoost(m))), GuildID: c.GuildID})
if err = c.Save(); err != nil {
- logger.Alert(
- "user/xp.go - Saving user",
- err.Error(),
+ s.Logger().Error(
+ "saving user",
+ "error", err.Error(),
"xp", c.CopaingXPs,
"discord_id", c.DiscordID,
"guild_id", c.GuildID,
@@ -51,12 +52,12 @@ func (c *Copaing) AddXP(s *discordgo.Session, m *discordgo.Member, xp uint, fn f
}
}
-func (c *Copaing) GetXP() (uint, error) {
+func (c *Copaing) GetXP(logger *slog.Logger) (uint, error) {
cfg := config.GetGuildConfig(c.GuildID)
- return c.GetXPForDays(cfg.DaysXPRemains)
+ return c.GetXPForDays(logger, cfg.DaysXPRemains)
}
-func (c *Copaing) GetXPForDays(n uint) (uint, error) {
+func (c *Copaing) GetXPForDays(logger *slog.Logger, n uint) (uint, error) {
xp := uint(0)
rows, err := gokord.DB.
Model(&CopaingXP{}).
@@ -67,15 +68,15 @@ func (c *Copaing) GetXPForDays(n uint) (uint, error) {
c.ID,
).
Rows()
- defer rows.Close()
if err != nil {
return 0, err
}
+ defer rows.Close()
for rows.Next() {
var cxp CopaingXP
err = gokord.DB.ScanRows(rows, &cxp)
if err != nil {
- logger.Alert("user/xp.go - Scanning rows", err.Error(), "copaing_id", c.ID, "guild_id", c.GuildID)
+ logger.Error("scanning rows", "error", err, "copaing", c.ID, "guild", c.GuildID)
continue
}
xp += cxp.XP
@@ -83,7 +84,7 @@ func (c *Copaing) GetXPForDays(n uint) (uint, error) {
return xp, nil
}
-func (c *Copaing) GetBoost(m *discordgo.Member) float64 {
+func (c *Copaing) GetBoost(m *user.Member) float64 {
boost := 1.0
if m.PremiumSince != nil {
boost = max(boost, 2.0)
@@ -103,31 +104,31 @@ func (c *Copaing) GetBoost(m *discordgo.Member) float64 {
// GetBestXP returns n Copaing with the best XP within d days (d <= cfg.DaysXPRemain; d < 0 <=> d = cfg.DaysXPRemain)
//
// This function is slow
-func GetBestXP(guildId string, n uint, d int) ([]CopaingAccess, error) {
+func GetBestXP(logger *slog.Logger, guildId string, n uint, d int) ([]CopaingAccess, error) {
if d < 0 {
cfg := config.GetGuildConfig(guildId)
d = int(cfg.DaysXPRemains)
}
rows, err := gokord.DB.Model(&Copaing{}).Where("guild_id = ?", guildId).Rows()
- defer rows.Close()
if err != nil {
return nil, err
}
+ defer rows.Close()
var l []*cXP
wg := sync.WaitGroup{}
for rows.Next() {
var c Copaing
err = gokord.DB.ScanRows(rows, &c)
if err != nil {
- logger.Alert("user/xp.go - Scanning rows", err.Error(), "guild_id", guildId)
+ logger.Error("scanning rows", "error", err, "copaing", c.ID, "guild", c.GuildID)
continue
}
wg.Add(1)
go func() {
defer wg.Done()
- xp, err := c.GetXPForDays(uint(d))
+ xp, err := c.GetXPForDays(logger, uint(d))
if err != nil {
- logger.Alert("user/xp.go - Fetching xp", err.Error(), "discord_id", c.DiscordID, "guild_id", guildId)
+ logger.Error("fetching xp", "error", err, "copaing", c.ID, "guild", c.GuildID)
return
}
l = append(l, &cXP{Cxp: xp, Copaing: &c})