Skip to content

Commit c0c9d30

Browse files
authored
Merge pull request #211 from diPhantxm/send-chat-message
Send Chat Message endpoint
2 parents 7714798 + a251eff commit c0c9d30

File tree

2 files changed

+196
-0
lines changed

2 files changed

+196
-0
lines changed

chat.go

+75
Original file line numberDiff line numberDiff line change
@@ -428,3 +428,78 @@ func (c *Client) UpdateUserChatColor(params *UpdateUserChatColorParams) (*Update
428428

429429
return update, nil
430430
}
431+
432+
type SendChatMessageParams struct {
433+
// The ID of the broadcaster whose chat room the message will be sent to
434+
BroadcasterID string `json:"broadcaster_id"`
435+
436+
// The ID of the user sending the message. This ID must match the user ID in the user access token
437+
SenderID string `json:"sender_id"`
438+
439+
// The message to send. The message is limited to a maximum of 500 characters.
440+
// Chat messages can also include emoticons.
441+
// To include emoticons, use the name of the emote.
442+
// The names are case sensitive.
443+
// Don’t include colons around the name (e.g., :bleedPurple:).
444+
// If Twitch recognizes the name, Twitch converts the name to the emote before writing the chat message to the chat room
445+
Message string `json:"message"`
446+
447+
// The ID of the chat message being replied to
448+
ReplyParentMessageID string `json:"reply_parent_message_id,omitempty"`
449+
}
450+
451+
type ChatMessageResponse struct {
452+
ResponseCommon
453+
454+
Data ManyChatMessages
455+
}
456+
457+
type ManyChatMessages struct {
458+
Messages []ChatMessage `json:"data"`
459+
}
460+
461+
type ChatMessage struct {
462+
// The message id for the message that was sent
463+
MessageID string `json:"message_id"`
464+
465+
// If the message passed all checks and was sent
466+
IsSent bool `json:"is_sent"`
467+
468+
// The reason the message was dropped, if any
469+
DropReasons ManyDropReasons `json:"drop_reason"`
470+
}
471+
472+
type ManyDropReasons struct {
473+
Data DropReason
474+
}
475+
476+
type DropReason struct {
477+
// Code for why the message was dropped
478+
Code string `json:"code"`
479+
480+
// Message for why the message was dropped
481+
Message string `json:"message"`
482+
}
483+
484+
// Requires an app access token or user access token that includes the user:write:chat scope.
485+
// If app access token used, then additionally requires user:bot scope from chatting user,
486+
// and either channel:bot scope from broadcaster or moderator status
487+
func (c *Client) SendChatMessage(params *SendChatMessageParams) (*ChatMessageResponse, error) {
488+
if params.BroadcasterID == "" {
489+
return nil, errors.New("error: broadcaster id must be specified")
490+
}
491+
if params.SenderID == "" {
492+
return nil, errors.New("error: sender id must be specified")
493+
}
494+
495+
resp, err := c.post("/chat/messages", &ManyChatMessages{}, params)
496+
if err != nil {
497+
return nil, err
498+
}
499+
500+
chatMessages := &ChatMessageResponse{}
501+
resp.HydrateResponseCommon(&chatMessages.ResponseCommon)
502+
chatMessages.Data.Messages = resp.Data.(*ManyChatMessages).Messages
503+
504+
return chatMessages, nil
505+
}

chat_test.go

+121
Original file line numberDiff line numberDiff line change
@@ -942,3 +942,124 @@ func TestUpdateUserChatColor(t *testing.T) {
942942
}
943943
}
944944
}
945+
946+
func TestSendChatMessage(t *testing.T) {
947+
t.Parallel()
948+
949+
testCases := []struct {
950+
statusCode int
951+
options *Options
952+
params *SendChatMessageParams
953+
respBody string
954+
err string
955+
}{
956+
{
957+
http.StatusOK,
958+
&Options{ClientID: "my-client-id"},
959+
&SendChatMessageParams{
960+
BroadcasterID: "1234",
961+
SenderID: "5678",
962+
Message: "Hello, world! twitchdevHype",
963+
},
964+
`{"data":[{"message_id": "abc-123-def","is_sent": true}]}`,
965+
``,
966+
},
967+
{
968+
http.StatusOK,
969+
&Options{ClientID: "my-client-id"},
970+
&SendChatMessageParams{
971+
BroadcasterID: "",
972+
SenderID: "5678",
973+
Message: "Hello, world! twitchdevHype",
974+
},
975+
``,
976+
`error: broadcaster id must be specified`,
977+
},
978+
{
979+
http.StatusOK,
980+
&Options{ClientID: "my-client-id"},
981+
&SendChatMessageParams{
982+
BroadcasterID: "1234",
983+
SenderID: "",
984+
Message: "Hello, world! twitchdevHype",
985+
},
986+
``,
987+
`error: sender id must be specified`,
988+
},
989+
{
990+
http.StatusUnauthorized,
991+
&Options{ClientID: "my-client-id"},
992+
&SendChatMessageParams{
993+
BroadcasterID: "1234",
994+
SenderID: "5678",
995+
Message: "Hello, world! twitchdevHype",
996+
},
997+
`{"error":"Unauthorized","status":401,"message":"Missing user:write:chat scope"}`, // missing required scope
998+
``,
999+
},
1000+
}
1001+
1002+
for _, testCase := range testCases {
1003+
c := newMockClient(testCase.options, newMockHandler(testCase.statusCode, testCase.respBody, nil))
1004+
1005+
resp, err := c.SendChatMessage(testCase.params)
1006+
if err != nil {
1007+
if err.Error() == testCase.err {
1008+
continue
1009+
}
1010+
t.Errorf("Unmatched error, expected '%v', got '%v'", testCase.err, err)
1011+
continue
1012+
}
1013+
1014+
if resp.StatusCode != testCase.statusCode {
1015+
t.Errorf("expected status code to be %d, got %d", testCase.statusCode, resp.StatusCode)
1016+
}
1017+
1018+
if resp.StatusCode == http.StatusUnauthorized {
1019+
if resp.Error != "Unauthorized" {
1020+
t.Errorf("expected error to be \"%s\", got \"%s\"", "Unauthorized", resp.Error)
1021+
}
1022+
1023+
if resp.ErrorStatus != testCase.statusCode {
1024+
t.Errorf("expected error status to be %d, got %d", testCase.statusCode, resp.ErrorStatus)
1025+
}
1026+
1027+
if resp.ErrorMessage != "Missing user:write:chat scope" {
1028+
t.Errorf("expected error message to be \"%s\", got \"%s\"", "Missing user:write:chat scope", resp.ErrorMessage)
1029+
}
1030+
1031+
continue
1032+
}
1033+
1034+
if len(resp.Data.Messages) < 1 {
1035+
t.Errorf("Expected the number of messages to be a positive number")
1036+
}
1037+
1038+
if len(resp.Data.Messages[0].MessageID) == 0 {
1039+
t.Errorf("Expected message_id not to be empty")
1040+
}
1041+
}
1042+
1043+
// Test with HTTP Failure
1044+
options := &Options{
1045+
ClientID: "my-client-id",
1046+
HTTPClient: &badMockHTTPClient{
1047+
newMockHandler(0, "", nil),
1048+
},
1049+
}
1050+
c := &Client{
1051+
opts: options,
1052+
ctx: context.Background(),
1053+
}
1054+
1055+
_, err := c.SendChatMessage(&SendChatMessageParams{BroadcasterID: "123", SenderID: "456", Message: "Hello, world! twitchdevHype"})
1056+
if err == nil {
1057+
t.Error("expected error but got nil")
1058+
}
1059+
1060+
const expectedHTTPError = "Failed to execute API request: Oops, that's bad :("
1061+
1062+
if err.Error() != expectedHTTPError {
1063+
t.Errorf("expected error does match return error, got '%s'", err.Error())
1064+
}
1065+
}

0 commit comments

Comments
 (0)