1
0
Fork 0
mirror of https://git.sr.ht/~ashkeel/strimertul synced 2024-09-18 01:50:50 +00:00
strimertul/loyalty/manager.go

371 lines
8.8 KiB
Go
Raw Normal View History

2021-05-02 12:29:43 +00:00
package loyalty
import (
2022-11-30 18:15:47 +00:00
"context"
"errors"
2022-06-16 22:51:27 +00:00
"fmt"
"strings"
2021-05-18 11:30:27 +00:00
"time"
2021-05-02 12:29:43 +00:00
2023-11-10 20:36:15 +00:00
"git.sr.ht/~ashkeel/strimertul/database"
"git.sr.ht/~ashkeel/strimertul/twitch"
"git.sr.ht/~ashkeel/strimertul/utils"
2023-11-10 20:36:15 +00:00
"git.sr.ht/~ashkeel/containers/sync"
2021-05-02 12:29:43 +00:00
jsoniter "github.com/json-iterator/go"
2022-02-01 11:35:34 +00:00
"go.uber.org/zap"
2021-05-02 12:29:43 +00:00
)
var json = jsoniter.ConfigFastest
var (
2023-04-23 00:26:46 +00:00
ErrRedeemNotFound = errors.New("redeem not found")
2021-07-11 13:34:39 +00:00
ErrRedeemInCooldown = errors.New("redeem is on cooldown")
2021-05-18 13:50:58 +00:00
ErrGoalNotFound = errors.New("goal not found")
ErrGoalAlreadyReached = errors.New("goal already reached")
)
2021-05-02 12:29:43 +00:00
type Manager struct {
points *sync.Map[string, PointsEntry]
Config *sync.RWSync[Config]
Rewards *sync.Slice[Reward]
Goals *sync.Slice[Goal]
Queue *sync.Slice[Redeem]
db *database.LocalDBClient
logger *zap.Logger
cooldowns map[string]time.Time
banlist map[string]bool
activeUsers *sync.Map[string, bool]
twitchManager *twitch.Manager
ctx context.Context
cancelFn context.CancelFunc
cancelSub database.CancelFunc
restartTwitchHandler chan struct{}
2021-05-02 12:29:43 +00:00
}
func NewManager(db *database.LocalDBClient, twitchManager *twitch.Manager, logger *zap.Logger) (*Manager, error) {
2022-11-30 18:15:47 +00:00
ctx, cancelFn := context.WithCancel(context.Background())
loyalty := &Manager{
Config: sync.NewRWSync(Config{Enabled: false}),
Rewards: sync.NewSlice[Reward](),
Goals: sync.NewSlice[Goal](),
Queue: sync.NewSlice[Redeem](),
2022-11-30 18:15:47 +00:00
logger: logger,
db: db,
points: sync.NewMap[string, PointsEntry](),
cooldowns: make(map[string]time.Time),
banlist: make(map[string]bool),
activeUsers: sync.NewMap[string, bool](),
twitchManager: twitchManager,
ctx: ctx,
cancelFn: cancelFn,
restartTwitchHandler: make(chan struct{}),
2021-05-02 12:29:43 +00:00
}
2022-06-16 22:51:27 +00:00
// Get data from DB
2022-11-30 18:15:47 +00:00
var config Config
if err := db.GetJSON(ConfigKey, &config); err == nil {
2022-11-30 18:15:47 +00:00
loyalty.Config.Set(config)
} else {
2022-06-16 22:51:27 +00:00
if !errors.Is(err, database.ErrEmptyKey) {
2022-11-30 18:15:47 +00:00
return nil, fmt.Errorf("could not retrieve loyalty config: %w", err)
2021-05-02 12:29:43 +00:00
}
}
// Retrieve configs
2022-12-03 23:04:15 +00:00
var rewards []Reward
if err := db.GetJSON(RewardsKey, &rewards); err == nil {
2022-11-30 18:15:47 +00:00
loyalty.Rewards.Set(rewards)
} else {
2022-06-16 22:51:27 +00:00
if !errors.Is(err, database.ErrEmptyKey) {
2022-11-30 18:15:47 +00:00
return nil, err
2021-05-02 12:29:43 +00:00
}
}
2022-11-30 18:15:47 +00:00
2022-12-03 23:04:15 +00:00
var goals []Goal
if err := db.GetJSON(GoalsKey, &goals); err == nil {
2022-11-30 18:15:47 +00:00
loyalty.Goals.Set(goals)
} else {
2022-06-16 22:51:27 +00:00
if !errors.Is(err, database.ErrEmptyKey) {
2022-11-30 18:15:47 +00:00
return nil, err
2021-05-02 12:29:43 +00:00
}
}
2022-11-30 18:15:47 +00:00
2022-12-03 23:04:15 +00:00
var queue []Redeem
if err := db.GetJSON(QueueKey, &queue); err == nil {
2022-11-30 18:15:47 +00:00
loyalty.Queue.Set(queue)
} else {
2022-06-16 22:51:27 +00:00
if !errors.Is(err, database.ErrEmptyKey) {
2022-11-30 18:15:47 +00:00
return nil, err
2021-05-02 12:29:43 +00:00
}
}
// Retrieve user points
points, err := db.GetAll(PointsPrefix)
if err != nil {
2022-06-16 22:51:27 +00:00
if !errors.Is(err, database.ErrEmptyKey) {
2022-11-30 18:15:47 +00:00
return nil, err
}
points = make(map[string]string)
}
2022-11-30 18:15:47 +00:00
for k, v := range points {
var entry PointsEntry
err := json.UnmarshalFromString(v, &entry)
if err != nil {
2022-11-30 18:15:47 +00:00
return nil, err
}
loyalty.points.SetKey(k[len(PointsPrefix):], entry)
}
// SubscribePrefix for changes
err, loyalty.cancelSub = db.SubscribePrefix(loyalty.update, "loyalty/")
if err != nil {
2023-04-23 00:26:46 +00:00
logger.Error("Could not setup loyalty reload subscription", zap.Error(err))
}
2022-11-30 18:15:47 +00:00
loyalty.SetBanList(config.BanList)
2022-11-30 18:15:47 +00:00
// Setup twitch integration
loyalty.SetupTwitch()
2021-05-02 12:29:43 +00:00
2022-11-30 18:15:47 +00:00
return loyalty, nil
2021-12-09 09:46:14 +00:00
}
func (m *Manager) Close() error {
// Stop subscription
if m.cancelSub != nil {
m.cancelSub()
}
2022-11-30 18:15:47 +00:00
// Send cancellation
m.cancelFn()
// Teardown twitch integration
m.StopTwitch()
return nil
2021-05-02 12:29:43 +00:00
}
2022-02-01 11:35:34 +00:00
func (m *Manager) update(key, value string) {
var err error
// Check for config changes/RPC
switch key {
case ConfigKey:
2022-11-30 18:15:47 +00:00
err = utils.LoadJSONToWrapped[Config](value, m.Config)
if err == nil {
m.SetBanList(m.Config.Get().BanList)
m.restartTwitchHandler <- struct{}{}
m.StopTwitch()
m.SetupTwitch()
2022-11-30 18:15:47 +00:00
}
2022-02-01 11:35:34 +00:00
case GoalsKey:
2022-12-03 23:04:15 +00:00
err = utils.LoadJSONToWrapped[[]Goal](value, m.Goals)
2022-02-01 11:35:34 +00:00
case RewardsKey:
2022-12-03 23:04:15 +00:00
err = utils.LoadJSONToWrapped[[]Reward](value, m.Rewards)
2022-02-01 11:35:34 +00:00
case QueueKey:
2022-12-03 23:04:15 +00:00
err = utils.LoadJSONToWrapped[[]Redeem](value, m.Queue)
2022-02-01 11:35:34 +00:00
case CreateRedeemRPC:
var redeem Redeem
err = json.UnmarshalFromString(value, &redeem)
2022-02-01 11:35:34 +00:00
if err == nil {
err = m.AddRedeem(redeem)
}
case RemoveRedeemRPC:
var redeem Redeem
err = json.UnmarshalFromString(value, &redeem)
2022-02-01 11:35:34 +00:00
if err == nil {
err = m.RemoveRedeem(redeem)
}
default:
// Check for prefix changes
switch {
// User point changed
case strings.HasPrefix(key, PointsPrefix):
var entry PointsEntry
err = json.UnmarshalFromString(value, &entry)
2022-02-01 11:35:34 +00:00
user := key[len(PointsPrefix):]
2022-11-30 18:15:47 +00:00
m.points.SetKey(user, entry)
2021-05-02 12:29:43 +00:00
}
}
2022-02-01 11:35:34 +00:00
if err != nil {
2023-04-23 00:26:46 +00:00
m.logger.Error("Subscribe error: invalid JSON received on key", zap.Error(err), zap.String("key", key))
2022-02-01 11:35:34 +00:00
} else {
2023-04-23 00:26:46 +00:00
m.logger.Debug("Updated key", zap.String("key", key))
2022-02-01 11:35:34 +00:00
}
2021-05-02 12:29:43 +00:00
}
func (m *Manager) GetPoints(user string) int64 {
2022-11-30 18:15:47 +00:00
points, ok := m.points.GetKey(user)
if ok {
return points.Points
}
return 0
}
func (m *Manager) setPoints(user string, points int64) error {
2022-11-30 18:15:47 +00:00
entry := PointsEntry{
Points: points,
}
2022-11-30 18:15:47 +00:00
m.points.SetKey(user, entry)
return m.db.PutJSON(PointsPrefix+user, entry)
}
2021-05-02 12:29:43 +00:00
func (m *Manager) GivePoints(pointsToGive map[string]int64) error {
// Add points to each user
for user, points := range pointsToGive {
balance := m.GetPoints(user)
if err := m.setPoints(user, balance+points); err != nil {
return err
}
2021-05-02 12:29:43 +00:00
}
return nil
2021-05-02 12:29:43 +00:00
}
func (m *Manager) TakePoints(pointsToTake map[string]int64) error {
// Add points to each user
for user, points := range pointsToTake {
balance := m.GetPoints(user)
if err := m.setPoints(user, balance-points); err != nil {
return err
}
2021-05-02 12:29:43 +00:00
}
return nil
2021-05-02 12:29:43 +00:00
}
func (m *Manager) saveQueue() error {
2022-11-30 18:15:47 +00:00
return m.db.PutJSON(QueueKey, m.Queue.Get())
2021-05-02 12:29:43 +00:00
}
2021-07-11 13:34:39 +00:00
func (m *Manager) GetRewardCooldown(rewardID string) time.Time {
cooldown, ok := m.cooldowns[rewardID]
if !ok {
// Return zero time for a reward with no cooldown
return time.Time{}
}
return cooldown
}
func (m *Manager) AddRedeem(redeem Redeem) error {
// Add to local list
2022-11-30 18:15:47 +00:00
m.Queue.Set(append(m.Queue.Get(), redeem))
// Send redeem event
if err := m.db.PutJSON(RedeemEvent, redeem); err != nil {
return err
}
2021-05-02 12:29:43 +00:00
2021-07-11 13:34:39 +00:00
// Add cooldown if applicable
if redeem.Reward.Cooldown > 0 {
m.cooldowns[redeem.Reward.ID] = time.Now().Add(time.Second * time.Duration(redeem.Reward.Cooldown))
}
2021-05-02 12:29:43 +00:00
// Save points
return m.saveQueue()
2021-05-02 12:29:43 +00:00
}
2021-05-18 11:30:27 +00:00
func (m *Manager) PerformRedeem(redeem Redeem) error {
2021-07-11 13:34:39 +00:00
// Check cooldown
if time.Now().Before(m.GetRewardCooldown(redeem.Reward.ID)) {
return ErrRedeemInCooldown
}
2021-05-18 11:30:27 +00:00
// Add redeem
err := m.AddRedeem(redeem)
if err != nil {
return err
}
// Remove points from user
return m.TakePoints(map[string]int64{redeem.Username: redeem.Reward.Price})
}
func (m *Manager) RemoveRedeem(redeem Redeem) error {
2022-11-30 18:15:47 +00:00
queue := m.Queue.Get()
for index, queued := range queue {
if queued.When == redeem.When && queued.Username == redeem.Username && queued.Reward.ID == redeem.Reward.ID {
// Remove redemption from list
2022-11-30 18:15:47 +00:00
m.Queue.Set(append(queue[:index], queue[index+1:]...))
// Save points
return m.saveQueue()
}
}
2023-04-23 00:26:46 +00:00
return ErrRedeemNotFound
}
func (m *Manager) SaveGoals() error {
2022-11-30 18:15:47 +00:00
return m.db.PutJSON(GoalsKey, m.Goals.Get())
}
func (m *Manager) ContributeGoal(goal Goal, user string, points int64) error {
2022-11-30 18:15:47 +00:00
goals := m.Goals.Get()
for i, savedGoal := range goals {
if savedGoal.ID != goal.ID {
continue
}
2022-11-30 18:15:47 +00:00
goals[i].Contributed += points
goals[i].Contributors[user] += points
m.Goals.Set(goals)
return m.SaveGoals()
}
return ErrGoalNotFound
}
2023-02-06 09:36:00 +00:00
func (m *Manager) PerformContribution(goal Goal, user string, points int64) (int64, error) {
2021-05-18 13:50:58 +00:00
// Get user balance
balance := m.GetPoints(user)
// If user specified more points than they have, pick the maximum possible
if points > balance {
points = balance
}
// Check if goal was reached already
if goal.Contributed >= goal.TotalGoal {
2023-02-06 09:36:00 +00:00
return 0, ErrGoalAlreadyReached
2021-05-18 13:50:58 +00:00
}
// If remaining points are lower than what user is contributing, only take what's needed
remaining := goal.TotalGoal - goal.Contributed
if points > remaining {
points = remaining
}
// Remove points from user
if err := m.TakePoints(map[string]int64{user: points}); err != nil {
2023-02-06 09:36:00 +00:00
return 0, err
2021-05-18 13:50:58 +00:00
}
// Add points to goal
2023-02-06 09:36:00 +00:00
return points, m.ContributeGoal(goal, user, points)
2021-05-18 13:50:58 +00:00
}
func (m *Manager) GetReward(id string) Reward {
2022-11-30 18:15:47 +00:00
for _, reward := range m.Rewards.Get() {
2021-05-18 11:30:27 +00:00
if reward.ID == id {
return reward
}
}
return Reward{}
}
2021-05-18 13:50:58 +00:00
func (m *Manager) GetGoal(id string) Goal {
2022-11-30 18:15:47 +00:00
for _, goal := range m.Goals.Get() {
2021-05-18 13:50:58 +00:00
if goal.ID == id {
return goal
}
}
return Goal{}
}
2022-11-30 18:15:47 +00:00
func (m *Manager) Equals(c utils.Comparable) bool {
if manager, ok := c.(*Manager); ok {
return m == manager
}
return false
}