twitch-bot/pkg/twitch/users.go
Knut Ahlers fbc76761b4
[core] Fix: Replace deprecated follow API
- add `moderator:read:followers` scope to bot-defaults
- document requirement of bot to be mod to read followers
- adjust `GetFollowDate` method to use new channel followers endpoint

Signed-off-by: Knut Ahlers <knut@ahlers.me>
2023-09-29 14:58:59 +02:00

236 lines
6.1 KiB
Go

package twitch
import (
"context"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/pkg/errors"
)
type (
User struct {
DisplayName string `json:"display_name"`
ID string `json:"id"`
Login string `json:"login"`
ProfileImageURL string `json:"profile_image_url"`
}
)
var ErrUserDoesNotFollow = errors.New("no follow-relation found")
func (c *Client) GetAuthorizedUser() (userID string, userName string, err error) {
var payload struct {
Data []User `json:"data"`
}
if err := c.Request(ClientRequestOpts{
AuthType: AuthTypeBearerToken,
Context: context.Background(),
Method: http.MethodGet,
OKStatus: http.StatusOK,
Out: &payload,
URL: "https://api.twitch.tv/helix/users",
}); err != nil {
return "", "", errors.Wrap(err, "request channel info")
}
if l := len(payload.Data); l != 1 {
return "", "", errors.Errorf("unexpected number of users returned: %d", l)
}
return payload.Data[0].ID, payload.Data[0].Login, nil
}
func (c *Client) GetDisplayNameForUser(username string) (string, error) {
username = strings.TrimLeft(username, "#@")
cacheKey := []string{"displayNameForUsername", username}
if d := c.apiCache.Get(cacheKey); d != nil {
return d.(string), nil
}
var payload struct {
Data []User `json:"data"`
}
if err := c.Request(ClientRequestOpts{
AuthType: AuthTypeAppAccessToken,
Context: context.Background(),
Method: http.MethodGet,
Out: &payload,
URL: fmt.Sprintf("https://api.twitch.tv/helix/users?login=%s", username),
}); err != nil {
return "", errors.Wrap(err, "request channel info")
}
if l := len(payload.Data); l != 1 {
return "", errors.Errorf("unexpected number of users returned: %d", l)
}
// The DisplayName for an username will not change (often), cache for a decent time
c.apiCache.Set(cacheKey, time.Hour, payload.Data[0].DisplayName)
return payload.Data[0].DisplayName, nil
}
func (c *Client) GetFollowDate(from, to string) (time.Time, error) {
cacheKey := []string{"followDate", from, to}
if d := c.apiCache.Get(cacheKey); d != nil {
return d.(time.Time), nil
}
fromID, err := c.GetIDForUsername(from)
if err != nil {
return time.Time{}, errors.Wrap(err, "getting id for 'from' user")
}
toID, err := c.GetIDForUsername(to)
if err != nil {
return time.Time{}, errors.Wrap(err, "getting id for 'to' user")
}
var payload struct {
Data []struct {
FollowedAt time.Time `json:"followed_at"`
} `json:"data"`
}
if err := c.Request(ClientRequestOpts{
AuthType: AuthTypeAppAccessToken,
Context: context.Background(),
Method: http.MethodGet,
OKStatus: http.StatusOK,
Out: &payload,
URL: fmt.Sprintf("https://api.twitch.tv/helix/channels/followers?broadcaster_id=%s&user_id=%s", toID, fromID),
}); err != nil {
return time.Time{}, errors.Wrap(err, "request follow info")
}
switch len(payload.Data) {
case 0:
return time.Time{}, ErrUserDoesNotFollow
case 1:
// Handled below, no error
default:
return time.Time{}, errors.Errorf("unexpected number of records returned: %d", len(payload.Data))
}
// Follow date will not change that often, cache for a long time
c.apiCache.Set(cacheKey, timeDay, payload.Data[0].FollowedAt)
return payload.Data[0].FollowedAt, nil
}
func (c *Client) GetIDForUsername(username string) (string, error) {
username = strings.TrimLeft(username, "#@")
cacheKey := []string{"idForUsername", username}
if d := c.apiCache.Get(cacheKey); d != nil {
return d.(string), nil
}
var payload struct {
Data []User `json:"data"`
}
if err := c.Request(ClientRequestOpts{
AuthType: AuthTypeAppAccessToken,
Context: context.Background(),
Method: http.MethodGet,
OKStatus: http.StatusOK,
Out: &payload,
URL: fmt.Sprintf("https://api.twitch.tv/helix/users?login=%s", username),
}); err != nil {
return "", errors.Wrap(err, "request channel info")
}
if l := len(payload.Data); l != 1 {
return "", errors.Errorf("unexpected number of users returned: %d", l)
}
// The ID for an username will not change (often), cache for a long time
c.apiCache.Set(cacheKey, timeDay, payload.Data[0].ID)
return payload.Data[0].ID, nil
}
// GetUsernameForID retrieves the login name (not the display name)
// for the given user ID
func (c *Client) GetUsernameForID(ctx context.Context, id string) (string, error) {
cacheKey := []string{"usernameForID", id}
if d := c.apiCache.Get(cacheKey); d != nil {
return d.(string), nil
}
var payload struct {
Data []User `json:"data"`
}
if err := c.Request(ClientRequestOpts{
AuthType: AuthTypeAppAccessToken,
Context: ctx,
Method: http.MethodGet,
OKStatus: http.StatusOK,
Out: &payload,
URL: fmt.Sprintf("https://api.twitch.tv/helix/users?id=%s", id),
}); err != nil {
return "", errors.Wrap(err, "request channel info")
}
if l := len(payload.Data); l != 1 {
return "", errors.Errorf("unexpected number of users returned: %d", l)
}
// The username for an ID will not change (often), cache for a long time
c.apiCache.Set(cacheKey, timeDay, payload.Data[0].Login)
return payload.Data[0].Login, nil
}
func (c *Client) GetUserInformation(user string) (*User, error) {
user = strings.TrimLeft(user, "#@")
var (
out User
param = "login"
payload struct {
Data []User `json:"data"`
}
)
cacheKey := []string{"userInformation", user}
if d := c.apiCache.Get(cacheKey); d != nil {
out = d.(User)
return &out, nil
}
if _, err := strconv.ParseInt(user, 10, 64); err == nil {
param = "id"
}
if err := c.Request(ClientRequestOpts{
AuthType: AuthTypeAppAccessToken,
Context: context.Background(),
Method: http.MethodGet,
OKStatus: http.StatusOK,
Out: &payload,
URL: fmt.Sprintf("https://api.twitch.tv/helix/users?%s=%s", param, user),
}); err != nil {
return nil, errors.Wrap(err, "request user info")
}
if l := len(payload.Data); l != 1 {
return nil, errors.Errorf("unexpected number of records returned: %d", l)
}
// User info will not change that often, cache for a long time
c.apiCache.Set(cacheKey, timeDay, payload.Data[0])
out = payload.Data[0]
return &out, nil
}