Repository: nicklaw5/helix
Pull request #164 (2023-01-03 12:50:49)

Implement Delete Chat Message(s)
* Add function to delete a single chat message
* Add function to delete all chat messages
* Extend documentation - I took the liberty of adding examples for other endpoints as I saw that they were missing

I decided to create separate functions for deleting a single message and all chat messages. My idea was to prevent unintended deletion if the user forgets to specify the message ID. I'll change it if this does not comply with the your conventions.


Patch:
diff --git a/README.md b/README.md
index a7d7d97..a6de585 100644
--- a/README.md
+++ b/README.md
@@ -37,4 +37,4 @@ It's also helpful if you can include usage examples in the [docs](docs) director
 
 ## License
 
-This package is distributed under the terms of the [MIT](License) license.
+This package is distributed under the terms of the [MIT](LICENSE) license.
diff --git a/SUPPORTED_ENDPOINTS.md b/SUPPORTED_ENDPOINTS.md
index 3aac2a3..18f2f41 100644
--- a/SUPPORTED_ENDPOINTS.md
+++ b/SUPPORTED_ENDPOINTS.md
@@ -70,12 +70,12 @@
 - [ ] Update AutoMod Settings
 - [ ] Get Banned Events
 - [x] Get Banned Users
-- [ ] Ban User
-- [ ] Unban User
-- [ ] Get Blocked Terms
-- [ ] Add Blocked Term
-- [ ] Remove Blocked Term
-- [ ] Delete Chat Messages
+- [x] Ban User
+- [x] Unban User
+- [x] Get Blocked Terms
+- [x] Add Blocked Term
+- [x] Remove Blocked Term
+- [x] Delete Chat Messages
 - [ ] Get Moderators
 - [ ] Get Moderator Events
 - [ ] Remove Channel Moderator
diff --git a/docs/moderation_docs.md b/docs/moderation_docs.md
index c60cfd1..faee2af 100644
--- a/docs/moderation_docs.md
+++ b/docs/moderation_docs.md
@@ -26,10 +26,221 @@ if err != nil {
 fmt.Printf("%+v\n", resp)
 ```
 
+## Ban User
+
+This is an example of how to ban or timeout a user.
+
+To use this function you need a user access token with the `moderator:manage:banned_users` scope.
+
+```go
+client, err := helix.NewClient(&helix.Options{
+    ClientID: "your-client-id",
+    UserAccessToken: "your-user-access-token",
+})
+if err != nil {
+    // handle error
+}
+
+// Ban user permanently
+resp, err := client.BanUser(&helix.BanUserParams{
+    BroadcasterID: "54946241",
+    ModeratorId:   "14532827",
+    Body: helix.BanUserRequestBody{
+        UserId: "23981723",
+        Reason: "no reason",
+    },
+})
+if err != nil {
+    // handle error
+}
+
+fmt.Printf("%+v\n", resp)
+
+// Put user in a timeout
+resp, err = client.BanUser(&helix.BanUserParams{
+    BroadcasterID: "54946241",
+    ModeratorId:   "14532827",
+    Body: helix.BanUserRequestBody{
+        UserId:   "23981723",
+        Duration: 3600,
+        Reason:   "no reason",
+    },
+})
+if err != nil {
+    // handle error
+}
+
+fmt.Printf("%+v\n", resp)
+```
+
+## Unban User
+
+This is an example of how to unban a user.
+
+To use this function you need a user access token with the `moderator:manage:banned_users` scope.
+
+```go
+client, err := helix.NewClient(&helix.Options{
+    ClientID: "your-client-id",
+    UserAccessToken: "your-user-access-token",
+})
+if err != nil {
+    // handle error
+}
+
+resp, err := client.UnbanUser(&helix.UnbanUserParams{
+    BroadcasterID: "54946241",
+    ModeratorID:   "14532827",
+    UserID:        "23981723",
+})
+if err != nil {
+    // handle error
+}
+
+fmt.Printf("%+v\n", resp)
+```
+
+## Get Blocked Terms
+
+This is an example of how to get the list of non-private, blocked words or phrases.
+
+To use this function you need a user access token with the `moderator:read:blocked_terms` scope.
+
+```go
+client, err := helix.NewClient(&helix.Options{
+    ClientID: "your-client-id",
+    UserAccessToken: "your-user-access-token",
+})
+if err != nil {
+    // handle error
+}
+
+resp, err := client.GetBlockedTerms(&helix.BlockedTermsParams{
+    BroadcasterID: "54946241",
+    ModeratorID:   "14532827",
+    First:         100,                                 // optional
+    After:         "eyJiIjpudWxsLCJhIjp7IkN1cnNvciI6I", // optional
+})
+if err != nil {
+    // handle error
+}
+
+fmt.Printf("%+v\n", resp)
+```
+
+## Add Blocked Term
+
+This is an example of how to add a word or phrase to the list of blocked terms.
+
+To use this function you need a user access token with the `moderator:manage:blocked_terms` scope.
+
+```go
+client, err := helix.NewClient(&helix.Options{
+    ClientID: "your-client-id",
+    UserAccessToken: "your-user-access-token",
+})
+if err != nil {
+    // handle error
+}
+
+resp, err := client.AddBlockedTerm(&helix.AddBlockedTermParams{
+    BroadcasterID: "54946241",
+    ModeratorID:   "14532827",
+    Text:          "crac*",
+})
+if err != nil {
+    // handle error
+}
+
+fmt.Printf("%+v\n", resp)
+```
+
+## Remove Blocked Term
+
+This is an example of how to remove a word or phrase to the list of blocked terms.
+
+To use this function you need a user access token with the `moderator:manage:blocked_terms` scope.
+
+```go
+client, err := helix.NewClient(&helix.Options{
+    ClientID: "your-client-id",
+    UserAccessToken: "your-user-access-token",
+})
+if err != nil {
+    // handle error
+}
+
+resp, err := client.RemoveBlockedTerm(&helix.RemoveBlockedTermParams{
+    BroadcasterID: "54946241",
+    ModeratorID:   "14532827",
+    ID:            "c9fc79b8-0f63-4ef7-9d38-efd811e74ac2",
+})
+if err != nil {
+    // handle error
+}
+
+fmt.Printf("%+v\n", resp)
+```
+
+## Delete Specific Chat Message
+
+This is an example of how to delete a specific chat message. 
+
+To use this function you need a user access token with the `moderator:manage:chat_messages` scope.
+
+```go
+client, err := helix.NewClient(&helix.Options{
+    ClientID: "your-client-id",
+    UserAccessToken: "your-user-access-token",
+})
+if err != nil {
+    // handle error
+}
+
+resp, err := client.DeleteChatMessage(&helix.DeleteChatMessageParams{
+    BroadcasterID: "54946241",
+    ModeratorID:   "145328278",
+    MessageID:     "885196de-cb67-427a-baa8-82f9b0fcd05f",
+})
+if err != nil {
+    // handle error
+}
+
+fmt.Printf("%+v\n", resp)
+```
+
+## Delete All Chat Messages
+
+This is an example of how to delete all chat message.
+
+To use this function you need a user access token with the `moderator:manage:chat_messages` scope.
+
+```go
+client, err := helix.NewClient(&helix.Options{
+    ClientID: "your-client-id",
+    UserAccessToken: "your-user-access-token",
+})
+if err != nil {
+    // handle error
+}
+
+resp, err := client.DeleteAllChatMessages(&helix.DeleteAllChatMessagesParams{
+    BroadcasterID: "54946241",
+    ModeratorID:   "145328278",
+})
+if err != nil {
+    // handle error
+}
+
+fmt.Printf("%+v\n", resp)
+```
+
 ## Manage Held AutoMod Messages
 
 This is an example of how to manage held automod message in a channel.
 
+To use this function you need a user access token with the `moderator:manage:automod` scope.
+
 ```go
 client, err := helix.NewClient(&helix.Options{
     ClientID: "your-client-id",
diff --git a/moderation.go b/moderation.go
index bb8d61e..5226940 100644
--- a/moderation.go
+++ b/moderation.go
@@ -53,8 +53,8 @@ type BanUserParams struct {
 
 type BanUserRequestBody struct {
 	Duration int    `json:"duration,omitempty"` // optional
-	Reason   string `json:"reason"`   // required
-	UserId   string `json:"user_id"`  // required
+	Reason   string `json:"reason"`             // required
+	UserId   string `json:"user_id"`            // required
 }
 
 type BanUserResponse struct {
@@ -232,3 +232,62 @@ func (c *Client) RemoveBlockedTerm(params *RemoveBlockedTermParams) (*RemoveBloc
 
 	return blockedTermResp, nil
 }
+
+type DeleteChatMessageParams struct {
+	BroadcasterID string `query:"broadcaster_id"`
+	ModeratorID   string `query:"moderator_id"`
+	MessageID     string `query:"message_id"`
+}
+
+type DeleteChatMessageResponse struct {
+	ResponseCommon
+}
+
+// DeleteChatMessage Removes a single chat message from the broadcaster’s chat room.
+// Required scope: moderator:manage:chat_messages
+func (c *Client) DeleteChatMessage(params *DeleteChatMessageParams) (*DeleteChatMessageResponse, error) {
+	if params.BroadcasterID == "" || params.ModeratorID == "" {
+		return nil, errors.New("broadcaster id and moderator id must be provided")
+	}
+
+	if params.MessageID == "" {
+		return nil, errors.New("message id must be provided")
+	}
+
+	resp, err := c.delete("/moderation/chat", nil, params)
+	if err != nil {
+		return nil, err
+	}
+
+	deletedMessageResp := &DeleteChatMessageResponse{}
+	resp.HydrateResponseCommon(&deletedMessageResp.ResponseCommon)
+
+	return deletedMessageResp, nil
+}
+
+type DeleteAllChatMessagesParams struct {
+	BroadcasterID string `query:"broadcaster_id"`
+	ModeratorID   string `query:"moderator_id"`
+}
+
+type DeleteAllChatMessagesResponse struct {
+	ResponseCommon
+}
+
+// DeleteAllChatMessages Removes all chat messages from the broadcaster’s chat room.
+// Required scope: moderator:manage:chat_messages
+func (c *Client) DeleteAllChatMessages(params *DeleteAllChatMessagesParams) (*DeleteAllChatMessagesResponse, error) {
+	if params.BroadcasterID == "" || params.ModeratorID == "" {
+		return nil, errors.New("broadcaster id and moderator id must be provided")
+	}
+
+	resp, err := c.delete("/moderation/chat", nil, params)
+	if err != nil {
+		return nil, err
+	}
+
+	deletedMessagesResp := &DeleteAllChatMessagesResponse{}
+	resp.HydrateResponseCommon(&deletedMessagesResp.ResponseCommon)
+
+	return deletedMessagesResp, nil
+}


Test patch:
diff --git a/helix_test.go b/helix_test.go
index 7bd3345..36597c6 100644
--- a/helix_test.go
+++ b/helix_test.go
@@ -29,7 +29,7 @@ func newMockClient(options *Options, mockHandler http.HandlerFunc) *Client {
 
 func newMockHandler(statusCode int, json string, headers map[string]string) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
-		if headers != nil && len(headers) > 0 {
+		if len(headers) > 0 {
 			for key, value := range headers {
 				w.Header().Add(key, value)
 			}
diff --git a/moderation_test.go b/moderation_test.go
index a6b8bcd..3c81433 100644
--- a/moderation_test.go
+++ b/moderation_test.go
@@ -539,3 +539,164 @@ func TestRemoveBlockedTerm(t *testing.T) {
 		t.Error("expected error does match return error")
 	}
 }
+
+func TestDeleteChatMessage(t *testing.T) {
+	t.Parallel()
+
+	testCases := []struct {
+		statusCode    int
+		options       *Options
+		BroadcasterID string
+		ModeratorID   string
+		MessageID     string
+		errorMsg      string
+	}{
+		{
+			http.StatusBadRequest,
+			&Options{ClientID: "my-client-id"},
+			"",
+			"test-moderator-id",
+			"test-message-id",
+			"broadcaster id and moderator id must be provided",
+		},
+		{
+			http.StatusBadRequest,
+			&Options{ClientID: "my-client-id"},
+			"test-broadcaster-id",
+			"",
+			"test-message-id",
+			"broadcaster id and moderator id must be provided",
+		},
+		{
+			http.StatusBadRequest,
+			&Options{ClientID: "my-client-id"},
+			"test-broadcaster-id",
+			"test-moderator-id",
+			"",
+			"message id must be provided",
+		},
+		{
+			http.StatusNoContent,
+			&Options{ClientID: "my-client-id"},
+			"test-broadcaster-id",
+			"test-moderator-id",
+			"test-message-id",
+			"",
+		},
+	}
+
+	for _, testCase := range testCases {
+		c := newMockClient(testCase.options, newMockHandler(testCase.statusCode, "", nil))
+
+		resp, err := c.DeleteChatMessage(&DeleteChatMessageParams{
+			BroadcasterID: testCase.BroadcasterID,
+			ModeratorID:   testCase.ModeratorID,
+			MessageID:     testCase.MessageID,
+		})
+
+		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)
+		}
+	}
+
+	// Test with HTTP Failure
+	options := &Options{
+		ClientID: "my-client-id",
+		HTTPClient: &badMockHTTPClient{
+			newMockHandler(0, "", nil),
+		},
+	}
+	c := &Client{
+		opts: options,
+	}
+
+	_, err := c.RemoveBlockedTerm(&RemoveBlockedTermParams{BroadcasterID: "1234", ModeratorID: "1234", ID: "test"})
+	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")
+	}
+}
+
+func TestDeleteAllChatMessages(t *testing.T) {
+	t.Parallel()
+
+	testCases := []struct {
+		statusCode    int
+		options       *Options
+		BroadcasterID string
+		ModeratorID   string
+		errorMsg      string
+	}{
+		{
+			http.StatusBadRequest,
+			&Options{ClientID: "my-client-id"},
+			"",
+			"test-moderator-id",
+			"broadcaster id and moderator id must be provided",
+		},
+		{
+			http.StatusBadRequest,
+			&Options{ClientID: "my-client-id"},
+			"test-broadcaster-id",
+			"",
+			"broadcaster id and moderator id must be provided",
+		},
+		{
+			http.StatusNoContent,
+			&Options{ClientID: "my-client-id"},
+			"test-broadcaster-id",
+			"test-moderator-id",
+			"",
+		},
+	}
+
+	for _, testCase := range testCases {
+		c := newMockClient(testCase.options, newMockHandler(testCase.statusCode, "", nil))
+
+		resp, err := c.DeleteAllChatMessages(&DeleteAllChatMessagesParams{
+			BroadcasterID: testCase.BroadcasterID,
+			ModeratorID:   testCase.ModeratorID,
+		})
+
+		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)
+		}
+	}
+
+	// Test with HTTP Failure
+	options := &Options{
+		ClientID: "my-client-id",
+		HTTPClient: &badMockHTTPClient{
+			newMockHandler(0, "", nil),
+		},
+	}
+	c := &Client{
+		opts: options,
+	}
+
+	_, err := c.RemoveBlockedTerm(&RemoveBlockedTermParams{BroadcasterID: "1234", ModeratorID: "1234", ID: "test"})
+	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")
+	}
+}