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 /xp | |
| parent | 0a445aa1c73bc1410899c53778ae090a24c38dac (diff) | |
style(files): reorganize everything
Diffstat (limited to 'xp')
| -rw-r--r-- | xp/events.go | 171 | ||||
| -rw-r--r-- | xp/functions.go | 41 | ||||
| -rw-r--r-- | xp/level.go | 206 | ||||
| -rw-r--r-- | xp/member.go | 296 |
4 files changed, 0 insertions, 714 deletions
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()) - } -} |
