diff --git a/docs/content/configuration/templating.md b/docs/content/configuration/templating.md index 2b4ed31..aac588b 100644 --- a/docs/content/configuration/templating.md +++ b/docs/content/configuration/templating.md @@ -389,7 +389,7 @@ Example: ``` # Your int this hour: {{ printf "%.0f" (mulf (seededRandom (list "int" .username (now | date "2006-01-02 15") | join ":")) 100) }}% -< Your int this hour: 77% +< Your int this hour: 46% ``` ### `streamUptime` diff --git a/functions_twitch.go b/functions_twitch.go deleted file mode 100644 index 0b2468d..0000000 --- a/functions_twitch.go +++ /dev/null @@ -1,254 +0,0 @@ -package main - -import ( - "context" - "strings" - "time" - - "github.com/pkg/errors" - - "github.com/Luzifer/twitch-bot/v3/internal/service/access" - "github.com/Luzifer/twitch-bot/v3/pkg/twitch" - "github.com/Luzifer/twitch-bot/v3/plugins" -) - -//nolint:funlen -func init() { - tplFuncs.Register("displayName", plugins.GenericTemplateFunctionGetter(tplTwitchDisplayName), plugins.TemplateFuncDocumentation{ - Description: "Returns the display name the specified user set for themselves", - Syntax: "displayName [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 ", - 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 ", - 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 ", - 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 ", - Example: &plugins.TemplateFuncDocumentationExample{ - Template: `{{ followDate "tezrian" "luziferus" }}`, - FakedOutput: "2021-04-10 16:07:07 +0000 UTC", - }, - }) - - tplFuncs.Register("idForUsername", plugins.GenericTemplateFunctionGetter(tplTwitchIDForUsername), plugins.TemplateFuncDocumentation{ - Description: "Returns the user-id for the given username", - Syntax: "idForUsername ", - Example: &plugins.TemplateFuncDocumentationExample{ - Template: `{{ idForUsername "twitch" }}`, - FakedOutput: "12826", - }, - }) - - 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 ", - 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 ", - 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 [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 [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 ", - Example: &plugins.TemplateFuncDocumentationExample{ - Template: `{{ formatDuration (streamUptime "luziferus") "hours" "minutes" "" }}`, - FakedOutput: "3 hours, 56 minutes", - }, - }) - - tplFuncs.Register("usernameForID", plugins.GenericTemplateFunctionGetter(tplTwitchUsernameForID), plugins.TemplateFuncDocumentation{ - Description: "Returns the current login name of an user-id", - Syntax: "usernameForID ", - Example: &plugins.TemplateFuncDocumentationExample{ - Template: `{{ usernameForID "12826" }}`, - FakedOutput: "twitch", - }, - }) -} - -func tplTwitchDisplayName(username string, v ...string) (string, error) { - displayName, err := twitchClient.GetDisplayNameForUser(strings.TrimLeft(username, "#")) - if len(v) > 0 && (err != nil || displayName == "") { - return v[0], nil //nolint:nilerr // Default value, no need to return error - } - - return displayName, err -} - -func tplTwitchDoesFollow(from, to string) (bool, error) { - _, err := twitchClient.GetFollowDate(from, to) - switch { - case err == nil: - return true, nil - - case errors.Is(err, twitch.ErrUserDoesNotFollow): - return false, nil - - default: - return false, errors.Wrap(err, "getting follow date") - } -} - -func tplTwitchFollowAge(from, to string) (time.Duration, error) { - since, err := twitchClient.GetFollowDate(from, to) - return time.Since(since), errors.Wrap(err, "getting follow date") -} - -func tplTwitchFollowDate(from, to string) (time.Time, error) { - return twitchClient.GetFollowDate(from, to) -} - -func tplTwitchDoesFollowLongerThan(from, to string, t any) (bool, error) { - var ( - age time.Duration - err error - ) - - switch v := t.(type) { - case int64: - age = time.Duration(v) * time.Second - - case string: - if age, err = time.ParseDuration(v); err != nil { - return false, errors.Wrap(err, "parsing duration") - } - - default: - return false, errors.Errorf("unexpected input for duration %t", t) - } - - fd, err := twitchClient.GetFollowDate(from, to) - switch { - case err == nil: - return time.Since(fd) > age, nil - - case errors.Is(err, twitch.ErrUserDoesNotFollow): - return false, nil - - default: - return false, errors.Wrap(err, "getting follow date") - } -} - -func tplTwitchIDForUsername(username string) (string, error) { - return twitchClient.GetIDForUsername(username) -} - -func tplTwitchLastPoll(username string) (*twitch.PollInfo, error) { - hasPollAccess, err := accessService.HasAnyPermissionForChannel(username, twitch.ScopeChannelReadPolls, twitch.ScopeChannelManagePolls) - if err != nil { - return nil, errors.Wrap(err, "checking read-poll-permission") - } - - if !hasPollAccess { - return nil, errors.Errorf("not authorized to read polls for channel %s", username) - } - - tc, err := accessService.GetTwitchClientForChannel(strings.TrimLeft(username, "#"), access.ClientConfig{ - TwitchClient: cfg.TwitchClient, - TwitchClientSecret: cfg.TwitchClientSecret, - }) - if err != nil { - return nil, errors.Wrap(err, "getting twitch client for user") - } - - poll, err := tc.GetLatestPoll(context.Background(), strings.TrimLeft(username, "#")) - return poll, errors.Wrap(err, "getting last poll") -} - -func tplTwitchProfileImage(username string) (string, error) { - user, err := twitchClient.GetUserInformation(strings.TrimLeft(username, "#@")) - if err != nil { - return "", errors.Wrap(err, "getting user info") - } - - return user.ProfileImageURL, nil -} - -func tplTwitchRecentGame(username string, v ...string) (string, error) { - game, _, err := twitchClient.GetRecentStreamInfo(strings.TrimLeft(username, "#")) - if len(v) > 0 && (err != nil || game == "") { - return v[0], nil - } - - return game, err -} - -func tplTwitchRecentTitle(username string, v ...string) (string, error) { - _, title, err := twitchClient.GetRecentStreamInfo(strings.TrimLeft(username, "#")) - if len(v) > 0 && (err != nil || title == "") { - return v[0], nil - } - - return title, err -} - -func tplTwitchStreamUptime(username string) (time.Duration, error) { - si, err := twitchClient.GetCurrentStreamInfo(strings.TrimLeft(username, "#")) - if err != nil { - return 0, err - } - return time.Since(si.StartedAt), nil -} - -func tplTwitchUsernameForID(id string) (string, error) { - return twitchClient.GetUsernameForID(context.Background(), id) -} diff --git a/internal/template/twitch/follow.go b/internal/template/twitch/follow.go new file mode 100644 index 0000000..cbfd01c --- /dev/null +++ b/internal/template/twitch/follow.go @@ -0,0 +1,110 @@ +package twitch + +import ( + "time" + + "github.com/Luzifer/twitch-bot/v3/pkg/twitch" + "github.com/Luzifer/twitch-bot/v3/plugins" + "github.com/pkg/errors" +) + +func init() { + regFn = append( + regFn, + tplTwitchDoesFollow, + tplTwitchDoesFollowLongerThan, + tplTwitchFollowAge, + tplTwitchFollowDate, + ) +} + +func tplTwitchDoesFollowLongerThan(args plugins.RegistrationArguments) { + args.RegisterTemplateFunction("doesFollowLongerThan", plugins.GenericTemplateFunctionGetter(func(from, to string, t any) (bool, error) { + var ( + age time.Duration + err error + ) + + switch v := t.(type) { + case int64: + age = time.Duration(v) * time.Second + + case string: + if age, err = time.ParseDuration(v); err != nil { + return false, errors.Wrap(err, "parsing duration") + } + + default: + return false, errors.Errorf("unexpected input for duration %t", t) + } + + fd, err := args.GetTwitchClient().GetFollowDate(from, to) + switch { + case err == nil: + return time.Since(fd) > age, nil + + case errors.Is(err, twitch.ErrUserDoesNotFollow): + return false, nil + + default: + return false, errors.Wrap(err, "getting follow date") + } + }), plugins.TemplateFuncDocumentation{ + Description: "Returns whether `from` follows `to` for more than `duration`", + Syntax: "doesFollowLongerThan ", + Example: &plugins.TemplateFuncDocumentationExample{ + Template: `{{ doesFollowLongerThan "tezrian" "luziferus" "168h" }}`, + FakedOutput: "true", + }, + }) +} + +func tplTwitchDoesFollow(args plugins.RegistrationArguments) { + args.RegisterTemplateFunction("doesFollow", plugins.GenericTemplateFunctionGetter(func(from, to string) (bool, error) { + _, err := args.GetTwitchClient().GetFollowDate(from, to) + switch { + case err == nil: + return true, nil + + case errors.Is(err, twitch.ErrUserDoesNotFollow): + return false, nil + + default: + return false, errors.Wrap(err, "getting follow date") + } + }), plugins.TemplateFuncDocumentation{ + Description: "Returns whether `from` follows `to`", + Syntax: "doesFollow ", + Example: &plugins.TemplateFuncDocumentationExample{ + Template: `{{ doesFollow "tezrian" "luziferus" }}`, + FakedOutput: "true", + }, + }) +} + +func tplTwitchFollowAge(args plugins.RegistrationArguments) { + args.RegisterTemplateFunction("followAge", plugins.GenericTemplateFunctionGetter(func(from, to string) (time.Duration, error) { + since, err := args.GetTwitchClient().GetFollowDate(from, to) + return time.Since(since), errors.Wrap(err, "getting follow date") + }), plugins.TemplateFuncDocumentation{ + Description: "Looks up when `from` followed `to` and returns the duration between then and now", + Syntax: "followAge ", + Example: &plugins.TemplateFuncDocumentationExample{ + Template: `{{ followAge "tezrian" "luziferus" }}`, + FakedOutput: "15004h14m59.116620989s", + }, + }) +} + +func tplTwitchFollowDate(args plugins.RegistrationArguments) { + args.RegisterTemplateFunction("followDate", plugins.GenericTemplateFunctionGetter(func(from, to string) (time.Time, error) { + return args.GetTwitchClient().GetFollowDate(from, to) + }), plugins.TemplateFuncDocumentation{ + Description: "Looks up when `from` followed `to`", + Syntax: "followDate ", + Example: &plugins.TemplateFuncDocumentationExample{ + Template: `{{ followDate "tezrian" "luziferus" }}`, + FakedOutput: "2021-04-10 16:07:07 +0000 UTC", + }, + }) +} diff --git a/internal/template/twitch/poll.go b/internal/template/twitch/poll.go new file mode 100644 index 0000000..fed3ff7 --- /dev/null +++ b/internal/template/twitch/poll.go @@ -0,0 +1,46 @@ +package twitch + +import ( + "context" + "strings" + + "github.com/Luzifer/twitch-bot/v3/pkg/twitch" + "github.com/Luzifer/twitch-bot/v3/plugins" + "github.com/pkg/errors" +) + +func init() { + regFn = append( + regFn, + tplTwitchLastPoll, + ) +} + +func tplTwitchLastPoll(args plugins.RegistrationArguments) { + args.RegisterTemplateFunction("lastPoll", plugins.GenericTemplateFunctionGetter(func(username string) (*twitch.PollInfo, error) { + hasPollAccess, err := args.HasAnyPermissionForChannel(username, twitch.ScopeChannelReadPolls, twitch.ScopeChannelManagePolls) + if err != nil { + return nil, errors.Wrap(err, "checking read-poll-permission") + } + + if !hasPollAccess { + return nil, errors.Errorf("not authorized to read polls for channel %s", username) + } + + tc, err := args.GetTwitchClientForChannel(strings.TrimLeft(username, "#")) + if err != nil { + return nil, errors.Wrap(err, "getting twitch client for user") + } + + poll, err := tc.GetLatestPoll(context.Background(), strings.TrimLeft(username, "#")) + return poll, errors.Wrap(err, "getting last poll") + }), 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 ", + 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)", + }) +} diff --git a/internal/template/twitch/stream.go b/internal/template/twitch/stream.go new file mode 100644 index 0000000..81deed0 --- /dev/null +++ b/internal/template/twitch/stream.go @@ -0,0 +1,70 @@ +package twitch + +import ( + "strings" + "time" + + "github.com/Luzifer/twitch-bot/v3/plugins" +) + +func init() { + regFn = append( + regFn, + tplTwitchRecentGame, + tplTwitchRecentTitle, + tplTwitchStreamUptime, + ) +} + +func tplTwitchRecentGame(args plugins.RegistrationArguments) { + args.RegisterTemplateFunction("recentGame", plugins.GenericTemplateFunctionGetter(func(username string, v ...string) (string, error) { + game, _, err := args.GetTwitchClient().GetRecentStreamInfo(strings.TrimLeft(username, "#")) + if len(v) > 0 && (err != nil || game == "") { + return v[0], nil + } + + return game, err + }), 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 [fallback]", + Example: &plugins.TemplateFuncDocumentationExample{ + Template: `{{ recentGame "luziferus" "none" }} - {{ recentGame "thisuserdoesnotexist123" "none" }}`, + FakedOutput: "Metro Exodus - none", + }, + }) +} + +func tplTwitchRecentTitle(args plugins.RegistrationArguments) { + args.RegisterTemplateFunction("recentTitle", plugins.GenericTemplateFunctionGetter(func(username string, v ...string) (string, error) { + _, title, err := args.GetTwitchClient().GetRecentStreamInfo(strings.TrimLeft(username, "#")) + if len(v) > 0 && (err != nil || title == "") { + return v[0], nil + } + + return title, err + }), 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 [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", + }, + }) +} + +func tplTwitchStreamUptime(args plugins.RegistrationArguments) { + args.RegisterTemplateFunction("streamUptime", plugins.GenericTemplateFunctionGetter(func(username string) (time.Duration, error) { + si, err := args.GetTwitchClient().GetCurrentStreamInfo(strings.TrimLeft(username, "#")) + if err != nil { + return 0, err + } + return time.Since(si.StartedAt), nil + }), plugins.TemplateFuncDocumentation{ + Description: "Returns the duration the stream is online (causes an error if no current stream is found)", + Syntax: "streamUptime ", + Example: &plugins.TemplateFuncDocumentationExample{ + Template: `{{ formatDuration (streamUptime "luziferus") "hours" "minutes" "" }}`, + FakedOutput: "3 hours, 56 minutes", + }, + }) +} diff --git a/internal/template/twitch/twitch.go b/internal/template/twitch/twitch.go new file mode 100644 index 0000000..4f8e114 --- /dev/null +++ b/internal/template/twitch/twitch.go @@ -0,0 +1,17 @@ +// Package twitch defines Twitch related template functions not having +// their place in any other package +package twitch + +import ( + "github.com/Luzifer/twitch-bot/v3/plugins" +) + +var regFn []func(plugins.RegistrationArguments) + +func Register(args plugins.RegistrationArguments) error { + for _, fn := range regFn { + fn(args) + } + + return nil +} diff --git a/internal/template/twitch/user.go b/internal/template/twitch/user.go new file mode 100644 index 0000000..d84d22d --- /dev/null +++ b/internal/template/twitch/user.go @@ -0,0 +1,81 @@ +package twitch + +import ( + "context" + "strings" + + "github.com/Luzifer/twitch-bot/v3/plugins" + "github.com/pkg/errors" +) + +func init() { + regFn = append( + regFn, + tplTwitchDisplayName, + tplTwitchIDForUsername, + tplTwitchProfileImage, + tplTwitchUsernameForID, + ) +} + +func tplTwitchDisplayName(args plugins.RegistrationArguments) { + args.RegisterTemplateFunction("displayName", plugins.GenericTemplateFunctionGetter(func(username string, v ...string) (string, error) { + displayName, err := args.GetTwitchClient().GetDisplayNameForUser(strings.TrimLeft(username, "#")) + if len(v) > 0 && (err != nil || displayName == "") { + return v[0], nil //nolint:nilerr // Default value, no need to return error + } + + return displayName, err + }), plugins.TemplateFuncDocumentation{ + Description: "Returns the display name the specified user set for themselves", + Syntax: "displayName [fallback]", + Example: &plugins.TemplateFuncDocumentationExample{ + Template: `{{ displayName "luziferus" }} - {{ displayName "notexistinguser" "foobar" }}`, + FakedOutput: "Luziferus - foobar", + }, + }) +} + +func tplTwitchIDForUsername(args plugins.RegistrationArguments) { + args.RegisterTemplateFunction("idForUsername", plugins.GenericTemplateFunctionGetter(func(username string) (string, error) { + return args.GetTwitchClient().GetIDForUsername(username) + }), plugins.TemplateFuncDocumentation{ + Description: "Returns the user-id for the given username", + Syntax: "idForUsername ", + Example: &plugins.TemplateFuncDocumentationExample{ + Template: `{{ idForUsername "twitch" }}`, + FakedOutput: "12826", + }, + }) +} + +func tplTwitchProfileImage(args plugins.RegistrationArguments) { + args.RegisterTemplateFunction("profileImage", plugins.GenericTemplateFunctionGetter(func(username string) (string, error) { + user, err := args.GetTwitchClient().GetUserInformation(strings.TrimLeft(username, "#@")) + if err != nil { + return "", errors.Wrap(err, "getting user info") + } + + return user.ProfileImageURL, nil + }), plugins.TemplateFuncDocumentation{ + Description: "Gets the URL of the given users profile image", + Syntax: "profileImage ", + Example: &plugins.TemplateFuncDocumentationExample{ + Template: `{{ profileImage .username }}`, + FakedOutput: "https://static-cdn.jtvnw.net/jtv_user_pictures/[...].png", + }, + }) +} + +func tplTwitchUsernameForID(args plugins.RegistrationArguments) { + args.RegisterTemplateFunction("usernameForID", plugins.GenericTemplateFunctionGetter(func(id string) (string, error) { + return args.GetTwitchClient().GetUsernameForID(context.Background(), id) + }), plugins.TemplateFuncDocumentation{ + Description: "Returns the current login name of an user-id", + Syntax: "usernameForID ", + Example: &plugins.TemplateFuncDocumentationExample{ + Template: `{{ usernameForID "12826" }}`, + FakedOutput: "twitch", + }, + }) +} diff --git a/plugins_core.go b/plugins_core.go index e9c56b0..05c3a93 100644 --- a/plugins_core.go +++ b/plugins_core.go @@ -48,6 +48,7 @@ import ( "github.com/Luzifer/twitch-bot/v3/internal/template/slice" "github.com/Luzifer/twitch-bot/v3/internal/template/strings" "github.com/Luzifer/twitch-bot/v3/internal/template/subscriber" + twitchFns "github.com/Luzifer/twitch-bot/v3/internal/template/twitch" "github.com/Luzifer/twitch-bot/v3/pkg/database" "github.com/Luzifer/twitch-bot/v3/pkg/twitch" "github.com/Luzifer/twitch-bot/v3/plugins" @@ -93,6 +94,7 @@ var ( slice.Register, strings.Register, subscriber.Register, + twitchFns.Register, // API-only modules customevent.Register,