Repository: nicklaw5/helix
Pull request #169 (2023-01-19 20:46:30)

feat: create shoutout & shoutout eventsubs
- add new shoutout create api endpoint
- add new shoutout create and receive eventsub data


Patch:
diff --git a/eventsub.go b/eventsub.go
index c23c74f..92a5303 100644
--- a/eventsub.go
+++ b/eventsub.go
@@ -123,6 +123,8 @@ const (
 	EventSubTypeStreamOffline                             = "stream.offline"
 	EventSubTypeUserAuthorizationRevoke                   = "user.authorization.revoke"
 	EventSubTypeUserUpdate                                = "user.update"
+	EventSubShoutoutCreate                                = "channel.shoutout.create"
+	EventSubShoutoutReceive                               = "channel.shoutout.receive"
 )
 
 // Event Notification Responses
@@ -642,6 +644,33 @@ type EventSubCharityStartEvent struct {
 	StartedAt            Time                  `json:"started_at"`
 }
 
+type EventSubShoutoutCreateEvent struct {
+	BroadcasterUserID      string `json:"broadcaster_user_id"`
+	BroadcasterUserName    string `json:"broadcaster_user_name"`
+	BroadcasterUserLogin   string `json:"broadcaster_user_login"`
+	ModeratorUserID        string `json:"moderator_user_id"`
+	ModeratorUserName      string `json:"moderator_user_name"`
+	ModeratorUserLogin     string `json:"moderator_user_login"`
+	ToBroadcasterUserID    string `json:"to_broadcaster_user_id"`
+	ToBroadcasterUserName  string `json:"to_broadcaster_user_name"`
+	ToBroadcasterUserLogin string `json:"to_broadcaster_user_login"`
+	StartedAt              Time   `json:"started_at"`
+	ViewerCount            int64  `json:"viewer_count"`
+	CooldownEndsAt         Time   `json:"cooldown_ends_at"`
+	TargetCooldownEndsAt   Time   `json:"target_cooldown_ends_at"`
+}
+
+type EventSubShoutoutReceiveEvent struct {
+	BroadcasterUserID        string `json:"broadcaster_user_id"`
+	BroadcasterUserName      string `json:"broadcaster_user_name"`
+	BroadcasterUserLogin     string `json:"broadcaster_user_login"`
+	FromBroadcasterUserID    string `json:"from_broadcaster_user_id"`
+	FromBroadcasterUserName  string `json:"from_broadcaster_user_name"`
+	FromBroadcasterUserLogin string `json:"from_broadcaster_user_login"`
+	ViewerCount              int64  `json:"viewer_count"`
+	StartedAt                Time   `json:"started_at"`
+}
+
 // Get all EventSub Subscriptions
 func (c *Client) GetEventSubSubscriptions(params *EventSubSubscriptionsParams) (*EventSubSubscriptionsResponse, error) {
 	resp, err := c.get("/eventsub/subscriptions", &ManyEventSubSubscriptions{}, params)
diff --git a/shoutout.go b/shoutout.go
new file mode 100644
index 0000000..45bcc9d
--- /dev/null
+++ b/shoutout.go
@@ -0,0 +1,27 @@
+package helix
+
+type SendShoutoutParams struct {
+	FromBroadcasterID string `query:"from_broadcaster_id"` // required
+	ToBroadcasterID   string `query:"to_broadcaster_id"`   // required
+	ModeratorID       string `query:"moderator_id"`        // required
+}
+
+type SendShoutoutResponse struct {
+	ResponseCommon
+}
+
+// SendShoutout sends a Shoutout to the specified broadcaster.
+// Required scope: moderator:manage:shoutouts
+// The broadcaster may send a Shoutout once every 2 minutes.
+// They may send the same broadcaster a Shoutout once every 60 minutes.
+func (c *Client) SendShoutout(params *SendShoutoutParams) (*SendShoutoutResponse, error) {
+	resp, err := c.post("/shoutouts", nil, params)
+	if err != nil {
+		return nil, err
+	}
+
+	shoutoutResp := &SendShoutoutResponse{}
+	resp.HydrateResponseCommon(&shoutoutResp.ResponseCommon)
+
+	return shoutoutResp, nil
+}


Test patch:
diff --git a/shoutout_test.go b/shoutout_test.go
new file mode 100644
index 0000000..362e5d7
--- /dev/null
+++ b/shoutout_test.go
@@ -0,0 +1,82 @@
+package helix
+
+import (
+	"context"
+	"net/http"
+	"testing"
+)
+
+func TestSendShoutout(t *testing.T) {
+	t.Parallel()
+
+	testCases := []struct {
+		statusCode         int
+		options            *Options
+		SendShoutoutParams *SendShoutoutParams
+		respBody           string
+	}{
+		{
+			http.StatusBadRequest,
+			&Options{ClientID: "my-client-id", UserAccessToken: "moderator-access-token"},
+			&SendShoutoutParams{FromBroadcasterID: "100249558", ModeratorID: "100249558"},
+			`{"error":"Bad Request","status":400,"message":"The parameter \"to_broadcaster_id\" was malformed: the value must be a valid"}`,
+		},
+		{
+			http.StatusNoContent,
+			&Options{ClientID: "my-client-id", UserAccessToken: "moderator-access-token"},
+			&SendShoutoutParams{FromBroadcasterID: "100249558", ModeratorID: "100249558", ToBroadcasterID: "80085"},
+			``,
+		},
+	}
+
+	for _, testCase := range testCases {
+		c := newMockClient(testCase.options, newMockHandler(testCase.statusCode, testCase.respBody, nil))
+
+		resp, err := c.SendShoutout(testCase.SendShoutoutParams)
+		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 := "The parameter \"to_broadcaster_id\" was malformed: the value must be a valid"
+			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,
+		ctx:  context.Background(),
+	}
+
+	_, err := c.SendShoutout(&SendShoutoutParams{})
+	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")
+	}
+}