From 34d1f90540ac9a0c230fbafba2479aac0c5ff337 Mon Sep 17 00:00:00 2001 From: Knut Ahlers Date: Wed, 13 Oct 2021 14:30:45 +0200 Subject: [PATCH] [core] add streamUptime / formatDuration template functions Signed-off-by: Knut Ahlers --- functions.go | 28 ++++++++++++++++++++++-- functions_twitch.go | 11 ++++++++++ twitch/twitch.go | 52 +++++++++++++++++++++++++++++++++++++++++++++ wiki/Home.md | 2 ++ 4 files changed, 91 insertions(+), 2 deletions(-) diff --git a/functions.go b/functions.go index 1b3a789..0a08ea7 100644 --- a/functions.go +++ b/functions.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "strings" "sync" "text/template" @@ -58,10 +59,33 @@ func init() { tplFuncs.Register(n, plugins.GenericTemplateFunctionGetter(f)) } + tplFuncs.Register("concat", plugins.GenericTemplateFunctionGetter(func(delim string, parts ...string) string { return strings.Join(parts, delim) })) + + tplFuncs.Register("formatDuration", plugins.GenericTemplateFunctionGetter(func(dur time.Duration, units ...string) string { + dLeft := dur + + if len(units) == 0 { + return "" + } + + var parts []string + for idx, div := range []time.Duration{time.Hour, time.Minute, time.Second} { + part := dLeft / div + dLeft -= part * div + + if len(units) <= idx || units[idx] == "" { + continue + } + + parts = append(parts, fmt.Sprintf("%d %s", part, units[idx])) + } + + return strings.Join(parts, ", ") + })) + tplFuncs.Register("toLower", plugins.GenericTemplateFunctionGetter(strings.ToLower)) tplFuncs.Register("toUpper", plugins.GenericTemplateFunctionGetter(strings.ToUpper)) - tplFuncs.Register("followDate", plugins.GenericTemplateFunctionGetter(func(from, to string) (time.Time, error) { return twitchClient.GetFollowDate(from, to) })) - tplFuncs.Register("concat", plugins.GenericTemplateFunctionGetter(func(delim string, parts ...string) string { return strings.Join(parts, delim) })) + tplFuncs.Register("variable", plugins.GenericTemplateFunctionGetter(func(name string, defVal ...string) string { value := store.GetVariable(name) if value == "" && len(defVal) > 0 { diff --git a/functions_twitch.go b/functions_twitch.go index 1b8cef4..1bbfe8d 100644 --- a/functions_twitch.go +++ b/functions_twitch.go @@ -2,6 +2,7 @@ package main import ( "strings" + "time" "github.com/Luzifer/twitch-bot/plugins" ) @@ -16,6 +17,8 @@ func init() { return displayName, err })) + tplFuncs.Register("followDate", plugins.GenericTemplateFunctionGetter(func(from, to string) (time.Time, error) { return twitchClient.GetFollowDate(from, to) })) + tplFuncs.Register("recentGame", plugins.GenericTemplateFunctionGetter(func(username string, v ...string) (string, error) { game, _, err := twitchClient.GetRecentStreamInfo(strings.TrimLeft(username, "#")) if len(v) > 0 && (err != nil || game == "") { @@ -24,4 +27,12 @@ func init() { return game, err })) + + tplFuncs.Register("streamUptime", plugins.GenericTemplateFunctionGetter(func(username string) (time.Duration, error) { + si, err := twitchClient.GetCurrentStreamInfo(strings.TrimLeft(username, "#")) + if err != nil { + return 0, err + } + return time.Since(si.StartedAt), nil + })) } diff --git a/twitch/twitch.go b/twitch/twitch.go index 7660ec0..5d535a2 100644 --- a/twitch/twitch.go +++ b/twitch/twitch.go @@ -39,6 +39,23 @@ type ( apiCache *APICache } + StreamInfo struct { + ID string `json:"id"` + UserID string `json:"user_id"` + UserLogin string `json:"user_login"` + UserName string `json:"user_name"` + GameID string `json:"game_id"` + GameName string `json:"game_name"` + Type string `json:"type"` + Title string `json:"title"` + ViewerCount int64 `json:"viewer_count"` + StartedAt time.Time `json:"started_at"` + Language string `json:"language"` + ThumbnailURL string `json:"thumbnail_url"` + TagIds []string `json:"tag_ids"` + IsMature bool `json:"is_mature"` + } + User struct { DisplayName string `json:"display_name"` ID string `json:"id"` @@ -253,6 +270,41 @@ func (c Client) HasLiveStream(username string) (bool, error) { return len(payload.Data) == 1 && payload.Data[0].Type == "live", nil } +func (c Client) GetCurrentStreamInfo(username string) (*StreamInfo, error) { + cacheKey := []string{"currentStreamInfo", username} + if si := c.apiCache.Get(cacheKey); si != nil { + return si.(*StreamInfo), nil + } + + id, err := c.GetIDForUsername(username) + if err != nil { + return nil, errors.Wrap(err, "getting ID for username") + } + + var payload struct { + Data []*StreamInfo `json:"data"` + } + + if err := c.request( + context.Background(), + http.MethodGet, + fmt.Sprintf("https://api.twitch.tv/helix/streams?user_id=%s", id), + nil, + &payload, + ); err != nil { + return nil, errors.Wrap(err, "request channel info") + } + + if l := len(payload.Data); l != 1 { + return nil, errors.Errorf("unexpected number of users returned: %d", l) + } + + // Stream-info can be changed at any moment, cache for a short period of time + c.apiCache.Set(cacheKey, twitchMinCacheTime, payload.Data[0]) + + return payload.Data[0], nil +} + func (c Client) GetIDForUsername(username string) (string, error) { cacheKey := []string{"idForUsername", username} if d := c.apiCache.Get(cacheKey); d != nil { diff --git a/wiki/Home.md b/wiki/Home.md index 7185ebb..1e3d01f 100644 --- a/wiki/Home.md +++ b/wiki/Home.md @@ -137,9 +137,11 @@ Additionally there are some functions available in the templates: - `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 - `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