mirror of
https://git.sr.ht/~ashkeel/strimertul
synced 2024-09-30 02:40:33 +00:00
395 lines
10 KiB
Go
395 lines
10 KiB
Go
|
package chat
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"strconv"
|
||
|
"strings"
|
||
|
"time"
|
||
|
|
||
|
"git.sr.ht/~ashkeel/strimertul/twitch"
|
||
|
|
||
|
"git.sr.ht/~ashkeel/containers/sync"
|
||
|
|
||
|
"github.com/nicklaw5/helix/v2"
|
||
|
"go.uber.org/zap"
|
||
|
|
||
|
"git.sr.ht/~ashkeel/strimertul/loyalty"
|
||
|
)
|
||
|
|
||
|
const (
|
||
|
commandRedeem = "!redeem"
|
||
|
commandGoals = "!goals"
|
||
|
commandBalance = "!balance"
|
||
|
commandContribute = "!contribute"
|
||
|
)
|
||
|
|
||
|
type loyaltyIntegration struct {
|
||
|
ctx context.Context
|
||
|
manager *loyalty.Manager
|
||
|
module *Module
|
||
|
|
||
|
activeUsers *sync.Map[string, bool]
|
||
|
}
|
||
|
|
||
|
func setupLoyaltyIntegration(ctx context.Context, mod *Module, manager *loyalty.Manager) *loyaltyIntegration {
|
||
|
li := &loyaltyIntegration{
|
||
|
ctx: ctx,
|
||
|
manager: manager,
|
||
|
module: mod,
|
||
|
|
||
|
activeUsers: sync.NewMap[string, bool](),
|
||
|
}
|
||
|
|
||
|
// Add loyalty-based commands
|
||
|
mod.commands.SetKey(commandRedeem, Command{
|
||
|
Description: "Redeem a reward with loyalty points",
|
||
|
Usage: fmt.Sprintf("%s <reward-id> [request text]", commandRedeem),
|
||
|
AccessLevel: ALTEveryone,
|
||
|
Handler: li.cmdRedeemReward,
|
||
|
Enabled: true,
|
||
|
})
|
||
|
mod.commands.SetKey(commandBalance, Command{
|
||
|
Description: "See your current point balance",
|
||
|
Usage: commandBalance,
|
||
|
AccessLevel: ALTEveryone,
|
||
|
Handler: li.cmdBalance,
|
||
|
Enabled: true,
|
||
|
})
|
||
|
mod.commands.SetKey(commandGoals, Command{
|
||
|
Description: "Check currently active community goals",
|
||
|
Usage: commandGoals,
|
||
|
AccessLevel: ALTEveryone,
|
||
|
Handler: li.cmdGoalList,
|
||
|
Enabled: true,
|
||
|
})
|
||
|
mod.commands.SetKey(commandContribute, Command{
|
||
|
Description: "Contribute points to a community goal",
|
||
|
Usage: fmt.Sprintf("%s <points> [<goal-id>]", commandContribute),
|
||
|
AccessLevel: ALTEveryone,
|
||
|
Handler: li.cmdContributeGoal,
|
||
|
Enabled: true,
|
||
|
})
|
||
|
|
||
|
// Setup handler for adding points over time
|
||
|
go func() {
|
||
|
config := li.manager.Config.Get()
|
||
|
// Stop handler if loyalty system is disabled or there is no valid point interval
|
||
|
if !config.Enabled || config.Points.Interval <= 0 {
|
||
|
return
|
||
|
}
|
||
|
for {
|
||
|
// Wait for next poll
|
||
|
select {
|
||
|
case <-li.ctx.Done():
|
||
|
return
|
||
|
case <-time.After(time.Duration(config.Points.Interval) * time.Second):
|
||
|
}
|
||
|
|
||
|
// If stream is confirmed offline, don't give points away!
|
||
|
var streamInfos []helix.Stream
|
||
|
err := mod.db.GetJSON(twitch.StreamInfoKey, &streamInfos)
|
||
|
if err != nil {
|
||
|
mod.logger.Error("Error retrieving stream info", zap.Error(err))
|
||
|
continue
|
||
|
}
|
||
|
if len(streamInfos) < 1 {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
// Get user list
|
||
|
cursor := ""
|
||
|
var users []string
|
||
|
for {
|
||
|
userClient, err := twitch.GetUserClient(mod.db, false)
|
||
|
if err != nil {
|
||
|
mod.logger.Error("Could not get user api client for list of chatters", zap.Error(err))
|
||
|
return
|
||
|
}
|
||
|
res, err := userClient.GetChannelChatChatters(&helix.GetChatChattersParams{
|
||
|
BroadcasterID: mod.user.ID,
|
||
|
ModeratorID: mod.user.ID,
|
||
|
First: "1000",
|
||
|
After: cursor,
|
||
|
})
|
||
|
if err != nil {
|
||
|
mod.logger.Error("Could not retrieve list of chatters", zap.Error(err))
|
||
|
return
|
||
|
}
|
||
|
for _, user := range res.Data.Chatters {
|
||
|
users = append(users, user.UserLogin)
|
||
|
}
|
||
|
cursor = res.Data.Pagination.Cursor
|
||
|
if cursor == "" {
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Iterate for each user in the list
|
||
|
pointsToGive := make(map[string]int64)
|
||
|
for _, user := range users {
|
||
|
// Check if user is blocked
|
||
|
if li.manager.IsBanned(user) {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
// Check if user was active (chatting) for the bonus dingus
|
||
|
award := config.Points.Amount
|
||
|
if li.IsActive(user) {
|
||
|
award += config.Points.ActivityBonus
|
||
|
}
|
||
|
|
||
|
// Add to point pool if already on it, otherwise initialize
|
||
|
pointsToGive[user] = award
|
||
|
}
|
||
|
|
||
|
li.ResetActivity()
|
||
|
|
||
|
// If changes were made, save the pool!
|
||
|
if len(users) > 0 {
|
||
|
err := li.manager.GivePoints(pointsToGive)
|
||
|
if err != nil {
|
||
|
mod.logger.Error("Error awarding loyalty points to user", zap.Error(err))
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}()
|
||
|
|
||
|
mod.logger.Info("Loyalty system integration with Twitch is ready")
|
||
|
|
||
|
return li
|
||
|
}
|
||
|
|
||
|
func (li *loyaltyIntegration) Close() {
|
||
|
li.module.commands.DeleteKey(commandRedeem)
|
||
|
li.module.commands.DeleteKey(commandBalance)
|
||
|
li.module.commands.DeleteKey(commandGoals)
|
||
|
li.module.commands.DeleteKey(commandContribute)
|
||
|
}
|
||
|
|
||
|
func (li *loyaltyIntegration) HandleMessage(message helix.EventSubChannelChatMessageEvent) {
|
||
|
li.activeUsers.SetKey(message.ChatterUserLogin, true)
|
||
|
}
|
||
|
|
||
|
func (li *loyaltyIntegration) IsActive(user string) bool {
|
||
|
active, ok := li.activeUsers.GetKey(user)
|
||
|
return ok && active
|
||
|
}
|
||
|
|
||
|
func (li *loyaltyIntegration) ResetActivity() {
|
||
|
li.activeUsers = sync.NewMap[string, bool]()
|
||
|
}
|
||
|
|
||
|
func (li *loyaltyIntegration) cmdBalance(message helix.EventSubChannelChatMessageEvent) {
|
||
|
// Get user balance
|
||
|
balance := li.manager.GetPoints(message.ChatterUserLogin)
|
||
|
|
||
|
li.module.WriteMessage(WriteMessageRequest{
|
||
|
Message: fmt.Sprintf("You have %d %s!", balance, li.manager.Config.Get().Currency),
|
||
|
ReplyTo: message.MessageID,
|
||
|
})
|
||
|
}
|
||
|
|
||
|
func (li *loyaltyIntegration) cmdRedeemReward(message helix.EventSubChannelChatMessageEvent) {
|
||
|
parts := strings.Fields(message.Message.Text)
|
||
|
if len(parts) < 2 {
|
||
|
return
|
||
|
}
|
||
|
redeemID := parts[1]
|
||
|
|
||
|
// Find reward
|
||
|
reward := li.manager.GetReward(redeemID)
|
||
|
if reward.ID == "" {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Reward not active, return early
|
||
|
if !reward.Enabled {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Get user balance
|
||
|
balance := li.manager.GetPoints(message.ChatterUserLogin)
|
||
|
config := li.manager.Config.Get()
|
||
|
|
||
|
// Check if user can afford the reward
|
||
|
if balance-reward.Price < 0 {
|
||
|
li.module.WriteMessage(WriteMessageRequest{
|
||
|
Message: fmt.Sprintf("I'm sorry but you cannot afford this (have %d %s, need %d)", balance, config.Currency, reward.Price),
|
||
|
ReplyTo: message.MessageID,
|
||
|
})
|
||
|
return
|
||
|
}
|
||
|
|
||
|
text := ""
|
||
|
if len(parts) > 2 {
|
||
|
text = strings.Join(parts[2:], " ")
|
||
|
}
|
||
|
|
||
|
// Perform redeem
|
||
|
if err := li.manager.PerformRedeem(loyalty.Redeem{
|
||
|
Username: message.ChatterUserLogin,
|
||
|
DisplayName: message.ChatterUserName,
|
||
|
When: time.Now(),
|
||
|
Reward: reward,
|
||
|
RequestText: text,
|
||
|
}); err != nil {
|
||
|
if errors.Is(err, loyalty.ErrRedeemInCooldown) {
|
||
|
nextAvailable := li.manager.GetRewardCooldown(reward.ID)
|
||
|
li.module.WriteMessage(WriteMessageRequest{
|
||
|
Message: fmt.Sprintf("That reward is in cooldown (available in %s)",
|
||
|
time.Until(nextAvailable).Truncate(time.Second)),
|
||
|
ReplyTo: message.MessageID,
|
||
|
})
|
||
|
return
|
||
|
}
|
||
|
li.module.logger.Error("Error while performing redeem", zap.Error(err))
|
||
|
return
|
||
|
}
|
||
|
|
||
|
li.module.WriteMessage(WriteMessageRequest{
|
||
|
Message: fmt.Sprintf("HolidayPresent %s has redeemed %s! (new balance: %d %s)",
|
||
|
message.ChatterUserName, reward.Name, li.manager.GetPoints(message.ChatterUserLogin), config.Currency),
|
||
|
})
|
||
|
}
|
||
|
|
||
|
func (li *loyaltyIntegration) cmdGoalList(message helix.EventSubChannelChatMessageEvent) {
|
||
|
goals := li.manager.Goals.Get()
|
||
|
if len(goals) < 1 {
|
||
|
li.module.WriteMessage(WriteMessageRequest{
|
||
|
Message: fmt.Sprintf("There are no active community goals right now :(!"),
|
||
|
ReplyTo: message.MessageID,
|
||
|
})
|
||
|
return
|
||
|
}
|
||
|
msg := "Current goals: "
|
||
|
for _, goal := range goals {
|
||
|
if !goal.Enabled {
|
||
|
continue
|
||
|
}
|
||
|
msg += fmt.Sprintf("%s (%d/%d %s) [id: %s] | ", goal.Name, goal.Contributed, goal.TotalGoal, li.manager.Config.Get().Currency, goal.ID)
|
||
|
}
|
||
|
msg += " Contribute with <!contribute POINTS GOALID>"
|
||
|
li.module.WriteMessage(WriteMessageRequest{
|
||
|
Message: msg,
|
||
|
})
|
||
|
}
|
||
|
|
||
|
func (li *loyaltyIntegration) cmdContributeGoal(message helix.EventSubChannelChatMessageEvent) {
|
||
|
goals := li.manager.Goals.Get()
|
||
|
|
||
|
// 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 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 {
|
||
|
li.module.WriteMessage(WriteMessageRequest{
|
||
|
Message: fmt.Sprintf("All active community goals have been reached already! NewRecord"),
|
||
|
ReplyTo: message.MessageID,
|
||
|
})
|
||
|
} else {
|
||
|
li.module.WriteMessage(WriteMessageRequest{
|
||
|
Message: fmt.Sprintf("There are no active community goals right now :(!"),
|
||
|
ReplyTo: message.MessageID,
|
||
|
})
|
||
|
}
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Parse parameters if provided
|
||
|
parts := strings.Fields(message.Message.Text)
|
||
|
if len(parts) > 1 {
|
||
|
newPoints, err := strconv.ParseInt(parts[1], 10, 64)
|
||
|
if err == nil {
|
||
|
if newPoints <= 0 {
|
||
|
li.module.WriteMessage(WriteMessageRequest{
|
||
|
Message: fmt.Sprintf("Nice try SoBayed"),
|
||
|
ReplyTo: message.MessageID,
|
||
|
})
|
||
|
return
|
||
|
}
|
||
|
points = newPoints
|
||
|
}
|
||
|
if len(parts) > 2 {
|
||
|
found := false
|
||
|
goalID := parts[2]
|
||
|
// Find Goal index
|
||
|
for index, goal := range goals {
|
||
|
if !goal.Enabled {
|
||
|
continue
|
||
|
}
|
||
|
if goal.ID == goalID {
|
||
|
goalIndex = index
|
||
|
found = true
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
// Invalid goal ID provided
|
||
|
if !found {
|
||
|
li.module.WriteMessage(WriteMessageRequest{
|
||
|
Message: fmt.Sprintf("I couldn't find that goal ID :("),
|
||
|
ReplyTo: message.MessageID,
|
||
|
})
|
||
|
return
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Get goal
|
||
|
selectedGoal := goals[goalIndex]
|
||
|
|
||
|
// Check if goal was reached already
|
||
|
if selectedGoal.Contributed >= selectedGoal.TotalGoal {
|
||
|
li.module.WriteMessage(WriteMessageRequest{
|
||
|
Message: fmt.Sprintf("This goal was already reached! ヾ(•ω•`)o"),
|
||
|
ReplyTo: message.MessageID,
|
||
|
})
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Add points to goal
|
||
|
points, err := li.manager.PerformContribution(selectedGoal, message.ChatterUserLogin, points)
|
||
|
if err != nil {
|
||
|
li.module.logger.Error("Error while contributing to goal", zap.Error(err))
|
||
|
return
|
||
|
}
|
||
|
if points == 0 {
|
||
|
li.module.WriteMessage(WriteMessageRequest{
|
||
|
Message: fmt.Sprintf("Sorry but you're broke"),
|
||
|
ReplyTo: message.MessageID,
|
||
|
})
|
||
|
return
|
||
|
}
|
||
|
|
||
|
selectedGoal = li.manager.Goals.Get()[goalIndex]
|
||
|
config := li.manager.Config.Get()
|
||
|
newRemaining := selectedGoal.TotalGoal - selectedGoal.Contributed
|
||
|
li.module.WriteMessage(WriteMessageRequest{
|
||
|
Message: fmt.Sprintf("NewRecord %s contributed %d %s to \"%s\"!! Only %d %s left!", message.ChatterUserName, points, config.Currency, selectedGoal.Name, newRemaining, config.Currency),
|
||
|
})
|
||
|
|
||
|
// Check if goal was reached!
|
||
|
if newRemaining <= 0 {
|
||
|
li.module.WriteMessage(WriteMessageRequest{
|
||
|
Message: fmt.Sprintf("FallWinning The community goal \"%s\" was reached! FallWinning", selectedGoal.Name),
|
||
|
Announce: true,
|
||
|
})
|
||
|
}
|
||
|
}
|