2022-11-30 18:15:47 +00:00
package loyalty
2021-09-16 13:55:43 +00:00
import (
"fmt"
"strconv"
"strings"
"time"
2022-11-30 18:15:47 +00:00
"git.sr.ht/~hamcha/containers"
"github.com/strimertul/strimertul/twitch"
2022-01-27 15:49:18 +00:00
"go.uber.org/zap"
2022-03-24 09:16:51 +00:00
irc "github.com/gempir/go-twitch-irc/v3"
2021-09-16 13:55:43 +00:00
)
2022-11-30 18:15:47 +00:00
func ( m * Manager ) SetupTwitch ( ) {
bot := m . twitchClient . Bot
if bot == nil {
return
}
2021-09-16 13:55:43 +00:00
// Add loyalty-based commands
2022-11-30 18:15:47 +00:00
bot . RegisterCommand ( "!redeem" , twitch . BotCommand {
2021-09-16 13:55:43 +00:00
Description : "Redeem a reward with loyalty points" ,
Usage : "!redeem <reward-id> [request text]" ,
2022-11-30 18:15:47 +00:00
AccessLevel : twitch . ALTEveryone ,
Handler : m . cmdRedeemReward ,
2022-01-12 11:02:54 +00:00
Enabled : true ,
2022-11-30 18:15:47 +00:00
} )
bot . RegisterCommand ( "!balance" , twitch . BotCommand {
2021-09-16 13:55:43 +00:00
Description : "See your current point balance" ,
Usage : "!balance" ,
2022-11-30 18:15:47 +00:00
AccessLevel : twitch . ALTEveryone ,
Handler : m . cmdBalance ,
2022-01-12 11:02:54 +00:00
Enabled : true ,
2022-11-30 18:15:47 +00:00
} )
bot . RegisterCommand ( "!goals" , twitch . BotCommand {
2021-09-16 13:55:43 +00:00
Description : "Check currently active community goals" ,
Usage : "!goals" ,
2022-11-30 18:15:47 +00:00
AccessLevel : twitch . ALTEveryone ,
Handler : m . cmdGoalList ,
2022-01-12 11:02:54 +00:00
Enabled : true ,
2022-11-30 18:15:47 +00:00
} )
bot . RegisterCommand ( "!contribute" , twitch . BotCommand {
2021-09-16 13:55:43 +00:00
Description : "Contribute points to a community goal" ,
Usage : "!contribute <points> [<goal-id>]" ,
2022-11-30 18:15:47 +00:00
AccessLevel : twitch . ALTEveryone ,
Handler : m . cmdContributeGoal ,
2022-01-12 11:02:54 +00:00
Enabled : true ,
2022-11-30 18:15:47 +00:00
} )
// Setup message handler for tracking user activity
bot . OnMessage . Subscribe ( m )
2021-09-16 13:55:43 +00:00
// Setup handler for adding points over time
2022-11-30 18:15:47 +00:00
go func ( ) {
config := m . Config . Get ( )
if config . Enabled && bot != nil {
2022-01-22 12:05:24 +00:00
for {
2022-11-30 18:15:47 +00:00
if config . Points . Interval > 0 {
// Wait for next poll
select {
case <- m . ctx . Done ( ) :
return
case <- time . After ( time . Duration ( config . Points . Interval ) * time . Second ) :
}
2021-09-16 13:55:43 +00:00
2022-11-30 18:15:47 +00:00
// If stream is confirmed offline, don't give points away!
isOnline := m . twitchClient . IsLive ( )
if ! isOnline {
continue
}
2021-09-16 13:55:43 +00:00
2022-11-30 18:15:47 +00:00
m . logger . Debug ( "awarding points" )
// Get user list
users , err := bot . Client . Userlist ( bot . Config . Channel )
if err != nil {
m . logger . Error ( "error listing users" , zap . Error ( err ) )
continue
}
// Iterate for each user in the list
pointsToGive := make ( map [ string ] int64 )
for _ , user := range users {
// Check if user is blocked
if m . IsBanned ( user ) {
2021-09-16 13:55:43 +00:00
continue
}
2022-11-30 18:15:47 +00:00
// Check if user was active (chatting) for the bonus dingus
award := config . Points . Amount
if m . IsActive ( user ) {
award += config . Points . ActivityBonus
2021-09-16 13:55:43 +00:00
}
2022-11-30 18:15:47 +00:00
// Add to point pool if already on it, otherwise initialize
pointsToGive [ user ] = award
}
m . ResetActivity ( )
2021-09-16 13:55:43 +00:00
2022-11-30 18:15:47 +00:00
// If changes were made, save the pool!
if len ( users ) > 0 {
err := m . GivePoints ( pointsToGive )
if err != nil {
m . logger . Error ( "error giving points to user" , zap . Error ( err ) )
2021-10-28 09:01:52 +00:00
}
2021-09-16 13:55:43 +00:00
}
}
2022-01-22 12:05:24 +00:00
}
2022-11-30 18:15:47 +00:00
}
} ( )
}
func ( m * Manager ) StopTwitch ( ) {
bot := m . twitchClient . Bot
if bot != nil {
bot . RemoveCommand ( "!redeem" )
bot . RemoveCommand ( "!balance" )
bot . RemoveCommand ( "!goals" )
bot . RemoveCommand ( "!contribute" )
// Remove message handler
bot . OnMessage . Unsubscribe ( m )
}
}
func ( m * Manager ) HandleBotMessage ( message irc . PrivateMessage ) {
m . activeUsers . SetKey ( message . User . Name , true )
2021-09-16 13:55:43 +00:00
}
2022-11-30 18:15:47 +00:00
func ( m * Manager ) SetBanList ( banned [ ] string ) {
m . banlist = make ( map [ string ] bool )
2021-09-16 13:55:43 +00:00
for _ , usr := range banned {
2022-11-30 18:15:47 +00:00
m . banlist [ usr ] = true
2021-09-16 13:55:43 +00:00
}
}
2022-11-30 18:15:47 +00:00
func ( m * Manager ) IsBanned ( user string ) bool {
banned , ok := m . banlist [ user ]
2021-09-16 13:55:43 +00:00
return ok && banned
}
2022-11-30 18:15:47 +00:00
func ( m * Manager ) IsActive ( user string ) bool {
active , ok := m . activeUsers . GetKey ( user )
2021-09-16 13:55:43 +00:00
return ok && active
}
2022-11-30 18:15:47 +00:00
func ( m * Manager ) ResetActivity ( ) {
m . activeUsers = containers . NewSyncMap [ string , bool ] ( )
2021-09-16 13:55:43 +00:00
}
2022-11-30 18:15:47 +00:00
func ( m * Manager ) cmdBalance ( bot * twitch . Bot , message irc . PrivateMessage ) {
2021-09-16 13:55:43 +00:00
// Get user balance
2022-11-30 18:15:47 +00:00
balance := m . GetPoints ( message . User . Name )
bot . Client . Say ( message . Channel , fmt . Sprintf ( "%s: You have %d %s!" , message . User . DisplayName , balance , m . Config . Get ( ) . Currency ) )
2021-09-16 13:55:43 +00:00
}
2022-11-30 18:15:47 +00:00
func ( m * Manager ) cmdRedeemReward ( bot * twitch . Bot , message irc . PrivateMessage ) {
2021-09-16 13:55:43 +00:00
parts := strings . Fields ( message . Message )
if len ( parts ) < 2 {
return
}
redeemID := parts [ 1 ]
// Find reward
2022-11-30 18:15:47 +00:00
reward := m . GetReward ( redeemID )
2021-09-16 13:55:43 +00:00
if reward . ID == "" {
return
}
// Reward not active, return early
if ! reward . Enabled {
return
}
// Get user balance
2022-11-30 18:15:47 +00:00
balance := m . GetPoints ( message . User . Name )
config := m . Config . Get ( )
2021-09-16 13:55:43 +00:00
// Check if user can afford the reward
if balance - reward . Price < 0 {
bot . Client . Say ( message . Channel , fmt . Sprintf ( "I'm sorry %s but you cannot afford this (have %d %s, need %d)" , message . User . DisplayName , balance , config . Currency , reward . Price ) )
return
}
text := ""
if len ( parts ) > 2 {
text = strings . Join ( parts [ 2 : ] , " " )
}
// Perform redeem
2022-11-30 18:15:47 +00:00
if err := m . PerformRedeem ( Redeem {
2021-09-16 13:55:43 +00:00
Username : message . User . Name ,
DisplayName : message . User . DisplayName ,
When : time . Now ( ) ,
Reward : reward ,
RequestText : text ,
} ) ; err != nil {
switch err {
2022-11-30 18:15:47 +00:00
case ErrRedeemInCooldown :
nextAvailable := m . GetRewardCooldown ( reward . ID )
2021-09-16 13:55:43 +00:00
bot . Client . Say ( message . Channel , fmt . Sprintf ( "%s: That reward is in cooldown (available in %s)" , message . User . DisplayName ,
time . Until ( nextAvailable ) . Truncate ( time . Second ) ) )
default :
2022-11-30 18:15:47 +00:00
m . logger . Error ( "error while performing redeem" , zap . Error ( err ) )
2021-09-16 13:55:43 +00:00
}
return
}
2022-11-30 18:15:47 +00:00
bot . Client . Say ( message . Channel , fmt . Sprintf ( "HolidayPresent %s has redeemed %s! (new balance: %d %s)" , message . User . DisplayName , reward . Name , m . GetPoints ( message . User . Name ) , config . Currency ) )
2021-09-16 13:55:43 +00:00
}
2022-11-30 18:15:47 +00:00
func ( m * Manager ) cmdGoalList ( bot * twitch . Bot , message irc . PrivateMessage ) {
goals := m . Goals . Get ( )
2021-09-16 13:55:43 +00:00
if len ( 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 goals {
if ! goal . Enabled {
continue
}
2022-11-30 18:15:47 +00:00
msg += fmt . Sprintf ( "%s (%d/%d %s) [id: %s] | " , goal . Name , goal . Contributed , goal . TotalGoal , m . Config . Get ( ) . Currency , goal . ID )
2021-09-16 13:55:43 +00:00
}
msg += " Contribute with <!contribute POINTS GOALID>"
bot . Client . Say ( message . Channel , msg )
}
2022-11-30 18:15:47 +00:00
func ( m * Manager ) cmdContributeGoal ( bot * twitch . Bot , message irc . PrivateMessage ) {
goals := m . Goals . Get ( )
2021-09-16 13:55:43 +00:00
// 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 {
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 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 goal
selectedGoal := 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
}
// Add points to goal
2022-11-30 18:15:47 +00:00
if err := m . PerformContribution ( selectedGoal , message . User . Name , points ) ; err != nil {
m . logger . Error ( "error while contributing to goal" , zap . Error ( err ) )
2021-09-16 13:55:43 +00:00
return
}
2022-11-30 18:15:47 +00:00
config := m . Config . Get ( )
2021-09-16 13:55:43 +00:00
newRemaining := selectedGoal . TotalGoal - selectedGoal . Contributed
bot . Client . Say ( message . Channel , fmt . Sprintf ( "ShowOfHands %s contributed %d %s to \"%s\"!! Only %d %s left!" , message . User . DisplayName , points , config . Currency , selectedGoal . Name , newRemaining , 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 ) )
}
}