2022-10-25 16:47:30 +00:00
|
|
|
package twitch
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"fmt"
|
|
|
|
"net/http"
|
|
|
|
"strconv"
|
2023-08-14 13:34:02 +00:00
|
|
|
"strings"
|
2022-10-25 16:47:30 +00:00
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
)
|
|
|
|
|
|
|
|
type (
|
2024-01-01 16:52:18 +00:00
|
|
|
// User represents the data known about an user
|
2022-10-25 16:47:30 +00:00
|
|
|
User struct {
|
|
|
|
DisplayName string `json:"display_name"`
|
|
|
|
ID string `json:"id"`
|
|
|
|
Login string `json:"login"`
|
|
|
|
ProfileImageURL string `json:"profile_image_url"`
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
2024-01-01 16:52:18 +00:00
|
|
|
// ErrUserDoesNotFollow states the user does not follow the given channel
|
2022-12-26 21:38:14 +00:00
|
|
|
var ErrUserDoesNotFollow = errors.New("no follow-relation found")
|
|
|
|
|
2024-01-01 16:52:18 +00:00
|
|
|
// GetAuthorizedUser returns the userID / userName of the user the
|
|
|
|
// client is authorized for
|
|
|
|
func (c *Client) GetAuthorizedUser(ctx context.Context) (userID string, userName string, err error) {
|
2022-10-25 16:47:30 +00:00
|
|
|
var payload struct {
|
|
|
|
Data []User `json:"data"`
|
|
|
|
}
|
|
|
|
|
2024-01-01 16:52:18 +00:00
|
|
|
if err := c.Request(ctx, ClientRequestOpts{
|
2023-07-01 14:48:21 +00:00
|
|
|
AuthType: AuthTypeBearerToken,
|
2022-10-25 16:47:30 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2024-01-01 16:52:18 +00:00
|
|
|
// GetDisplayNameForUser returns the display name for a login name set
|
|
|
|
// by the user themselves
|
|
|
|
func (c *Client) GetDisplayNameForUser(ctx context.Context, username string) (string, error) {
|
2023-08-14 13:34:02 +00:00
|
|
|
username = strings.TrimLeft(username, "#@")
|
|
|
|
|
2022-10-25 16:47:30 +00:00
|
|
|
cacheKey := []string{"displayNameForUsername", username}
|
|
|
|
if d := c.apiCache.Get(cacheKey); d != nil {
|
|
|
|
return d.(string), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
var payload struct {
|
|
|
|
Data []User `json:"data"`
|
|
|
|
}
|
|
|
|
|
2024-01-01 16:52:18 +00:00
|
|
|
if err := c.Request(ctx, ClientRequestOpts{
|
2023-07-01 14:48:21 +00:00
|
|
|
AuthType: AuthTypeAppAccessToken,
|
2022-10-25 16:47:30 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2024-01-01 16:52:18 +00:00
|
|
|
// GetFollowDate returns the point-in-time the {from} followed the {to}
|
|
|
|
// or an ErrUserDoesNotFollow in case they do not follow
|
|
|
|
func (c *Client) GetFollowDate(ctx context.Context, from, to string) (time.Time, error) {
|
2022-10-25 16:47:30 +00:00
|
|
|
cacheKey := []string{"followDate", from, to}
|
|
|
|
if d := c.apiCache.Get(cacheKey); d != nil {
|
|
|
|
return d.(time.Time), nil
|
|
|
|
}
|
|
|
|
|
2024-01-01 16:52:18 +00:00
|
|
|
fromID, err := c.GetIDForUsername(ctx, from)
|
2022-10-25 16:47:30 +00:00
|
|
|
if err != nil {
|
|
|
|
return time.Time{}, errors.Wrap(err, "getting id for 'from' user")
|
|
|
|
}
|
2024-01-01 16:52:18 +00:00
|
|
|
toID, err := c.GetIDForUsername(ctx, to)
|
2022-10-25 16:47:30 +00:00
|
|
|
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"`
|
|
|
|
}
|
|
|
|
|
2024-01-01 16:52:18 +00:00
|
|
|
if err := c.Request(ctx, ClientRequestOpts{
|
2023-10-08 10:26:43 +00:00
|
|
|
AuthType: AuthTypeBearerToken,
|
2022-10-25 16:47:30 +00:00
|
|
|
Method: http.MethodGet,
|
|
|
|
OKStatus: http.StatusOK,
|
|
|
|
Out: &payload,
|
2023-09-29 12:54:24 +00:00
|
|
|
URL: fmt.Sprintf("https://api.twitch.tv/helix/channels/followers?broadcaster_id=%s&user_id=%s", toID, fromID),
|
2022-10-25 16:47:30 +00:00
|
|
|
}); err != nil {
|
|
|
|
return time.Time{}, errors.Wrap(err, "request follow info")
|
|
|
|
}
|
|
|
|
|
2022-12-26 21:38:14 +00:00
|
|
|
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))
|
2022-10-25 16:47:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2024-01-01 16:52:18 +00:00
|
|
|
// GetIDForUsername takes a login name and returns the userID for that
|
|
|
|
// username
|
|
|
|
func (c *Client) GetIDForUsername(ctx context.Context, username string) (string, error) {
|
2023-08-14 13:34:02 +00:00
|
|
|
username = strings.TrimLeft(username, "#@")
|
|
|
|
|
2022-10-25 16:47:30 +00:00
|
|
|
cacheKey := []string{"idForUsername", username}
|
|
|
|
if d := c.apiCache.Get(cacheKey); d != nil {
|
|
|
|
return d.(string), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
var payload struct {
|
|
|
|
Data []User `json:"data"`
|
|
|
|
}
|
|
|
|
|
2024-01-01 16:52:18 +00:00
|
|
|
if err := c.Request(ctx, ClientRequestOpts{
|
2023-07-01 14:48:21 +00:00
|
|
|
AuthType: AuthTypeAppAccessToken,
|
2022-10-25 16:47:30 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2023-09-03 10:09:33 +00:00
|
|
|
// 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"`
|
|
|
|
}
|
|
|
|
|
2024-01-01 16:52:18 +00:00
|
|
|
if err := c.Request(ctx, ClientRequestOpts{
|
2023-09-03 10:09:33 +00:00
|
|
|
AuthType: AuthTypeAppAccessToken,
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2024-01-01 16:52:18 +00:00
|
|
|
// GetUserInformation takes an userID or an userName and returns the
|
|
|
|
// User information for them
|
|
|
|
func (c *Client) GetUserInformation(ctx context.Context, user string) (*User, error) {
|
2023-08-14 13:34:02 +00:00
|
|
|
user = strings.TrimLeft(user, "#@")
|
|
|
|
|
2022-10-25 16:47:30 +00:00
|
|
|
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"
|
|
|
|
}
|
|
|
|
|
2024-01-01 16:52:18 +00:00
|
|
|
if err := c.Request(ctx, ClientRequestOpts{
|
2023-07-01 14:48:21 +00:00
|
|
|
AuthType: AuthTypeAppAccessToken,
|
2022-10-25 16:47:30 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2023-09-29 12:54:24 +00:00
|
|
|
// User info will not change that often, cache for a long time
|
2022-10-25 16:47:30 +00:00
|
|
|
c.apiCache.Set(cacheKey, timeDay, payload.Data[0])
|
|
|
|
out = payload.Data[0]
|
|
|
|
|
|
|
|
return &out, nil
|
|
|
|
}
|