From c408afc8797b0da5e1d73d190a8f5884870b510c Mon Sep 17 00:00:00 2001 From: Anhgelus Morhtuuzh Date: Tue, 13 May 2025 12:50:20 +0200 Subject: style(files): reorganize everything --- commands/config.go | 8 +- commands/rank.go | 17 +-- commands/reset.go | 14 +-- commands/top.go | 13 +-- config/guild.go | 4 +- config/redis.go | 28 +++++ events.go | 173 +++++++++++++++++++++++++++++++ exp/functions.go | 45 ++++++++ main.go | 26 ++--- user/level.go | 207 +++++++++++++++++++++++++++++++++++++ user/member.go | 107 +++++++++++++++++++ user/xp.go | 185 +++++++++++++++++++++++++++++++++ xp/events.go | 171 ------------------------------- xp/functions.go | 41 -------- xp/level.go | 206 ------------------------------------- xp/member.go | 296 ----------------------------------------------------- 16 files changed, 787 insertions(+), 754 deletions(-) create mode 100644 config/redis.go create mode 100644 events.go create mode 100644 exp/functions.go create mode 100644 user/level.go create mode 100644 user/member.go create mode 100644 user/xp.go delete mode 100644 xp/events.go delete mode 100644 xp/functions.go delete mode 100644 xp/level.go delete mode 100644 xp/member.go diff --git a/commands/config.go b/commands/config.go index 1e680c7..32da9c6 100644 --- a/commands/config.go +++ b/commands/config.go @@ -5,7 +5,7 @@ import ( "github.com/anhgelus/gokord" "github.com/anhgelus/gokord/utils" "github.com/anhgelus/les-copaings-bot/config" - "github.com/anhgelus/les-copaings-bot/xp" + "github.com/anhgelus/les-copaings-bot/exp" "github.com/bwmarrin/discordgo" "strings" ) @@ -17,9 +17,9 @@ func ConfigShow(s *discordgo.Session, i *discordgo.InteractionCreate) { l := len(cfg.XpRoles) - 1 for i, r := range cfg.XpRoles { if i == l { - roles += fmt.Sprintf("> Niveau %d - <@&%s>", xp.Level(r.XP), r.RoleID) + roles += fmt.Sprintf("> Niveau %d - <@&%s>", exp.Level(r.XP), r.RoleID) } else { - roles += fmt.Sprintf("> Niveau %d - <@&%s>\n", xp.Level(r.XP), r.RoleID) + roles += fmt.Sprintf("> Niveau %d - <@&%s>\n", exp.Level(r.XP), r.RoleID) } } if len(roles) == 0 { @@ -98,7 +98,7 @@ func ConfigXP(s *discordgo.Session, i *discordgo.InteractionCreate) { } return } - exp := xp.XPForLevel(uint(level)) + exp := exp.LevelXP(uint(level)) r, ok := optMap["role"] if !ok { err := resp.Message("Le rôle n'a pas été renseigné.").Send() diff --git a/commands/rank.go b/commands/rank.go index 4d6f817..c659f2b 100644 --- a/commands/rank.go +++ b/commands/rank.go @@ -3,19 +3,20 @@ package commands import ( "fmt" "github.com/anhgelus/gokord/utils" - "github.com/anhgelus/les-copaings-bot/xp" + "github.com/anhgelus/les-copaings-bot/exp" + "github.com/anhgelus/les-copaings-bot/user" "github.com/bwmarrin/discordgo" ) func Rank(s *discordgo.Session, i *discordgo.InteractionCreate) { optMap := utils.GenerateOptionMap(i) - c := xp.GetCopaing(i.Member.User.ID, i.GuildID) // current copaing = member who used /rank - xp.LastEventUpdate(s, c) // update xp and reset last event + c := user.GetCopaing(i.Member.User.ID, i.GuildID) // current user = member who used /rank + user.LastEventUpdate(s, c) // update exp and reset last event msg := "Votre niveau" m := i.Member var err error resp := utils.ResponseBuilder{C: s, I: i} - if v, ok := optMap["copaing"]; ok { + if v, ok := optMap["user"]; ok { u := v.UserValue(s) if u.Bot { err = resp.Message("Imagine si les bots avaient un niveau :rolling_eyes:").IsEphemeral().Send() @@ -39,12 +40,12 @@ func Rank(s *discordgo.Session, i *discordgo.InteractionCreate) { } return } - c = xp.GetCopaing(u.ID, i.GuildID) // current copaing = member targeted by member who wrote /rank - xp.XPUpdate(s, c) // update xp without resetting event + c = user.GetCopaing(u.ID, i.GuildID) // current user = member targeted by member who wrote /rank + user.UpdateXP(s, c) // update exp without resetting event msg = fmt.Sprintf("Le niveau de %s", m.DisplayName()) } - lvl := xp.Level(c.XP) - nxtLvlXP := xp.XPForLevel(lvl + 1) + lvl := exp.Level(c.XP) + nxtLvlXP := exp.LevelXP(lvl + 1) err = resp.Message(fmt.Sprintf( "%s : **%d**\n> XP : %d\n> Prochain niveau dans %d XP", msg, diff --git a/commands/reset.go b/commands/reset.go index 0c18044..1e8df6a 100644 --- a/commands/reset.go +++ b/commands/reset.go @@ -3,12 +3,12 @@ package commands import ( "github.com/anhgelus/gokord" "github.com/anhgelus/gokord/utils" - "github.com/anhgelus/les-copaings-bot/xp" + "github.com/anhgelus/les-copaings-bot/user" "github.com/bwmarrin/discordgo" ) func Reset(s *discordgo.Session, i *discordgo.InteractionCreate) { - var copaings []*xp.Copaing + var copaings []*user.Copaing gokord.DB.Where("guild_id = ?", i.GuildID).Delete(&copaings) resp := utils.ResponseBuilder{C: s, I: i} if err := resp.IsEphemeral().Message("L'XP a été reset.").Send(); err != nil { @@ -20,9 +20,9 @@ func ResetUser(s *discordgo.Session, i *discordgo.InteractionCreate) { resp := utils.ResponseBuilder{C: s, I: i} resp.IsEphemeral() optMap := utils.GenerateOptionMap(i) - v, ok := optMap["copaing"] + v, ok := optMap["user"] if !ok { - if err := resp.Message("Le copaing n'a pas été renseigné.").Send(); err != nil { + if err := resp.Message("Le user n'a pas été renseigné.").Send(); err != nil { utils.SendAlert("commands/reset.go - Copaing not set", err.Error()) } return @@ -34,8 +34,8 @@ func ResetUser(s *discordgo.Session, i *discordgo.InteractionCreate) { } return } - xp.GetCopaing(m.ID, i.GuildID).Reset() - if err := resp.Message("Le copaing bien été reset.").Send(); err != nil { - utils.SendAlert("commands/reset.go - Sending success (copaing)", err.Error()) + user.GetCopaing(m.ID, i.GuildID).Reset() + if err := resp.Message("Le user bien été reset.").Send(); err != nil { + utils.SendAlert("commands/reset.go - Sending success (user)", err.Error()) } } diff --git a/commands/top.go b/commands/top.go index 8320fe9..22574ce 100644 --- a/commands/top.go +++ b/commands/top.go @@ -4,12 +4,13 @@ import ( "fmt" "github.com/anhgelus/gokord" "github.com/anhgelus/gokord/utils" - "github.com/anhgelus/les-copaings-bot/xp" + "github.com/anhgelus/les-copaings-bot/exp" + "github.com/anhgelus/les-copaings-bot/user" "github.com/bwmarrin/discordgo" ) func Top(s *discordgo.Session, i *discordgo.InteractionCreate) { - xp.LastEventUpdate(s, xp.GetCopaing(i.Member.User.ID, i.GuildID)) + user.LastEventUpdate(s, user.GetCopaing(i.Member.User.ID, i.GuildID)) resp := utils.ResponseBuilder{C: s, I: i} err := resp.IsDeferred().Send() if err != nil { @@ -18,14 +19,14 @@ func Top(s *discordgo.Session, i *discordgo.InteractionCreate) { } resp.NotDeferred().IsEdit() go func() { - var tops []xp.Copaing - gokord.DB.Where("guild_id = ?", i.GuildID).Limit(10).Order("xp desc").Find(&tops) + var tops []user.Copaing + gokord.DB.Where("guild_id = ?", i.GuildID).Limit(10).Order("exp desc").Find(&tops) msg := "" for i, c := range tops { if i == 9 { - msg += fmt.Sprintf("%d. **<@%s>** - niveau %d", i+1, c.DiscordID, xp.Level(c.XP)) + msg += fmt.Sprintf("%d. **<@%s>** - niveau %d", i+1, c.DiscordID, exp.Level(c.XP)) } else { - msg += fmt.Sprintf("%d. **<@%s>** - niveau %d\n", i+1, c.DiscordID, xp.Level(c.XP)) + msg += fmt.Sprintf("%d. **<@%s>** - niveau %d\n", i+1, c.DiscordID, exp.Level(c.XP)) } } err = resp.Embeds([]*discordgo.MessageEmbed{ diff --git a/config/guild.go b/config/guild.go index b4b7014..0971837 100644 --- a/config/guild.go +++ b/config/guild.go @@ -10,7 +10,7 @@ import ( type GuildConfig struct { gorm.Model GuildID string `gorm:"not null;unique"` - XpRoles []*XpRole + XpRoles []XpRole DisabledChannels string FallbackChannel string } @@ -46,7 +46,7 @@ func (cfg *GuildConfig) IsDisabled(channelID string) bool { func (cfg *GuildConfig) FindXpRole(roleID string) (int, *XpRole) { for i, r := range cfg.XpRoles { if r.RoleID == roleID { - return i, r + return i, &r } } return 0, nil diff --git a/config/redis.go b/config/redis.go new file mode 100644 index 0000000..8aa370d --- /dev/null +++ b/config/redis.go @@ -0,0 +1,28 @@ +package config + +import ( + "github.com/anhgelus/gokord" + "github.com/anhgelus/gokord/utils" + "github.com/redis/go-redis/v9" +) + +var redisClient *redis.Client + +func GetRedisClient() (*redis.Client, error) { + if redisClient == nil { + var err error + redisClient, err = gokord.BaseCfg.GetRedisCredentials().Connect() + return redisClient, err + } + return redisClient, nil +} + +func CloseRedisClient() { + if redisClient == nil { + return + } + err := redisClient.Close() + if err != nil { + utils.SendAlert("exp/member.go - Closing redis client", err.Error()) + } +} diff --git a/events.go b/events.go new file mode 100644 index 0000000..e35ced0 --- /dev/null +++ b/events.go @@ -0,0 +1,173 @@ +package main + +import ( + "context" + "errors" + "fmt" + "github.com/anhgelus/gokord" + "github.com/anhgelus/gokord/utils" + "github.com/anhgelus/les-copaings-bot/config" + xp2 "github.com/anhgelus/les-copaings-bot/exp" + "github.com/anhgelus/les-copaings-bot/user" + "github.com/bwmarrin/discordgo" + "github.com/redis/go-redis/v9" + "slices" + "strconv" + "strings" + "time" +) + +const ( + ConnectedSince = "connected_since" + NotConnected = -1 + MaxTimeInVocal = 60 * 60 * 6 + MaxXpPerMessage = 250 +) + +func OnMessage(s *discordgo.Session, m *discordgo.MessageCreate) { + if m.Author.Bot { + return + } + cfg := config.GetGuildConfig(m.GuildID) + if cfg.IsDisabled(m.ChannelID) { + return + } + c := user.GetCopaing(m.Author.ID, m.GuildID) + user.LastEventUpdate(s, c) + // add exp + trimmed := utils.TrimMessage(strings.ToLower(m.Content)) + m.Member.User = m.Author + m.Member.GuildID = m.GuildID + xp := xp2.MessageXP(uint(len(trimmed)), calcDiversity(trimmed)) + if xp > MaxXpPerMessage { + xp = MaxXpPerMessage + } + c.AddXP(s, m.Member, xp, func(_ uint, _ uint) { + if err := s.MessageReactionAdd(m.ChannelID, m.Message.ID, "⬆"); err != nil { + utils.SendAlert( + "exp/events.go - add reaction for new level", err.Error(), + "channel id", m.ChannelID, + "message id", m.Message.ID, + ) + } + }) +} + +func calcDiversity(msg string) uint { + var chars []rune + for _, c := range []rune(msg) { + if !slices.Contains(chars, c) { + chars = append(chars, c) + } + } + return uint(len(chars)) +} + +func OnVoiceUpdate(s *discordgo.Session, e *discordgo.VoiceStateUpdate) { + if e.Member.User.Bot { + return + } + user.LastEventUpdate(s, user.GetCopaing(e.UserID, e.GuildID)) + cfg := config.GetGuildConfig(e.GuildID) + client, err := config.GetRedisClient() + if err != nil { + utils.SendAlert("exp/events.go - Getting redis client", err.Error()) + return + } + if e.BeforeUpdate == nil && e.ChannelID != "" { + if cfg.IsDisabled(e.ChannelID) { + return + } + onConnection(s, e, client) + } else if e.BeforeUpdate != nil && e.ChannelID == "" { + if cfg.IsDisabled(e.BeforeUpdate.ChannelID) { + return + } + onDisconnect(s, e, client) + } +} + +func onConnection(_ *discordgo.Session, e *discordgo.VoiceStateUpdate, client *redis.Client) { + utils.SendDebug("User connected", "username", e.Member.DisplayName()) + c := user.GetCopaing(e.UserID, e.GuildID) + err := client.Set( + context.Background(), + c.GenKey(ConnectedSince), + strconv.FormatInt(time.Now().Unix(), 10), + 0, + ).Err() + if err != nil { + utils.SendAlert("exp/events.go - Setting connected_since", err.Error()) + } +} + +func onDisconnect(s *discordgo.Session, e *discordgo.VoiceStateUpdate, client *redis.Client) { + now := time.Now().Unix() + c := user.GetCopaing(e.UserID, e.GuildID) + key := c.GenKey(ConnectedSince) + res := client.Get(context.Background(), key) + // check validity of user (1) + if errors.Is(res.Err(), redis.Nil) { + utils.SendWarn(fmt.Sprintf( + "User %s diconnect from a vocal but does not have a connected_since", e.Member.DisplayName(), + )) + return + } + if res.Err() != nil { + utils.SendAlert("exp/events.go - Getting connected_since", res.Err().Error()) + err := client.Set(context.Background(), key, strconv.Itoa(NotConnected), 0).Err() + if err != nil { + utils.SendAlert("exp/events.go - Set connected_since to not connected after get err", err.Error()) + } + return + } + con, err := res.Int64() + if err != nil { + utils.SendAlert("exp/events.go - Converting result to int64", err.Error()) + return + } + // check validity of user (2) + if con == NotConnected { + utils.SendWarn(fmt.Sprintf( + "User %s diconnect from a vocal but was registered as not connected", e.Member.DisplayName(), + )) + return + } + utils.SendDebug("User disconnected", "username", e.Member.DisplayName(), "since", con) + err = client.Set(context.Background(), key, strconv.Itoa(NotConnected), 0).Err() + if err != nil { + utils.SendAlert("exp/events.go - Set connected_since to not connected", err.Error()) + } + // add exp + timeInVocal := now - con + if timeInVocal < 0 { + utils.SendAlert("exp/events.go - Calculating time spent in vocal", "the time is negative") + return + } + if timeInVocal > MaxTimeInVocal { + utils.SendWarn(fmt.Sprintf("User %s spent more than 6 hours in vocal", e.Member.DisplayName())) + timeInVocal = MaxTimeInVocal + } + e.Member.GuildID = e.GuildID + c.AddXP(s, e.Member, xp2.VocalXP(uint(timeInVocal)), func(_ uint, newLevel uint) { + cfg := config.GetGuildConfig(e.GuildID) + _, err = s.ChannelMessageSend(cfg.FallbackChannel, fmt.Sprintf( + "%s est maintenant niveau %d", e.Member.Mention(), newLevel, + )) + if err != nil { + utils.SendAlert("exp/events.go - Sending new level in fallback channel", err.Error()) + } + }) +} + +func OnLeave(_ *discordgo.Session, e *discordgo.GuildMemberRemove) { + utils.SendDebug("Leave event", "user_id", e.User.ID) + c := user.GetCopaing(e.User.ID, e.GuildID) + if err := gokord.DB.Where("guild_id = ?", e.GuildID).Delete(c).Error; err != nil { + utils.SendAlert( + "exp/events.go - deleting user from db", err.Error(), + "user_id", e.User.ID, + "guild_id", e.GuildID, + ) + } +} diff --git a/exp/functions.go b/exp/functions.go new file mode 100644 index 0000000..da535c4 --- /dev/null +++ b/exp/functions.go @@ -0,0 +1,45 @@ +package exp + +import ( + "github.com/anhgelus/gokord" + "math" +) + +func MessageXP(length uint, diversity uint) uint { + return uint(math.Floor( + 0.025*math.Pow(float64(length), 1.25)*math.Sqrt(float64(diversity)) + 1, + )) +} + +func VocalXP(time uint) uint { + return uint(math.Floor( + 0.01*math.Pow(float64(time), 1.3) + 1, + )) +} + +// 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)), + )) +} + +// LevelXP gives the XP required to get this level. +// See Level to get the level with the given XP. +func LevelXP(level uint) uint { + return uint(math.Floor( + math.Pow(float64(5*level), 2), + )) +} + +func Lose(time uint, xp uint) uint { + if gokord.Debug { + return uint(math.Floor( + math.Pow(float64(time), 3) * math.Pow(10, -2+math.Log(float64(time))) * math.Floor(float64(xp/500)+1), + )) // a little bit faster to lose exp + } + return uint(math.Floor( + math.Pow(float64(time), 2) * math.Pow(10, -2+math.Log(float64(time/85))) * math.Floor(float64(xp/500)+1), + )) +} diff --git a/main.go b/main.go index 5bbc289..7b8e75f 100644 --- a/main.go +++ b/main.go @@ -7,7 +7,7 @@ import ( "github.com/anhgelus/gokord/utils" "github.com/anhgelus/les-copaings-bot/commands" "github.com/anhgelus/les-copaings-bot/config" - "github.com/anhgelus/les-copaings-bot/xp" + "github.com/anhgelus/les-copaings-bot/user" "github.com/bwmarrin/discordgo" "time" ) @@ -36,18 +36,18 @@ func main() { panic(err) } - err = gokord.DB.AutoMigrate(&xp.Copaing{}, &config.GuildConfig{}, &config.XpRole{}) + err = gokord.DB.AutoMigrate(&user.Copaing{}, &config.GuildConfig{}, &config.XpRole{}) if err != nil { panic(err) } adm := gokord.AdminPermission - rankCmd := gokord.NewCommand("rank", "Affiche le niveau d'un copaing"). + rankCmd := gokord.NewCommand("rank", "Affiche le niveau d'un user"). HasOption(). AddOption(gokord.NewOption( discordgo.ApplicationCommandOptionUser, - "copaing", + "user", "Le niveau du Copaing que vous souhaitez obtenir", )). SetHandler(commands.Rank) @@ -58,7 +58,7 @@ func main() { gokord.NewCommand("show", "Affiche la config").SetHandler(commands.ConfigShow), ). AddSub( - gokord.NewCommand("xp", "Modifie l'xp"). + gokord.NewCommand("exp", "Modifie l'exp"). HasOption(). AddOption(gokord.NewOption( discordgo.ApplicationCommandOptionString, @@ -114,16 +114,16 @@ func main() { HasOption(). SetHandler(commands.Top) - resetCmd := gokord.NewCommand("reset", "Reset l'xp"). + resetCmd := gokord.NewCommand("reset", "Reset l'exp"). HasOption(). SetHandler(commands.Reset). SetPermission(&adm) - resetUserCmd := gokord.NewCommand("reset-user", "Reset l'xp d'un utilisation"). + resetUserCmd := gokord.NewCommand("reset-user", "Reset l'exp d'un utilisation"). HasOption(). AddOption(gokord.NewOption( discordgo.ApplicationCommandOptionUser, - "copaing", + "user", "Copaing a reset", ).IsRequired()). SetHandler(commands.ResetUser). @@ -176,14 +176,14 @@ func main() { stopPeriodicReducer <- true } - xp.CloseRedisClient() + config.CloseRedisClient() } func afterInit(dg *discordgo.Session) { // handlers - dg.AddHandler(xp.OnMessage) - dg.AddHandler(xp.OnVoiceUpdate) - dg.AddHandler(xp.OnLeave) + dg.AddHandler(OnMessage) + dg.AddHandler(OnVoiceUpdate) + dg.AddHandler(OnLeave) // setup timer for periodic reducer d := 24 * time.Hour @@ -192,6 +192,6 @@ func afterInit(dg *discordgo.Session) { d = time.Minute } stopPeriodicReducer = utils.NewTimer(d, func(stop chan<- interface{}) { - xp.PeriodicReducer(dg) + user.PeriodicReducer(dg) }) } diff --git a/user/level.go b/user/level.go new file mode 100644 index 0000000..0abd642 --- /dev/null +++ b/user/level.go @@ -0,0 +1,207 @@ +package user + +import ( + "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" + "slices" + "sync" + "time" +) + +func onNewLevel(dg *discordgo.Session, m *discordgo.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) { + utils.SendDebug( + "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) + if err != nil { + utils.SendAlert("exp/level.go - Adding role", err.Error(), "role_id", role.RoleID) + } + } else if role.XP > xpForLevel && slices.Contains(m.Roles, role.RoleID) { + utils.SendDebug( + "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) + if err != nil { + utils.SendAlert("exp/level.go - Removing role", err.Error(), "role_id", role.RoleID) + } + } + } +} + +func (c *Copaing) OnNewLevel(dg *discordgo.Session, level uint) { + m, err := dg.GuildMember(c.GuildID, c.DiscordID) + if err != nil { + utils.SendAlert( + "exp/level.go - Getting member for new level", err.Error(), + "discord_id", c.DiscordID, + "guild_id", c.GuildID, + ) + return + } + onNewLevel(dg, m, level) +} + +func LastEventUpdate(dg *discordgo.Session, c *Copaing) { + h := c.HourSinceLastEvent() + l := exp.Lose(h, c.XP) + xp := c.XPAlreadyRemoved() + oldXP := c.XP + if l-xp < 0 { + utils.SendWarn("lose - exp already removed is negative", "lose", l, "exp", xp) + c.XP = 0 + } else { + calc := int(c.XP) - int(l) + int(c.XPAlreadyRemoved()) + if calc < 0 { + c.XP = 0 + } else { + c.XP = uint(calc) + } + } + if oldXP != c.XP { + lvl := exp.Level(c.XP) + if exp.Level(oldXP) != lvl { + utils.SendDebug( + "Level changed", + "old", exp.Level(oldXP), + "new", lvl, + "discord_id", c.DiscordID, + "guild_id", c.GuildID, + ) + c.OnNewLevel(dg, lvl) + } + if err := c.Save(); err != nil { + utils.SendAlert( + "exp/level.go - Saving user", err.Error(), + "exp", c.XP, + "discord_id", c.DiscordID, + "guild_id", c.GuildID, + ) + } + } + c.SetLastEvent() +} + +func UpdateXP(dg *discordgo.Session, c *Copaing) { + oldXP := c.XP + if oldXP == 0 { + return + } + h := c.HourSinceLastEvent() + l := exp.Lose(h, c.XP) + xp := c.XPAlreadyRemoved() + if l-xp < 0 { + utils.SendWarn("lose - xp_removed is negative", "lose", l, "exp removed", xp) + c.AddXPAlreadyRemoved(0) + } else { + calc := int(c.XP) - int(l) + int(xp) + if calc < 0 { + c.AddXPAlreadyRemoved(c.XP) + c.XP = 0 + } else { + c.XP = uint(calc) + c.AddXPAlreadyRemoved(l - xp) + } + } + if oldXP != c.XP { + lvl := exp.Level(c.XP) + if exp.Level(oldXP) != lvl { + utils.SendDebug( + "Level updated", + "old", exp.Level(oldXP), + "new", lvl, + "discord_id", c.DiscordID, + "guild_id", c.GuildID, + ) + c.OnNewLevel(dg, lvl) + } + utils.SendDebug("Save XP", "old", oldXP, "new", c.XP, "user", c.DiscordID) + if err := c.Save(); err != nil { + utils.SendAlert( + "exp/level.go - Saving user", err.Error(), + "exp", c.XP, + "discord_id", c.DiscordID, + "guild_id", c.GuildID, + ) + } + } +} + +func PeriodicReducer(dg *discordgo.Session) { + var wg sync.WaitGroup + for _, g := range dg.State.Guilds { + var cs []*Copaing + err := gokord.DB.Where("guild_id = ?", g.ID).Find(&cs).Error + if err != nil { + utils.SendAlert("exp/level.go - Querying all copaings in Guild", err.Error(), "guild_id", g.ID) + continue + } + for i, c := range cs { + if i%50 == 49 { + time.Sleep(15 * time.Second) // sleep prevents from spamming the Discord API and the database + } + var u *discordgo.User + u, err = dg.User(c.DiscordID) + if err != nil { + utils.SendAlert( + "exp/level.go - Fetching user", err.Error(), + "discord_id", c.DiscordID, + "guild_id", g.ID, + ) + utils.SendWarn("Removing user from database", "discord_id", c.DiscordID) + if err = gokord.DB.Delete(c).Error; err != nil { + utils.SendAlert( + "exp/level.go - Removing user from database", err.Error(), + "discord_id", c.DiscordID, + "guild_id", g.ID, + ) + } + continue + } + if u.Bot { + continue + } + if _, err = dg.GuildMember(g.ID, c.DiscordID); err != nil { + utils.SendAlert( + "exp/level.go - Fetching member", err.Error(), + "discord_id", c.DiscordID, + "guild_id", g.ID, + ) + utils.SendWarn( + "Removing user from guild in database", + "discord_id", c.DiscordID, + "guild_id", g.ID, + ) + if err = gokord.DB.Where("guild_id = ?", g.ID).Delete(c).Error; err != nil { + utils.SendAlert( + "exp/level.go - Removing user from guild in database", err.Error(), + "discord_id", c.DiscordID, + "guild_id", g.ID, + ) + } + continue + } + wg.Add(1) + go func() { + UpdateXP(dg, c) + wg.Done() + }() + } + wg.Wait() // finish the entire guild before starting another + utils.SendDebug("Periodic reduce, guild finished", "guild", g.Name) + time.Sleep(15 * time.Second) // sleep prevents from spamming the Discord API and the database + } + utils.SendDebug("Periodic reduce finished", "len(guilds)", len(dg.State.Guilds)) +} diff --git a/user/member.go b/user/member.go new file mode 100644 index 0000000..8a33ed3 --- /dev/null +++ b/user/member.go @@ -0,0 +1,107 @@ +package user + +import ( + "fmt" + "github.com/anhgelus/gokord" + "github.com/anhgelus/gokord/utils" + "gorm.io/gorm" +) + +type Copaing struct { + gorm.Model + DiscordID string `gorm:"not null"` + //XP []CopaingXP + XP uint `gorm:"default:0"` + GuildID string `gorm:"not null"` +} + +type leftCopaing struct { + ID uint + StopDelete chan<- interface{} +} + +//type CopaingXP struct { +// gorm.Model +// XP uint `gorm:"default:0"` +// CopaingID uint +//} + +var ( + leftCopaingsMap = map[string]*leftCopaing{} +) + +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 { + utils.SendAlert( + "exp/member.go - Loading user", + err.Error(), + "discord_id", + discordID, + "guild_id", + guildID, + ) + return nil + } + return &c +} + +func (c *Copaing) Load() error { + // check if user left in the past 48 hours + k := c.GuildID + ":" + c.DiscordID + l, ok := leftCopaingsMap[k] + if !ok || l == nil { + // if not, common first or create + return gokord.DB.Where("discord_id = ? and guild_id = ?", c.DiscordID, c.GuildID).FirstOrCreate(c).Error + } + // else, getting last data + tmp := Copaing{ + Model: gorm.Model{ + ID: c.ID, + }, + DiscordID: c.DiscordID, + GuildID: c.GuildID, + } + if err := gokord.DB.Unscoped().Find(&tmp).Error; err != nil { + // if error, avoid getting old data and use new one + utils.SendAlert( + "exp/member.go - Getting user in soft delete", err.Error(), + "discord_id", c.DiscordID, + "guild_id", c.DiscordID, + "last_id", l.ID, + ) + return gokord.DB.Where("discord_id = ? and guild_id = ?", c.DiscordID, c.GuildID).FirstOrCreate(c).Error + } + // resetting internal data + tmp.Model = gorm.Model{} + l.StopDelete <- true + leftCopaingsMap[k] = nil + // creating new data + err := gokord.DB.Create(&tmp).Error + if err != nil { + return err + } + // delete old data + if err = gokord.DB.Unscoped().Delete(&tmp).Error; err != nil { + utils.SendAlert( + "exp/member.go - Deleting user in soft delete", err.Error(), + "discord_id", c.DiscordID, + "guild_id", c.DiscordID, + "last_id", l.ID, + ) + } + return nil +} + +func (c *Copaing) Save() error { + return gokord.DB.Save(c).Error +} + +func (c *Copaing) GenKey(key string) string { + return fmt.Sprintf("%s:%s:%s", c.GuildID, c.DiscordID, key) +} diff --git a/user/xp.go b/user/xp.go new file mode 100644 index 0000000..6cd9b75 --- /dev/null +++ b/user/xp.go @@ -0,0 +1,185 @@ +package user + +import ( + "context" + "errors" + "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" + "github.com/redis/go-redis/v9" + "gorm.io/gorm" + "math" + "strconv" + "time" +) + +func (c *Copaing) AddXP(s *discordgo.Session, m *discordgo.Member, xp uint, fn func(uint, uint)) { + pastLevel := exp.Level(c.XP) + old := c.XP + c.XP += xp + if err := c.Save(); err != nil { + utils.SendAlert( + "exp/level.go - Saving user", + err.Error(), + "exp", + c.XP, + "discord_id", + c.DiscordID, + "guild_id", + c.GuildID, + ) + c.XP = old + return + } + newLevel := exp.Level(c.XP) + if newLevel > pastLevel { + fn(c.XP, newLevel) + onNewLevel(s, m, newLevel) + } +} + +func (c *Copaing) SetLastEvent() { + client, err := config.GetRedisClient() + if err != nil { + utils.SendAlert("exp/member.go - Getting redis client (set)", err.Error()) + return + } + t := time.Now().Unix() + err = client.Set(context.Background(), c.GenKey(LastEvent), strconv.FormatInt(t, 10), 0).Err() + if err != nil { + utils.SendAlert("exp/member.go - Setting last event", err.Error(), "time", t, "base_key", c.GenKey("")) + return + } + err = client.Set(context.Background(), c.GenKey(AlreadyRemoved), "0", 0).Err() + if err != nil { + utils.SendAlert( + "exp/member.go - Setting already removed to 0", + err.Error(), + "time", + t, + "base_key", + c.GenKey(""), + ) + return + } +} + +func (c *Copaing) HourSinceLastEvent() uint { + client, err := config.GetRedisClient() + if err != nil { + utils.SendAlert("exp/member.go - Getting redis client (get)", err.Error()) + return 0 + } + res := client.Get(context.Background(), c.GenKey(LastEvent)) + if errors.Is(res.Err(), redis.Nil) { + return 0 + } else if res.Err() != nil { + utils.SendAlert("exp/member.go - Getting last event", res.Err().Error(), "base_key", c.GenKey("")) + return 0 + } + t := time.Now().Unix() + last, err := strconv.Atoi(res.Val()) + if err != nil { + utils.SendAlert( + "exp/member.go - Converting time fetched into int (last event)", + err.Error(), + "base_key", + c.GenKey(""), + "val", + res.Val(), + ) + return 0 + } + if gokord.Debug { + return uint(math.Floor(float64(t-int64(last)) / 60)) // not hours of unix, is minutes of unix + } + return utils.HoursOfUnix(t - int64(last)) +} + +func (c *Copaing) AddXPAlreadyRemoved(xp uint) uint { + client, err := config.GetRedisClient() + if err != nil { + utils.SendAlert("exp/member.go - Getting redis client (set)", err.Error()) + return 0 + } + exp := xp + c.XPAlreadyRemoved() + err = client.Set(context.Background(), c.GenKey(AlreadyRemoved), exp, 0).Err() + if err != nil { + utils.SendAlert( + "exp/member.go - Setting already removed", + err.Error(), + "exp already removed", + exp, + "base_key", + c.GenKey(""), + ) + return 0 + } + return exp +} + +func (c *Copaing) XPAlreadyRemoved() uint { + client, err := config.GetRedisClient() + if err != nil { + utils.SendAlert("exp/member.go - Getting redis client (exp)", err.Error()) + return 0 + } + res := client.Get(context.Background(), fmt.Sprintf("%s:%s", c.GenKey(""), AlreadyRemoved)) + if errors.Is(res.Err(), redis.Nil) { + return 0 + } else if res.Err() != nil { + utils.SendAlert("exp/member.go - Getting already removed", res.Err().Error(), "base_key", c.GenKey("")) + return 0 + } + xp, err := strconv.Atoi(res.Val()) + if err != nil { + utils.SendAlert( + "exp/member.go - Converting time fetched into int (already removed)", + err.Error(), + "base_key", + c.GenKey(""), + "val", + res.Val(), + ) + return 0 + } + if xp < 0 { + utils.SendAlert( + "exp/member.go - Assertion exp >= 0", + "exp is negative", + "base_key", + c.GenKey(""), + "exp", + xp, + ) + return 0 + } + return uint(xp) +} + +func (c *Copaing) Reset() { + gokord.DB.Where("guild_id = ? AND discord_id = ?", c.GuildID, c.DiscordID).Delete(c) +} + +func (c *Copaing) AfterDelete(db *gorm.DB) error { + id := c.ID + dID := c.DiscordID + gID := c.GuildID + k := c.GuildID + ":" + c.DiscordID + ch := utils.NewTimer(48*time.Hour, func(stop chan<- interface{}) { + if err := db.Unscoped().Where("id = ?", id).Delete(c).Error; err != nil { + utils.SendAlert( + "exp/member.go - Removing user from database", err.Error(), + "discord_id", dID, + "guild_id", gID, + ) + } + stop <- true + leftCopaingsMap[k] = nil + }) + leftCopaingsMap[k] = &leftCopaing{id, ch} + return nil +} diff --git a/xp/events.go b/xp/events.go deleted file mode 100644 index 94d8889..0000000 --- a/xp/events.go +++ /dev/null @@ -1,171 +0,0 @@ -package xp - -import ( - "context" - "errors" - "fmt" - "github.com/anhgelus/gokord" - "github.com/anhgelus/gokord/utils" - "github.com/anhgelus/les-copaings-bot/config" - "github.com/bwmarrin/discordgo" - "github.com/redis/go-redis/v9" - "slices" - "strconv" - "strings" - "time" -) - -const ( - ConnectedSince = "connected_since" - NotConnected = -1 - MaxTimeInVocal = 60 * 60 * 6 - MaxXpPerMessage = 250 -) - -func OnMessage(s *discordgo.Session, m *discordgo.MessageCreate) { - if m.Author.Bot { - return - } - cfg := config.GetGuildConfig(m.GuildID) - if cfg.IsDisabled(m.ChannelID) { - return - } - c := GetCopaing(m.Author.ID, m.GuildID) - LastEventUpdate(s, c) - // add xp - trimmed := utils.TrimMessage(strings.ToLower(m.Content)) - m.Member.User = m.Author - m.Member.GuildID = m.GuildID - xp := XPMessage(uint(len(trimmed)), calcDiversity(trimmed)) - if xp > MaxXpPerMessage { - xp = MaxXpPerMessage - } - c.AddXP(s, m.Member, xp, func(_ uint, _ uint) { - if err := s.MessageReactionAdd(m.ChannelID, m.Message.ID, "⬆"); err != nil { - utils.SendAlert( - "xp/events.go - add reaction for new level", err.Error(), - "channel id", m.ChannelID, - "message id", m.Message.ID, - ) - } - }) -} - -func calcDiversity(msg string) uint { - var chars []rune - for _, c := range []rune(msg) { - if !slices.Contains(chars, c) { - chars = append(chars, c) - } - } - return uint(len(chars)) -} - -func OnVoiceUpdate(s *discordgo.Session, e *discordgo.VoiceStateUpdate) { - if e.Member.User.Bot { - return - } - LastEventUpdate(s, GetCopaing(e.UserID, e.GuildID)) - cfg := config.GetGuildConfig(e.GuildID) - client, err := getRedisClient() - if err != nil { - utils.SendAlert("xp/events.go - Getting redis client", err.Error()) - return - } - if e.BeforeUpdate == nil && e.ChannelID != "" { - if cfg.IsDisabled(e.ChannelID) { - return - } - onConnection(s, e, client) - } else if e.BeforeUpdate != nil && e.ChannelID == "" { - if cfg.IsDisabled(e.BeforeUpdate.ChannelID) { - return - } - onDisconnect(s, e, client) - } -} - -func onConnection(_ *discordgo.Session, e *discordgo.VoiceStateUpdate, client *redis.Client) { - utils.SendDebug("User connected", "username", e.Member.DisplayName()) - c := GetCopaing(e.UserID, e.GuildID) - err := client.Set( - context.Background(), - c.GenKey(ConnectedSince), - strconv.FormatInt(time.Now().Unix(), 10), - 0, - ).Err() - if err != nil { - utils.SendAlert("xp/events.go - Setting connected_since", err.Error()) - } -} - -func onDisconnect(s *discordgo.Session, e *discordgo.VoiceStateUpdate, client *redis.Client) { - now := time.Now().Unix() - c := GetCopaing(e.UserID, e.GuildID) - key := c.GenKey(ConnectedSince) - res := client.Get(context.Background(), key) - // check validity of user (1) - if errors.Is(res.Err(), redis.Nil) { - utils.SendWarn(fmt.Sprintf( - "User %s diconnect from a vocal but does not have a connected_since", e.Member.DisplayName(), - )) - return - } - if res.Err() != nil { - utils.SendAlert("xp/events.go - Getting connected_since", res.Err().Error()) - err := client.Set(context.Background(), key, strconv.Itoa(NotConnected), 0).Err() - if err != nil { - utils.SendAlert("xp/events.go - Set connected_since to not connected after get err", err.Error()) - } - return - } - con, err := res.Int64() - if err != nil { - utils.SendAlert("xp/events.go - Converting result to int64", err.Error()) - return - } - // check validity of user (2) - if con == NotConnected { - utils.SendWarn(fmt.Sprintf( - "User %s diconnect from a vocal but was registered as not connected", e.Member.DisplayName(), - )) - return - } - utils.SendDebug("User disconnected", "username", e.Member.DisplayName(), "since", con) - err = client.Set(context.Background(), key, strconv.Itoa(NotConnected), 0).Err() - if err != nil { - utils.SendAlert("xp/events.go - Set connected_since to not connected", err.Error()) - } - // add xp - timeInVocal := now - con - if timeInVocal < 0 { - utils.SendAlert("xp/events.go - Calculating time spent in vocal", "the time is negative") - return - } - if timeInVocal > MaxTimeInVocal { - utils.SendWarn(fmt.Sprintf("User %s spent more than 6 hours in vocal", e.Member.DisplayName())) - timeInVocal = MaxTimeInVocal - } - e.Member.GuildID = e.GuildID - c.AddXP(s, e.Member, XPVocal(uint(timeInVocal)), func(_ uint, newLevel uint) { - cfg := config.GetGuildConfig(e.GuildID) - _, err = s.ChannelMessageSend(cfg.FallbackChannel, fmt.Sprintf( - "%s est maintenant niveau %d", e.Member.Mention(), newLevel, - )) - if err != nil { - utils.SendAlert("xp/events.go - Sending new level in fallback channel", err.Error()) - } - }) -} - -func OnLeave(_ *discordgo.Session, e *discordgo.GuildMemberRemove) { - utils.SendDebug("Leave event", "user_id", e.User.ID) - c := GetCopaing(e.User.ID, e.GuildID) - if err := gokord.DB.Where("guild_id = ?", e.GuildID).Delete(c).Error; err != nil { - utils.SendAlert( - "xp/events.go - deleting copaing from db", err.Error(), - "user_id", e.User.ID, - "guild_id", e.GuildID, - ) - } -} diff --git a/xp/functions.go b/xp/functions.go deleted file mode 100644 index 7ab57f2..0000000 --- a/xp/functions.go +++ /dev/null @@ -1,41 +0,0 @@ -package xp - -import ( - "github.com/anhgelus/gokord" - "math" -) - -func XPMessage(length uint, diversity uint) uint { - return uint(math.Floor( - 0.025*math.Pow(float64(length), 1.25)*math.Sqrt(float64(diversity)) + 1, - )) -} - -func XPVocal(time uint) uint { - return uint(math.Floor( - 0.01*math.Pow(float64(time), 1.3) + 1, - )) -} - -func Level(xp uint) uint { - return uint(math.Floor( - 0.2 * math.Sqrt(float64(xp)), - )) -} - -func XPForLevel(level uint) uint { - return uint(math.Floor( - math.Pow(float64(5*level), 2), - )) -} - -func Lose(time uint, xp uint) uint { - if gokord.Debug { - return uint(math.Floor( - math.Pow(float64(time), 3) * math.Pow(10, -2+math.Log(float64(time))) * math.Floor(float64(xp/500)+1), - )) // a little bit faster to lose xp - } - return uint(math.Floor( - math.Pow(float64(time), 2) * math.Pow(10, -2+math.Log(float64(time/85))) * math.Floor(float64(xp/500)+1), - )) -} diff --git a/xp/level.go b/xp/level.go deleted file mode 100644 index a20d5cf..0000000 --- a/xp/level.go +++ /dev/null @@ -1,206 +0,0 @@ -package xp - -import ( - "github.com/anhgelus/gokord" - "github.com/anhgelus/gokord/utils" - "github.com/anhgelus/les-copaings-bot/config" - "github.com/bwmarrin/discordgo" - "slices" - "sync" - "time" -) - -func onNewLevel(dg *discordgo.Session, m *discordgo.Member, level uint) { - cfg := config.GetGuildConfig(m.GuildID) - xpForLevel := XPForLevel(level) - for _, role := range cfg.XpRoles { - if role.XP <= xpForLevel && !slices.Contains(m.Roles, role.RoleID) { - utils.SendDebug( - "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) - if err != nil { - utils.SendAlert("xp/level.go - Adding role", err.Error(), "role_id", role.RoleID) - } - } else if role.XP > xpForLevel && slices.Contains(m.Roles, role.RoleID) { - utils.SendDebug( - "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) - if err != nil { - utils.SendAlert("xp/level.go - Removing role", err.Error(), "role_id", role.RoleID) - } - } - } -} - -func (c *Copaing) OnNewLevel(dg *discordgo.Session, level uint) { - m, err := dg.GuildMember(c.GuildID, c.DiscordID) - if err != nil { - utils.SendAlert( - "xp/level.go - Getting member for new level", err.Error(), - "discord_id", c.DiscordID, - "guild_id", c.GuildID, - ) - return - } - onNewLevel(dg, m, level) -} - -func LastEventUpdate(dg *discordgo.Session, c *Copaing) { - h := c.HourSinceLastEvent() - l := Lose(h, c.XP) - xp := c.XPAlreadyRemoved() - oldXP := c.XP - if l-xp < 0 { - utils.SendWarn("lose - xp already removed is negative", "lose", l, "xp", xp) - c.XP = 0 - } else { - calc := int(c.XP) - int(l) + int(c.XPAlreadyRemoved()) - if calc < 0 { - c.XP = 0 - } else { - c.XP = uint(calc) - } - } - if oldXP != c.XP { - lvl := Level(c.XP) - if Level(oldXP) != lvl { - utils.SendDebug( - "Level changed", - "old", Level(oldXP), - "new", lvl, - "discord_id", c.DiscordID, - "guild_id", c.GuildID, - ) - c.OnNewLevel(dg, lvl) - } - if err := c.Save(); err != nil { - utils.SendAlert( - "xp/level.go - Saving copaing", err.Error(), - "xp", c.XP, - "discord_id", c.DiscordID, - "guild_id", c.GuildID, - ) - } - } - c.SetLastEvent() -} - -func XPUpdate(dg *discordgo.Session, c *Copaing) { - oldXP := c.XP - if oldXP == 0 { - return - } - h := c.HourSinceLastEvent() - l := Lose(h, c.XP) - xp := c.XPAlreadyRemoved() - if l-xp < 0 { - utils.SendWarn("lose - xp_removed is negative", "lose", l, "xp removed", xp) - c.AddXPAlreadyRemoved(0) - } else { - calc := int(c.XP) - int(l) + int(xp) - if calc < 0 { - c.AddXPAlreadyRemoved(c.XP) - c.XP = 0 - } else { - c.XP = uint(calc) - c.AddXPAlreadyRemoved(l - xp) - } - } - if oldXP != c.XP { - lvl := Level(c.XP) - if Level(oldXP) != lvl { - utils.SendDebug( - "Level updated", - "old", Level(oldXP), - "new", lvl, - "discord_id", c.DiscordID, - "guild_id", c.GuildID, - ) - c.OnNewLevel(dg, lvl) - } - utils.SendDebug("Save XP", "old", oldXP, "new", c.XP, "user", c.DiscordID) - if err := c.Save(); err != nil { - utils.SendAlert( - "xp/level.go - Saving copaing", err.Error(), - "xp", c.XP, - "discord_id", c.DiscordID, - "guild_id", c.GuildID, - ) - } - } -} - -func PeriodicReducer(dg *discordgo.Session) { - var wg sync.WaitGroup - for _, g := range dg.State.Guilds { - var cs []*Copaing - err := gokord.DB.Where("guild_id = ?", g.ID).Find(&cs).Error - if err != nil { - utils.SendAlert("xp/level.go - Querying all copaings in Guild", err.Error(), "guild_id", g.ID) - continue - } - for i, c := range cs { - if i%50 == 49 { - time.Sleep(15 * time.Second) // sleep prevents from spamming the Discord API and the database - } - var u *discordgo.User - u, err = dg.User(c.DiscordID) - if err != nil { - utils.SendAlert( - "xp/level.go - Fetching user", err.Error(), - "discord_id", c.DiscordID, - "guild_id", g.ID, - ) - utils.SendWarn("Removing user from database", "discord_id", c.DiscordID) - if err = gokord.DB.Delete(c).Error; err != nil { - utils.SendAlert( - "xp/level.go - Removing user from database", err.Error(), - "discord_id", c.DiscordID, - "guild_id", g.ID, - ) - } - continue - } - if u.Bot { - continue - } - if _, err = dg.GuildMember(g.ID, c.DiscordID); err != nil { - utils.SendAlert( - "xp/level.go - Fetching member", err.Error(), - "discord_id", c.DiscordID, - "guild_id", g.ID, - ) - utils.SendWarn( - "Removing user from guild in database", - "discord_id", c.DiscordID, - "guild_id", g.ID, - ) - if err = gokord.DB.Where("guild_id = ?", g.ID).Delete(c).Error; err != nil { - utils.SendAlert( - "xp/level.go - Removing user from guild in database", err.Error(), - "discord_id", c.DiscordID, - "guild_id", g.ID, - ) - } - continue - } - wg.Add(1) - go func() { - XPUpdate(dg, c) - wg.Done() - }() - } - wg.Wait() // finish the entire guild before starting another - utils.SendDebug("Periodic reduce, guild finished", "guild", g.Name) - time.Sleep(15 * time.Second) // sleep prevents from spamming the Discord API and the database - } - utils.SendDebug("Periodic reduce finished", "len(guilds)", len(dg.State.Guilds)) -} diff --git a/xp/member.go b/xp/member.go deleted file mode 100644 index ad9e6c5..0000000 --- a/xp/member.go +++ /dev/null @@ -1,296 +0,0 @@ -package xp - -import ( - "context" - "errors" - "fmt" - "github.com/anhgelus/gokord" - "github.com/anhgelus/gokord/utils" - "github.com/bwmarrin/discordgo" - "github.com/redis/go-redis/v9" - "gorm.io/gorm" - "math" - "strconv" - "time" -) - -type Copaing struct { - gorm.Model - DiscordID string `gorm:"not null"` - XP uint `gorm:"default:0"` - GuildID string `gorm:"not null"` -} - -type leftCopaing struct { - ID uint - StopDelete chan<- interface{} -} - -var ( - redisClient *redis.Client - - leftCopaingsMap = map[string]*leftCopaing{} -) - -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 { - utils.SendAlert( - "xp/member.go - Loading copaing", - err.Error(), - "discord_id", - discordID, - "guild_id", - guildID, - ) - return nil - } - return &c -} - -func (c *Copaing) Load() error { - // check if user left in the past 48 hours - k := c.GuildID + ":" + c.DiscordID - l, ok := leftCopaingsMap[k] - if !ok || l == nil { - // if not, common first or create - return gokord.DB.Where("discord_id = ? and guild_id = ?", c.DiscordID, c.GuildID).FirstOrCreate(c).Error - } - // else, getting last data - tmp := Copaing{ - Model: gorm.Model{ - ID: c.ID, - }, - DiscordID: c.DiscordID, - GuildID: c.GuildID, - } - if err := gokord.DB.Unscoped().Find(&tmp).Error; err != nil { - // if error, avoid getting old data and use new one - utils.SendAlert( - "xp/member.go - Getting copaing in soft delete", err.Error(), - "discord_id", c.DiscordID, - "guild_id", c.DiscordID, - "last_id", l.ID, - ) - return gokord.DB.Where("discord_id = ? and guild_id = ?", c.DiscordID, c.GuildID).FirstOrCreate(c).Error - } - // resetting internal data - tmp.Model = gorm.Model{} - l.StopDelete <- true - leftCopaingsMap[k] = nil - // creating new data - err := gokord.DB.Create(&tmp).Error - if err != nil { - return err - } - // delete old data - if err = gokord.DB.Unscoped().Delete(&tmp).Error; err != nil { - utils.SendAlert( - "xp/member.go - Deleting copaing in soft delete", err.Error(), - "discord_id", c.DiscordID, - "guild_id", c.DiscordID, - "last_id", l.ID, - ) - } - return nil -} - -func (c *Copaing) Save() error { - return gokord.DB.Save(c).Error -} - -func (c *Copaing) GenKey(key string) string { - return fmt.Sprintf("%s:%s:%s", c.GuildID, c.DiscordID, key) -} - -func (c *Copaing) AddXP(s *discordgo.Session, m *discordgo.Member, xp uint, fn func(uint, uint)) { - pastLevel := Level(c.XP) - old := c.XP - c.XP += xp - if err := c.Save(); err != nil { - utils.SendAlert( - "xp/level.go - Saving copaing", - err.Error(), - "xp", - c.XP, - "discord_id", - c.DiscordID, - "guild_id", - c.GuildID, - ) - c.XP = old - return - } - newLevel := Level(c.XP) - if newLevel > pastLevel { - fn(c.XP, newLevel) - onNewLevel(s, m, newLevel) - } -} - -func (c *Copaing) SetLastEvent() { - client, err := getRedisClient() - if err != nil { - utils.SendAlert("xp/member.go - Getting redis client (set)", err.Error()) - return - } - t := time.Now().Unix() - err = client.Set(context.Background(), c.GenKey(LastEvent), strconv.FormatInt(t, 10), 0).Err() - if err != nil { - utils.SendAlert("xp/member.go - Setting last event", err.Error(), "time", t, "base_key", c.GenKey("")) - return - } - err = client.Set(context.Background(), c.GenKey(AlreadyRemoved), "0", 0).Err() - if err != nil { - utils.SendAlert( - "xp/member.go - Setting already removed to 0", - err.Error(), - "time", - t, - "base_key", - c.GenKey(""), - ) - return - } -} - -func (c *Copaing) HourSinceLastEvent() uint { - client, err := getRedisClient() - if err != nil { - utils.SendAlert("xp/member.go - Getting redis client (get)", err.Error()) - return 0 - } - res := client.Get(context.Background(), c.GenKey(LastEvent)) - if errors.Is(res.Err(), redis.Nil) { - return 0 - } else if res.Err() != nil { - utils.SendAlert("xp/member.go - Getting last event", res.Err().Error(), "base_key", c.GenKey("")) - return 0 - } - t := time.Now().Unix() - last, err := strconv.Atoi(res.Val()) - if err != nil { - utils.SendAlert( - "xp/member.go - Converting time fetched into int (last event)", - err.Error(), - "base_key", - c.GenKey(""), - "val", - res.Val(), - ) - return 0 - } - if gokord.Debug { - return uint(math.Floor(float64(t-int64(last)) / 60)) // not hours of unix, is minutes of unix - } - return utils.HoursOfUnix(t - int64(last)) -} - -func (c *Copaing) AddXPAlreadyRemoved(xp uint) uint { - client, err := getRedisClient() - if err != nil { - utils.SendAlert("xp/member.go - Getting redis client (set)", err.Error()) - return 0 - } - exp := xp + c.XPAlreadyRemoved() - err = client.Set(context.Background(), c.GenKey(AlreadyRemoved), exp, 0).Err() - if err != nil { - utils.SendAlert( - "xp/member.go - Setting already removed", - err.Error(), - "xp already removed", - exp, - "base_key", - c.GenKey(""), - ) - return 0 - } - return exp -} - -func (c *Copaing) XPAlreadyRemoved() uint { - client, err := getRedisClient() - if err != nil { - utils.SendAlert("xp/member.go - Getting redis client (xp)", err.Error()) - return 0 - } - res := client.Get(context.Background(), fmt.Sprintf("%s:%s", c.GenKey(""), AlreadyRemoved)) - if errors.Is(res.Err(), redis.Nil) { - return 0 - } else if res.Err() != nil { - utils.SendAlert("xp/member.go - Getting already removed", res.Err().Error(), "base_key", c.GenKey("")) - return 0 - } - xp, err := strconv.Atoi(res.Val()) - if err != nil { - utils.SendAlert( - "xp/member.go - Converting time fetched into int (already removed)", - err.Error(), - "base_key", - c.GenKey(""), - "val", - res.Val(), - ) - return 0 - } - if xp < 0 { - utils.SendAlert( - "xp/member.go - Assertion xp >= 0", - "xp is negative", - "base_key", - c.GenKey(""), - "xp", - xp, - ) - return 0 - } - return uint(xp) -} - -func (c *Copaing) Reset() { - gokord.DB.Where("guild_id = ? AND discord_id = ?", c.GuildID, c.DiscordID).Delete(c) -} - -func (c *Copaing) AfterDelete(db *gorm.DB) error { - id := c.ID - dID := c.DiscordID - gID := c.GuildID - k := c.GuildID + ":" + c.DiscordID - ch := utils.NewTimer(48*time.Hour, func(stop chan<- interface{}) { - if err := db.Unscoped().Where("id = ?", id).Delete(c).Error; err != nil { - utils.SendAlert( - "xp/member.go - Removing copaing from database", err.Error(), - "discord_id", dID, - "guild_id", gID, - ) - } - stop <- true - leftCopaingsMap[k] = nil - }) - leftCopaingsMap[k] = &leftCopaing{id, ch} - return nil -} - -func getRedisClient() (*redis.Client, error) { - if redisClient == nil { - var err error - redisClient, err = gokord.BaseCfg.GetRedisCredentials().Connect() - return redisClient, err - } - return redisClient, nil -} - -func CloseRedisClient() { - if redisClient == nil { - return - } - err := redisClient.Close() - if err != nil { - utils.SendAlert("xp/member.go - Closing redis client", err.Error()) - } -} -- cgit v1.2.3