Repository: nicklaw5/helix
Pull request #178 (2023-02-05 13:01:05)

Add support for the Update Chat Settings endpoint
API reference: https://dev.twitch.tv/docs/api/reference/#update-chat-settings

This is the first time pointers are used in input parameters but I believe it is required here to be able to use the API to its full potential (i.e. to update the emote only mode of a chat without knowing all of its other settings).
Normally, libraries that do this include some helper functions to return a pointer value from a value so you can write
```
client.UpdateChatSettings(&helix.UpdateChatSettingsParams{
    BroadcasterID: "...",
    ModeratorID: "...",
    EmoteOnly: helix.BoolPtr(true),
})
```

This is similar to how it's implemented in the aws-sdk-go-v2 library: https://github.com/aws/aws-sdk-go-v2/blob/main/aws/to_ptr.go#L10-L13 except they use "Bool" instead of "BoolPtr"
See usage of it here: https://github.com/aws/aws-sdk-go-v2/blob/9b93441d7f73f222a79fdaa55d99ff52f15d62e1/service/ec2/internal/customizations/unit_test.go#L61-L64

I have tested each field for this. I would especially appreciate feedback on the comments in the Params field


Patch:
diff --git a/SUPPORTED_ENDPOINTS.md b/SUPPORTED_ENDPOINTS.md
index 8e0c680..3c337a5 100644
--- a/SUPPORTED_ENDPOINTS.md
+++ b/SUPPORTED_ENDPOINTS.md
@@ -34,7 +34,7 @@
 - [x] Get Channel Chat Badges
 - [x] Get Global Chat Badges
 - [x] Get Chat Settings
-- [ ] Update Chat Settings
+- [x] Update Chat Settings
 - [ ] Send Chat Announcement
 - [ ] Get User Chat Color
 - [ ] Update User Chat Color
diff --git a/chat.go b/chat.go
index 47f3e69..558029b 100644
--- a/chat.go
+++ b/chat.go
@@ -268,3 +268,80 @@ func (c *Client) GetChatSettings(params *GetChatSettingsParams) (*GetChatSetting
 
 	return settings, nil
 }
+
+type UpdateChatSettingsParams struct {
+	// Required, the ID of the broadcaster whose chat settings you want to update
+	BroadcasterID string `query:"broadcaster_id"`
+
+	// Required, the ID of a user that has moderator privileges in the BroadcasterID's channel.
+	// The ID must match the specified User Access Token
+	ModeratorID string `query:"moderator_id"`
+
+	// Optional, set to true if only emotes are allowed
+	// If unset (i.e. nil), no change to this setting will be made
+	EmoteMode *bool `json:"emote_mode,omitempty"`
+
+	// Optional, set to true if only followers may chat
+	// If unset (i.e. nil), no change to this setting will be made
+	FollowerMode *bool `json:"follower_mode,omitempty"`
+
+	// Optional, time in minutes a user must have been following to chat.
+	// If unset (i.e. nil), no change to this setting will be made
+	// If set, FollowerMode must be set to true.
+	// Possible values are 0 (no time restriction) through 129600 (3 months)
+	FollowerModeDuration *int `json:"follower_mode_duration,omitempty"`
+
+	// Optional, set to true if there's a delay before chat messages appear for non-moderators
+	// If unset (i.e. nil), no change to this setting will be made
+	NonModeratorChatDelay *bool `json:"non_moderator_chat_delay,omitempty"`
+
+	// Optional, time in seconds before messages appear for non-moderators
+	// If unset (i.e. nil), no change to this setting will be made
+	// If set, FollowerMode must be set to true.
+	// Possible values are 2, 4, or 6
+	NonModeratorChatDelayDuration *int `json:"non_moderator_chat_delay_duration,omitempty"`
+
+	// Optional, set to true if chatters must wait some extra time between sending more messages
+	// If unset (i.e. nil), no change to this setting will be made
+	SlowMode *bool `json:"slow_mode,omitempty"`
+
+	// Optional, time in seconds chatters must wait between sending messages
+	// If unset (i.e. nil), no change to this setting will be made
+	// If set, SlowMode must be set to true.
+	// Possible values are 3 through 120 seconds
+	SlowModeWaitTime *int `json:"slow_mode_wait_time,omitempty"`
+
+	// Optional, set to true if only subscribers may chat
+	// If unset (i.e. nil), no change to this setting will be made
+	SubscriberMode *bool `json:"subscriber_mode,omitempty"`
+
+	// Optional, set to true if users may only post "unique messages" in chat
+	// If unset (i.e. nil), no change to this setting will be made
+	UniqueChatMode *bool `json:"unique_chat_mode,omitempty"`
+}
+
+type UpdateChatSettingsResponse struct {
+	ResponseCommon
+	Data ManyChatSettings
+}
+
+// UpdateChatSettings updates the broadcaster's chat settings.
+// Required scope: moderator:manage:chat_settings
+func (c *Client) UpdateChatSettings(params *UpdateChatSettingsParams) (*UpdateChatSettingsResponse, error) {
+	if params.BroadcasterID == "" {
+		return nil, errors.New("error: broadcaster id must be specified")
+	}
+	if params.ModeratorID == "" {
+		return nil, errors.New("error: moderator id must be specified")
+	}
+	resp, err := c.patchAsJSON("/chat/settings", &ManyChatSettings{}, params)
+	if err != nil {
+		return nil, err
+	}
+
+	settings := &UpdateChatSettingsResponse{}
+	resp.HydrateResponseCommon(&settings.ResponseCommon)
+	settings.Data.Settings = resp.Data.(*ManyChatSettings).Settings
+
+	return settings, nil
+}


Test patch:
diff --git a/chat_test.go b/chat_test.go
index c8b5a76..9ddcb39 100644
--- a/chat_test.go
+++ b/chat_test.go
@@ -701,3 +701,128 @@ func TestGetChatSettings(t *testing.T) {
 		t.Errorf("expected error does match return error, got '%s'", err.Error())
 	}
 }
+
+func TestUpdateChatSettings(t *testing.T) {
+	t.Parallel()
+
+	testCases := []struct {
+		statusCode    int
+		options       *Options
+		params        *UpdateChatSettingsParams
+		respBody      string
+		validationErr string
+		expected      *ChatSettings
+	}{
+		{ // Early-out error thrown by us
+			http.StatusBadRequest,
+			&Options{ClientID: "my-client-id", UserAccessToken: "moderator-access-token"},
+			&UpdateChatSettingsParams{BroadcasterID: "", ModeratorID: "82008718"},
+			``,
+			"error: broadcaster id must be specified",
+			nil,
+		},
+		{ // Early-out error thrown by us
+			http.StatusBadRequest,
+			&Options{ClientID: "my-client-id", UserAccessToken: "moderator-access-token"},
+			&UpdateChatSettingsParams{BroadcasterID: "11148817", ModeratorID: ""},
+			``,
+			"error: moderator id must be specified",
+			nil,
+		},
+		{ // Request that updates nothing still return all data
+			http.StatusOK,
+			&Options{ClientID: "my-client-id", UserAccessToken: "moderator-access-token"},
+			&UpdateChatSettingsParams{BroadcasterID: "22484632", ModeratorID: "11148817"},
+			`{"data":[{"broadcaster_id":"22484632","emote_mode":false,"follower_mode":true,"follower_mode_duration":10080,"moderator_id":"11148817","non_moderator_chat_delay":true,"non_moderator_chat_delay_duration":1,"slow_mode":false,"slow_mode_wait_time":null,"subscriber_mode":false,"unique_chat_mode":false}]}`,
+			``,
+			&ChatSettings{
+				BroadcasterID: "22484632",
+				ModeratorID:   "11148817",
+
+				EmoteMode: false,
+
+				FollowerMode:         true,
+				FollowerModeDuration: 10080,
+
+				SlowMode:         false,
+				SlowModeWaitTime: 0,
+
+				SubscriberMode: false,
+
+				UniqueChatMode: false,
+
+				NonModeratorChatDelay:         true,
+				NonModeratorChatDelayDuration: 1,
+			},
+		},
+	}
+
+	for _, testCase := range testCases {
+		c := newMockClient(testCase.options, newMockHandler(testCase.statusCode, testCase.respBody, nil))
+
+		resp, err := c.UpdateChatSettings(testCase.params)
+		if err != nil {
+			if err.Error() == testCase.validationErr {
+				continue
+			}
+			t.Errorf("Unmatched error, expected '%v', got '%v'", testCase.validationErr, err)
+			continue
+		}
+
+		if resp.StatusCode != testCase.statusCode {
+			t.Errorf("expected status code to be %d, got %d", testCase.statusCode, resp.StatusCode)
+		}
+
+		if resp.StatusCode == http.StatusBadRequest {
+			if resp.Error != "Bad Request" {
+				t.Errorf("expected error to be %s, got %s", "Bad Request", resp.Error)
+			}
+
+			if resp.ErrorStatus != http.StatusBadRequest {
+				t.Errorf("expected error status to be %d, got %d", http.StatusBadRequest, resp.ErrorStatus)
+			}
+
+			continue
+		}
+
+		if len(resp.Data.Settings) != 1 {
+			t.Errorf("expected %d channel settings, got %d", 1, len(resp.Data.Settings))
+		}
+
+		if resp.Data.Settings[0].BroadcasterID != "22484632" {
+			t.Errorf("expected broadcaster_id to be %s, got %s", "22484632", resp.Data.Settings[0].BroadcasterID)
+		}
+
+		if testCase.expected != nil {
+			expected := testCase.expected
+			actual := resp.Data.Settings[0]
+
+			if *expected != actual {
+				t.Errorf("expected %v channel settings, got %v", *expected, actual)
+			}
+		}
+	}
+
+	// Test with HTTP Failure
+	options := &Options{
+		ClientID: "my-client-id",
+		HTTPClient: &badMockHTTPClient{
+			newMockHandler(0, "", nil),
+		},
+	}
+	c := &Client{
+		opts: options,
+		ctx:  context.Background(),
+	}
+
+	_, err := c.UpdateChatSettings(&UpdateChatSettingsParams{BroadcasterID: "123", ModeratorID: "123"})
+	if err == nil {
+		t.Error("expected error but got nil")
+	}
+
+	const expectedHTTPError = "Failed to execute API request: Oops, that's bad :("
+
+	if err.Error() != expectedHTTPError {
+		t.Errorf("expected error does match return error, got '%s'", err.Error())
+	}
+}