2024-01-01 16:52:18 +00:00
// Package counter contains actors and template functions to work with
// database stored counters
2022-09-10 11:39:07 +00:00
package counter
2020-12-21 00:32:39 +00:00
import (
2021-08-28 15:27:24 +00:00
"fmt"
"net/http"
2021-03-06 00:42:40 +00:00
"strconv"
2022-09-10 11:39:07 +00:00
"strings"
2024-04-04 16:25:03 +00:00
"time"
2021-03-06 00:42:40 +00:00
2021-08-28 15:27:24 +00:00
"github.com/gorilla/mux"
2020-12-21 00:32:39 +00:00
"github.com/pkg/errors"
2023-09-11 17:51:38 +00:00
"gopkg.in/irc.v4"
2023-11-25 19:23:34 +00:00
"gorm.io/gorm"
2021-11-25 22:48:16 +00:00
2024-04-03 19:00:28 +00:00
"github.com/Luzifer/go_helpers/v2/fieldcollection"
"github.com/Luzifer/twitch-bot/v3/internal/helpers"
2022-11-02 21:38:14 +00:00
"github.com/Luzifer/twitch-bot/v3/pkg/database"
"github.com/Luzifer/twitch-bot/v3/plugins"
2020-12-21 00:32:39 +00:00
)
2022-09-10 11:39:07 +00:00
var (
db database . Connector
formatMessage plugins . MsgFormatter
2024-04-08 17:47:15 +00:00
errNotAValue = fmt . Errorf ( "not a value" )
2022-09-10 11:39:07 +00:00
)
2024-01-01 16:52:18 +00:00
// Register provides the plugins.RegisterFunc
//
2022-09-10 11:39:07 +00:00
//nolint:funlen // This function is a few lines too long but only contains definitions
2024-01-01 16:52:18 +00:00
func Register ( args plugins . RegistrationArguments ) ( err error ) {
2022-09-10 11:39:07 +00:00
db = args . GetDatabaseConnector ( )
2024-01-01 16:52:18 +00:00
if err = db . DB ( ) . AutoMigrate ( & counter { } ) ; err != nil {
2022-09-10 11:39:07 +00:00
return errors . Wrap ( err , "applying schema migration" )
}
2023-11-25 19:23:34 +00:00
args . RegisterCopyDatabaseFunc ( "counter" , func ( src , target * gorm . DB ) error {
2024-03-27 23:15:14 +00:00
return database . CopyObjects ( src , target , & counter { } )
2023-11-25 19:23:34 +00:00
} )
2022-09-10 11:39:07 +00:00
formatMessage = args . FormatMessage
2024-01-01 16:52:18 +00:00
args . RegisterActor ( "counter" , func ( ) plugins . Actor { return & actorCounter { } } )
2022-09-10 11:39:07 +00:00
args . RegisterActorDocumentation ( plugins . ActionDocumentation {
2021-09-22 13:36:45 +00:00
Description : "Update counter values" ,
Name : "Modify Counter" ,
Type : "counter" ,
Fields : [ ] plugins . ActionDocumentationField {
{
Default : "" ,
Description : "Name of the counter to update" ,
Key : "counter" ,
Name : "Counter" ,
Optional : false ,
SupportTemplate : true ,
Type : plugins . ActionDocumentationFieldTypeString ,
} ,
{
Default : "1" ,
Description : "Value to add to the counter" ,
Key : "counter_step" ,
Name : "Counter Step" ,
Optional : true ,
2022-01-31 00:50:08 +00:00
SupportTemplate : true ,
Type : plugins . ActionDocumentationFieldTypeString ,
2021-09-22 13:36:45 +00:00
} ,
{
Default : "" ,
Description : "Value to set the counter to" ,
Key : "counter_set" ,
Name : "Counter Set" ,
Optional : true ,
SupportTemplate : true ,
Type : plugins . ActionDocumentationFieldTypeString ,
} ,
} ,
} )
2021-08-28 15:27:24 +00:00
2024-01-01 16:52:18 +00:00
if err = args . RegisterAPIRoute ( plugins . HTTPRouteRegistrationArgs {
2021-08-28 15:27:24 +00:00
Description : "Returns the (formatted) value as a plain string" ,
HandlerFunc : routeActorCounterGetValue ,
Method : http . MethodGet ,
Module : "counter" ,
Name : "Get Counter Value" ,
Path : "/{name}" ,
QueryParams : [ ] plugins . HTTPRouteParamDocumentation {
{
Description : "Template to apply to the value: Variations of %d sprintf template are supported once" ,
Name : "template" ,
Required : false ,
Type : "string" ,
} ,
} ,
ResponseType : plugins . HTTPRouteResponseTypeTextPlain ,
RouteParams : [ ] plugins . HTTPRouteParamDocumentation {
{
Description : "Name of the counter to query" ,
Name : "name" ,
} ,
} ,
2024-01-01 16:52:18 +00:00
} ) ; err != nil {
return fmt . Errorf ( "registering API route: %w" , err )
}
2021-08-28 15:27:24 +00:00
2024-01-01 16:52:18 +00:00
if err = args . RegisterAPIRoute ( plugins . HTTPRouteRegistrationArgs {
2021-08-28 15:27:24 +00:00
Description : "Updates the value of the counter" ,
HandlerFunc : routeActorCounterSetValue ,
Method : http . MethodPatch ,
Module : "counter" ,
Name : "Set Counter Value" ,
Path : "/{name}" ,
QueryParams : [ ] plugins . HTTPRouteParamDocumentation {
{
Description : "If set to `true` the given value is set instead of added" ,
Name : "absolute" ,
Required : false ,
Type : "boolean" ,
} ,
{
Description : "Value to add / set for the given counter" ,
Name : "value" ,
Required : true ,
Type : "int64" ,
} ,
} ,
2021-10-23 15:22:58 +00:00
RequiresWriteAuth : true ,
2021-08-28 15:27:24 +00:00
RouteParams : [ ] plugins . HTTPRouteParamDocumentation {
{
Description : "Name of the counter to update" ,
Name : "name" ,
} ,
} ,
2024-01-01 16:52:18 +00:00
} ) ; err != nil {
return fmt . Errorf ( "registering API route: %w" , err )
}
2022-09-10 11:39:07 +00:00
2024-04-03 19:00:28 +00:00
args . RegisterTemplateFunction ( "channelCounter" , func ( _ * irc . Message , _ * plugins . Rule , fields * fieldcollection . FieldCollection ) interface { } {
2022-09-10 11:39:07 +00:00
return func ( name string ) ( string , error ) {
channel , err := fields . String ( "channel" )
if err != nil {
return "" , errors . Wrap ( err , "channel not available" )
}
return strings . Join ( [ ] string { channel , name } , ":" ) , nil
}
2023-08-25 21:37:37 +00:00
} , plugins . TemplateFuncDocumentation {
Description : "Wraps the counter name into a channel specific counter name including the channel name" ,
Syntax : "channelCounter <counter name>" ,
Example : & plugins . TemplateFuncDocumentationExample {
Template : ` {{ channelCounter "test" }} ` ,
ExpectedOutput : "#example:test" ,
} ,
2022-09-10 11:39:07 +00:00
} )
2023-10-23 18:28:58 +00:00
args . RegisterTemplateFunction ( "counterRank" , plugins . GenericTemplateFunctionGetter ( func ( prefix , name string ) ( res struct { Rank , Count int64 } , err error ) {
res . Rank , res . Count , err = getCounterRank ( db , prefix , name )
return res , errors . Wrap ( err , "getting counter rank" )
} ) , plugins . TemplateFuncDocumentation {
Description : "Returns the rank of the given counter and the total number of counters in given counter prefix" ,
Syntax : ` counterRank <prefix> <name> ` ,
Example : & plugins . TemplateFuncDocumentationExample {
Template : ` {{ $cr := counterRank ( list .channel "test" "" | join ":" ) ( list .channel "test" "foo" | join ":" ) }} {{ $cr .Rank }} / {{ $cr .Count }} ` ,
FakedOutput : "2/6" ,
} ,
} )
2024-04-04 17:04:06 +00:00
args . RegisterTemplateFunction ( "counterTopList" , plugins . GenericTemplateFunctionGetter ( func ( prefix string , n int , orderBy string ) ( [ ] counter , error ) {
return getCounterTopList ( db , prefix , n , orderBy )
2023-10-23 18:28:58 +00:00
} ) , plugins . TemplateFuncDocumentation {
2024-04-04 17:04:06 +00:00
Description : "Returns the top n counters for the given prefix as objects with Name and Value fields. Can be ordered by `name` / `value` / `first_seen` / `last_modified` ascending (`ASC`) or descending (`DESC`): i.e. `last_modified DESC` defaults to `value DESC`" ,
Syntax : ` counterTopList <prefix> <n> [orderBy] ` ,
2023-10-23 18:28:58 +00:00
Example : & plugins . TemplateFuncDocumentationExample {
Template : ` {{ range ( counterTopList ( list .channel "test" "" | join ":" ) 3 ) }} {{ .Name }} : {{ .Value }} - {{ end }} ` ,
FakedOutput : "#example:test:foo: 5 - #example:test:bar: 4 - " ,
} ,
} )
2022-09-10 11:39:07 +00:00
args . RegisterTemplateFunction ( "counterValue" , plugins . GenericTemplateFunctionGetter ( func ( name string , _ ... string ) ( int64 , error ) {
2024-01-01 16:52:18 +00:00
return getCounterValue ( db , name )
2023-08-25 21:37:37 +00:00
} ) , plugins . TemplateFuncDocumentation {
Description : "Returns the current value of the counter which identifier was supplied" ,
Syntax : "counterValue <counter name>" ,
Example : & plugins . TemplateFuncDocumentationExample {
Template : ` {{ counterValue ( list .channel "test" | join ":" ) }} ` ,
FakedOutput : "5" ,
} ,
} )
2022-09-10 11:39:07 +00:00
2023-06-12 21:48:22 +00:00
args . RegisterTemplateFunction ( "counterValueAdd" , plugins . GenericTemplateFunctionGetter ( func ( name string , val ... int64 ) ( int64 , error ) {
var mod int64 = 1
if len ( val ) > 0 {
mod = val [ 0 ]
}
2024-04-04 16:25:03 +00:00
if err := updateCounter ( db , name , mod , false , time . Now ( ) ) ; err != nil {
2023-06-12 21:48:22 +00:00
return 0 , errors . Wrap ( err , "updating counter" )
}
2024-01-01 16:52:18 +00:00
return getCounterValue ( db , name )
2023-08-25 21:37:37 +00:00
} ) , plugins . TemplateFuncDocumentation {
Description : "Adds the given value (or 1 if no value) to the counter and returns its new value" ,
Syntax : "counterValueAdd <counter name> [increase=1]" ,
Example : & plugins . TemplateFuncDocumentationExample {
Template : ` {{ counterValueAdd "myCounter" }} {{ counterValueAdd "myCounter" 5 }} ` ,
FakedOutput : "1 6" ,
} ,
} )
2023-06-12 21:48:22 +00:00
2022-09-10 11:39:07 +00:00
return nil
2021-06-11 11:52:42 +00:00
}
2020-12-21 00:32:39 +00:00
2024-01-01 16:52:18 +00:00
type actorCounter struct { }
2020-12-25 23:31:49 +00:00
2024-04-08 17:47:15 +00:00
func ( a actorCounter ) Execute ( _ * irc . Client , m * irc . Message , r * plugins . Rule , eventData * fieldcollection . FieldCollection , attrs * fieldcollection . FieldCollection ) ( preventCooldown bool , err error ) {
2021-09-22 13:36:45 +00:00
counterName , err := formatMessage ( attrs . MustString ( "counter" , nil ) , m , r , eventData )
2021-06-11 11:52:42 +00:00
if err != nil {
2021-08-11 22:12:10 +00:00
return false , errors . Wrap ( err , "preparing response" )
2021-06-11 11:52:42 +00:00
}
2024-04-08 17:47:15 +00:00
// First lets look whether we shall set the counter (counter_set is
// defined and the template evaluates into something which is not
// an empty string)
counterSet , err := a . parseAttributeTemplateToNumber ( m , r , eventData , attrs , "counter_set" , 0 )
switch {
case err == nil :
// Nice, we got a value to set
if err = updateCounter ( db , counterName , counterSet , true , time . Now ( ) ) ; err != nil {
return false , fmt . Errorf ( "setting counter: %w" , err )
2021-03-06 00:35:20 +00:00
}
2024-04-08 17:47:15 +00:00
return false , nil
2021-03-06 00:35:20 +00:00
2024-04-08 17:47:15 +00:00
case errors . Is ( err , errNotAValue ) :
// Nope, not a set but that's fine, we just go to step-adjustment
2020-12-21 00:32:39 +00:00
2024-04-08 17:47:15 +00:00
default :
// B0rked
return false , fmt . Errorf ( "parsing counter-set: %w" , err )
2021-06-11 11:52:42 +00:00
}
2024-04-08 17:47:15 +00:00
// Second check whether we do have a template in counter_step and it
// evaluates into a non-empty string and then adjust the counter
// accordingly
counterStep , err := a . parseAttributeTemplateToNumber ( m , r , eventData , attrs , "counter_step" , 1 )
switch {
case err == nil , errors . Is ( err , errNotAValue ) :
// Either got a value or there was none, therefore the default was
// returned which is 1 and we can apply this
if err = updateCounter ( db , counterName , counterStep , false , time . Now ( ) ) ; err != nil {
return false , fmt . Errorf ( "updating counter: %w" , err )
2022-01-31 00:50:08 +00:00
}
2024-04-08 17:47:15 +00:00
return false , nil
2022-01-31 00:50:08 +00:00
2024-04-08 17:47:15 +00:00
default :
// B0rked
return false , fmt . Errorf ( "parsing counter-step: %w" , err )
2021-06-11 11:52:42 +00:00
}
2020-12-21 00:32:39 +00:00
}
2021-06-11 11:52:42 +00:00
2024-01-01 16:52:18 +00:00
func ( actorCounter ) IsAsync ( ) bool { return false }
func ( actorCounter ) Name ( ) string { return "counter" }
2021-08-28 15:27:24 +00:00
2024-04-03 19:00:28 +00:00
func ( actorCounter ) Validate ( tplValidator plugins . TemplateValidatorFunc , attrs * fieldcollection . FieldCollection ) ( err error ) {
if err = attrs . ValidateSchema (
fieldcollection . MustHaveField ( fieldcollection . SchemaField { Name : "counter" , NonEmpty : true , Type : fieldcollection . SchemaFieldTypeString } ) ,
fieldcollection . CanHaveField ( fieldcollection . SchemaField { Name : "counter_step" , Type : fieldcollection . SchemaFieldTypeString } ) ,
fieldcollection . CanHaveField ( fieldcollection . SchemaField { Name : "counter_set" , Type : fieldcollection . SchemaFieldTypeString } ) ,
2024-04-08 13:56:12 +00:00
fieldcollection . MustHaveNoUnknowFields ,
2024-04-03 19:00:28 +00:00
helpers . SchemaValidateTemplateField ( tplValidator , "counter" , "counter_step" , "counter_set" ) ,
) ; err != nil {
return fmt . Errorf ( "validating attributes: %w" , err )
2022-10-31 16:26:53 +00:00
}
2021-09-22 13:36:45 +00:00
return nil
}
2024-04-08 17:47:15 +00:00
func ( actorCounter ) parseAttributeTemplateToNumber (
m * irc . Message ,
r * plugins . Rule ,
eventData * fieldcollection . FieldCollection ,
attrs * fieldcollection . FieldCollection ,
field string ,
defaultValue int64 ,
) ( v int64 , err error ) {
// Get the string
sv , err := attrs . String ( field )
switch {
case err == nil :
// We got a string and continue below
case errors . Is ( err , fieldcollection . ErrValueNotSet ) :
// That's fine, the string is not available, we report that and
// return the default value
return defaultValue , errNotAValue
default :
// Not sure what brought us here but we should report that
return defaultValue , fmt . Errorf ( "getting string value: %w" , err )
}
// Now we need to evaluate the template
sv , err = formatMessage ( sv , m , r , eventData )
if err != nil {
return defaultValue , fmt . Errorf ( "executing template: %w" , err )
}
// The template evaluated into an empty string, we don't try to
// parse that and report it as a missing value with default
if sv == "" {
return defaultValue , errNotAValue
}
// The template was not empty, we need to parse the resulting int
// and return it
v , err = strconv . ParseInt ( sv , 10 , 64 )
if err != nil {
return defaultValue , fmt . Errorf ( "parsing to int: %w" , err )
}
return v , nil
}
2021-08-28 15:27:24 +00:00
func routeActorCounterGetValue ( w http . ResponseWriter , r * http . Request ) {
template := r . FormValue ( "template" )
if template == "" {
template = "%d"
}
2024-01-01 16:52:18 +00:00
cv , err := getCounterValue ( db , mux . Vars ( r ) [ "name" ] )
2022-09-10 11:39:07 +00:00
if err != nil {
http . Error ( w , errors . Wrap ( err , "getting value" ) . Error ( ) , http . StatusInternalServerError )
return
}
2021-08-28 15:27:24 +00:00
w . Header ( ) . Set ( "Content-Type" , "text-plain" )
2024-06-09 10:44:41 +00:00
http . Error ( w , fmt . Sprintf ( template , cv ) , http . StatusOK )
2021-08-28 15:27:24 +00:00
}
func routeActorCounterSetValue ( w http . ResponseWriter , r * http . Request ) {
var (
absolute = r . FormValue ( "absolute" ) == "true"
err error
value int64
)
2021-09-22 13:36:45 +00:00
if value , err = strconv . ParseInt ( r . FormValue ( "value" ) , 10 , 64 ) ; err != nil {
2021-08-28 15:27:24 +00:00
http . Error ( w , errors . Wrap ( err , "parsing value" ) . Error ( ) , http . StatusBadRequest )
return
}
2024-04-04 16:25:03 +00:00
if err = updateCounter ( db , mux . Vars ( r ) [ "name" ] , value , absolute , time . Now ( ) ) ; err != nil {
2021-08-28 15:27:24 +00:00
http . Error ( w , errors . Wrap ( err , "updating value" ) . Error ( ) , http . StatusInternalServerError )
return
}
w . WriteHeader ( http . StatusNoContent )
}