From 695e998b1fbf6ccdd99e891148c51a8d16a3411f Mon Sep 17 00:00:00 2001 From: Evan Fiordeliso Date: Sun, 3 Mar 2024 18:34:26 -0500 Subject: [PATCH] Add Users endpoints to API --- api/api.go | 3 + api/users/block_user.go | 49 ++++++++++ api/users/get_user_active_extensions.go | 52 ++++++++++ api/users/get_user_block_list.go | 67 +++++++++++++ api/users/get_user_extensions.go | 57 +++++++++++ api/users/get_users.go | 60 ++++++++++++ api/users/models.go | 122 ++++++++++++++++++++++++ api/users/unblock_user.go | 28 ++++++ api/users/update_user.go | 50 ++++++++++ api/users/update_user_extensions.go | 66 +++++++++++++ api/users/users.go | 18 ++++ 11 files changed, 572 insertions(+) create mode 100644 api/users/block_user.go create mode 100644 api/users/get_user_active_extensions.go create mode 100644 api/users/get_user_block_list.go create mode 100644 api/users/get_user_extensions.go create mode 100644 api/users/get_users.go create mode 100644 api/users/models.go create mode 100644 api/users/unblock_user.go create mode 100644 api/users/update_user.go create mode 100644 api/users/update_user_extensions.go create mode 100644 api/users/users.go diff --git a/api/api.go b/api/api.go index a6eea13..747055c 100644 --- a/api/api.go +++ b/api/api.go @@ -29,6 +29,7 @@ import ( "go.fifitido.net/twitch/api/streams" "go.fifitido.net/twitch/api/subscriptions" "go.fifitido.net/twitch/api/teams" + "go.fifitido.net/twitch/api/users" ) const HelixBaseUrl = "https://api.twitch.tv/helix" @@ -62,6 +63,7 @@ type API struct { Streams *streams.Streams Subscriptions *subscriptions.Subscriptions Teams *teams.Teams + Users *users.Users } func New(client *http.Client, baseUrl *url.URL) *API { @@ -94,6 +96,7 @@ func New(client *http.Client, baseUrl *url.URL) *API { Streams: streams.New(client, baseUrl), Subscriptions: subscriptions.New(client, baseUrl), Teams: teams.New(client, baseUrl), + Users: users.New(client, baseUrl), } } diff --git a/api/users/block_user.go b/api/users/block_user.go new file mode 100644 index 0000000..0795ae6 --- /dev/null +++ b/api/users/block_user.go @@ -0,0 +1,49 @@ +package users + +import ( + "context" + "net/http" + "net/url" + + "github.com/google/go-querystring/query" +) + +type BlockUserParams struct { + // The ID of the user to block. The API ignores the request if the broadcaster has already blocked the user. + TargetUserID string `url:"target_user_id"` + + // The location where the harassment took place that is causing the brodcaster to block the user. Possible values are: + // + // chat, whisper + SourceContext *string `url:"source_context,omitempty"` + + // The reason that the broadcaster is blocking the user. Possible values are: + // + // harassment, spam, other + Reason *string `url:"reason,omitempty"` +} + +// Blocks the specified user from interacting with or having contact with the broadcaster. +// The user ID in the OAuth token identifies the broadcaster who is blocking the user. +// +// To learn more about blocking users, +// see Block Other Users on Twitch: https://help.twitch.tv/s/article/how-to-manage-harassment-in-chat?language=en_US#BlockWhispersandMessagesfromStrangers +// +// Requires a user access token that includes the user:manage:blocked_users scope. +func (u *Users) BlockUser(ctx context.Context, params *BlockUserParams) error { + v, _ := query.Values(params) + endpoint := u.baseUrl.ResolveReference(&url.URL{Path: "users/blocks", RawQuery: v.Encode()}) + + req, err := http.NewRequestWithContext(ctx, http.MethodPut, endpoint.String(), nil) + if err != nil { + return err + } + + res, err := u.client.Do(req) + if err != nil { + return err + } + defer res.Body.Close() + + return nil +} diff --git a/api/users/get_user_active_extensions.go b/api/users/get_user_active_extensions.go new file mode 100644 index 0000000..a16ad9d --- /dev/null +++ b/api/users/get_user_active_extensions.go @@ -0,0 +1,52 @@ +package users + +import ( + "context" + "encoding/json" + "net/http" + "net/url" + + "github.com/google/go-querystring/query" +) + +type GetUserActiveExtensionsParams struct { + // The ID of the broadcaster whose active extensions you want to get. + // + // This parameter is required if you specify an app access token and is optional if you specify a user access token. + // If you specify a user access token and don’t specify this parameter, the API uses the user ID from the access token. + UserID string `url:"user_id,omitempty"` +} + +type GetUserActiveExtensionsResponse struct { + // The active extensions that the broadcaster has installed. + Data []ActiveExtension `json:"data"` +} + +// Gets the active extensions that the broadcaster has installed for each configuration. +// +// NOTE: To include extensions that you have under development, +// you must specify a user access token that includes the user:read:broadcast or user:edit:broadcast scope. +// +// Requires an app access token or user access token. +func (u *Users) GetUserActiveExtensions(ctx context.Context, params *GetUserActiveExtensionsParams) (*GetUserActiveExtensionsResponse, error) { + v, _ := query.Values(params) + endpoint := u.baseUrl.ResolveReference(&url.URL{Path: "users/extensions", RawQuery: v.Encode()}) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil) + if err != nil { + return nil, err + } + + res, err := u.client.Do(req) + if err != nil { + return nil, err + } + defer res.Body.Close() + + var data GetUserActiveExtensionsResponse + if err := json.NewDecoder(res.Body).Decode(&data); err != nil { + return nil, err + } + + return &data, nil +} diff --git a/api/users/get_user_block_list.go b/api/users/get_user_block_list.go new file mode 100644 index 0000000..863e0b2 --- /dev/null +++ b/api/users/get_user_block_list.go @@ -0,0 +1,67 @@ +package users + +import ( + "context" + "encoding/json" + "net/http" + "net/url" + + "github.com/google/go-querystring/query" + "go.fifitido.net/twitch/api/types" +) + +type GetUserBlockListParams struct { + // The ID of the broadcaster whose list of blocked users you want to get. + BroadcasterID string `url:"broadcaster_id"` + + // The maximum number of items to return per page in the response. + // The minimum page size is 1 item per page and the maximum is 100. + // The default is 20. + First *int `url:"first,omitempty"` + + // The cursor used to get the next page of results. + // The Pagination object in the response contains the cursor’s value. + // Read More: https://dev.twitch.tv/docs/api/guide#pagination + After *types.Cursor `url:"after,omitempty"` +} + +type GetUserBlockListResponse struct { + // The list of blocked users. The list is in descending order by when the user was blocked. + Data []struct { + // An ID that identifies the blocked user. + UserID string `json:"user_id"` + + // The blocked user’s login name. + UserLogin string `json:"user_login"` + + // The blocked user’s display name. + DisplayName string `json:"display_name"` + } `json:"data"` +} + +// Gets the list of users that the broadcaster has blocked. +// Read More: https://help.twitch.tv/s/article/how-to-manage-harassment-in-chat?language=en_US#BlockWhispersandMessagesfromStrangers +// +// Requires a user access token that includes the user:read:blocked_users scope. +func (u *Users) GetUserBlockList(ctx context.Context, params *GetUserBlockListParams) (*GetUserBlockListResponse, error) { + v, _ := query.Values(params) + endpoint := u.baseUrl.ResolveReference(&url.URL{Path: "users/blocks", RawQuery: v.Encode()}) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil) + if err != nil { + return nil, err + } + + res, err := u.client.Do(req) + if err != nil { + return nil, err + } + defer res.Body.Close() + + var data GetUserBlockListResponse + if err := json.NewDecoder(res.Body).Decode(&data); err != nil { + return nil, err + } + + return &data, nil +} diff --git a/api/users/get_user_extensions.go b/api/users/get_user_extensions.go new file mode 100644 index 0000000..1db7f38 --- /dev/null +++ b/api/users/get_user_extensions.go @@ -0,0 +1,57 @@ +package users + +import ( + "context" + "encoding/json" + "net/http" + "net/url" +) + +type GetUserExtensionsResponse struct { + // The list of extensions that the user has installed. + Data []struct { + // An ID that identifies the extension. + ID string `json:"id"` + + // The extension's version. + Version string `json:"version"` + + // The extension's name. + Name string `json:"name"` + + // A Boolean value that determines whether the extension is configured and can be activated. Is true if the extension is configured and can be activated. + CanActivate bool `json:"can_activate"` + + // The extension types that you can activate for this extension. Possible values are: + // + // component, mobile, overlay, panel + Type []string `json:"type"` + } `json:"data"` +} + +// Gets a list of all extensions (both active and inactive) that the broadcaster has installed. +// The user ID in the access token identifies the broadcaster. +// +// Requires a user access token that includes the user:read:broadcast or user:edit:broadcast scope. +// To include inactive extensions, you must include the user:edit:broadcast scope. +func (u *Users) GetUserExtensions(ctx context.Context) (*GetUserExtensionsResponse, error) { + endpoint := u.baseUrl.ResolveReference(&url.URL{Path: "users/extensions/list"}) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil) + if err != nil { + return nil, err + } + + res, err := u.client.Do(req) + if err != nil { + return nil, err + } + defer res.Body.Close() + + var data GetUserExtensionsResponse + if err := json.NewDecoder(res.Body).Decode(&data); err != nil { + return nil, err + } + + return &data, nil +} diff --git a/api/users/get_users.go b/api/users/get_users.go new file mode 100644 index 0000000..b6c6557 --- /dev/null +++ b/api/users/get_users.go @@ -0,0 +1,60 @@ +package users + +import ( + "context" + "encoding/json" + "net/http" + "net/url" + + "github.com/google/go-querystring/query" +) + +type GetUsersParams struct { + // The ID of the user to get. + // To specify more than one user, include the id parameter for each user to get. For example, id=1234&id=5678. + // The maximum number of IDs you may specify is 100. + IDs []string `url:"id,omitempty"` + + // The login name of the user to get. + // To specify more than one user, include the login parameter for each user to get. For example, login=foo&login=bar. + // The maximum number of login names you may specify is 100. + Logins []string `url:"login,omitempty"` +} + +type GetUsersResponse struct { + // The list of users. + Data []User `json:"data"` +} + +// Gets information about one or more users. +// +// You may look up users using their user ID, login name, or both but the sum total of the number of users you may look up is 100. +// For example, you may specify 50 IDs and 50 names or 100 IDs or names, but you cannot specify 100 IDs and 100 names. +// +// If you don’t specify IDs or login names, the request returns information about the user in the access token if you specify a user access token. +// +// To include the user’s verified email address in the response, you must use a user access token that includes the user:read:email scope. +// +// Requires an app access token or user access token. +func (u *Users) GetUsers(ctx context.Context, params *GetUsersParams) (*GetUsersResponse, error) { + v, _ := query.Values(params) + endpoint := u.baseUrl.ResolveReference(&url.URL{Path: "users", RawQuery: v.Encode()}) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil) + if err != nil { + return nil, err + } + + res, err := u.client.Do(req) + if err != nil { + return nil, err + } + defer res.Body.Close() + + var data GetUsersResponse + if err := json.NewDecoder(res.Body).Decode(&data); err != nil { + return nil, err + } + + return &data, nil +} diff --git a/api/users/models.go b/api/users/models.go new file mode 100644 index 0000000..6ea45f1 --- /dev/null +++ b/api/users/models.go @@ -0,0 +1,122 @@ +package users + +import "time" + +type User struct { + // An ID that identifies the user. + ID string `json:"id"` + + // The user's login name. + Login string `json:"login"` + + // The user's display name. + DisplayName string `json:"display_name"` + + // The type of user. Possible values are: + // + // admin — Twitch administrator + // + // global_mod + // + // staff — Twitch staff + // + // "" — Normal user + Type Type `json:"type"` + + // The type of broadcaster. Possible values are: + // + // affiliate — An affiliate broadcaster + // + // partner — A partner broadcaster + // + // "" — A normal broadcaster + BroadcasterType BroadcasterType `json:"broadcaster_type"` + + // The user's description of their channel. + Description string `json:"description"` + + // A URL to the user's profile image. + ProfileImageUrl string `json:"profile_image_url"` + + // A URL to the user's offline image. + OfflineImageUrl string `json:"offline_image_url"` + + // The user's verified email address. The object includes this field only if the user access token includes the user:read:email scope. + // + // If the request contains more than one user, only the user associated with the access token that provided consent will include an email address — + // the email address for all other users will be empty. + Email *string `json:"email"` + + // The UTC date and time that the user's account was created. The timestamp is in RFC3339 format. + CreatedAt time.Time `json:"created_at"` +} + +type Type string + +const ( + // Twitch administrator + TypeAdmin Type = "admin" + + // Twitch global moderator + TypeGlobalMod Type = "global_mod" + + // Twitch staff + TypeStaff Type = "staff" + + // Normal user + TypeNormal Type = "" +) + +type BroadcasterType string + +const ( + // An affiliate broadcaster + BroadcasterTypeAffiliate BroadcasterType = "affiliate" + + // A partner broadcaster + BroadcasterTypePartner BroadcasterType = "partner" + + // A normal broadcaster + BroadcasterTypeNormal BroadcasterType = "" +) + +type ActiveExtension struct { + // A dictionary that contains the data for a panel extension. + // The dictionary’s key is a sequential number beginning with 1. + // The following fields contain the panel’s data for each key. + Panel map[string]ExtensionData `json:"panel"` + + // A dictionary that contains the data for a video-overlay extension. + // The dictionary’s key is a sequential number beginning with 1. + // The following fields contain the overlay’s data for each key. + Overlay map[string]ExtensionData `json:"overlay"` + + // A dictionary that contains the data for a video-component extension. + // The dictionary’s key is a sequential number beginning with 1. + // The following fields contain the component’s data for each key. + Component map[string]ComponentExtensionData `json:"component"` +} + +type ExtensionData struct { + // A Boolean value that determines the extension’s activation state. If false, the user has not configured this panel extension. + Active bool `json:"active"` + + // An ID that identifies the extension. + ID string `json:"id"` + + // The extension’s version. + Version string `json:"version"` + + // The extension’s name. + Name string `json:"name"` +} + +type ComponentExtensionData struct { + ExtensionData `json:",inline"` + + // The x-coordinate where the extension is placed. + X int `json:"x"` + + // The y-coordinate where the extension is placed. + Y int `json:"y"` +} diff --git a/api/users/unblock_user.go b/api/users/unblock_user.go new file mode 100644 index 0000000..0067fda --- /dev/null +++ b/api/users/unblock_user.go @@ -0,0 +1,28 @@ +package users + +import ( + "context" + "net/http" + "net/url" +) + +// Removes the user from the broadcaster’s list of blocked users. +// The user ID in the OAuth token identifies the broadcaster who’s removing the block. +// +// Requires a user access token that includes the user:manage:blocked_users scope. +func (u *Users) UnblockUser(ctx context.Context, targetUserID string) error { + endpoint := u.baseUrl.ResolveReference(&url.URL{Path: "users/blocks", RawQuery: url.Values{"target_user_id": {targetUserID}}.Encode()}) + + req, err := http.NewRequestWithContext(ctx, http.MethodDelete, endpoint.String(), nil) + if err != nil { + return err + } + + res, err := u.client.Do(req) + if err != nil { + return err + } + defer res.Body.Close() + + return nil +} diff --git a/api/users/update_user.go b/api/users/update_user.go new file mode 100644 index 0000000..af8c9a9 --- /dev/null +++ b/api/users/update_user.go @@ -0,0 +1,50 @@ +package users + +import ( + "context" + "encoding/json" + "net/http" + "net/url" + + "github.com/google/go-querystring/query" +) + +type UpdateUserParams struct { + // The string to update the channel’s description to. The description is limited to a maximum of 300 characters. + // + // To remove the description, specify this parameter but don’t set it’s value (for example, ?description=). + Description *string `url:"description,omitempty"` +} + +type UpdateUserResponse struct { + // A list contains the single user that you updated. + Data []User `json:"data"` +} + +// Updates the specified user’s information. The user ID in the OAuth token identifies the user whose information you want to update. +// +// To include the user’s verified email address in the response, the user access token must also include the user:read:email scope. +// +// Requires a user access token that includes the user:edit scope. +func (u *Users) UpdateUser(ctx context.Context, params *UpdateUserParams) (*UpdateUserResponse, error) { + v, _ := query.Values(params) + endpoint := u.baseUrl.ResolveReference(&url.URL{Path: "users", RawQuery: v.Encode()}) + + req, err := http.NewRequestWithContext(ctx, http.MethodPatch, endpoint.String(), nil) + if err != nil { + return nil, err + } + + res, err := u.client.Do(req) + if err != nil { + return nil, err + } + defer res.Body.Close() + + var data UpdateUserResponse + if err := json.NewDecoder(res.Body).Decode(&data); err != nil { + return nil, err + } + + return &data, nil +} diff --git a/api/users/update_user_extensions.go b/api/users/update_user_extensions.go new file mode 100644 index 0000000..fd5325c --- /dev/null +++ b/api/users/update_user_extensions.go @@ -0,0 +1,66 @@ +package users + +import ( + "context" + "encoding/json" + "io" + "net/http" + "net/url" +) + +type UpdateUserExtensionsRequest struct { + // The extensions to update. The data field is a dictionary of extension types. + // The dictionary’s possible keys are: panel, overlay, or component. + // The key’s value is a dictionary of extensions. + // + // For the extension’s dictionary, the key is a sequential number beginning with 1. + // For panel and overlay extensions, the key’s value is an object that contains the following fields: + // active (true/false), id (the extension’s ID), and version (the extension’s version). + // + // For component extensions, the key’s value includes the above fields plus the x and y fields, + // which identify the coordinate where the extension is placed. + Data map[string]map[string]interface{} `json:"data"` +} + +type UpdateUserExtensionsResponse struct { + // The extensions that the broadcaster updated. + Data []ActiveExtension `json:"data"` +} + +// Updates an installed extension’s information. You can update the extension’s activation state, ID, and version number. +// The user ID in the access token identifies the broadcaster whose extensions you’re updating. +// +// NOTE: If you try to activate an extension under multiple extension types, the last write wins (and there is no guarantee of write order). +// +// Requires a user access token that includes the user:edit:broadcast scope. +func (u *Users) UpdateUserExtensions(ctx context.Context, body *UpdateUserExtensionsRequest) (*UpdateUserExtensionsResponse, error) { + endpoint := u.baseUrl.ResolveReference(&url.URL{Path: "users/extensions"}) + + r, w := io.Pipe() + + go func() { + if err := json.NewEncoder(w).Encode(body); err != nil { + w.CloseWithError(err) + } else { + w.Close() + } + }() + + req, err := http.NewRequestWithContext(ctx, http.MethodPatch, endpoint.String(), r) + if err != nil { + return nil, err + } + + res, err := u.client.Do(req) + if err != nil { + return nil, err + } + defer res.Body.Close() + + var data UpdateUserExtensionsResponse + if err := json.NewDecoder(res.Body).Decode(&data); err != nil { + return nil, err + } + + return &data, nil +} diff --git a/api/users/users.go b/api/users/users.go new file mode 100644 index 0000000..17bf716 --- /dev/null +++ b/api/users/users.go @@ -0,0 +1,18 @@ +package users + +import ( + "net/http" + "net/url" +) + +type Users struct { + client *http.Client + baseUrl *url.URL +} + +func New(client *http.Client, baseUrl *url.URL) *Users { + return &Users{ + client: client, + baseUrl: baseUrl, + } +}