[docs] Add auto-generated template documentation (#50)

This commit is contained in:
Knut Ahlers 2023-08-25 23:37:37 +02:00 committed by GitHub
parent a39dc5e4c6
commit 1585df5e90
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 630 additions and 91 deletions

View file

@ -57,9 +57,14 @@ trivy:
# -- Documentation Site --
docs: actor_docs template_docs
actor_docs:
go run . --storage-conn-string $(shell mktemp).db actor-docs >docs/content/configuration/actors.md
template_docs:
go run . --storage-conn-string $(shell mktemp).db tpl-docs >docs/content/configuration/templating.md
eventclient_docs:
echo -e "---\ntitle: EventClient\nweight: 10000\n---\n" >docs/content/overlays/eventclient.md
docker run --rm -i -v $(CURDIR):$(CURDIR) -w $(CURDIR) node:18-alpine sh -ec 'npx --yes jsdoc-to-markdown --files ./internal/apimodules/overlays/default/eventclient.js' >>docs/content/overlays/eventclient.md

26
cli_tplDocs.go Normal file
View file

@ -0,0 +1,26 @@
package main
import (
"bytes"
"os"
"github.com/pkg/errors"
)
func init() {
cli.Add(cliRegistryEntry{
Name: "tpl-docs",
Description: "Generate markdown documentation for available template functions",
Run: func(args []string) error {
doc, err := generateTplDocs()
if err != nil {
return errors.Wrap(err, "generating template docs")
}
if _, err = os.Stdout.Write(append(bytes.TrimSpace(doc), '\n')); err != nil {
return errors.Wrap(err, "writing actor docs to stdout")
}
return nil
},
})
}

View file

@ -30,7 +30,8 @@ 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
< Output from the template (Rendered during docs generation)
* Output from the template (Static output, template not rendered)
```
### `arg`
@ -96,7 +97,7 @@ Example:
```
# {{ channelCounter "test" }}
< 5
< #example:test
```
### `counterValue`
@ -109,7 +110,7 @@ Example:
```
# {{ counterValue (list .channel "test" | join ":") }}
< 5
* 5
```
### `counterValueAdd`
@ -119,9 +120,10 @@ Adds the given value (or 1 if no value) to the counter and returns its new value
Syntax: `counterValueAdd <counter name> [increase=1]`
Example:
```
# {{ counterValueAdd "myCounter" }} {{ counterValueAdd "myCounter" 5 }}
< 1 6
* 1 6
```
### `displayName`
@ -134,7 +136,7 @@ Example:
```
# {{ displayName "luziferus" }} - {{ displayName "notexistinguser" "foobar" }}
< Luziferus - foobar
* Luziferus - foobar
```
### `doesFollow`
@ -147,7 +149,7 @@ Example:
```
# {{ doesFollow "tezrian" "luziferus" }}
< true
* true
```
### `doesFollowLongerThan`
@ -160,7 +162,7 @@ Example:
```
# {{ doesFollowLongerThan "tezrian" "luziferus" "168h" }}
< true
* true
```
### `fixUsername`
@ -173,20 +175,7 @@ Example:
```
# {{ fixUsername .channel }} - {{ fixUsername "@luziferus" }}
< luziferus - luziferus
```
### `formatDuration`
Returns a formated duration. Pass empty strings to leave out the specific duration part.
Syntax: `formatDuration <duration> <hours> <minutes> <seconds>`
Example:
```
# {{ formatDuration (streamUptime .channel) "hours" "minutes" "seconds" }} - {{ formatDuration (streamUptime .channel) "hours" "minutes" "" }}
< 5 hours, 33 minutes, 12 seconds - 5 hours, 33 minutes
< example - luziferus
```
### `followAge`
@ -199,7 +188,7 @@ Example:
```
# {{ followAge "tezrian" "luziferus" }}
< 15004h14m59.116620989s
* 15004h14m59.116620989s
```
### `followDate`
@ -212,7 +201,20 @@ Example:
```
# {{ followDate "tezrian" "luziferus" }}
< 2021-04-10 16:07:07 +0000 UTC
* 2021-04-10 16:07:07 +0000 UTC
```
### `formatDuration`
Returns a formated duration. Pass empty strings to leave out the specific duration part.
Syntax: `formatDuration <duration> <hours> <minutes> <seconds>`
Example:
```
# {{ formatDuration .testDuration "hours" "minutes" "seconds" }} - {{ formatDuration .testDuration "hours" "minutes" "" }}
< 5 hours, 33 minutes, 12 seconds - 5 hours, 33 minutes
```
### `group`
@ -224,7 +226,7 @@ Syntax: `group <idx> [fallback]`
Example:
```
! !command ([0-9]+) ([a-z]+) ([a-z]*)
! !command ([0-9]+) ([a-z]+) ?([a-z]*)
> !command 12 test
# {{ group 2 "oops" }} - {{ group 3 "oops" }}
< test - oops
@ -234,7 +236,7 @@ Example:
Tests whether a string is in a given list of strings (for conditional templates).
Syntax: `inList "search" "item1" "item2" [...]`
Syntax: `inList <search> <...string>`
Example:
@ -249,15 +251,13 @@ Example:
Fetches remote URL and applies jq-like query to it returning the result as string. (Remote API needs to return status 200 within 5 seconds.)
Syntax: `jsonAPI "https://example.com/doc.json" ".data.exampleString" ["fallback"]`
Syntax: `jsonAPI <url> <jq-like path> [fallback]`
Example:
```
! !mycmd
> !mycmd
# {{ jsonAPI "https://example.com/doc.json" ".data.exampleString" }}
< example string
# {{ jsonAPI "https://api.github.com/repos/Luzifer/twitch-bot" ".owner.login" }}
* Luzifer
```
### `lastPoll`
@ -270,7 +270,7 @@ Example:
```
# Last Poll: {{ (lastPoll .channel).Title }}
< Last Poll: Und wie siehts im Template aus?
* Last Poll: Und wie siehts im Template aus?
```
See schema of returned object in [`pkg/twitch/polls.go#L13`](https://github.com/Luzifer/twitch-bot/blob/master/pkg/twitch/polls.go#L13)
@ -285,7 +285,7 @@ Example:
```
# Last Quote: #{{ lastQuoteIndex }}
< Last Quote: #32
* Last Quote: #32
```
### `mention`
@ -310,11 +310,11 @@ Syntax: `pow <float1> <float2>`
Example:
```
# {{ printf "%.0f" (pow 10 4) }}%
# {{ printf "%.0f" (pow 10 4) }}
< 10000
```
### `profileImage`
### `profileImage`
Gets the URL of the given users profile image
@ -324,20 +324,20 @@ Example:
```
# {{ profileImage .username }}
< https://static-cdn.jtvnw.net/jtv_user_pictures/[...].png
* https://static-cdn.jtvnw.net/jtv_user_pictures/[...].png
```
### `randomString`
Randomly picks a string from a list of strings
Syntax: `randomString "a" [...]`
Syntax: `randomString <string> [...string]`
Example:
```
# {{ randomString "a" "b" "c" "d" }}
< a
* a
```
### `recentGame`
@ -350,10 +350,9 @@ Example:
```
# {{ recentGame "luziferus" "none" }} - {{ recentGame "thisuserdoesnotexist123" "none" }}
< Metro Exodus - none
* Metro Exodus - none
```
### `recentTitle`
Returns the last stream title of the specified user or the `fallback` if the title could not be fetched. If no fallback was supplied the message will fail and not be sent.
@ -364,7 +363,7 @@ Example:
```
# {{ recentGame "luziferus" "none" }} - {{ recentGame "thisuserdoesnotexist123" "none" }}
< Die Oper haben wir überlebt, mal sehen was uns sonst noch alles töten möchte - none
* Die Oper haben wir überlebt, mal sehen was uns sonst noch alles töten möchte… - none
```
### `seededRandom`
@ -376,8 +375,8 @@ Syntax: `seededRandom <string-seed>`
Example:
```
# Your int this hour: {{ printf "%.0f" (mul (seededRandom (list "int" .username (now | date "2006-01-02 15") | join ":")) 100) }}%
< Your int this hour: 17%
# Your int this hour: {{ printf "%.0f" (mulf (seededRandom (list "int" .username (now | date "2006-01-02 15") | join ":")) 100) }}%
< Your int this hour: 84%
```
### `streamUptime`
@ -390,7 +389,7 @@ Example:
```
# {{ formatDuration (streamUptime "luziferus") "hours" "minutes" "" }}
< 3 hours, 56 minutes
* 3 hours, 56 minutes
```
### `subCount`
@ -403,7 +402,7 @@ Example:
```
# {{ subCount "luziferus" }}
< 26
* 26
```
### `subPoints`
@ -416,7 +415,7 @@ Example:
```
# {{ subPoints "luziferus" }}
< 26
* 26
```
### `tag`
@ -428,15 +427,15 @@ Syntax: `tag <tagname>`
Example:
```
# {{ tag "login" }}
< luziferus
# {{ tag "display-name" }}
< ExampleUser
```
### `textAPI`
Fetches remote URL and returns the result as string. (Remote API needs to return status 200 within 5 seconds.)
Syntax: `textAPI "https://example.com/" ["fallback"]`
Syntax: `textAPI <url> [fallback]`
Example:
@ -444,7 +443,7 @@ Example:
! !weather (.*)
> !weather Hamburg
# {{ textAPI (printf "https://api.scorpstuff.com/weather.php?units=metric&city=%s" (urlquery (group 1))) }}
< Weather for Hamburg, DE: Few clouds with a temperature of 22 C (71.6 F). [...]
* Weather for Hamburg, DE: Few clouds with a temperature of 22 C (71.6 F). [...]
```
### `variable`
@ -457,7 +456,7 @@ Example:
```
# {{ variable "foo" "fallback" }} - {{ variable "unsetvar" "fallback" }}
< test - fallback
* test - fallback
```
## Upgrade from `v2.x` to `v3.x`

View file

@ -23,6 +23,7 @@ var (
)
type templateFuncProvider struct {
docs []plugins.TemplateFuncDocumentation
funcs map[string]plugins.TemplateFuncGetter
lock *sync.RWMutex
}
@ -72,7 +73,7 @@ func (t *templateFuncProvider) GetFuncNames() []string {
return out
}
func (t *templateFuncProvider) Register(name string, fg plugins.TemplateFuncGetter) {
func (t *templateFuncProvider) Register(name string, fg plugins.TemplateFuncGetter, doc ...plugins.TemplateFuncDocumentation) {
t.lock.Lock()
defer t.lock.Unlock()
@ -81,6 +82,11 @@ func (t *templateFuncProvider) Register(name string, fg plugins.TemplateFuncGett
}
t.funcs[name] = fg
if len(doc) > 0 {
doc[0].Name = name
t.docs = append(t.docs, doc[0])
}
}
func init() {
@ -112,5 +118,12 @@ func init() {
}
return strings.Join(parts, ", ")
}))
}), plugins.TemplateFuncDocumentation{
Description: "Returns a formated duration. Pass empty strings to leave out the specific duration part.",
Syntax: "formatDuration <duration> <hours> <minutes> <seconds>",
Example: &plugins.TemplateFuncDocumentationExample{
Template: `{{ formatDuration .testDuration "hours" "minutes" "seconds" }} - {{ formatDuration .testDuration "hours" "minutes" "" }}`,
ExpectedOutput: "5 hours, 33 minutes, 12 seconds - 5 hours, 33 minutes",
},
})
}

View file

@ -5,8 +5,8 @@ import (
"github.com/go-irc/irc"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/Luzifer/twitch-bot/v3/pkg/twitch"
"github.com/Luzifer/twitch-bot/v3/plugins"
)
@ -20,26 +20,42 @@ func init() {
return msgParts[arg], nil
}
}, plugins.TemplateFuncDocumentation{
Description: "Takes the message sent to the channel, splits by space and returns the Nth element",
Syntax: "arg <index>",
Example: &plugins.TemplateFuncDocumentationExample{
MessageContent: "!bsg @tester",
Template: `{{ arg 1 }} please refrain from BSG`,
ExpectedOutput: `@tester please refrain from BSG`,
},
})
tplFuncs.Register("botHasBadge", func(m *irc.Message, r *plugins.Rule, fields *plugins.FieldCollection) interface{} {
return func(badge string) bool {
channel, err := fields.String("channel")
if err != nil {
log.Trace("Fields for botHasBadge function had no channel")
return false
}
state := botUserstate.Get(channel)
if state == nil {
return false
}
return state.Badges.Has(badge)
badges := twitch.ParseBadgeLevels(m)
return badges.Has(badge)
}
}, plugins.TemplateFuncDocumentation{
Description: "Checks whether bot has the given badge in the current channel",
Syntax: "botHasBadge <badge>",
Example: &plugins.TemplateFuncDocumentationExample{
Template: `{{ botHasBadge "moderator" }}`,
ExpectedOutput: "true",
},
})
tplFuncs.Register("fixUsername", plugins.GenericTemplateFunctionGetter(func(username string) string { return strings.TrimLeft(username, "@#") }))
tplFuncs.Register(
"fixUsername",
plugins.GenericTemplateFunctionGetter(func(username string) string { return strings.TrimLeft(username, "@#") }),
plugins.TemplateFuncDocumentation{
Description: "Ensures the username no longer contains the `@` or `#` prefix",
Syntax: "fixUsername <username>",
Example: &plugins.TemplateFuncDocumentationExample{
Template: `{{ fixUsername .channel }} - {{ fixUsername "@luziferus" }}`,
ExpectedOutput: "example - luziferus",
},
},
)
tplFuncs.Register("group", func(m *irc.Message, r *plugins.Rule, fields *plugins.FieldCollection) interface{} {
return func(idx int, fallback ...string) (string, error) {
@ -54,14 +70,41 @@ func init() {
return fields[idx], nil
}
}, plugins.TemplateFuncDocumentation{
Description: "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 <idx> [fallback]",
Example: &plugins.TemplateFuncDocumentationExample{
MatchMessage: "!command ([0-9]+) ([a-z]+) ?([a-z]*)",
MessageContent: "!command 12 test",
Template: `{{ group 2 "oops" }} - {{ group 3 "oops" }}`,
ExpectedOutput: "test - oops",
},
})
tplFuncs.Register("mention", plugins.GenericTemplateFunctionGetter(func(username string) string { return "@" + strings.TrimLeft(username, "@#") }))
tplFuncs.Register(
"mention",
plugins.GenericTemplateFunctionGetter(func(username string) string { return "@" + strings.TrimLeft(username, "@#") }),
plugins.TemplateFuncDocumentation{
Description: "Strips username and converts into a mention",
Syntax: "mention <username>",
Example: &plugins.TemplateFuncDocumentationExample{
Template: `{{ mention "@user" }} {{ mention "user" }} {{ mention "#user" }}`,
ExpectedOutput: "@user @user @user",
},
},
)
tplFuncs.Register("tag", func(m *irc.Message, r *plugins.Rule, fields *plugins.FieldCollection) interface{} {
return func(tag string) string {
s, _ := m.GetTag(tag)
return s
}
}, plugins.TemplateFuncDocumentation{
Description: "Takes the message sent to the channel, returns the value of the tag specified",
Syntax: "tag <tagname>",
Example: &plugins.TemplateFuncDocumentationExample{
Template: `{{ tag "display-name" }}`,
ExpectedOutput: "ExampleUser",
},
})
}

View file

@ -13,16 +13,96 @@ import (
)
func init() {
tplFuncs.Register("displayName", plugins.GenericTemplateFunctionGetter(tplTwitchDisplayName))
tplFuncs.Register("doesFollow", plugins.GenericTemplateFunctionGetter(tplTwitchDoesFollow))
tplFuncs.Register("followAge", plugins.GenericTemplateFunctionGetter(tplTwitchFollowAge))
tplFuncs.Register("followDate", plugins.GenericTemplateFunctionGetter(tplTwitchFollowDate))
tplFuncs.Register("doesFollowLongerThan", plugins.GenericTemplateFunctionGetter(tplTwitchDoesFollowLongerThan))
tplFuncs.Register("lastPoll", plugins.GenericTemplateFunctionGetter(tplTwitchLastPoll))
tplFuncs.Register("profileImage", plugins.GenericTemplateFunctionGetter(tplTwitchProfileImage))
tplFuncs.Register("recentGame", plugins.GenericTemplateFunctionGetter(tplTwitchRecentGame))
tplFuncs.Register("recentTitle", plugins.GenericTemplateFunctionGetter(tplTwitchRecentTitle))
tplFuncs.Register("streamUptime", plugins.GenericTemplateFunctionGetter(tplTwitchStreamUptime))
tplFuncs.Register("displayName", plugins.GenericTemplateFunctionGetter(tplTwitchDisplayName), plugins.TemplateFuncDocumentation{
Description: "Returns the display name the specified user set for themselves",
Syntax: "displayName <username> [fallback]",
Example: &plugins.TemplateFuncDocumentationExample{
Template: `{{ displayName "luziferus" }} - {{ displayName "notexistinguser" "foobar" }}`,
FakedOutput: "Luziferus - foobar",
},
})
tplFuncs.Register("doesFollowLongerThan", plugins.GenericTemplateFunctionGetter(tplTwitchDoesFollowLongerThan), plugins.TemplateFuncDocumentation{
Description: "Returns whether `from` follows `to` for more than `duration`",
Syntax: "doesFollowLongerThan <from> <to> <duration>",
Example: &plugins.TemplateFuncDocumentationExample{
Template: `{{ doesFollowLongerThan "tezrian" "luziferus" "168h" }}`,
FakedOutput: "true",
},
})
tplFuncs.Register("doesFollow", plugins.GenericTemplateFunctionGetter(tplTwitchDoesFollow), plugins.TemplateFuncDocumentation{
Description: "Returns whether `from` follows `to`",
Syntax: "doesFollow <from> <to>",
Example: &plugins.TemplateFuncDocumentationExample{
Template: `{{ doesFollow "tezrian" "luziferus" }}`,
FakedOutput: "true",
},
})
tplFuncs.Register("followAge", plugins.GenericTemplateFunctionGetter(tplTwitchFollowAge), plugins.TemplateFuncDocumentation{
Description: "Looks up when `from` followed `to` and returns the duration between then and now",
Syntax: "followAge <from> <to>",
Example: &plugins.TemplateFuncDocumentationExample{
Template: `{{ followAge "tezrian" "luziferus" }}`,
FakedOutput: "15004h14m59.116620989s",
},
})
tplFuncs.Register("followDate", plugins.GenericTemplateFunctionGetter(tplTwitchFollowDate), plugins.TemplateFuncDocumentation{
Description: "Looks up when `from` followed `to`",
Syntax: "followDate <from> <to>",
Example: &plugins.TemplateFuncDocumentationExample{
Template: `{{ followDate "tezrian" "luziferus" }}`,
FakedOutput: "2021-04-10 16:07:07 +0000 UTC",
},
})
tplFuncs.Register("lastPoll", plugins.GenericTemplateFunctionGetter(tplTwitchLastPoll), plugins.TemplateFuncDocumentation{
Description: "Gets the last (currently running or archived) poll for the given channel (the channel must have given extended permission for poll access!)",
Syntax: "lastPoll <channel>",
Example: &plugins.TemplateFuncDocumentationExample{
Template: `Last Poll: {{ (lastPoll .channel).Title }}`,
FakedOutput: "Last Poll: Und wie siehts im Template aus?",
},
Remarks: "See schema of returned object in [`pkg/twitch/polls.go#L13`](https://github.com/Luzifer/twitch-bot/blob/master/pkg/twitch/polls.go#L13)",
})
tplFuncs.Register("profileImage", plugins.GenericTemplateFunctionGetter(tplTwitchProfileImage), plugins.TemplateFuncDocumentation{
Description: "Gets the URL of the given users profile image",
Syntax: "profileImage <username>",
Example: &plugins.TemplateFuncDocumentationExample{
Template: `{{ profileImage .username }}`,
FakedOutput: "https://static-cdn.jtvnw.net/jtv_user_pictures/[...].png",
},
})
tplFuncs.Register("recentGame", plugins.GenericTemplateFunctionGetter(tplTwitchRecentGame), plugins.TemplateFuncDocumentation{
Description: "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 <username> [fallback]",
Example: &plugins.TemplateFuncDocumentationExample{
Template: `{{ recentGame "luziferus" "none" }} - {{ recentGame "thisuserdoesnotexist123" "none" }}`,
FakedOutput: "Metro Exodus - none",
},
})
tplFuncs.Register("recentTitle", plugins.GenericTemplateFunctionGetter(tplTwitchRecentTitle), plugins.TemplateFuncDocumentation{
Description: "Returns the last stream title of the specified user or the `fallback` if the title could not be fetched. If no fallback was supplied the message will fail and not be sent.",
Syntax: "recentTitle <username> [fallback]",
Example: &plugins.TemplateFuncDocumentationExample{
Template: `{{ recentGame "luziferus" "none" }} - {{ recentGame "thisuserdoesnotexist123" "none" }}`,
FakedOutput: "Die Oper haben wir überlebt, mal sehen was uns sonst noch alles töten möchte… - none",
},
})
tplFuncs.Register("streamUptime", plugins.GenericTemplateFunctionGetter(tplTwitchStreamUptime), plugins.TemplateFuncDocumentation{
Description: "Returns the duration the stream is online (causes an error if no current stream is found)",
Syntax: "streamUptime <username>",
Example: &plugins.TemplateFuncDocumentationExample{
Template: `{{ formatDuration (streamUptime "luziferus") "hours" "minutes" "" }}`,
FakedOutput: "3 hours, 56 minutes",
},
})
}
func tplTwitchDisplayName(username string, v ...string) (string, error) {

View file

@ -131,11 +131,25 @@ func Register(args plugins.RegistrationArguments) error {
return strings.Join([]string{channel, name}, ":"), nil
}
}, 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",
},
})
args.RegisterTemplateFunction("counterValue", plugins.GenericTemplateFunctionGetter(func(name string, _ ...string) (int64, error) {
return GetCounterValue(db, name)
}))
}), 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",
},
})
args.RegisterTemplateFunction("counterValueAdd", plugins.GenericTemplateFunctionGetter(func(name string, val ...int64) (int64, error) {
var mod int64 = 1
@ -148,7 +162,14 @@ func Register(args plugins.RegistrationArguments) error {
}
return GetCounterValue(db, name)
}))
}), 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",
},
})
return nil
}

View file

@ -87,6 +87,13 @@ func Register(args plugins.RegistrationArguments) error {
return func() (int, error) {
return GetMaxQuoteIdx(db, plugins.DeriveChannel(m, nil))
}
}, plugins.TemplateFuncDocumentation{
Description: "Gets the last quote index in the quote database for the current channel",
Syntax: "lastQuoteIndex",
Example: &plugins.TemplateFuncDocumentationExample{
Template: `Last Quote: #{{ lastQuoteIndex }}`,
FakedOutput: "Last Quote: #32",
},
})
return nil

View file

@ -20,6 +20,7 @@ var (
ptrStringEmpty = func(s string) *string { return &s }("")
)
//nolint:funlen // Function contains only documentation registration
func Register(args plugins.RegistrationArguments) error {
db = args.GetDatabaseConnector()
if err := db.DB().AutoMigrate(&variable{}); err != nil {
@ -116,7 +117,14 @@ func Register(args plugins.RegistrationArguments) error {
return defVal[0], nil
}
return value, nil
}))
}), plugins.TemplateFuncDocumentation{
Description: "Returns the variable value or default in case it is empty",
Syntax: "variable <name> [default]",
Example: &plugins.TemplateFuncDocumentationExample{
Template: `{{ variable "foo" "fallback" }} - {{ variable "unsetvar" "fallback" }}`,
FakedOutput: "test - fallback",
},
})
return nil
}

View file

@ -3,8 +3,25 @@ package api
import "github.com/Luzifer/twitch-bot/v3/plugins"
func Register(args plugins.RegistrationArguments) error {
args.RegisterTemplateFunction("jsonAPI", plugins.GenericTemplateFunctionGetter(jsonAPI))
args.RegisterTemplateFunction("textAPI", plugins.GenericTemplateFunctionGetter(textAPI))
args.RegisterTemplateFunction("jsonAPI", plugins.GenericTemplateFunctionGetter(jsonAPI), plugins.TemplateFuncDocumentation{
Description: "Fetches remote URL and applies jq-like query to it returning the result as string. (Remote API needs to return status 200 within 5 seconds.)",
Syntax: "jsonAPI <url> <jq-like path> [fallback]",
Example: &plugins.TemplateFuncDocumentationExample{
Template: `{{ jsonAPI "https://api.github.com/repos/Luzifer/twitch-bot" ".owner.login" }}`,
FakedOutput: "Luzifer",
},
})
args.RegisterTemplateFunction("textAPI", plugins.GenericTemplateFunctionGetter(textAPI), plugins.TemplateFuncDocumentation{
Description: "Fetches remote URL and returns the result as string. (Remote API needs to return status 200 within 5 seconds.)",
Syntax: "textAPI <url> [fallback]",
Example: &plugins.TemplateFuncDocumentationExample{
MatchMessage: "!weather (.*)",
MessageContent: "!weather Hamburg",
Template: `{{ textAPI (printf "https://api.scorpstuff.com/weather.php?units=metric&city=%s" (urlquery (group 1))) }}`,
FakedOutput: "Weather for Hamburg, DE: Few clouds with a temperature of 22 C (71.6 F). [...]",
},
})
return nil
}

View file

@ -7,6 +7,14 @@ import (
)
func Register(args plugins.RegistrationArguments) error {
args.RegisterTemplateFunction("pow", plugins.GenericTemplateFunctionGetter(math.Pow))
args.RegisterTemplateFunction("pow", plugins.GenericTemplateFunctionGetter(math.Pow), plugins.TemplateFuncDocumentation{
Description: "Returns float from calculation: `float1 ** float2`",
Syntax: "pow <float1> <float2>",
Example: &plugins.TemplateFuncDocumentationExample{
Template: `{{ printf "%.0f" (pow 10 4) }}`,
ExpectedOutput: "10000",
},
})
return nil
}

View file

@ -12,8 +12,23 @@ import (
)
func Register(args plugins.RegistrationArguments) error {
args.RegisterTemplateFunction("randomString", plugins.GenericTemplateFunctionGetter(randomString))
args.RegisterTemplateFunction("seededRandom", plugins.GenericTemplateFunctionGetter(stableRandomFromSeed))
args.RegisterTemplateFunction("randomString", plugins.GenericTemplateFunctionGetter(randomString), plugins.TemplateFuncDocumentation{
Description: "Randomly picks a string from a list of strings",
Syntax: "randomString <string> [...string]",
Example: &plugins.TemplateFuncDocumentationExample{
Template: `{{ randomString "a" "b" "c" "d" }}`,
FakedOutput: "a",
},
})
args.RegisterTemplateFunction("seededRandom", plugins.GenericTemplateFunctionGetter(stableRandomFromSeed), plugins.TemplateFuncDocumentation{
Description: "Returns a float value stable for the given seed",
Syntax: "seededRandom <string-seed>",
Example: &plugins.TemplateFuncDocumentationExample{
Template: `Your int this hour: {{ printf "%.0f" (mulf (seededRandom (list "int" .username (now | date "2006-01-02 15") | join ":")) 100) }}%`,
},
})
return nil
}

View file

@ -6,6 +6,17 @@ import (
)
func Register(args plugins.RegistrationArguments) error {
args.RegisterTemplateFunction("inList", plugins.GenericTemplateFunctionGetter(str.StringInSlice))
args.RegisterTemplateFunction("inList", plugins.GenericTemplateFunctionGetter(func(search string, list ...string) bool {
return str.StringInSlice(search, list)
}), plugins.TemplateFuncDocumentation{
Description: "Tests whether a string is in a given list of strings (for conditional templates).",
Syntax: "inList <search> <...string>",
Example: &plugins.TemplateFuncDocumentationExample{
MatchMessage: "!command (.*)",
MessageContent: "!command foo",
Template: `{{ inList (group 1) "foo" "bar" }}`,
ExpectedOutput: "true",
},
})
return nil
}

View file

@ -9,8 +9,24 @@ import (
)
func Register(args plugins.RegistrationArguments) error {
args.RegisterTemplateFunction("b64urlenc", plugins.GenericTemplateFunctionGetter(base64URLEncode))
args.RegisterTemplateFunction("b64urldec", plugins.GenericTemplateFunctionGetter(base64URLDecode))
args.RegisterTemplateFunction("b64urlenc", plugins.GenericTemplateFunctionGetter(base64URLEncode), plugins.TemplateFuncDocumentation{
Description: "Encodes the input using base64 URL-encoding (like `b64enc` but using `URLEncoding` instead of `StdEncoding`)",
Syntax: "b64urlenc <input>",
Example: &plugins.TemplateFuncDocumentationExample{
Template: `{{ b64urlenc "mystring" }}`,
ExpectedOutput: "bXlzdHJpbmc=",
},
})
args.RegisterTemplateFunction("b64urldec", plugins.GenericTemplateFunctionGetter(base64URLDecode), plugins.TemplateFuncDocumentation{
Description: "Decodes the input using base64 URL-encoding (like `b64dec` but using `URLEncoding` instead of `StdEncoding`)",
Syntax: "b64urldec <input>",
Example: &plugins.TemplateFuncDocumentationExample{
Template: `{{ b64urldec "bXlzdHJpbmc=" }}`,
ExpectedOutput: "mystring",
},
})
return nil
}

View file

@ -19,8 +19,24 @@ func Register(args plugins.RegistrationArguments) error {
permCheckFn = args.HasPermissionForChannel
tcGetter = args.GetTwitchClientForChannel
args.RegisterTemplateFunction("subCount", plugins.GenericTemplateFunctionGetter(subCount))
args.RegisterTemplateFunction("subPoints", plugins.GenericTemplateFunctionGetter(subPoints))
args.RegisterTemplateFunction("subCount", plugins.GenericTemplateFunctionGetter(subCount), plugins.TemplateFuncDocumentation{
Description: "Returns the number of subscribers (accounts) currently subscribed to the given channel",
Syntax: "subCount <channel>",
Example: &plugins.TemplateFuncDocumentationExample{
Template: `{{ subCount "luziferus" }}`,
FakedOutput: "26",
},
})
args.RegisterTemplateFunction("subPoints", plugins.GenericTemplateFunctionGetter(subPoints), plugins.TemplateFuncDocumentation{
Description: "Returns the number of sub-points currently given through the T1 / T2 / T3 subscriptions to the given channel",
Syntax: "subPoints <channel>",
Example: &plugins.TemplateFuncDocumentationExample{
Template: `{{ subPoints "luziferus" }}`,
FakedOutput: "26",
},
})
return nil
}

37
main_test.go Normal file
View file

@ -0,0 +1,37 @@
package main
import (
"os"
"testing"
"github.com/robfig/cron/v3"
log "github.com/sirupsen/logrus"
"github.com/Luzifer/twitch-bot/v3/internal/service/access"
"github.com/Luzifer/twitch-bot/v3/internal/service/timer"
"github.com/Luzifer/twitch-bot/v3/pkg/database"
)
func TestMain(m *testing.M) {
var err error
if db, err = database.New("sqlite", "file::memory:?cache=shared", "encpass"); err != nil {
log.WithError(err).Fatal("opening storage backend")
}
if accessService, err = access.New(db); err != nil {
log.WithError(err).Fatal("applying access migration")
}
cronService = cron.New(cron.WithSeconds())
if timerService, err = timer.New(db, cronService); err != nil {
log.WithError(err).Fatal("applying timer migration")
}
if err = initCorePlugins(); err != nil {
log.WithError(err).Fatal("Unable to load core plugins")
}
os.Exit(m.Run())
}

View file

@ -112,7 +112,7 @@ type (
}
TemplateFuncGetter func(*irc.Message, *Rule, *FieldCollection) interface{}
TemplateFuncRegister func(name string, fg TemplateFuncGetter)
TemplateFuncRegister func(name string, fg TemplateFuncGetter, doc ...TemplateFuncDocumentation)
TemplateValidatorFunc func(raw string) error

19
plugins/tplfuncdoc.go Normal file
View file

@ -0,0 +1,19 @@
package plugins
type (
TemplateFuncDocumentation struct {
Name string
Description string
Syntax string
Example *TemplateFuncDocumentationExample
Remarks string
}
TemplateFuncDocumentationExample struct {
MatchMessage string
MessageContent string
Template string
ExpectedOutput string
FakedOutput string
}
)

94
tplDocs.go Normal file
View file

@ -0,0 +1,94 @@
package main
import (
"bytes"
_ "embed"
"runtime/debug"
"sort"
"text/template"
"time"
"github.com/go-irc/irc"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/Luzifer/twitch-bot/v3/plugins"
)
//go:embed tplDocs.tpl
var tplDocsTemplate string
func generateTplDocs() ([]byte, error) {
tpl, err := template.New("tplDocs").Funcs(map[string]any{
"renderExample": generateTplDocsRender,
}).Parse(tplDocsTemplate)
if err != nil {
return nil, errors.Wrap(err, "parsing tplDocs template")
}
sort.Slice(tplFuncs.docs, func(i, j int) bool { return tplFuncs.docs[i].Name < tplFuncs.docs[j].Name })
buf := new(bytes.Buffer)
if err := tpl.Execute(buf, struct {
Funcs []plugins.TemplateFuncDocumentation
}{
Funcs: tplFuncs.docs,
}); err != nil {
return nil, errors.Wrap(err, "rendering tplDocs template")
}
return buf.Bytes(), nil
}
func generateTplDocsRender(e *plugins.TemplateFuncDocumentationExample) (string, error) {
defer func() {
if err := recover(); err != nil {
logrus.WithError(err.(error)).Fatalf("%s", debug.Stack())
}
}()
content := e.MessageContent
if content == "" {
content = "Hello World"
}
msg := &irc.Message{
Command: "PRIVMSG",
Params: []string{
"#example",
content,
},
Prefix: &irc.Prefix{
Name: "exampleuser",
User: "exampleuser",
Host: "exampleuser.tmi.twitch.tv",
},
Tags: map[string]irc.TagValue{
"badge-info": "subscriber/26",
"badges": "moderator/1,subscriber/24",
"color": "#8A2BE2",
"display-name": "ExampleUser",
"emotes": "",
"first-msg": "0",
"flags": "",
"id": "d3167f1f-5a0c-4d78-ba68-1a6c0018d284",
"mod": "1",
"returning-chatter": "0",
"room-id": "123456",
"subscriber": "1",
"tmi-sent-ts": "1679582970403",
"turbo": "0",
"user-id": "987654",
"user-type": "mod",
},
}
rule := &plugins.Rule{}
if e.MatchMessage != "" {
rule.MatchMessage = &e.MatchMessage
}
return formatMessage(e.Template, msg, rule, plugins.FieldCollectionFromData(map[string]any{
"testDuration": 5*time.Hour + 33*time.Minute + 12*time.Second,
}))
}

79
tplDocs.tpl Normal file
View file

@ -0,0 +1,79 @@
---
title: "Templating"
---
{{"{{< lead >}}"}}
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.
{{"{{< /lead >}}"}}
## 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
Within templates following functions can be used:
- built-in functions in `text/template` engine
- functions from [sprig](https://masterminds.github.io/sprig/) function collection
- functions mentioned below
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 (Rendered during docs generation)
* Output from the template (Static output, template not rendered)
```
{{ range .Funcs -}}
### `{{ .Name }}`
{{ .Description }}
Syntax: `{{ .Syntax }}`
{{- if .Example }}
Example:
```
{{- if .Example.MatchMessage }}
! {{ .Example.MatchMessage }}
{{- end }}
{{- if .Example.MessageContent }}
> {{ .Example.MessageContent }}
{{- end }}
# {{ .Example.Template }}
{{- if .Example.FakedOutput }}
* {{ .Example.FakedOutput }}
{{- else }}
< {{ renderExample .Example }}
{{- end }}
```
{{- end }}
{{ if .Remarks -}}
{{ .Remarks }}
{{ end -}}
{{- end -}}
## Upgrade from `v2.x` to `v3.x`
When adding [sprig](https://masterminds.github.io/sprig/) function collection some functions collided and needed replacement. You need to adapt your templates accordingly:
- Math functions (`add`, `div`, `mod`, `mul`, `multiply`, `sub`) were replaced with their sprig-equivalent and are now working with integers instead of floats. If you need them to continue to work with floats you need to use their [float-variants](https://masterminds.github.io/sprig/mathf.html).
- `now` does no longer format the current date as a string but return the current date. You need to replace this: `now "2006-01-02"` becomes `now | date "2006-01-02"`.
- `concat` is now used to concat arrays. To join strings you will need to modify your code: `concat ":" "string1" "string2"` becomes `lists "string1" "string2" | join ":"`.
- `toLower` / `toUpper` need to be replaced with their sprig equivalent `lower` and `upper`.
{{ if false }}<!-- vim: set ft=markdown: -->{{ end }}

25
tplDocs_test.go Normal file
View file

@ -0,0 +1,25 @@
package main
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestTemplateFuncDocs(t *testing.T) {
for _, fd := range tplFuncs.docs {
t.Run(fd.Name, func(t *testing.T) {
if fd.Example == nil {
t.Skip("no example present")
}
if fd.Example.ExpectedOutput == "" {
t.Skip("no expected output present")
}
out, err := generateTplDocsRender(fd.Example)
assert.NoError(t, err)
assert.Equal(t, fd.Example.ExpectedOutput, out)
})
}
}