diff --git a/internal/template/subscriber/subscriber.go b/internal/template/subscriber/subscriber.go new file mode 100644 index 0000000..dad4668 --- /dev/null +++ b/internal/template/subscriber/subscriber.go @@ -0,0 +1,56 @@ +package subscriber + +import ( + "context" + "strings" + + "github.com/pkg/errors" + + "github.com/Luzifer/twitch-bot/v3/pkg/twitch" + "github.com/Luzifer/twitch-bot/v3/plugins" +) + +var ( + permCheckFn plugins.ChannelPermissionCheckFunc + tcGetter func(string) (*twitch.Client, error) +) + +func Register(args plugins.RegistrationArguments) error { + permCheckFn = args.HasPermissionForChannel + tcGetter = args.GetTwitchClientForChannel + + args.RegisterTemplateFunction("subCount", plugins.GenericTemplateFunctionGetter(subCount)) + args.RegisterTemplateFunction("subPoints", plugins.GenericTemplateFunctionGetter(subPoints)) + return nil +} + +func getSubInfo(broadcasterName string) (subCount, subPoints int64, err error) { + broadcasterName = strings.TrimLeft(broadcasterName, "#") + + ok, err := permCheckFn(broadcasterName, twitch.ScopeChannelReadSubscriptions) + if err != nil { + return 0, 0, errors.Wrap(err, "checking for channel permissions") + } + + if !ok { + return 0, 0, errors.Errorf("channel %q is missing permission %s", broadcasterName, twitch.ScopeChannelReadSubscriptions) + } + + tc, err := tcGetter(broadcasterName) + if err != nil { + return 0, 0, errors.Wrap(err, "getting channel twitch-client") + } + + sc, sp, err := tc.GetBroadcasterSubscriptionCount(context.Background(), broadcasterName) + return sc, sp, errors.Wrap(err, "fetching sub info") +} + +func subCount(broadcasterName string) (int64, error) { + sc, _, err := getSubInfo(broadcasterName) + return sc, err +} + +func subPoints(broadcasterName string) (int64, error) { + _, sp, err := getSubInfo(broadcasterName) + return sp, err +} diff --git a/pkg/twitch/scopes.go b/pkg/twitch/scopes.go index e81f451..be09ad6 100644 --- a/pkg/twitch/scopes.go +++ b/pkg/twitch/scopes.go @@ -12,6 +12,7 @@ const ( ScopeChannelManageVIPS = "channel:manage:vips" ScopeChannelManageWhispers = "user:manage:whispers" ScopeChannelReadRedemptions = "channel:read:redemptions" + ScopeChannelReadSubscriptions = "channel:read:subscriptions" ScopeModeratorManageAnnoucements = "moderator:manage:announcements" ScopeModeratorManageBannedUsers = "moderator:manage:banned_users" ScopeModeratorManageChatMessages = "moderator:manage:chat_messages" diff --git a/pkg/twitch/subscriptions.go b/pkg/twitch/subscriptions.go new file mode 100644 index 0000000..a80aec5 --- /dev/null +++ b/pkg/twitch/subscriptions.go @@ -0,0 +1,51 @@ +package twitch + +import ( + "context" + "fmt" + "net/http" + "time" + + "github.com/pkg/errors" +) + +const subInfoCacheTimeout = 300 * time.Second + +type ( + subInfo struct { + Total int64 `json:"total"` + Points int64 `json:"points"` + } +) + +// GetBroadcasterSubscriptionCount gets a list of users that subscribe to the specified broadcaster. +func (c *Client) GetBroadcasterSubscriptionCount(ctx context.Context, broadcasterName string) (subCount, subPoints int64, err error) { + cacheKey := []string{"broadcasterSubscriptionCountByChannel", broadcasterName} + if d := c.apiCache.Get(cacheKey); d != nil { + data := d.(subInfo) + return data.Total, data.Points, nil + } + + broadcaster, err := c.GetIDForUsername(broadcasterName) + if err != nil { + return 0, 0, errors.Wrap(err, "getting ID for broadcaster name") + } + + var data subInfo + + if err = c.request(clientRequestOpts{ + AuthType: authTypeBearerToken, + Context: ctx, + Method: http.MethodGet, + OKStatus: http.StatusOK, + Out: &data, + URL: fmt.Sprintf("https://api.twitch.tv/helix/subscriptions?broadcaster_id=%s", broadcaster), + }); err != nil { + return 0, 0, errors.Wrap(err, "executing request") + } + + // Lets not annoy the API but only ask every 5m + c.apiCache.Set(cacheKey, subInfoCacheTimeout, data) + + return data.Total, data.Points, nil +} diff --git a/plugins_core.go b/plugins_core.go index 26c1654..e4c1a93 100644 --- a/plugins_core.go +++ b/plugins_core.go @@ -43,6 +43,7 @@ import ( "github.com/Luzifer/twitch-bot/v3/internal/template/numeric" "github.com/Luzifer/twitch-bot/v3/internal/template/random" "github.com/Luzifer/twitch-bot/v3/internal/template/slice" + "github.com/Luzifer/twitch-bot/v3/internal/template/subscriber" "github.com/Luzifer/twitch-bot/v3/pkg/database" "github.com/Luzifer/twitch-bot/v3/pkg/twitch" "github.com/Luzifer/twitch-bot/v3/plugins" @@ -84,6 +85,7 @@ var ( numeric.Register, random.Register, slice.Register, + subscriber.Register, // API-only modules customevent.Register, diff --git a/scopes.go b/scopes.go index becc094..1daf7ce 100644 --- a/scopes.go +++ b/scopes.go @@ -11,6 +11,7 @@ var ( twitch.ScopeChannelManageRaids: "start raids", twitch.ScopeChannelManageVIPS: "manage VIPs", twitch.ScopeChannelReadRedemptions: "see channel-point redemptions", + twitch.ScopeChannelReadSubscriptions: "see subscribed users / sub count / points", twitch.ScopeModeratorReadFollowers: "see who follows this channel", twitch.ScopeModeratorReadShoutouts: "see shoutouts created / received", } diff --git a/wiki/Templating.md b/wiki/Templating.md index 9570ba9..23bfaae 100644 --- a/wiki/Templating.md +++ b/wiki/Templating.md @@ -284,6 +284,19 @@ Example: < Die Oper haben wir überlebt, mal sehen was uns sonst noch alles töten möchte… - none ``` +#### `seededRandom` + +Returns a float value stable for the given seed + +Syntax: `seededRandom ` + +Example: + +``` +# Your int this hour: {{ printf "%.0f" (multiply (seededRandom (list "int" .username (now "2006-01-02 15") | join ":")) 100) }}% +< Your int this hour: 17% +``` + #### `streamUptime` Returns the duration the stream is online (causes an error if no current stream is found) @@ -297,17 +310,30 @@ Example: < 3 hours, 56 minutes ``` -#### `seededRandom` +#### `subCount` -Returns a float value stable for the given seed +Returns the number of subscribers (accounts) currently subscribed to the given channel -Syntax: `seededRandom ` +Syntax: `subCount ` Example: ``` -# Your int this hour: {{ printf "%.0f" (multiply (seededRandom (list "int" .username (now "2006-01-02 15") | join ":")) 100) }}% -< Your int this hour: 17% +# {{ subCount "luziferus" }} +< 26 +``` + +#### `subPoints` + +Returns the number of sub-points currently given through the T1 / T2 / T3 subscriptions to the given channel + +Syntax: `subPoints ` + +Example: + +``` +# {{ subPoints "luziferus" }} +< 26 ``` #### `tag`