Skip to content

Commit

Permalink
Merge pull request #211 from diPhantxm/send-chat-message
Browse files Browse the repository at this point in the history
Send Chat Message endpoint
  • Loading branch information
nicklaw5 committed Feb 8, 2024
2 parents 7714798 + a251eff commit c0c9d30
Show file tree
Hide file tree
Showing 2 changed files with 196 additions and 0 deletions.
75 changes: 75 additions & 0 deletions chat.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
121 changes: 121 additions & 0 deletions chat_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
}

0 comments on commit c0c9d30

Please sign in to comment.