aboutsummaryrefslogtreecommitdiff
path: root/user
diff options
context:
space:
mode:
authorWilliam Hergès <anhgelus.morhtuuzh@proton.me>2025-05-13 21:13:59 +0200
committerGitHub <noreply@github.com>2025-05-13 21:13:59 +0200
commit8d6af4b6aa8f4902316c7f30c5229c97b0ec1a81 (patch)
treed19607355cfa0a180d3269d78e7e2249aa3d2277 /user
parent9e826eee980634b82d6981a868b045f3d4b48852 (diff)
parent75ca960199b85f3f4b491652c837d297827e40ce (diff)
Merge pull request #8 from anhgelus/v3
V3
Diffstat (limited to 'user')
-rw-r--r--user/level.go116
-rw-r--r--user/member.go69
-rw-r--r--user/xp.go137
3 files changed, 322 insertions, 0 deletions
diff --git a/user/level.go b/user/level.go
new file mode 100644
index 0000000..6d9b674
--- /dev/null
+++ b/user/level.go
@@ -0,0 +1,116 @@
+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("user/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("user/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(
+ "user/level.go - Getting member for new level", err.Error(),
+ "discord_id", c.DiscordID,
+ "guild_id", c.GuildID,
+ )
+ return
+ }
+ onNewLevel(dg, m, level)
+}
+
+func PeriodicReducer(dg *discordgo.Session) {
+ wg := &sync.WaitGroup{}
+ var cs []*Copaing
+ if err := gokord.DB.Find(&cs).Error; err != nil {
+ utils.SendAlert("user/level.go - Fetching all copaings", err.Error())
+ return
+ }
+ cxps := make([]*cXP, len(cs))
+ for i, c := range cs {
+ if i%10 == 9 {
+ wg.Wait() // prevents spamming the DB
+ }
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ xp, err := c.GetXP()
+ if err != nil {
+ utils.SendAlert("user/level.go - Getting XP", err.Error(), "copaing_id", c.ID, "guild_id", c.GuildID)
+ xp = 0
+ }
+ cxps[i] = &cXP{
+ Cxp: xp,
+ Copaing: c,
+ }
+ }()
+ }
+ wg.Wait()
+ for _, g := range dg.State.Guilds {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ cfg := config.GetGuildConfig(g.ID)
+ err := gokord.DB.
+ Model(&CopaingXP{}).
+ Where("guild_id = ? and created_at < ?", g.ID, exp.TimeStampNDaysBefore(cfg.DaysXPRemains)).
+ Delete(&CopaingXP{}).
+ Error
+ if err != nil {
+ utils.SendAlert("user/level.go - Removing old XP", err.Error(), "guild_id", g.ID)
+ }
+ }()
+ }
+ wg.Wait()
+ for i, c := range cxps {
+ if i%50 == 49 {
+ utils.SendDebug("Sleeping...")
+ time.Sleep(15 * time.Second) // prevents spamming the API
+ }
+ oldXp := c.GetXP()
+ xp, err := c.ToCopaing().GetXP()
+ if err != nil {
+ utils.SendAlert("user/level.go - Getting XP", err.Error(), "guild_id", c.ID, "discord_id", c.DiscordID)
+ continue
+ }
+ if exp.Level(oldXp) != exp.Level(xp) {
+ c.OnNewLevel(dg, exp.Level(xp))
+ }
+ }
+ 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..71a369b
--- /dev/null
+++ b/user/member.go
@@ -0,0 +1,69 @@
+package user
+
+import (
+ "fmt"
+ "github.com/anhgelus/gokord"
+ "github.com/anhgelus/gokord/utils"
+ "time"
+)
+
+type Copaing struct {
+ ID uint `gorm:"primarykey"`
+ DiscordID string `gorm:"not null"`
+ CopaingXPs []CopaingXP `gorm:"constraint:OnDelete:SET NULL;"`
+ GuildID string `gorm:"not null"`
+}
+
+type CopaingXP struct {
+ ID uint `gorm:"primarykey"`
+ XP uint `gorm:"default:0"`
+ CopaingID uint
+ GuildID string `gorm:"not null;"`
+ CreatedAt time.Time
+}
+
+type CopaingAccess interface {
+ ToCopaing() *Copaing
+ 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 {
+ utils.SendAlert(
+ "user/member.go - Loading user",
+ err.Error(),
+ "discord_id",
+ discordID,
+ "guild_id",
+ guildID,
+ )
+ return nil
+ }
+ return &c
+}
+
+func (c *Copaing) Load() error {
+ return gokord.DB.
+ Where("discord_id = ? and guild_id = ?", c.DiscordID, c.GuildID).
+ Preload("CopaingXPs").
+ FirstOrCreate(c).
+ Error
+}
+
+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) Delete() error {
+ return gokord.DB.Where("guild_id = ? AND discord_id = ?", c.GuildID, c.DiscordID).Delete(c).Error
+}
diff --git a/user/xp.go b/user/xp.go
new file mode 100644
index 0000000..71a1ced
--- /dev/null
+++ b/user/xp.go
@@ -0,0 +1,137 @@
+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"
+)
+
+type cXP struct {
+ Cxp uint
+ *Copaing
+}
+
+func (c *cXP) ToCopaing() *Copaing {
+ return c.Copaing
+}
+
+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()
+ if err != nil {
+ utils.SendAlert("user/xp.go - Getting xp", err.Error(), "discord_id", c.DiscordID, "guild_id", c.GuildID)
+ return
+ }
+ pastLevel := exp.Level(old)
+ utils.SendDebug("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: xp, GuildID: c.GuildID})
+ if err = c.Save(); err != nil {
+ utils.SendAlert(
+ "user/xp.go - Saving user",
+ err.Error(),
+ "xp",
+ c.CopaingXPs,
+ "discord_id",
+ c.DiscordID,
+ "guild_id",
+ c.GuildID,
+ )
+ return
+ }
+ newLevel := exp.Level(old + xp)
+ if newLevel > pastLevel {
+ fn(old+xp, newLevel)
+ onNewLevel(s, m, newLevel)
+ }
+}
+
+func (c *Copaing) GetXP() (uint, error) {
+ cfg := config.GetGuildConfig(c.GuildID)
+ return c.GetXPForDays(cfg.DaysXPRemains)
+}
+
+func (c *Copaing) GetXPForDays(n uint) (uint, error) {
+ xp := uint(0)
+ rows, err := gokord.DB.
+ Model(&CopaingXP{}).
+ Where(
+ "created_at >= ? and guild_id = ? and copaing_id = ?",
+ exp.TimeStampNDaysBefore(n),
+ c.GuildID,
+ c.ID,
+ ).
+ Rows()
+ defer rows.Close()
+ if err != nil {
+ return 0, err
+ }
+ for rows.Next() {
+ var cxp CopaingXP
+ err = gokord.DB.ScanRows(rows, &cxp)
+ if err != nil {
+ utils.SendAlert("user/xp.go - Scanning rows", err.Error(), "copaing_id", c.ID, "guild_id", c.GuildID)
+ continue
+ }
+ xp += cxp.XP
+ }
+ return xp, nil
+}
+
+// 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) {
+ 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
+ }
+ var l []*cXP
+ wg := sync.WaitGroup{}
+ for rows.Next() {
+ var c Copaing
+ err = gokord.DB.ScanRows(rows, &c)
+ if err != nil {
+ utils.SendAlert("user/xp.go - Scanning rows", err.Error(), "guild_id", guildId)
+ continue
+ }
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ xp, err := c.GetXPForDays(uint(d))
+ if err != nil {
+ utils.SendAlert("user/xp.go - Fetching xp", err.Error(), "discord_id", c.DiscordID, "guild_id", guildId)
+ return
+ }
+ l = append(l, &cXP{Cxp: xp, Copaing: &c})
+ }()
+ }
+ wg.Wait()
+ slices.SortFunc(l, func(a, b *cXP) int {
+ // desc order
+ if a.Cxp < b.Cxp {
+ return 1
+ }
+ if a.Cxp > b.Cxp {
+ return -1
+ }
+ return 0
+ })
+ m := min(len(l), int(n))
+ cs := make([]CopaingAccess, m)
+ for i, c := range l[:m] {
+ cs[i] = c
+ }
+ return cs, nil
+}