Repository: nicklaw5/helix
Pull request #179 (2023-02-05 22:36:48)

Adding VIPs support
- Add Get VIPs
[https://dev.twitch.tv/docs/api/reference/#get-vips](url)

- Add Channel VIP
[https://dev.twitch.tv/docs/api/reference/#add-channel-vip](url)

- Remove Channel VIP
[https://dev.twitch.tv/docs/api/reference/#remove-channel-vip](url)


Patch:
diff --git a/SUPPORTED_ENDPOINTS.md b/SUPPORTED_ENDPOINTS.md
index 8e0c680..6399852 100644
--- a/SUPPORTED_ENDPOINTS.md
+++ b/SUPPORTED_ENDPOINTS.md
@@ -79,9 +79,9 @@
 - [x] Get Moderators
 - [ ] Get Moderator Events
 - [ ] Remove Channel Moderator
-- [ ] Get VIPs
-- [ ] Add Channel VIP
-- [ ] Remove Channel VIP
+- [x] Get VIPs
+- [x] Add Channel VIP
+- [x] Remove Channel VIP
 - [ ] Get Soundtrack Current Track
 - [ ] Get Soundtrack Playlist
 - [ ] Get Soundtrack Playlists
diff --git a/channels_vips.go b/channels_vips.go
new file mode 100644
index 0000000..0b4b221
--- /dev/null
+++ b/channels_vips.go
@@ -0,0 +1,88 @@
+package helix
+
+type GetChannelVipsParams struct {
+	UserID        string `query:"user_id"`
+	BroadcasterID string `query:"broadcaster_id"` // required
+	First         int    `query:"first"`
+	After         string `query:"after"`
+}
+
+type ManyChannelVips struct {
+	ChannelsVips []ChannelVips `json:"data"`
+	Pagination   Pagination    `json:"pagination"`
+}
+
+type ChannelVips struct {
+	UserID    string `json:"user_id"`
+	UserName  string `json:"user_name"`
+	UserLogin string `json:"user_login"`
+}
+
+type ChannelVipsResponse struct {
+	ResponseCommon
+	Data ManyChannelVips
+}
+
+type AddChannelVipParams struct {
+	UserID        string `query:"user_id"`        // required
+	BroadcasterID string `query:"broadcaster_id"` // required
+}
+
+type AddChannelVipResponse struct {
+	ResponseCommon
+}
+
+type RemoveChannelVipParams struct {
+	UserID        string `query:"user_id"`        // required
+	BroadcasterID string `query:"broadcaster_id"` // required
+}
+
+type RemoveChannelVipResponse struct {
+	ResponseCommon
+}
+
+// GetChannelVips Gets a list of the broadcaster’s VIPs.
+// Required scope: channel:read:vips
+func (c *Client) GetChannelVips(params *GetChannelVipsParams) (*ChannelVipsResponse, error) {
+	resp, err := c.get("/channels/vips", &ManyChannelVips{}, params)
+	if err != nil {
+		return nil, err
+	}
+
+	vips := &ChannelVipsResponse{}
+	resp.HydrateResponseCommon(&vips.ResponseCommon)
+	vips.Data.ChannelsVips = resp.Data.(*ManyChannelVips).ChannelsVips
+	vips.Data.Pagination = resp.Data.(*ManyChannelVips).Pagination
+
+	return vips, nil
+}
+
+// AddChannelVip Adds the specified user as a VIP in the broadcaster’s channel.
+// Required scope: channel:manage:vips
+// Rate Limits: The broadcaster may add a maximum of 10 VIPs within a 10-second window.
+func (c *Client) AddChannelVip(params *AddChannelVipParams) (*AddChannelVipResponse, error) {
+	resp, err := c.post("/channels/vips", nil, params)
+	if err != nil {
+		return nil, err
+	}
+
+	vips := &AddChannelVipResponse{}
+	resp.HydrateResponseCommon(&vips.ResponseCommon)
+
+	return vips, nil
+}
+
+// RemoveChannelVip : Removes the specified user as a VIP in the broadcaster’s channel.
+// Required scope: channel:manage:vips
+// Rate Limits: The broadcaster may remove a maximum of 10 VIPs within a 10-second window.
+func (c *Client) RemoveChannelVip(params *RemoveChannelVipParams) (*RemoveChannelVipResponse, error) {
+	resp, err := c.delete("/channels/vips", nil, params)
+	if err != nil {
+		return nil, err
+	}
+
+	vips := &RemoveChannelVipResponse{}
+	resp.HydrateResponseCommon(&vips.ResponseCommon)
+
+	return vips, nil
+}
diff --git a/docs/README.md b/docs/README.md
index 0e6c397..f4b4e49 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -25,6 +25,7 @@ Follow the links below to their respective API usage examples:
 - [Users](users_docs.md)
 - [Videos](videos_docs.md)
 - [Webhook Subscriptions](webhook_docs.md)
+- [Vips](vips_docs.md)
 
 ## Getting Started
 
diff --git a/docs/vips_docs.md b/docs/vips_docs.md
new file mode 100644
index 0000000..b538903
--- /dev/null
+++ b/docs/vips_docs.md
@@ -0,0 +1,81 @@
+# Vips Documentation
+
+## Get VIPs
+
+Gets a list of the broadcaster’s VIPs.
+
+To use this function you need a user access token with the `channel:read:vips` scope.
+`BroadcasterID` is required and need to be the same as the user id of the user access token.
+
+```go
+client, err := helix.NewClient(&helix.Options{
+    ClientID: "your-client-id",
+    UserAccessToken: "your-user-access-token",
+})
+if err != nil {
+    // handle error
+}
+
+resp, err := client.GetChannelVips(&helix.GetChannelVipsParams{
+    BroadcasterID: "54946241",
+})
+if err != nil {
+    // handle error
+}
+
+fmt.Printf("%+v\n", resp)
+```
+
+## Add Channel VIP
+
+Adds the specified user as a VIP in the broadcaster’s channel.
+
+To use this function you need a user access token with the `channel:manage:vips` scope.
+
+```go
+client, err := helix.NewClient(&helix.Options{
+    ClientID: "your-client-id",
+    UserAccessToken: "your-user-access-token",
+})
+if err != nil {
+    // handle error
+}
+
+//Add Vip
+resp, err := client.AddChannelVip(&helix.AddChannelVipParams{
+    UserID:        "23981723",
+    BroadcasterID: "54946241",
+})
+if err != nil {
+    // handle error
+}
+
+fmt.Printf("%+v\n", resp)
+```
+
+## Remove Channel VIP
+
+Removes the specified user as a VIP in the broadcaster’s channel
+
+To use this function you need a user access token with the `channel:manage:vips` scope.
+
+```go
+client, err := helix.NewClient(&helix.Options{
+    ClientID: "your-client-id",
+    UserAccessToken: "your-user-access-token",
+})
+if err != nil {
+    // handle error
+}
+
+resp, err := client.RemoveChannelVip(&helix.RemoveChannelVipParams{
+    UserID:        "23981723",
+    BroadcasterID: "54946241",
+})
+if err != nil {
+    // handle error
+}
+
+fmt.Printf("%+v\n", resp)
+```
+


Test patch:
diff --git a/channels_vips_test.go b/channels_vips_test.go
new file mode 100644
index 0000000..2a78c1b
--- /dev/null
+++ b/channels_vips_test.go
@@ -0,0 +1,219 @@
+package helix
+
+import (
+	"context"
+	"net/http"
+	"testing"
+)
+
+func TestGetChannelVips(t *testing.T) {
+	t.Parallel()
+
+	testCases := []struct {
+		statusCode int
+		options    *Options
+		params     *GetChannelVipsParams
+		respBody   string
+	}{
+		{
+			http.StatusOK,
+			&Options{ClientID: "my-client-id"},
+			&GetChannelVipsParams{
+				UserID: "123",
+			},
+			`{ "data": [{ "user_id": "11111", "user_name": "UserDisplayName", "user_login": "userloginname" }], "pagination": { "cursor": "eyJiIjpudWxsLCJhIjp7Ik9mZnNldCI6NX19" } }`,
+		},
+		{
+			http.StatusBadRequest,
+			&Options{ClientID: "my-client-id"},
+			&GetChannelVipsParams{
+				UserID: "",
+			},
+			`{"error":"Bad Request","status":400,"message":"the user id was not provided"}`,
+		},
+	}
+
+	for _, testCase := range testCases {
+		c := newMockClient(testCase.options, newMockHandler(testCase.statusCode, testCase.respBody, nil))
+
+		resp, err := c.GetChannelVips(testCase.params)
+		if err != nil {
+			t.Error(err)
+		}
+
+		// Test Bad Request Responses
+		if resp.StatusCode == http.StatusBadRequest {
+			broadcasterIDErrStr := "the user id was not provided"
+
+			if resp.ErrorMessage != broadcasterIDErrStr {
+				t.Errorf("expected error message to be \"%s\", got \"%s\"", broadcasterIDErrStr, resp.ErrorMessage)
+				continue
+			}
+		}
+
+		if resp.StatusCode != testCase.statusCode {
+			t.Errorf("expected status code to be \"%d\", got \"%d\"", testCase.statusCode, resp.StatusCode)
+		}
+	}
+
+	// Test with HTTP Failure
+	options := &Options{
+		ClientID: "my-client-id",
+		HTTPClient: &badMockHTTPClient{
+			newMockHandler(0, "", nil),
+		},
+	}
+	c := &Client{
+		opts: options,
+		ctx:  context.Background(),
+	}
+
+	_, err := c.GetChannelVips(&GetChannelVipsParams{})
+	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 TestAddChannelVips(t *testing.T) {
+	t.Parallel()
+
+	testCases := []struct {
+		statusCode int
+		options    *Options
+		params     *AddChannelVipParams
+		respBody   string
+	}{
+		{
+			http.StatusOK,
+			&Options{ClientID: "my-client-id"},
+			&AddChannelVipParams{
+				BroadcasterID: "123",
+				UserID:        "456",
+			},
+			``,
+		},
+		{
+			http.StatusBadRequest,
+			&Options{ClientID: "my-client-id"},
+			&AddChannelVipParams{},
+			`{"error":"Bad Request","status":400,"message":"Missing required parameter \"broadcaster_id\""}`,
+		},
+	}
+
+	for _, testCase := range testCases {
+		c := newMockClient(testCase.options, newMockHandler(testCase.statusCode, testCase.respBody, nil))
+
+		resp, err := c.AddChannelVip(testCase.params)
+		if err != nil {
+			t.Error(err)
+		}
+
+		// Test Bad Request Responses
+		if resp.StatusCode == http.StatusBadRequest {
+			firstErrStr := "Missing required parameter \"broadcaster_id\""
+			if resp.ErrorMessage != firstErrStr {
+				t.Errorf("expected error message to be \"%s\", got \"%s\"", firstErrStr, resp.ErrorMessage)
+			}
+			continue
+		}
+
+		if resp.StatusCode != testCase.statusCode {
+			t.Errorf("expected status code to be \"%d\", got \"%d\"", testCase.statusCode, resp.StatusCode)
+		}
+	}
+
+	// Test with HTTP Failure
+	options := &Options{
+		ClientID: "my-client-id",
+		HTTPClient: &badMockHTTPClient{
+			newMockHandler(0, "", nil),
+		},
+	}
+	c := &Client{
+		opts: options,
+		ctx:  context.Background(),
+	}
+
+	_, err := c.AddChannelVip(&AddChannelVipParams{})
+	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 TestRemoveChannelVips(t *testing.T) {
+	t.Parallel()
+
+	testCases := []struct {
+		statusCode int
+		options    *Options
+		params     *RemoveChannelVipParams
+		respBody   string
+	}{
+		{
+			http.StatusOK,
+			&Options{ClientID: "my-client-id"},
+			&RemoveChannelVipParams{
+				BroadcasterID: "123",
+				UserID:        "456",
+			},
+			``,
+		},
+		{
+			http.StatusBadRequest,
+			&Options{ClientID: "my-client-id"},
+			&RemoveChannelVipParams{},
+			`{"error":"Bad Request","status":400,"message":"Missing required parameter \"broadcaster_id\""}`,
+		},
+	}
+
+	for _, testCase := range testCases {
+		c := newMockClient(testCase.options, newMockHandler(testCase.statusCode, testCase.respBody, nil))
+
+		resp, err := c.RemoveChannelVip(testCase.params)
+		if err != nil {
+			t.Error(err)
+		}
+
+		// Test Bad Request Responses
+		if resp.StatusCode == http.StatusBadRequest {
+			firstErrStr := "Missing required parameter \"broadcaster_id\""
+			if resp.ErrorMessage != firstErrStr {
+				t.Errorf("expected error message to be \"%s\", got \"%s\"", firstErrStr, resp.ErrorMessage)
+			}
+			continue
+		}
+
+		if resp.StatusCode != testCase.statusCode {
+			t.Errorf("expected status code to be \"%d\", got \"%d\"", testCase.statusCode, resp.StatusCode)
+		}
+	}
+
+	// Test with HTTP Failure
+	options := &Options{
+		ClientID: "my-client-id",
+		HTTPClient: &badMockHTTPClient{
+			newMockHandler(0, "", nil),
+		},
+	}
+	c := &Client{
+		opts: options,
+		ctx:  context.Background(),
+	}
+
+	_, err := c.RemoveChannelVip(&RemoveChannelVipParams{})
+	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")
+	}
+}