Repository: nicklaw5/helix
Pull request #91 (2021-05-30 21:18:37)

Add Chat Badges features
From twitch changelog : 2021‑05‑28

Twitch API endpoints have been added for chat badges.
 - Get Channel Chat Badges
 - Get Global Chat Badges

EventSub subscription type updates.
 - Channel Subscription End has moved from public beta to v1. This was previously known as “Channel Unsubscribe.”
 - Channel Subscription Gift has been added as a public beta.


Patch:
diff --git a/README.md b/README.md
index 0f22a37..10dd5cc 100644
--- a/README.md
+++ b/README.md
@@ -43,6 +43,8 @@ looking for the Twitch API docs, see the [Twitch Developer website](https://dev.
 - [ ] Get Custom Reward Redemption
 - [ ] Update Custom Reward
 - [ ] Update Redemption Status
+- [x] Get Channel Chat Badges
+- [x] Get Global Chat Badges
 - [x] Create Clip
 - [x] Get Clip
 - [x] Get Clips
diff --git a/chat.go b/chat.go
new file mode 100644
index 0000000..69f6b04
--- /dev/null
+++ b/chat.go
@@ -0,0 +1,59 @@
+package helix
+
+// GetChatBadgeParams ...
+type GetChatBadgeParams struct {
+	BroadcasterID string `query:"broadcaster_id"`
+}
+
+// GetChatBadgeResponse ...
+type GetChatBadgeResponse struct {
+	ResponseCommon
+	Data ManyChatBadge
+}
+
+// ManyChatBadge ...
+type ManyChatBadge struct {
+	Badges []ChatBadge `json:"data"`
+}
+
+// ChatBadge ...
+type ChatBadge struct {
+	SetID    string         `json:"set_id"`
+	Versions []BadgeVersion `json:"versions"`
+}
+
+// BadgeVersion ...
+type BadgeVersion struct {
+	ID         string `json:"id"`
+	ImageUrl1x string `json:"image_url_1x"`
+	ImageUrl2x string `json:"image_url_2x"`
+	ImageUrl4x string `json:"image_url_4x"`
+}
+
+// GetChannelChatBadges ...
+func (c *Client) GetChannelChatBadges(params *GetChatBadgeParams) (*GetChatBadgeResponse, error) {
+	resp, err := c.get("/chat/badges", &ManyChatBadge{}, params)
+	if err != nil {
+		return nil, err
+	}
+
+	channels := &GetChatBadgeResponse{}
+	resp.HydrateResponseCommon(&channels.ResponseCommon)
+	channels.Data.Badges = resp.Data.(*ManyChatBadge).Badges
+
+	return channels, nil
+}
+
+// GetGlobalChatBadges ...
+func (c *Client) GetGlobalChatBadges() (*GetChatBadgeResponse, error) {
+	resp, err := c.get("/chat/badges/global", &ManyChatBadge{}, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	channels := &GetChatBadgeResponse{}
+	resp.HydrateResponseCommon(&channels.ResponseCommon)
+	channels.Data.Badges = resp.Data.(*ManyChatBadge).Badges
+
+	return channels, nil
+}
diff --git a/docs/chat_docs.md b/docs/chat_docs.md
new file mode 100644
index 0000000..277359d
--- /dev/null
+++ b/docs/chat_docs.md
@@ -0,0 +1,43 @@
+# Chat Documentation
+
+## Get Channel Chat Badges
+
+This is an example of how to get channel chat badges
+
+```go
+client, err := helix.NewClient(&helix.Options{
+    ClientID: "your-client-id",
+})
+if err != nil {
+    // handle error
+}
+
+resp, err := client.GetChannelChatBadges(&helix.GetChatBadgeParams{
+    BroadcasterID: "145328278",
+})
+if err != nil {
+    // handle error
+}
+
+fmt.Printf("%+v\n", resp)
+```
+
+## Get Global Chat Badges
+
+This is an example of how to get global chat badges
+
+```go
+client, err := helix.NewClient(&helix.Options{
+    ClientID: "your-client-id",
+})
+if err != nil {
+    // handle error
+}
+
+resp, err := client.GetGlobalChatBadges()
+if err != nil {
+    // handle error
+}
+
+fmt.Printf("%+v\n", resp)
+```
diff --git a/eventsub.go b/eventsub.go
index 751a57e..8e31431 100644
--- a/eventsub.go
+++ b/eventsub.go
@@ -82,7 +82,8 @@ const (
 	EventSubTypeChannelUpdate                             = "channel.update"
 	EventSubTypeChannelFollow                             = "channel.follow"
 	EventSubTypeChannelSubscription                       = "channel.subscribe"
-	EventSubTypeChannelUnsubscribe                        = "channel.unsubscribe" /* beta */
+	EventSubTypeChannelSubscriptionEnd                    = "channel.subscription.end"
+	EventSubTypeChannelSubscriptionGift                   = "channel.subscription.gift" /* beta */
 	EventSubTypeChannelCheer                              = "channel.cheer"
 	EventSubTypeChannelRaid                               = "channel.raid"
 	EventSubTypeChannelBan                                = "channel.ban"
@@ -141,6 +142,19 @@ type EventSubChannelSubscribeEvent struct {
 	IsGift               bool   `json:"is_gift"`
 }
 
+// EventSubChannelSubscriptionGiftEvent
+type EventSubChannelSubscriptionGiftEvent struct {
+	UserID               string `json:"user_id"`
+	UserLogin            string `json:"user_login"`
+	UserName             string `json:"user_name"`
+	BroadcasterUserID    string `json:"broadcaster_user_id"`
+	BroadcasterUserLogin string `json:"broadcaster_user_login"`
+	BroadcasterUserName  string `json:"broadcaster_user_name"`
+	Total                int    `json:"total"`
+	Tier                 string `json:"tier"`
+	CumulativeTotal      int    `json:"cumulative_total"`
+}
+
 // Data for a channel cheer notification
 type EventSubChannelCheerEvent struct {
 	IsAnonymous          bool   `json:"is_anonymous"`


Test patch:
diff --git a/chat_test.go b/chat_test.go
new file mode 100644
index 0000000..3355efe
--- /dev/null
+++ b/chat_test.go
@@ -0,0 +1,151 @@
+package helix
+
+import (
+	"net/http"
+	"testing"
+)
+
+func TestGetChannelChatBadges(t *testing.T) {
+	t.Parallel()
+
+	testCases := []struct {
+		statusCode         int
+		options            *Options
+		GetChatBadgeParams *GetChatBadgeParams
+		respBody           string
+	}{
+		{
+			http.StatusBadRequest,
+			&Options{ClientID: "my-client-id"},
+			&GetChatBadgeParams{BroadcasterID: ""},
+			`{"error":"Bad Request","status":400,"message":"Missing required parameter \"broadcaster_id\""}`,
+		},
+		{
+			http.StatusOK,
+			&Options{ClientID: "my-client-id"},
+			&GetChatBadgeParams{BroadcasterID: "121445595"},
+			`{"data": [{"set_id": "bits","versions": [{"id": "1","image_url_1x": "https://static-cdn.jtvnw.net/badges/v1/743a0f3b-84b3-450b-96a0-503d7f4a9764/1","image_url_2x": "https://static-cdn.jtvnw.net/badges/v1/743a0f3b-84b3-450b-96a0-503d7f4a9764/2","image_url_4x": "https://static-cdn.jtvnw.net/badges/v1/743a0f3b-84b3-450b-96a0-503d7f4a9764/3"}]},{"set_id": "subscriber","versions": [{"id": "0","image_url_1x": "https://static-cdn.jtvnw.net/badges/v1/eb4a8a4c-eacd-4f5e-b9f2-394348310442/1","image_url_2x": "https://static-cdn.jtvnw.net/badges/v1/eb4a8a4c-eacd-4f5e-b9f2-394348310442/2","image_url_4x": "https://static-cdn.jtvnw.net/badges/v1/eb4a8a4c-eacd-4f5e-b9f2-394348310442/3"}]}]}`,
+		},
+	}
+
+	for _, testCase := range testCases {
+		c := newMockClient(testCase.options, newMockHandler(testCase.statusCode, testCase.respBody, nil))
+
+		resp, err := c.GetChannelChatBadges(testCase.GetChatBadgeParams)
+		if err != nil {
+			t.Error(err)
+		}
+
+		if resp.StatusCode != testCase.statusCode {
+			t.Errorf("expected status code to be %d, got %d", testCase.statusCode, resp.StatusCode)
+		}
+
+		if resp.StatusCode == http.StatusBadRequest {
+			if resp.Error != "Bad Request" {
+				t.Errorf("expected error to be %s, got %s", "Bad Request", resp.Error)
+			}
+
+			if resp.ErrorStatus != http.StatusBadRequest {
+				t.Errorf("expected error status to be %d, got %d", http.StatusBadRequest, resp.ErrorStatus)
+			}
+
+			expectedErrMsg := "Missing required parameter \"broadcaster_id\""
+			if resp.ErrorMessage != expectedErrMsg {
+				t.Errorf("expected error message to be %s, got %s", expectedErrMsg, resp.ErrorMessage)
+			}
+
+			continue
+		}
+	}
+
+	// Test with HTTP Failure
+	options := &Options{
+		ClientID: "my-client-id",
+		HTTPClient: &badMockHTTPClient{
+			newMockHandler(0, "", nil),
+		},
+	}
+	c := &Client{
+		opts: options,
+	}
+
+	_, err := c.GetChannelChatBadges(&GetChatBadgeParams{})
+	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 TestGetGlobalChatBadges(t *testing.T) {
+	t.Parallel()
+
+	testCases := []struct {
+		statusCode         int
+		options            *Options
+		respBody           string
+	}{
+		{
+			http.StatusUnauthorized,
+			&Options{ClientID: "my-client-id"},
+			`{"error":"Unauthorized","status":401,"message":"OAuth token is missing"}`,
+		},
+		{
+			http.StatusOK,
+			&Options{ClientID: "my-client-id", UserAccessToken: "my-user-access-token"},
+			`{"data": [{"set_id": "vip","versions": [{"id": "1","image_url_1x": "https://static-cdn.jtvnw.net/badges/v1/b817aba4-fad8-49e2-b88a-7cc744dfa6ec/1","image_url_2x": "https://static-cdn.jtvnw.net/badges/v1/b817aba4-fad8-49e2-b88a-7cc744dfa6ec/2","image_url_4x": "https://static-cdn.jtvnw.net/badges/v1/b817aba4-fad8-49e2-b88a-7cc744dfa6ec/3"}]}]}`,
+		},
+	}
+
+	for _, testCase := range testCases {
+		c := newMockClient(testCase.options, newMockHandler(testCase.statusCode, testCase.respBody, nil))
+
+		resp, err := c.GetGlobalChatBadges()
+		if err != nil {
+			t.Error(err)
+		}
+
+		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 != http.StatusUnauthorized {
+				t.Errorf("expected error status to be %d, got %d", http.StatusBadRequest, resp.ErrorStatus)
+			}
+
+			expectedErrMsg := "OAuth token is missing"
+			if resp.ErrorMessage != expectedErrMsg {
+				t.Errorf("expected error message to be %s, got %s", expectedErrMsg, resp.ErrorMessage)
+			}
+
+			continue
+		}
+	}
+
+	// Test with HTTP Failure
+	options := &Options{
+		ClientID: "my-client-id",
+		HTTPClient: &badMockHTTPClient{
+			newMockHandler(0, "", nil),
+		},
+	}
+	c := &Client{
+		opts: options,
+	}
+
+	_, err := c.GetGlobalChatBadges()
+	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")
+	}
+}