diff options
| author | Anhgelus Morhtuuzh <anhgelus@anhgelus.world> | 2025-05-13 12:50:20 +0200 |
|---|---|---|
| committer | Anhgelus Morhtuuzh <anhgelus@anhgelus.world> | 2025-05-13 12:50:20 +0200 |
| commit | c408afc8797b0da5e1d73d190a8f5884870b510c (patch) | |
| tree | b87eb4aab9f3e1f89bea29a2ce846efe324f7374 /user | |
| parent | 0a445aa1c73bc1410899c53778ae090a24c38dac (diff) | |
style(files): reorganize everything
Diffstat (limited to 'user')
| -rw-r--r-- | user/level.go | 207 | ||||
| -rw-r--r-- | user/member.go | 107 | ||||
| -rw-r--r-- | user/xp.go | 185 |
3 files changed, 499 insertions, 0 deletions
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 +} |
