Repository: nicklaw5/helix (go)

Issue:
Add send message endpoint request
https://dev.twitch.tv/docs/api/reference/#send-chat-message

Pull request:
Send Chat Message endpoint
Closes #210 

Patch:
diff --git a/chat.go b/chat.go
index c16c6b0..35a06df 100644
--- a/chat.go
+++ b/chat.go
@@ -428,3 +428,78 @@ func (c *Client) UpdateUserChatColor(params *UpdateUserChatColorParams) (*Update
 
 	return update, nil
 }
+
+type SendChatMessageParams struct {
+	// The ID of the broadcaster whose chat room the message will be sent to
+	BroadcasterID string `json:"broadcaster_id"`
+
+	// The ID of the user sending the message. This ID must match the user ID in the user access token
+	SenderID string `json:"sender_id"`
+
+	// The message to send. The message is limited to a maximum of 500 characters.
+	// Chat messages can also include emoticons.
+	// To include emoticons, use the name of the emote.
+	// The names are case sensitive.
+	// Don’t include colons around the name (e.g., :bleedPurple:).
+	// If Twitch recognizes the name, Twitch converts the name to the emote before writing the chat message to the chat room
+	Message string `json:"message"`
+
+	// The ID of the chat message being replied to
+	ReplyParentMessageID string `json:"reply_parent_message_id,omitempty"`
+}
+
+type ChatMessageResponse struct {
+	ResponseCommon
+
+	Data ManyChatMessages
+}
+
+type ManyChatMessages struct {
+	Messages []ChatMessage `json:"data"`
+}
+
+type ChatMessage struct {
+	// The message id for the message that was sent
+	MessageID string `json:"message_id"`
+
+	// If the message passed all checks and was sent
+	IsSent bool `json:"is_sent"`
+
+	// The reason the message was dropped, if any
+	DropReasons ManyDropReasons `json:"drop_reason"`
+}
+
+type ManyDropReasons struct {
+	Data DropReason
+}
+
+type DropReason struct {
+	// Code for why the message was dropped
+	Code string `json:"code"`
+
+	// Message for why the message was dropped
+	Message string `json:"message"`
+}
+
+// Requires an app access token or user access token that includes the user:write:chat scope.
+// If app access token used, then additionally requires user:bot scope from chatting user,
+// and either channel:bot scope from broadcaster or moderator status
+func (c *Client) SendChatMessage(params *SendChatMessageParams) (*ChatMessageResponse, error) {
+	if params.BroadcasterID == "" {
+		return nil, errors.New("error: broadcaster id must be specified")
+	}
+	if params.SenderID == "" {
+		return nil, errors.New("error: sender id must be specified")
+	}
+
+	resp, err := c.post("/chat/messages", &ManyChatMessages{}, params)
+	if err != nil {
+		return nil, err
+	}
+
+	chatMessages := &ChatMessageResponse{}
+	resp.HydrateResponseCommon(&chatMessages.ResponseCommon)
+	chatMessages.Data.Messages = resp.Data.(*ManyChatMessages).Messages
+
+	return chatMessages, nil
+}


Test patch:
diff --git a/chat_test.go b/chat_test.go
index e8f108d..ba74b94 100644
--- a/chat_test.go
+++ b/chat_test.go
@@ -942,3 +942,124 @@ func TestUpdateUserChatColor(t *testing.T) {
 		}
 	}
 }
+
+func TestSendChatMessage(t *testing.T) {
+	t.Parallel()
+
+	testCases := []struct {
+		statusCode int
+		options    *Options
+		params     *SendChatMessageParams
+		respBody   string
+		err        string
+	}{
+		{
+			http.StatusOK,
+			&Options{ClientID: "my-client-id"},
+			&SendChatMessageParams{
+				BroadcasterID: "1234",
+				SenderID:      "5678",
+				Message:       "Hello, world! twitchdevHype",
+			},
+			`{"data":[{"message_id": "abc-123-def","is_sent": true}]}`,
+			``,
+		},
+		{
+			http.StatusOK,
+			&Options{ClientID: "my-client-id"},
+			&SendChatMessageParams{
+				BroadcasterID: "",
+				SenderID:      "5678",
+				Message:       "Hello, world! twitchdevHype",
+			},
+			``,
+			`error: broadcaster id must be specified`,
+		},
+		{
+			http.StatusOK,
+			&Options{ClientID: "my-client-id"},
+			&SendChatMessageParams{
+				BroadcasterID: "1234",
+				SenderID:      "",
+				Message:       "Hello, world! twitchdevHype",
+			},
+			``,
+			`error: sender id must be specified`,
+		},
+		{
+			http.StatusUnauthorized,
+			&Options{ClientID: "my-client-id"},
+			&SendChatMessageParams{
+				BroadcasterID: "1234",
+				SenderID:      "5678",
+				Message:       "Hello, world! twitchdevHype",
+			},
+			`{"error":"Unauthorized","status":401,"message":"Missing user:write:chat scope"}`, // missing required scope
+			``,
+		},
+	}
+
+	for _, testCase := range testCases {
+		c := newMockClient(testCase.options, newMockHandler(testCase.statusCode, testCase.respBody, nil))
+
+		resp, err := c.SendChatMessage(testCase.params)
+		if err != nil {
+			if err.Error() == testCase.err {
+				continue
+			}
+			t.Errorf("Unmatched error, expected '%v', got '%v'", testCase.err, 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.StatusUnauthorized {
+			if resp.Error != "Unauthorized" {
+				t.Errorf("expected error to be \"%s\", got \"%s\"", "Unauthorized", resp.Error)
+			}
+
+			if resp.ErrorStatus != testCase.statusCode {
+				t.Errorf("expected error status to be %d, got %d", testCase.statusCode, resp.ErrorStatus)
+			}
+
+			if resp.ErrorMessage != "Missing user:write:chat scope" {
+				t.Errorf("expected error message to be \"%s\", got \"%s\"", "Missing user:write:chat scope", resp.ErrorMessage)
+			}
+
+			continue
+		}
+
+		if len(resp.Data.Messages) < 1 {
+			t.Errorf("Expected the number of messages to be a positive number")
+		}
+
+		if len(resp.Data.Messages[0].MessageID) == 0 {
+			t.Errorf("Expected message_id not to be empty")
+		}
+	}
+
+	// Test with HTTP Failure
+	options := &Options{
+		ClientID: "my-client-id",
+		HTTPClient: &badMockHTTPClient{
+			newMockHandler(0, "", nil),
+		},
+	}
+	c := &Client{
+		opts: options,
+		ctx:  context.Background(),
+	}
+
+	_, err := c.SendChatMessage(&SendChatMessageParams{BroadcasterID: "123", SenderID: "456", Message: "Hello, world! twitchdevHype"})
+	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())
+	}
+}