From 29b75a2d83d277df9004ba3f4518b6c5e37af8f2 Mon Sep 17 00:00:00 2001 From: dx9er Date: Tue, 25 Jun 2024 20:03:10 +0300 Subject: [PATCH 1/3] feat: Implement Get Moderated Channels endpoint Adds GetModeratedChannels https://dev.twitch.tv/docs/api/reference/#get-moderated-channels --- SUPPORTED_ENDPOINTS.md | 1 + moderation.go | 46 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/SUPPORTED_ENDPOINTS.md b/SUPPORTED_ENDPOINTS.md index 2c70ce5..0c6235c 100644 --- a/SUPPORTED_ENDPOINTS.md +++ b/SUPPORTED_ENDPOINTS.md @@ -77,6 +77,7 @@ - [x] Add Blocked Term - [x] Remove Blocked Term - [x] Delete Chat Messages +- [x] Get Moderated Channels - [x] Get Moderators - [x] Add Channel Moderator - [x] Remove Channel Moderator diff --git a/moderation.go b/moderation.go index e69c1dd..9d18806 100644 --- a/moderation.go +++ b/moderation.go @@ -380,3 +380,49 @@ func (c *Client) RemoveChannelModerator(params *RemoveChannelModeratorParams) (* return moderators, nil } + +// `UserID` must match the user ID in the User-Access token +type GetModeratedChannelsParams struct { + // Required + UserID string `query:"user_id"` + + // Optional + After string `query:"after"` + First int `query:"first"` +} + +type ModeratedChannel struct { + BroadcasterID string `json:"broadcaster_id"` + BroadcasterLogin string `json:"broadcaster_login"` + BroadcasterName string `json:"broadcaster_name"` +} + +type ManyModeratedChannels struct { + ModeratedChannels []ModeratedChannel `json:"data"` + Pagination Pagination `json:"pagination"` +} + +type GetModeratedChannelsResponse struct { + ResponseCommon + Data ManyModeratedChannels +} + +// GetModeratedChannels Gets a list of channels that the specified user has moderator privileges in. +// Required scope: user:read:moderated_channels +func (c *Client) GetModeratedChannels(params *GetModeratedChannelsParams) (*GetModeratedChannelsResponse, error) { + if params.UserID == "" { + return nil, errors.New("user id is required") + } + + resp, err := c.get("/moderation/channels", &ManyModeratedChannels{}, params) + if err != nil { + return nil, err + } + + moderatedChannels := &GetModeratedChannelsResponse{} + resp.HydrateResponseCommon(&moderatedChannels.ResponseCommon) + moderatedChannels.Data.ModeratedChannels = resp.Data.(*ManyModeratedChannels).ModeratedChannels + moderatedChannels.Data.Pagination = resp.Data.(*ManyModeratedChannels).Pagination + + return moderatedChannels, nil +} From 7ff91c83d36365ce4c344c36896d623e32332fac Mon Sep 17 00:00:00 2001 From: dx9er Date: Tue, 25 Jun 2024 20:05:01 +0300 Subject: [PATCH 2/3] feat: Add test for GetModeratedChannels --- moderation_test.go | 168 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) diff --git a/moderation_test.go b/moderation_test.go index 91d2dbe..b1742b0 100644 --- a/moderation_test.go +++ b/moderation_test.go @@ -949,3 +949,171 @@ func TestRemoveChannelModerator(t *testing.T) { t.Error("expected error does match return error") } } + +func TestGetModeratedChannels(t *testing.T) { + t.Parallel() + + testCases := []struct { + statusCode int + options *Options + params *GetModeratedChannelsParams + respBody string + parsed *ManyModeratedChannels + errorMsg string + }{ + { + http.StatusOK, + &Options{ClientID: "my-client-id", UserAccessToken: "moderatedchannels-access-token"}, + &GetModeratedChannelsParams{UserID: "154315414", First: 2}, + `{ + "data": [ + { + "broadcaster_id": "183094685", + "broadcaster_login": "spaceashes", + "broadcaster_name": "spaceashes" + }, + { + "broadcaster_id": "113944563", + "broadcaster_login": "reapex_1", + "broadcaster_name": "Reapex_1" + } + ], + "pagination": { + "cursor": "eyJiIjpudWxsLCJhIjp7IkN1cnNvciI6ImV5SjBjQ0k2SW5WelpYSTZNVFUwTXpFMU5ERTBPbTF2WkdWeVlYUmxjeUlzSW5Seklqb2lZMmhoYm01bGJEb3hNVE01TkRRMU5qTWlMQ0pwY0NJNkluVnpaWEk2TVRVME16RTFOREUwT20xdlpHVnlZWFJsY3lJc0ltbHpJam9pTVRjeE5EVXhNelF4T0RFNE9UTXlPREV4TnlKOSJ9fQ" + } + }`, + &ManyModeratedChannels{ + ModeratedChannels: []ModeratedChannel{ + { + BroadcasterID: "183094685", + BroadcasterLogin: "spaceashes", + BroadcasterName: "spaceashes", + }, + { + BroadcasterID: "113944563", + BroadcasterLogin: "reapex_1", + BroadcasterName: "Reapex_1", + }, + }, + Pagination: Pagination{ + Cursor: "eyJiIjpudWxsLCJhIjp7IkN1cnNvciI6ImV5SjBjQ0k2SW5WelpYSTZNVFUwTXpFMU5ERTBPbTF2WkdWeVlYUmxjeUlzSW5Seklqb2lZMmhoYm01bGJEb3hNVE01TkRRMU5qTWlMQ0pwY0NJNkluVnpaWEk2TVRVME16RTFOREUwT20xdlpHVnlZWFJsY3lJc0ltbHpJam9pTVRjeE5EVXhNelF4T0RFNE9UTXlPREV4TnlKOSJ9fQ", + }, + }, + "", + }, + { + http.StatusOK, + &Options{ClientID: "my-client-id", UserAccessToken: "moderatedchannels-access-token"}, + &GetModeratedChannelsParams{UserID: "154315414", After: "eyJiIjpudWxsLCJhIjp7IkN1cnNvciI6ImV5SjBjQ0k2SW5WelpYSTZNVFUwTXpFMU5ERTBPbTF2WkdWeVlYUmxjeUlzSW5Seklqb2lZMmhoYm01bGJEb3hNVE01TkRRMU5qTWlMQ0pwY0NJNkluVnpaWEk2TVRVME16RTFOREUwT20xdlpHVnlZWFJsY3lJc0ltbHpJam9pTVRjeE5EVXhNelF4T0RFNE9UTXlPREV4TnlKOSJ9fQ"}, + `{ + "data": [ + { + "broadcaster_id": "106590483", + "broadcaster_login": "vaiastol", + "broadcaster_name": "vaiastol" + } + ], + "pagination": {} + }`, + &ManyModeratedChannels{ + ModeratedChannels: []ModeratedChannel{ + { + BroadcasterID: "106590483", + BroadcasterLogin: "vaiastol", + BroadcasterName: "vaiastol", + }, + }, + }, + "", + }, + { + http.StatusUnauthorized, + &Options{ClientID: "my-client-id", UserAccessToken: "invalid-access-token"}, + &GetModeratedChannelsParams{UserID: "154315414"}, + `{"error":"Unauthorized","status":401,"message":"Invalid OAuth token"}`, + &ManyModeratedChannels{}, + "", + }, + { + http.StatusUnauthorized, + &Options{ClientID: "my-client-id", UserAccessToken: "moderatedchannels-access-token"}, + &GetModeratedChannelsParams{UserID: "123456789"}, + `{"error":"Unauthorized","status":401,"message":"The ID in user_id must match the user ID found in the request's OAuth token."}`, + &ManyModeratedChannels{}, + "", + }, + { + http.StatusUnauthorized, + &Options{ClientID: "my-client-id", UserAccessToken: "missingscope-access-token"}, + &GetModeratedChannelsParams{UserID: "154315414"}, + `{"error":"Unauthorized","status":401,"message":"Missing scope: user:read:moderated_channels"}`, + &ManyModeratedChannels{}, + "", + }, + { + http.StatusBadRequest, + &Options{ClientID: "my-client-id", UserAccessToken: "moderatedchannels-access-token"}, + &GetModeratedChannelsParams{}, + `{"error":"Bad Request","status":400,"message":"Missing required parameter \"user_id\""}`, + &ManyModeratedChannels{}, + "user id is required", + }, + } + + for _, testCase := range testCases { + c := newMockClient(testCase.options, newMockHandler(testCase.statusCode, testCase.respBody, nil)) + + resp, err := c.GetModeratedChannels(testCase.params) + + if err != nil { + if err.Error() != testCase.errorMsg { + t.Errorf("expected error message to be %s, got %s", testCase.errorMsg, err.Error()) + } + continue + } + + if resp.StatusCode != testCase.statusCode { + t.Errorf("expected status code to be %d, got %d", testCase.statusCode, resp.StatusCode) + } + + for i, channel := range resp.Data.ModeratedChannels { + if channel.BroadcasterID != testCase.parsed.ModeratedChannels[i].BroadcasterID { + t.Errorf("Expected ModeratedChannel field BroadcasterID = %s, was %s", testCase.parsed.ModeratedChannels[i].BroadcasterID, channel.BroadcasterID) + } + + if channel.BroadcasterLogin != testCase.parsed.ModeratedChannels[i].BroadcasterLogin { + t.Errorf("Expected ModeratedChannel field BroadcasterLogin = %s, was %s", testCase.parsed.ModeratedChannels[i].BroadcasterLogin, channel.BroadcasterLogin) + } + + if channel.BroadcasterName != testCase.parsed.ModeratedChannels[i].BroadcasterName { + t.Errorf("Expected ModeratedChannel field BroadcasterName = %s, was %s", testCase.parsed.ModeratedChannels[i].BroadcasterName, channel.BroadcasterName) + } + } + + if resp.Data.Pagination.Cursor != testCase.parsed.Pagination.Cursor { + t.Errorf("Expected Pagination field Cursor = %s, was %s", testCase.parsed.Pagination.Cursor, resp.Data.Pagination.Cursor) + } + + } + + // Test with HTTP Failure + options := &Options{ + ClientID: "my-client-id", + HTTPClient: &badMockHTTPClient{ + newMockHandler(0, "", nil), + }, + } + c := &Client{ + opts: options, + ctx: context.Background(), + } + + _, err := c.GetModeratedChannels(&GetModeratedChannelsParams{UserID: "154315414"}) + if err == nil { + t.Error("expected error but got nil") + } + + if err.Error() != "Failed to execute API request: Oops, that's bad :(" { + t.Error("expected error does match return error") + } +} From 7916a0c6deda54db41f9b46b20daeb4f7d1f1779 Mon Sep 17 00:00:00 2001 From: dx9er Date: Tue, 25 Jun 2024 20:05:32 +0300 Subject: [PATCH 3/3] docs: Document GetModeratedChannels --- docs/moderation_docs.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/docs/moderation_docs.md b/docs/moderation_docs.md index 86fce21..d08a13c 100644 --- a/docs/moderation_docs.md +++ b/docs/moderation_docs.md @@ -288,3 +288,29 @@ if err != nil { fmt.Printf("%+v\n", resp) ``` + +## Get Moderated Channels + +To use this function you need a user access token with the `user:read:moderated_channels` scope. +`UserID` is required and must match the user ID of the user access token. + +This is an example of how to get channels the user has moderator privileges in. + +```go +client, err := helix.NewClient(&helix.Options{ + ClientID: "your-client-id", + UserAccessToken: "your-user-access-token", +}) +if err != nil { + // handle error +} + +resp, err := client.GetModeratedChannels(&helix.GetModeratedChannelsParams{ + UserID: "154315414", +}) +if err != nil { + // handle error +} + +fmt.Printf("%+v\n", resp) +```