mirror of
https://git.sr.ht/~ashkeel/strimertul
synced 2024-09-20 02:00:49 +00:00
Add community goals bot commands and start fixing race conditions
This commit is contained in:
parent
3996b896b3
commit
dfb36dcdf8
3 changed files with 196 additions and 23 deletions
|
@ -3,6 +3,7 @@ package loyalty
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/dgraph-io/badger/v3"
|
"github.com/dgraph-io/badger/v3"
|
||||||
jsoniter "github.com/json-iterator/go"
|
jsoniter "github.com/json-iterator/go"
|
||||||
|
@ -14,11 +15,12 @@ import (
|
||||||
|
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
Config Config
|
Config Config
|
||||||
Points PointStorage
|
|
||||||
Rewards RewardStorage
|
Rewards RewardStorage
|
||||||
Goals GoalStorage
|
Goals GoalStorage
|
||||||
RedeemQueue RedeemQueueStorage
|
RedeemQueue RedeemQueueStorage
|
||||||
|
|
||||||
|
points PointStorage
|
||||||
|
pointsmu sync.Mutex
|
||||||
hub *kv.Hub
|
hub *kv.Hub
|
||||||
logger logrus.FieldLogger
|
logger logrus.FieldLogger
|
||||||
}
|
}
|
||||||
|
@ -31,6 +33,7 @@ func NewManager(db *database.DB, hub *kv.Hub, log logrus.FieldLogger) (*Manager,
|
||||||
manager := &Manager{
|
manager := &Manager{
|
||||||
logger: log,
|
logger: log,
|
||||||
hub: hub,
|
hub: hub,
|
||||||
|
pointsmu: sync.Mutex{},
|
||||||
}
|
}
|
||||||
// Ger data from DB
|
// Ger data from DB
|
||||||
if err := db.GetJSON(ConfigKey, &manager.Config); err != nil {
|
if err := db.GetJSON(ConfigKey, &manager.Config); err != nil {
|
||||||
|
@ -40,9 +43,9 @@ func NewManager(db *database.DB, hub *kv.Hub, log logrus.FieldLogger) (*Manager,
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := db.GetJSON(PointsKey, &manager.Points); err != nil {
|
if err := db.GetJSON(PointsKey, &manager.points); err != nil {
|
||||||
if err == badger.ErrKeyNotFound {
|
if err == badger.ErrKeyNotFound {
|
||||||
manager.Points = make(PointStorage)
|
manager.points = make(PointStorage)
|
||||||
} else {
|
} else {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -78,7 +81,9 @@ func (m *Manager) update(kvs []database.ModifiedKV) error {
|
||||||
case ConfigKey:
|
case ConfigKey:
|
||||||
err = jsoniter.ConfigFastest.Unmarshal(kv.Data, &m.Config)
|
err = jsoniter.ConfigFastest.Unmarshal(kv.Data, &m.Config)
|
||||||
case PointsKey:
|
case PointsKey:
|
||||||
err = jsoniter.ConfigFastest.Unmarshal(kv.Data, &m.Points)
|
m.pointsmu.Lock()
|
||||||
|
err = jsoniter.ConfigFastest.Unmarshal(kv.Data, &m.points)
|
||||||
|
m.pointsmu.Unlock()
|
||||||
case GoalsKey:
|
case GoalsKey:
|
||||||
err = jsoniter.ConfigFastest.Unmarshal(kv.Data, &m.Goals)
|
err = jsoniter.ConfigFastest.Unmarshal(kv.Data, &m.Goals)
|
||||||
case RewardsKey:
|
case RewardsKey:
|
||||||
|
@ -111,14 +116,33 @@ func (m *Manager) update(kvs []database.ModifiedKV) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) SavePoints() error {
|
func (m *Manager) SavePoints() error {
|
||||||
data, _ := jsoniter.ConfigFastest.Marshal(m.Points)
|
m.pointsmu.Lock()
|
||||||
|
defer m.pointsmu.Unlock()
|
||||||
|
data, _ := jsoniter.ConfigFastest.Marshal(m.points)
|
||||||
return m.hub.WriteKey(PointsKey, string(data))
|
return m.hub.WriteKey(PointsKey, string(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Manager) GetPoints(user string) int64 {
|
||||||
|
m.pointsmu.Lock()
|
||||||
|
defer m.pointsmu.Unlock()
|
||||||
|
points, ok := m.points[user]
|
||||||
|
if ok {
|
||||||
|
return points
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) SetPoints(user string, points int64) {
|
||||||
|
m.pointsmu.Lock()
|
||||||
|
defer m.pointsmu.Unlock()
|
||||||
|
m.points[user] = points
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Manager) GivePoints(pointsToGive map[string]int64) error {
|
func (m *Manager) GivePoints(pointsToGive map[string]int64) error {
|
||||||
// Add points to each user
|
// Add points to each user
|
||||||
for user, points := range pointsToGive {
|
for user, points := range pointsToGive {
|
||||||
m.Points[user] += points
|
balance := m.GetPoints(user)
|
||||||
|
m.SetPoints(user, balance+points)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save points
|
// Save points
|
||||||
|
@ -128,7 +152,8 @@ func (m *Manager) GivePoints(pointsToGive map[string]int64) error {
|
||||||
func (m *Manager) TakePoints(pointsToTake map[string]int64) error {
|
func (m *Manager) TakePoints(pointsToTake map[string]int64) error {
|
||||||
// Add points to each user
|
// Add points to each user
|
||||||
for user, points := range pointsToTake {
|
for user, points := range pointsToTake {
|
||||||
m.Points[user] -= points
|
balance := m.GetPoints(user)
|
||||||
|
m.SetPoints(user, balance-points)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save points
|
// Save points
|
||||||
|
@ -165,3 +190,14 @@ func (m *Manager) RemoveRedeem(redeem Redeem) error {
|
||||||
|
|
||||||
return errors.New("redeem not found")
|
return errors.New("redeem not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Manager) SaveGoals() error {
|
||||||
|
data, _ := jsoniter.ConfigFastest.Marshal(m.Goals)
|
||||||
|
return m.hub.WriteKey(GoalsKey, string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) ContributeGoal(goal *Goal, user string, points int64) error {
|
||||||
|
goal.Contributed += points
|
||||||
|
goal.Contributors[user] += points
|
||||||
|
return m.SaveGoals()
|
||||||
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ import (
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
Client *stulbe.Client
|
Client *stulbe.Client
|
||||||
db *database.DB
|
db *database.DB
|
||||||
|
logger logrus.FieldLogger
|
||||||
}
|
}
|
||||||
|
|
||||||
func Initialize(db *database.DB, logger logrus.FieldLogger) (*Manager, error) {
|
func Initialize(db *database.DB, logger logrus.FieldLogger) (*Manager, error) {
|
||||||
|
@ -35,6 +36,7 @@ func Initialize(db *database.DB, logger logrus.FieldLogger) (*Manager, error) {
|
||||||
return &Manager{
|
return &Manager{
|
||||||
Client: stulbeClient,
|
Client: stulbeClient,
|
||||||
db: db,
|
db: db,
|
||||||
|
logger: logger,
|
||||||
}, err
|
}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,6 +55,9 @@ func (m *Manager) ReplicateKey(key string) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
m.logger.WithFields(logrus.Fields{
|
||||||
|
"key": key,
|
||||||
|
}).Debug("set to remote")
|
||||||
|
|
||||||
// Subscribe to local datastore and update remote on change
|
// Subscribe to local datastore and update remote on change
|
||||||
return m.db.Subscribe(context.Background(), func(pairs []database.ModifiedKV) error {
|
return m.db.Subscribe(context.Background(), func(pairs []database.ModifiedKV) error {
|
||||||
|
@ -61,6 +66,9 @@ func (m *Manager) ReplicateKey(key string) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
m.logger.WithFields(logrus.Fields{
|
||||||
|
"key": changed.Key,
|
||||||
|
}).Debug("replicated to remote")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -2,6 +2,7 @@ package twitchbot
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -32,7 +33,7 @@ var commands = map[string]BotCommand{
|
||||||
Description: "Redeem a reward with loyalty points",
|
Description: "Redeem a reward with loyalty points",
|
||||||
Usage: "!redeem reward-id",
|
Usage: "!redeem reward-id",
|
||||||
AccessLevel: ALTEveryone,
|
AccessLevel: ALTEveryone,
|
||||||
Handler: cmdRedeem,
|
Handler: cmdRedeemReward,
|
||||||
},
|
},
|
||||||
"!balance": {
|
"!balance": {
|
||||||
Description: "See your current point balance",
|
Description: "See your current point balance",
|
||||||
|
@ -40,19 +41,27 @@ var commands = map[string]BotCommand{
|
||||||
AccessLevel: ALTEveryone,
|
AccessLevel: ALTEveryone,
|
||||||
Handler: cmdBalance,
|
Handler: cmdBalance,
|
||||||
},
|
},
|
||||||
|
"!goals": {
|
||||||
|
Description: "Check currently active community goals",
|
||||||
|
Usage: "!goals",
|
||||||
|
AccessLevel: ALTEveryone,
|
||||||
|
Handler: cmdGoalList,
|
||||||
|
},
|
||||||
|
"!contribute": {
|
||||||
|
Description: "Contribute points to a community goal",
|
||||||
|
Usage: "!contribute <points> [<goal-id>]",
|
||||||
|
AccessLevel: ALTEveryone,
|
||||||
|
Handler: cmdContributeGoal,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func cmdBalance(bot *TwitchBot, message irc.PrivateMessage) {
|
func cmdBalance(bot *TwitchBot, message irc.PrivateMessage) {
|
||||||
// Get user balance
|
// Get user balance
|
||||||
balance, ok := bot.Loyalty.Points[message.User.Name]
|
balance := bot.Loyalty.GetPoints(message.User.Name)
|
||||||
if !ok {
|
|
||||||
balance = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
bot.Client.Say(message.Channel, fmt.Sprintf("%s: You have %d %s!", message.User.DisplayName, balance, bot.Loyalty.Config.Currency))
|
bot.Client.Say(message.Channel, fmt.Sprintf("%s: You have %d %s!", message.User.DisplayName, balance, bot.Loyalty.Config.Currency))
|
||||||
}
|
}
|
||||||
|
|
||||||
func cmdRedeem(bot *TwitchBot, message irc.PrivateMessage) {
|
func cmdRedeemReward(bot *TwitchBot, message irc.PrivateMessage) {
|
||||||
parts := strings.Fields(message.Message)
|
parts := strings.Fields(message.Message)
|
||||||
if len(parts) < 2 {
|
if len(parts) < 2 {
|
||||||
return
|
return
|
||||||
|
@ -71,10 +80,7 @@ func cmdRedeem(bot *TwitchBot, message irc.PrivateMessage) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user balance
|
// Get user balance
|
||||||
balance, ok := bot.Loyalty.Points[message.User.Name]
|
balance := bot.Loyalty.GetPoints(message.User.Name)
|
||||||
if !ok {
|
|
||||||
balance = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if user can afford the reward
|
// Check if user can afford the reward
|
||||||
if balance-reward.Price < 0 {
|
if balance-reward.Price < 0 {
|
||||||
|
@ -96,10 +102,133 @@ func cmdRedeem(bot *TwitchBot, message irc.PrivateMessage) {
|
||||||
// Remove points from user
|
// Remove points from user
|
||||||
if err := bot.Loyalty.TakePoints(map[string]int64{message.User.Name: reward.Price}); err != nil {
|
if err := bot.Loyalty.TakePoints(map[string]int64{message.User.Name: reward.Price}); err != nil {
|
||||||
bot.logger.WithError(err).Error("error while taking points for redeem")
|
bot.logger.WithError(err).Error("error while taking points for redeem")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
bot.Client.Say(message.Channel, fmt.Sprintf("%s has redeemed %s! (new balance: %d %s)", message.User.DisplayName, reward.Name, bot.Loyalty.Points[message.User.Name], bot.Loyalty.Config.Currency))
|
bot.Client.Say(message.Channel, fmt.Sprintf("HolidayPresent %s has redeemed %s! (new balance: %d %s)", message.User.DisplayName, reward.Name, bot.Loyalty.GetPoints(message.User.Name), bot.Loyalty.Config.Currency))
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cmdGoalList(bot *TwitchBot, message irc.PrivateMessage) {
|
||||||
|
if len(bot.Loyalty.Goals) < 1 {
|
||||||
|
bot.Client.Say(message.Channel, fmt.Sprintf("%s: There are no active community goals right now :(!", message.User.DisplayName))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
msg := "Current goals: "
|
||||||
|
for _, goal := range bot.Loyalty.Goals {
|
||||||
|
if !goal.Enabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
msg += fmt.Sprintf("%s (%d/%d %s) [id: %s] | ", goal.Name, goal.Contributed, goal.TotalGoal, bot.Loyalty.Config.Currency, goal.ID)
|
||||||
|
}
|
||||||
|
msg += " Contribute with <!contribute POINTS GOALID>"
|
||||||
|
bot.Client.Say(message.Channel, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdContributeGoal(bot *TwitchBot, message irc.PrivateMessage) {
|
||||||
|
// Set defaults if user doesn't provide them
|
||||||
|
points := int64(100)
|
||||||
|
goalIndex := -1
|
||||||
|
hasGoals := false
|
||||||
|
|
||||||
|
// Get first unreached goal for default
|
||||||
|
for index, goal := range bot.Loyalty.Goals {
|
||||||
|
if !goal.Enabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
hasGoals = true
|
||||||
|
if goal.Contributed < goal.TotalGoal {
|
||||||
|
goalIndex = index
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do we not have any goal we can contribute to? Hooray I guess?
|
||||||
|
if goalIndex < 0 {
|
||||||
|
if hasGoals {
|
||||||
|
bot.Client.Say(message.Channel, fmt.Sprintf("%s: All active community goals have been reached already! ShowOfHands", message.User.DisplayName))
|
||||||
|
} else {
|
||||||
|
bot.Client.Say(message.Channel, fmt.Sprintf("%s: There are no active community goals right now :(!", message.User.DisplayName))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse parameters if provided
|
||||||
|
parts := strings.Fields(message.Message)
|
||||||
|
if len(parts) > 1 {
|
||||||
|
newpoints, err := strconv.ParseInt(parts[1], 10, 64)
|
||||||
|
if err == nil {
|
||||||
|
if newpoints <= 0 {
|
||||||
|
bot.Client.Say(message.Channel, fmt.Sprintf("Nice try %s SoBayed", message.User.DisplayName))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
points = newpoints
|
||||||
|
}
|
||||||
|
if len(parts) > 2 {
|
||||||
|
found := false
|
||||||
|
goalID := parts[2]
|
||||||
|
// Find Goal index
|
||||||
|
for index, goal := range bot.Loyalty.Goals {
|
||||||
|
if !goal.Enabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if goal.ID == goalID {
|
||||||
|
goalIndex = index
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Invalid goal ID provided
|
||||||
|
if !found {
|
||||||
|
bot.Client.Say(message.Channel, fmt.Sprintf("%s: I couldn't find that goal ID :(", message.User.DisplayName))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user balance
|
||||||
|
balance := bot.Loyalty.GetPoints(message.User.Name)
|
||||||
|
|
||||||
|
// If user specified more points than they have, pick the maximum possible
|
||||||
|
if points > balance {
|
||||||
|
points = balance
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get goal
|
||||||
|
selectedGoal := &bot.Loyalty.Goals[goalIndex]
|
||||||
|
|
||||||
|
// Check if goal was reached already
|
||||||
|
if selectedGoal.Contributed >= selectedGoal.TotalGoal {
|
||||||
|
bot.Client.Say(message.Channel, fmt.Sprintf("%s: This goal was already reached! ヾ(•ω•`)o", message.User.DisplayName))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If remaining points are lower than what user is contributing, only take what's needed
|
||||||
|
remaining := selectedGoal.TotalGoal - selectedGoal.Contributed
|
||||||
|
if points > remaining {
|
||||||
|
points = remaining
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove points from user
|
||||||
|
if err := bot.Loyalty.TakePoints(map[string]int64{message.User.Name: points}); err != nil {
|
||||||
|
bot.logger.WithError(err).Error("error while taking points for redeem")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add points to goal
|
||||||
|
if err := bot.Loyalty.ContributeGoal(selectedGoal, message.User.Name, points); err != nil {
|
||||||
|
bot.logger.WithError(err).Error("error while contributing to goal")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newRemaining := selectedGoal.TotalGoal - selectedGoal.Contributed
|
||||||
|
bot.Client.Say(message.Channel, fmt.Sprintf("ShowOfHands %s contributed %d %s to \"%s\"!! Only %d %s left ShowOfHands!", message.User.DisplayName, points, bot.Loyalty.Config.Currency, selectedGoal.Name, newRemaining, bot.Loyalty.Config.Currency))
|
||||||
|
|
||||||
|
// Check if goal was reached!
|
||||||
|
// TODO Replace this with sub from loyalty system or something?
|
||||||
|
if newRemaining <= 0 {
|
||||||
|
bot.Client.Say(message.Channel, fmt.Sprintf("ShowOfHands The community goal \"%s\" was reached! ShowOfHands", selectedGoal.Name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue