aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--commands/rank.go7
-rw-r--r--commands/top.go11
-rw-r--r--exp/functions.go2
-rw-r--r--justfile5
-rw-r--r--user/level.go4
-rw-r--r--user/state.go63
-rw-r--r--user/xp.go96
7 files changed, 79 insertions, 109 deletions
diff --git a/commands/rank.go b/commands/rank.go
index c65c758..3a017f6 100644
--- a/commands/rank.go
+++ b/commands/rank.go
@@ -38,15 +38,12 @@ func Rank(ctx context.Context) func(s bot.Session, i *event.InteractionCreate, o
c = user.GetCopaing(ctx, u.ID, i.GuildID) // current user = member targeted by member who wrote /rank
msg = fmt.Sprintf("Le niveau de %s", m.DisplayName())
}
- xp := c.XPs
+ xp := c.XP
lvl := exp.Level(xp)
nxtLvlXP := exp.LevelXP(lvl + 1)
err = resp.SetMessage(fmt.Sprintf(
"%s : **%d**\n> XP : %d\n> Prochain niveau dans %d XP",
- msg,
- lvl,
- xp,
- nxtLvlXP-xp,
+ msg, lvl, xp, nxtLvlXP-xp,
)).Send()
if err != nil {
s.Logger().Error("sending rank", "error", err)
diff --git a/commands/top.go b/commands/top.go
index fa12a66..99c8f16 100644
--- a/commands/top.go
+++ b/commands/top.go
@@ -16,17 +16,12 @@ import (
func Top(ctx context.Context) func(s bot.Session, i *event.InteractionCreate, _ cmd.OptionMap, resp *cmd.ResponseBuilder) {
return func(s bot.Session, i *event.InteractionCreate, _ cmd.OptionMap, resp *cmd.ResponseBuilder) {
- err := resp.IsDeferred().Send()
- if err != nil {
- s.Logger().Error("sending deferred", "error", err)
- return
- }
embeds := make([]*channel.MessageEmbed, 3)
wg := sync.WaitGroup{}
fn := func(str string, n uint, d int, id int) {
defer wg.Done()
- tops, err := user.GetBestXP(ctx, s.Logger(), i.GuildID, n, d)
+ tops, err := user.GetBestXP(ctx, i.GuildID, n, d)
if err != nil {
s.Logger().Error("fetching best xp", "error", err, "n", n, "d", d, "id", id, "guild", i.GuildID)
embeds[id] = &channel.MessageEmbed{
@@ -60,7 +55,7 @@ func Top(ctx context.Context) func(s bot.Session, i *event.InteractionCreate, _
resp.AddEmbed(embeds[1]).
AddEmbed(embeds[2])
}
- err = resp.Send()
+ err := resp.Send()
if err != nil {
s.Logger().Error("sending response top", "error", err)
}
@@ -71,7 +66,7 @@ func Top(ctx context.Context) func(s bot.Session, i *event.InteractionCreate, _
func genTopsMessage(tops []user.CopaingCached) string {
msg := ""
for i, c := range tops {
- msg += fmt.Sprintf("%d. **<@%s>** - niveau %d", i+1, c.DiscordID, exp.Level(c.XPs))
+ msg += fmt.Sprintf("%d. **<@%s>** - niveau %d", i+1, c.DiscordID, exp.Level(c.XP))
if i != len(tops)-1 {
msg += "\n"
}
diff --git a/exp/functions.go b/exp/functions.go
index 1b2fe89..681c135 100644
--- a/exp/functions.go
+++ b/exp/functions.go
@@ -11,7 +11,7 @@ import (
"github.com/anhgelus/gokord"
)
-const DebugFactor = 5
+const DebugFactor = 30
func MessageXP(length uint, diversity uint) uint {
return uint(math.Floor(
diff --git a/justfile b/justfile
index 4e0c917..e66752f 100644
--- a/justfile
+++ b/justfile
@@ -12,10 +12,7 @@ update:
go run .
stop:
- podman stop postgres adminer || (echo "no container"; exit 0)
- podman network rm db
-
-clean-network:
+ podman stop postgres adminer || (echo "no container")
podman network rm db || echo "no network"
build:
diff --git a/user/level.go b/user/level.go
index 07df16f..92c10ab 100644
--- a/user/level.go
+++ b/user/level.go
@@ -93,13 +93,13 @@ func syncCopaings(ctx context.Context, s *discordgo.Session, ccs []CopaingCached
s.Logger().Debug("sleeping...")
time.Sleep(15 * time.Second) // prevents spamming the API
}
- oldXp := cc.XPs
+ oldXp := cc.XP
err := cc.Sync(ctx)
if err != nil {
s.Logger().Error("syncing copaing", "error", err, "copaing", cc.ID, "guild", cc.GuildID)
continue
}
- xp := cc.XPs
+ xp := cc.XP
if exp.Level(oldXp) != exp.Level(xp) {
cc.onNewLevel(s, exp.Level(xp))
}
diff --git a/user/state.go b/user/state.go
index b977fb6..effdc80 100644
--- a/user/state.go
+++ b/user/state.go
@@ -6,18 +6,24 @@ import (
"sync"
"time"
+ "github.com/anhgelus/gokord"
"github.com/nyttikord/gokord/state"
)
var ErrSyncingUnsavedData = errors.New("trying to sync unsaved data")
+type XPCached struct {
+ XP uint
+ Time time.Duration
+}
+
type CopaingCached struct {
ID uint
DiscordID string
GuildID string
- XPs uint
+ XP uint
+ XPs []XPCached
XPToAdd uint
- lastSync time.Time // time.Time of the lastSync
}
// copaing turns a CopaingCached into a Copaing.
@@ -39,9 +45,8 @@ func (cc *CopaingCached) Sync(ctx context.Context) error {
return ErrSyncingUnsavedData
}
synced := FromCopaing(cc.copaing())
- synced.XPs += cc.XPToAdd
+ synced.XP += cc.XPToAdd
synced.XPToAdd = cc.XPToAdd
- synced.lastSync = time.Now()
err := synced.Save(ctx)
if err != nil {
return err
@@ -89,7 +94,12 @@ func (cc *CopaingCached) mustSave() bool {
}
func saveStateInDB(ctx context.Context) error {
- for _, v := range GetState(ctx).storage {
+ state := GetState(ctx)
+
+ state.saveInDB.Lock()
+ defer state.saveInDB.Unlock()
+
+ for _, v := range state.storage {
if v.mustSave() {
err := v.SaveInDB(ctx)
if err != nil {
@@ -105,7 +115,8 @@ func FromCopaing(c *Copaing) *CopaingCached {
ID: c.ID,
DiscordID: c.DiscordID,
GuildID: c.GuildID,
- XPs: calcXP(c),
+ XP: calcXP(c),
+ XPs: generateXPs(c),
XPToAdd: 0,
}
}
@@ -121,14 +132,24 @@ func KeyCopaingCachedRaw(guildID, copaingID string) state.Key {
}
type State struct {
- mu sync.RWMutex
- storage state.MapStorage[CopaingCached]
+ mu sync.RWMutex
+ saveInDB sync.Mutex
+ storage state.MapStorage[CopaingCached]
}
func NewState() *State {
- return &State{
+ state := &State{
storage: state.MapStorage[CopaingCached]{},
}
+ var cs []*Copaing
+ err := gokord.DB.Find(&cs).Error
+ if err != nil {
+ panic(err)
+ }
+ for _, v := range cs {
+ FromCopaing(v).Save(SetState(context.Background(), state))
+ }
+ return state
}
const ContextKeyState = "state"
@@ -173,3 +194,27 @@ func calcXP(c *Copaing) uint {
}
return sum
}
+
+func generateXPs(c *Copaing) []XPCached {
+ data := map[time.Duration]XPCached{}
+ sixH := 6 * time.Hour
+ for _, xp := range c.CopaingXPs {
+ // we add sixH at the end because we want it to be rounded to ceil
+ since := time.Since(xp.CreatedAt)/sixH + sixH
+ if v, ok := data[since]; ok {
+ v.XP += xp.XP
+ } else {
+ data[since] = XPCached{
+ Time: since,
+ XP: xp.XP,
+ }
+ }
+ }
+ ccs := make([]XPCached, len(data))
+ i := 0
+ for _, v := range data {
+ ccs[i] = v
+ i++
+ }
+ return ccs
+}
diff --git a/user/xp.go b/user/xp.go
index 9c6c6ab..c87c450 100644
--- a/user/xp.go
+++ b/user/xp.go
@@ -2,13 +2,10 @@ package user
import (
"context"
- "log/slog"
"slices"
- "sync"
+ "time"
- "git.anhgelus.world/anhgelus/les-copaings-bot/config"
"git.anhgelus.world/anhgelus/les-copaings-bot/exp"
- "github.com/anhgelus/gokord"
"github.com/nyttikord/gokord/bot"
"github.com/nyttikord/gokord/user"
)
@@ -27,10 +24,10 @@ func (c *cXP) GetXP() uint {
}
func (cc *CopaingCached) AddXP(ctx context.Context, s bot.Session, m *user.Member, xp uint, fn func(uint, uint)) {
- old := cc.XPs
+ old := cc.XP
pastLevel := exp.Level(old)
s.Logger().Debug("adding xp", "user", m.DisplayName(), "old", old, "to add", xp)
- cc.XPs += xp
+ cc.XP += xp
cc.XPToAdd += xp
if err := cc.Save(ctx); err != nil {
s.Logger().Error("saving user in state", "error", err, "user", m.DisplayName(), "xp", xp, "guild", cc.GuildID)
@@ -43,88 +40,27 @@ func (cc *CopaingCached) AddXP(ctx context.Context, s bot.Session, m *user.Membe
}
}
-func (c *Copaing) GetXP(logger *slog.Logger) (uint, error) {
- cfg := config.GetGuildConfig(c.GuildID)
- return c.GetXPForDays(logger, cfg.DaysXPRemains)
-}
-
-func (c *Copaing) GetXPForDays(logger *slog.Logger, n uint) (uint, error) {
+func (cc *CopaingCached) GetXPForDays(n uint) uint {
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()
- 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.Error("scanning rows", "error", err, "copaing", c.ID, "guild", c.GuildID)
- continue
+ for _, v := range cc.XPs {
+ if v.Time <= time.Duration(n*24)*time.Hour {
+ xp += v.XP
}
- xp += cxp.XP
}
- return xp, nil
+ return xp + cc.XPToAdd
}
// 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(ctx context.Context, logger *slog.Logger, guildId string, n uint, d int) ([]CopaingCached, error) {
- if d < 0 {
- cfg := config.GetGuildConfig(guildId)
- d = int(cfg.DaysXPRemains)
- return getBestXPFull(ctx, guildId, n), nil
- }
- rows, err := gokord.DB.Model(&Copaing{}).Where("guild_id = ?", guildId).Rows()
- if err != nil {
- return nil, err
- }
- defer rows.Close()
- var l []*cXP
- var wg sync.WaitGroup
- for rows.Next() {
- var c Copaing
- err = gokord.DB.ScanRows(rows, &c)
- if err != nil {
- logger.Error("scanning rows", "error", err, "copaing", c.ID, "guild", c.GuildID)
- continue
+func GetBestXP(ctx context.Context, guildId string, n uint, d int) ([]CopaingCached, error) {
+ ccs := GetState(ctx).Copaings(guildId)
+ if d > 0 {
+ for _, v := range ccs {
+ v.XP = v.GetXPForDays(n)
}
- wg.Go(func() {
- xp, err := c.GetXPForDays(logger, uint(d))
- if err != nil {
- logger.Error("fetching xp", "error", err, "copaing", c.ID, "guild", c.GuildID)
- return
- }
- l = append(l, &cXP{Cxp: xp, copaing: &c})
- })
}
- wg.Wait()
- slices.SortFunc(l, func(a, b *cXP) int {
- // desc order
- return int(b.Cxp) - int(a.Cxp)
- })
- m := min(len(l), int(n))
- cs := make([]CopaingCached, m)
- for i, c := range l[:m] {
- cs[i] = CopaingCached{DiscordID: c.copaing.DiscordID, XPs: c.Cxp}
- }
- return cs, nil
-}
-
-func getBestXPFull(ctx context.Context, guildId string, n uint) []CopaingCached {
- ccs := GetState(ctx).Copaings(guildId)
slices.SortFunc(ccs, func(a, b CopaingCached) int {
- return int(b.XPs) - int(a.XPs)
+ // desc order
+ return int(b.XP) - int(a.XP)
})
- m := min(len(ccs), int(n))
- return ccs[:m]
+ return ccs[:min(len(ccs), int(n))], nil
}