From c884a7c53253ec4f47eb25e74a5c23eac0d983a6 Mon Sep 17 00:00:00 2001 From: Knut Ahlers Date: Fri, 19 Nov 2021 22:53:30 +0100 Subject: [PATCH] [templating] Add `multiply` and `seededRandom` template functions Signed-off-by: Knut Ahlers --- internal/template/numeric/numeric.go | 10 + internal/template/random/random.go | 43 +++++ action_core.go => plugins_core.go | 11 +- wiki/Home.md | 29 --- wiki/Templating.md | 264 +++++++++++++++++++++++++++ 5 files changed, 326 insertions(+), 31 deletions(-) create mode 100644 internal/template/numeric/numeric.go create mode 100644 internal/template/random/random.go rename action_core.go => plugins_core.go (90%) create mode 100644 wiki/Templating.md diff --git a/internal/template/numeric/numeric.go b/internal/template/numeric/numeric.go new file mode 100644 index 0000000..e0d6cc7 --- /dev/null +++ b/internal/template/numeric/numeric.go @@ -0,0 +1,10 @@ +package numeric + +import "github.com/Luzifer/twitch-bot/plugins" + +func Register(args plugins.RegistrationArguments) error { + args.RegisterTemplateFunction("multiply", plugins.GenericTemplateFunctionGetter(multiply)) + return nil +} + +func multiply(m1, m2 float64) float64 { return m1 * m2 } diff --git a/internal/template/random/random.go b/internal/template/random/random.go new file mode 100644 index 0000000..ea62f30 --- /dev/null +++ b/internal/template/random/random.go @@ -0,0 +1,43 @@ +package random + +import ( + "crypto/md5" // #nosec G501 // Unly used to convert a string into a numer, no need for cryptographic safety + "fmt" + "math" + "math/rand" + + "github.com/Luzifer/twitch-bot/plugins" + "github.com/pkg/errors" +) + +func Register(args plugins.RegistrationArguments) error { + args.RegisterTemplateFunction("seededRandom", plugins.GenericTemplateFunctionGetter(stableRandomFromSeed)) + return nil +} + +func stableRandomFromSeed(seed string) (float64, error) { + seedValue, err := stringToSeed(seed) + if err != nil { + return 0, errors.Wrap(err, "generating seed") + } + + return rand.New(rand.NewSource(seedValue)).Float64(), nil // #nosec G404 // Only used for generating a random number from static string, no need for cryptographic safety +} + +func stringToSeed(s string) (int64, error) { + hash := md5.New() // #nosec G401 // Unly used to convert a string into a numer, no need for cryptographic safety + if _, err := fmt.Fprint(hash, s); err != nil { + return 0, errors.Wrap(err, "writing string to hasher") + } + + var ( + hashSum = hash.Sum(nil) + sum int64 + ) + + for i := 0; i < len(hashSum); i++ { + sum += int64(float64(hashSum[len(hashSum)-1-i]%10) * math.Pow(10, float64(i))) //nolint:gomnd // No need to put the 10 of 10**i into a constant named "ten" + } + + return sum, nil +} diff --git a/action_core.go b/plugins_core.go similarity index 90% rename from action_core.go rename to plugins_core.go index eab9aae..489f758 100644 --- a/action_core.go +++ b/plugins_core.go @@ -16,6 +16,8 @@ import ( "github.com/Luzifer/twitch-bot/internal/actors/respond" "github.com/Luzifer/twitch-bot/internal/actors/timeout" "github.com/Luzifer/twitch-bot/internal/actors/whisper" + "github.com/Luzifer/twitch-bot/internal/template/numeric" + "github.com/Luzifer/twitch-bot/internal/template/random" "github.com/Luzifer/twitch-bot/plugins" "github.com/Luzifer/twitch-bot/twitch" "github.com/pkg/errors" @@ -23,7 +25,8 @@ import ( ) var ( - coreActorRegistations = []plugins.RegisterFunc{ + corePluginRegistrations = []plugins.RegisterFunc{ + // Actors ban.Register, delay.Register, deleteactor.Register, @@ -35,13 +38,17 @@ var ( respond.Register, timeout.Register, whisper.Register, + + // Template functions + numeric.Register, + random.Register, } knownModules []string ) func initCorePlugins() error { args := getRegistrationArguments() - for _, rf := range coreActorRegistations { + for _, rf := range corePluginRegistrations { if err := rf(args); err != nil { return errors.Wrap(err, "registering core plugin") } diff --git a/wiki/Home.md b/wiki/Home.md index 4936635..c23df3e 100644 --- a/wiki/Home.md +++ b/wiki/Home.md @@ -140,35 +140,6 @@ rules: # See below for examples ... ``` -## Templating - -There are certain variables available in the strings with templating enabled: - -- `channel` - Channel the message was sent to, only available for regular messages not events -- `msg` - The message object, used in functions, should not be sent to chat -- `permitTimeout` - Value of `permit_timeout` in seconds -- `username` - The username of the message author - -Additionally there are some functions available in the templates: - -- `arg ` - Takes the message sent to the channel, splits by space and returns the Nth element -- `botHasBadge ` - Checks whether bot has the given badge in the current channel -- `channelCounter ` - Wraps the counter name into a channel specific counter name including the channel name -- `concat <...parts>` - Join the given string parts with delimiter -- `counterValue ` - Returns the current value of the counter which identifier was supplied -- `displayName [fallback]` - Returns the display name the specified user set for themselves -- `fixUsername ` - Ensures the username no longer contains the `@` or `#` prefix -- `formatDuration ` - Returns a formated duration. Pass empty strings to leave out the part: `{{ formatDuration .dur "hours" "minutes" "" }}` yields `N hours, M minutes` -- `followDate ` - Looks up when `from` followed `to` -- `group [fallback]` - Gets matching group specified by index from `match_message` regular expression, when `fallback` is defined, it is used when group has an empty match -- `lastQuoteIndex` - Gets the last quote index in the quote database for the current channel -- `recentGame [fallback]` - Returns the last played game name of the specified user (see shoutout example) or the `fallback` if the game could not be fetched. If no fallback was supplied the message will fail and not be sent. -- `streamUptime ` - Returns the duration the stream is online (causes an error if no current stream is found) -- `tag ` - Takes the message sent to the channel, returns the value of the tag specified -- `toLower ` - Converts the given string to lower-case -- `toUpper ` - Converts the given string to upper-case -- `variable [default]` - Returns the variable value or default in case it is empty - ## Command executions Your command will get a JSON object passed through `stdin` you can parse to gain details about the message. It is expected to yield an array of actions on `stdout` and exit with status `0`. If it does not the action will be marked failed. In case you need to output debug output you can use `stderr` which is directly piped to the bots `stderr`. diff --git a/wiki/Templating.md b/wiki/Templating.md new file mode 100644 index 0000000..31e784c --- /dev/null +++ b/wiki/Templating.md @@ -0,0 +1,264 @@ +## Templating + +Generally speaking the templating uses [Golang `text/template`](https://pkg.go.dev/text/template) template syntax. All fields with templating enabled do support the full synax from the `text/template` package. + +### Variables + +There are certain variables available in the strings with templating enabled: + +- `channel` - Channel the message was sent to, only available for regular messages not events +- `msg` - The message object, used in functions, should not be sent to chat +- `permitTimeout` - Value of `permit_timeout` in seconds +- `username` - The username of the message author + + +### Functions + +Additionally to the built-in functions there are extra functions available in the templates: + +Examples below are using this syntax in the code block: + +``` +! Message matcher used for the input message +> Input message if used in the example +# Template used in the fields +< Output from the template +``` + +#### `arg` + +Takes the message sent to the channel, splits by space and returns the Nth element + +Syntax: `arg ` + +Example: + +``` +> !bsg @tester +# {{ arg 1 }} please refrain from BSG +< @tester please refrain from BSG +``` + +#### `botHasBadge` + +Checks whether bot has the given badge in the current channel + +Syntax: `botHasBadge ` + +Example: + +``` +# {{ botHasBadge "moderator" }} +< true +``` + +#### `channelCounter` + +Wraps the counter name into a channel specific counter name including the channel name + +Syntax: `channelCounter ` + +Example: + +``` +# {{ channelCounter "test" }} +< 5 +``` + +#### `concat` + +Join the given string parts with delimiter + +Syntax: `concat <...parts>` + +Example: + +``` +# {{ concat ":" "test" .username }} +< test:luziferus +``` + +#### `counterValue` + +Returns the current value of the counter which identifier was supplied + +Syntax: `counterValue ` + +Example: + +``` +# {{ counterValue (concat ":" .channel "test") }} +< 5 +``` + +#### `displayName` + +Returns the display name the specified user set for themselves + +Syntax: `displayName [fallback]` + +Example: + +``` +# {{ displayName "luziferus" }} - {{ displayName "notexistinguser" "foobar" }} +< Luziferus - foobar +``` + +#### `fixUsername` + +Ensures the username no longer contains the `@` or `#` prefix + +Syntax: `fixUsername ` + +Example: + +``` +# {{ fixUsername .channel }} - {{ fixUsername "@luziferus" }} +< luziferus - luziferus +``` + +#### `formatDuration` + +Returns a formated duration. Pass empty strings to leave out the specific duration part. + +Syntax: `formatDuration ` + +Example: + +``` +# {{ formatDuration (streamUptime .channel) "hours" "minutes" "seconds" }} - {{ formatDuration (streamUptime .channel) "hours" "minutes" "" }} +< 5 hours, 33 minutes, 12 seconds - 5 hours, 33 minutes +``` + +#### `followDate` + +Looks up when `from` followed `to` + +Syntax: `followDate ` + +Example: + +``` +# {{ followDate "tezrian" "luziferus" }} +< 2021-04-10 16:07:07 +0000 UTC +``` + +#### `group` + +Gets matching group specified by index from `match_message` regular expression, when `fallback` is defined, it is used when group has an empty match + +Syntax: `group [fallback]` + +Example: + +``` +! !command ([0-9]+) ([a-z]+) ([a-z]*) +> !command 12 test +# {{ group 2 "oops" }} - {{ group 3 "oops" }} +< test - oops +``` + +#### `lastQuoteIndex` + +Gets the last quote index in the quote database for the current channel + +Syntax: `lastQuoteIndex` + +Example: + +``` +# Last Quote: #{{ lastQuoteIndex }} +< Last Quote: #32 +``` + +#### `multiply` + +Returns float from calculation: `float1 * float2` + +Syntax: `multiply ` + +Example: + +``` +# {{ printf "%.0f" (multiply 100 (seededRandom "test")) }}% +< 35% +``` + +#### `recentGame` + +Returns the last played game name of the specified user (see shoutout example) or the `fallback` if the game could not be fetched. If no fallback was supplied the message will fail and not be sent. + +Syntax: `recentGame [fallback]` + +Example: + +``` +# {{ recentGame "luziferus" "none" }} - {{ recentGame "thisuserdoesnotexist123" "none" }} +< Metro Exodus - none +``` + +#### `streamUptime` + +Returns the duration the stream is online (causes an error if no current stream is found) + +Syntax: `streamUptime ` + +Example: + +``` +# {{ formatDuration (streamUptime "luziferus") "hours" "minutes" "" }} +< 3 hours, 56 minutes +``` + +#### `seededRandom` + +Returns a float value stable for the given seed + +Syntax: `seededRandom ` + +Example: + +``` +# Your int this hour: {{ printf "%.0f" (multiply (seededRandom (concat ":" "int" .username (now "2006-01-02 15"))) 100) }}% +< Your int this hour: 17% +``` + +#### `tag` + +Takes the message sent to the channel, returns the value of the tag specified + +Syntax: `tag ` + +Example: + +``` +# {{ tag "login" }} +< luziferus +``` + +#### `toLower` / `toUpper` + +Converts the given string to lower-case / upper-case + +Syntax: `toLower ` / `toUpper ` + +Example: + +``` +# {{ toLower "Test" }} - {{ toUpper "Test" }} +< test - TEST +``` + +#### `variable` + +Returns the variable value or default in case it is empty + +Syntax: `variable [default]` + +Example: + +``` +# {{ variable "foo" "fallback" }} - {{ variable "unsetvar" "fallback" }} +< test - fallback +``` +