diff options
Diffstat (limited to 'user/state.go')
| -rw-r--r-- | user/state.go | 220 |
1 files changed, 220 insertions, 0 deletions
diff --git a/user/state.go b/user/state.go new file mode 100644 index 0000000..effdc80 --- /dev/null +++ b/user/state.go @@ -0,0 +1,220 @@ +package user + +import ( + "context" + "errors" + "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 + XP uint + XPs []XPCached + XPToAdd uint +} + +// copaing turns a CopaingCached into a Copaing. +// This operation get the copaing synced with the database. +// It doesn't: +// - save the copaing in the database, use CopaingCached.SaveInDB for that; +// - sync the copaing cached, use CopaingCached.Sync for that. +// TL;DR: don't use this method, unless you know what are you doing. +func (cc *CopaingCached) copaing() *Copaing { + c := &Copaing{DiscordID: cc.DiscordID, GuildID: cc.GuildID} + if err := c.load(); err != nil { + panic(err) + } + return c +} + +func (cc *CopaingCached) Sync(ctx context.Context) error { + if cc.mustSave() { + return ErrSyncingUnsavedData + } + synced := FromCopaing(cc.copaing()) + synced.XP += cc.XPToAdd + synced.XPToAdd = cc.XPToAdd + err := synced.Save(ctx) + if err != nil { + return err + } + *cc = *synced + return nil +} + +func (cc *CopaingCached) Save(ctx context.Context) error { + state := GetState(ctx) + + state.mu.Lock() + defer state.mu.Unlock() + + return state.storage.Write(KeyCopaingCachedRaw(cc.GuildID, cc.DiscordID), *cc) +} + +func (cc *CopaingCached) SaveInDB(ctx context.Context) error { + c := cc.copaing() + c.CopaingXPs = append(c.CopaingXPs, CopaingXP{CopaingID: c.ID, XP: cc.XPToAdd, GuildID: c.GuildID}) + err := c.Save() + if err != nil { + return err + } + cc.XPToAdd = 0 + return cc.Save(ctx) +} + +func (cc *CopaingCached) Delete(ctx context.Context) error { + c := cc.copaing() + err := c.Delete() + if err != nil { + return err + } + state := GetState(ctx) + + state.mu.Lock() + defer state.mu.Unlock() + + return state.storage.Delete(KeyCopaingCached(c)) +} + +func (cc *CopaingCached) mustSave() bool { + return cc.XPToAdd > 0 +} + +func saveStateInDB(ctx context.Context) error { + 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 { + return err + } + } + } + return nil +} + +func FromCopaing(c *Copaing) *CopaingCached { + return &CopaingCached{ + ID: c.ID, + DiscordID: c.DiscordID, + GuildID: c.GuildID, + XP: calcXP(c), + XPs: generateXPs(c), + XPToAdd: 0, + } +} + +const KeyCopaingCachedPrefix = "cc:" + +func KeyCopaingCached(c *Copaing) state.Key { + return KeyCopaingCachedRaw(c.GuildID, c.DiscordID) +} + +func KeyCopaingCachedRaw(guildID, copaingID string) state.Key { + return KeyCopaingCachedPrefix + state.Key(guildID+":"+copaingID) +} + +type State struct { + mu sync.RWMutex + saveInDB sync.Mutex + storage state.MapStorage[CopaingCached] +} + +func NewState() *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" + +func GetState(ctx context.Context) *State { + return ctx.Value(ContextKeyState).(*State) +} + +func SetState(ctx context.Context, state *State) context.Context { + return context.WithValue(ctx, ContextKeyState, state) +} + +func (s *State) Copaing(guildID, copaingID string) (*CopaingCached, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + raw, err := s.storage.Get(KeyCopaingCachedRaw(guildID, copaingID)) + if err != nil { + return nil, err + } + c := raw.(CopaingCached) + return &c, nil +} + +func (s *State) Copaings(guild string) []CopaingCached { + s.mu.RLock() + defer s.mu.RUnlock() + + var ccs []CopaingCached + for _, cc := range s.storage { + if cc.GuildID == guild { + ccs = append(ccs, cc) + } + } + return ccs +} + +func calcXP(c *Copaing) uint { + var sum uint + for _, entry := range c.CopaingXPs { + sum += entry.XP + } + 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 +} |
