From 2f8fdffb9e9c87fca10afea4ce2fc2da66fd9d9b Mon Sep 17 00:00:00 2001 From: diPhantxm Date: Fri, 2 Feb 2024 17:45:15 +0300 Subject: [PATCH 1/2] implement send chat message --- chat.go | 75 +++++++++++++++++++++++++++++++ chat_test.go | 121 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 196 insertions(+) diff --git a/chat.go b/chat.go index c16c6b0..5695073 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 `query:"broadcaster_id"` + + // The ID of the user sending the message. This ID must match the user ID in the user access token + SenderID string `query:"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 `query:"message"` + + // The ID of the chat message being replied to + ReplyParentMessageID string `query:"reply_parent_message_id"` +} + +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.put("/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 +} 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()) + } +} From a251efff20c26167848e184d2e65d13d343d2514 Mon Sep 17 00:00:00 2001 From: diPhantxm Date: Thu, 8 Feb 2024 17:02:04 +0300 Subject: [PATCH 2/2] change put to post http method and change params to json body --- chat.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/chat.go b/chat.go index 5695073..35a06df 100644 --- a/chat.go +++ b/chat.go @@ -431,10 +431,10 @@ func (c *Client) UpdateUserChatColor(params *UpdateUserChatColorParams) (*Update type SendChatMessageParams struct { // The ID of the broadcaster whose chat room the message will be sent to - BroadcasterID string `query:"broadcaster_id"` + 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 `query:"sender_id"` + 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. @@ -442,10 +442,10 @@ type SendChatMessageParams struct { // 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 `query:"message"` + Message string `json:"message"` // The ID of the chat message being replied to - ReplyParentMessageID string `query:"reply_parent_message_id"` + ReplyParentMessageID string `json:"reply_parent_message_id,omitempty"` } type ChatMessageResponse struct { @@ -492,7 +492,7 @@ func (c *Client) SendChatMessage(params *SendChatMessageParams) (*ChatMessageRes return nil, errors.New("error: sender id must be specified") } - resp, err := c.put("/chat/messages", &ManyChatMessages{}, params) + resp, err := c.post("/chat/messages", &ManyChatMessages{}, params) if err != nil { return nil, err }