From a8e86ecb4e168d6a7cbd6edf562aaf24b2e07c4d Mon Sep 17 00:00:00 2001
From: Marc Szanto <11840265+Xemdo@users.noreply.github.com>
Date: Thu, 19 Jan 2023 13:15:38 -0800
Subject: [PATCH 1/2] Added support for eventsub triggeopics chann
 channel.shoutout.create and channel.shoutout.receive

---
 internal/events/models.go                  |   2 +
 internal/events/types/shoutout/shoutout.go | 169 +++++++++++++++++++++
 internal/events/types/types.go             |   2 +
 internal/models/shoutout.go                |  44 ++++++
 4 files changed, 217 insertions(+)
 create mode 100644 internal/events/types/shoutout/shoutout.go
 create mode 100644 internal/models/shoutout.go

diff --git a/internal/events/models.go b/internal/events/models.go
index c43ae867..88062a00 100644
--- a/internal/events/models.go
+++ b/internal/events/models.go
@@ -35,6 +35,8 @@ var triggerSupported = map[string]bool{
 	"revoke":              true,
 	"shield-mode-begin":   true,
 	"shield-mode-end":     true,
+	"shoutout-create":     true,
+	"shoutout-received":   true,
 	"stream-change":       true,
 	"streamdown":          true,
 	"streamup":            true,
diff --git a/internal/events/types/shoutout/shoutout.go b/internal/events/types/shoutout/shoutout.go
new file mode 100644
index 00000000..3621faea
--- /dev/null
+++ b/internal/events/types/shoutout/shoutout.go
@@ -0,0 +1,169 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+package shoutout
+
+import (
+	"encoding/json"
+	"strings"
+	"time"
+
+	"github.com/twitchdev/twitch-cli/internal/events"
+	"github.com/twitchdev/twitch-cli/internal/models"
+	"github.com/twitchdev/twitch-cli/internal/util"
+)
+
+var transportsSupported = map[string]bool{
+	models.TransportEventSub: true,
+}
+var triggers = []string{"shoutout-create", "shoutout-received"}
+
+var triggerMapping = map[string]map[string]string{
+	models.TransportEventSub: {
+		"shoutout-create":   "channel.shoutout.create",
+		"shoutout-received": "channel.shoutout.receive",
+	},
+}
+
+type Event struct{}
+
+func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEventResponse, error) {
+	var event []byte
+	var err error
+
+	switch params.Transport {
+	case models.TransportEventSub:
+		viewerCount := util.RandomInt(2000)
+		startedAt := util.GetTimestamp()
+
+		moderatorUserID := "3502151007"
+
+		if params.Trigger == "shoutout-create" {
+			body := models.ShoutoutCreateEventSubResponse{
+				Subscription: models.EventsubSubscription{
+					ID:      params.ID,
+					Status:  params.SubscriptionStatus,
+					Type:    triggerMapping[params.Transport][params.Trigger],
+					Version: e.SubscriptionVersion(),
+					Condition: models.EventsubCondition{
+						BroadcasterUserID: params.FromUserID,
+						ModeratorUserID:   moderatorUserID,
+					},
+					Transport: models.EventsubTransport{
+						Method:   "webhook",
+						Callback: "null",
+					},
+					Cost:      0,
+					CreatedAt: params.Timestamp,
+				},
+				Event: models.ShoutoutCreateEventSubEvent{
+					BroadcasterUserID:      params.FromUserID,
+					BroadcasterUserName:    params.FromUserName,
+					BroadcasterUserLogin:   params.FromUserName,
+					ToBroadcasterUserID:    params.ToUserID,
+					ToBroadcasterUserName:  params.ToUserName,
+					ToBroadcasterUserLogin: params.ToUserName,
+					ModeratorUserID:        moderatorUserID,
+					ModeratorUserName:      "TrustedUser123",
+					ModeratorUserLogin:     "trusteduser123",
+					ViewerCount:            int(viewerCount),
+					StartedAt:              startedAt.Format(time.RFC3339Nano),
+					CooldownEndsAt:         startedAt.Add(2 * time.Minute).Format(time.RFC3339Nano),
+					TargetCooldownEndsAt:   startedAt.Add(1 * time.Hour).Format(time.RFC3339Nano),
+				},
+			}
+
+			event, err = json.Marshal(body)
+			if err != nil {
+				return events.MockEventResponse{}, err
+			}
+		} else if params.Trigger == "shoutout-received" {
+			body := models.ShoutoutReceivedEventSubResponse{
+				Subscription: models.EventsubSubscription{
+					ID:      params.ID,
+					Status:  params.SubscriptionStatus,
+					Type:    triggerMapping[params.Transport][params.Trigger],
+					Version: e.SubscriptionVersion(),
+					Condition: models.EventsubCondition{
+						BroadcasterUserID: params.ToUserID,
+						ModeratorUserID:   moderatorUserID,
+					},
+					Transport: models.EventsubTransport{
+						Method:   "webhook",
+						Callback: "null",
+					},
+					Cost:      0,
+					CreatedAt: params.Timestamp,
+				},
+				Event: models.ShoutoutReceivedEventSubEvent{
+					BroadcasterUserID:        params.ToUserID,
+					BroadcasterUserName:      params.ToUserName,
+					BroadcasterUserLogin:     params.ToUserName,
+					FromBroadcasterUserID:    params.FromUserID,
+					FromBroadcasterUserName:  params.FromUserName,
+					FromBroadcasterUserLogin: params.FromUserName,
+					ViewerCount:              int(viewerCount),
+					StartedAt:                startedAt.Format(time.RFC3339Nano),
+				},
+			}
+
+			event, err = json.Marshal(body)
+			if err != nil {
+				return events.MockEventResponse{}, err
+			}
+		}
+
+		// Delete event info if Subscription.Status is not set to "enabled"
+		if !strings.EqualFold(params.SubscriptionStatus, "enabled") {
+			var i interface{}
+			if err := json.Unmarshal([]byte(event), &i); err != nil {
+				return events.MockEventResponse{}, err
+			}
+			if m, ok := i.(map[string]interface{}); ok {
+				delete(m, "event") // Matches JSON key defined in body variable above
+			}
+
+			event, err = json.Marshal(i)
+			if err != nil {
+				return events.MockEventResponse{}, err
+			}
+		}
+	default:
+		return events.MockEventResponse{}, nil
+	}
+
+	return events.MockEventResponse{
+		ID:       params.ID,
+		JSON:     event,
+		ToUser:   params.ToUserID,
+		FromUser: params.FromUserID,
+	}, nil
+}
+
+func (e Event) ValidTransport(transport string) bool {
+	return transportsSupported[transport]
+}
+
+func (e Event) ValidTrigger(trigger string) bool {
+	for _, t := range triggers {
+		if t == trigger {
+			return true
+		}
+	}
+	return false
+}
+func (e Event) GetTopic(transport string, trigger string) string {
+	return triggerMapping[transport][trigger]
+}
+func (e Event) GetEventSubAlias(t string) string {
+	// check for aliases
+	for trigger, topic := range triggerMapping[models.TransportEventSub] {
+		if topic == t {
+			return trigger
+		}
+	}
+	return ""
+}
+
+func (e Event) SubscriptionVersion() string {
+	return "beta"
+}
diff --git a/internal/events/types/types.go b/internal/events/types/types.go
index 0d7642b8..d7d468c9 100644
--- a/internal/events/types/types.go
+++ b/internal/events/types/types.go
@@ -23,6 +23,7 @@ import (
 	"github.com/twitchdev/twitch-cli/internal/events/types/prediction"
 	"github.com/twitchdev/twitch-cli/internal/events/types/raid"
 	"github.com/twitchdev/twitch-cli/internal/events/types/shield_mode"
+	"github.com/twitchdev/twitch-cli/internal/events/types/shoutout"
 	"github.com/twitchdev/twitch-cli/internal/events/types/stream_change"
 	"github.com/twitchdev/twitch-cli/internal/events/types/streamdown"
 	"github.com/twitchdev/twitch-cli/internal/events/types/streamup"
@@ -51,6 +52,7 @@ func All() []events.MockEvent {
 		prediction.Event{},
 		raid.Event{},
 		shield_mode.Event{},
+		shoutout.Event{},
 		stream_change.Event{},
 		streamup.Event{},
 		streamdown.Event{},
diff --git a/internal/models/shoutout.go b/internal/models/shoutout.go
new file mode 100644
index 00000000..c63baec5
--- /dev/null
+++ b/internal/models/shoutout.go
@@ -0,0 +1,44 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+package models
+
+// channel.shoutout.create
+
+type ShoutoutCreateEventSubResponse struct {
+	Subscription EventsubSubscription        `json:"subscription"`
+	Event        ShoutoutCreateEventSubEvent `json:"event"`
+}
+
+type ShoutoutCreateEventSubEvent struct {
+	BroadcasterUserID      string `json:"broadcaster_user_id"`
+	BroadcasterUserName    string `json:"broadcaster_user_name"`
+	BroadcasterUserLogin   string `json:"broadcaster_user_login"`
+	ToBroadcasterUserID    string `json:"to_broadcaster_user_id"`
+	ToBroadcasterUserName  string `json:"to_broadcaster_user_name"`
+	ToBroadcasterUserLogin string `json:"to_broadcaster_user_login"`
+	ModeratorUserID        string `json:"moderator_user_id"`
+	ModeratorUserName      string `json:"moderator_user_name"`
+	ModeratorUserLogin     string `json:"moderator_user_login"`
+	ViewerCount            int    `json:"viewer_count"`
+	StartedAt              string `json:"started_at"`
+	CooldownEndsAt         string `json:"cooldown_ends_at"`
+	TargetCooldownEndsAt   string `json:"target_cooldown_ends_at"`
+}
+
+// channel.shoutout.receive
+
+type ShoutoutReceivedEventSubResponse struct {
+	Subscription EventsubSubscription          `json:"subscription"`
+	Event        ShoutoutReceivedEventSubEvent `json:"event"`
+}
+
+type ShoutoutReceivedEventSubEvent 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              int    `json:"viewer_count"`
+	StartedAt                string `json:"started_at"`
+}

From 7db9615c6a6ba5879f14a41650cda4dd53676df2 Mon Sep 17 00:00:00 2001
From: Marc Szanto <11840265+Xemdo@users.noreply.github.com>
Date: Thu, 19 Jan 2023 13:35:04 -0800
Subject: [PATCH 2/2] Forgot tests

---
 .../events/types/shoutout/shoutout_test.go    | 107 ++++++++++++++++++
 1 file changed, 107 insertions(+)
 create mode 100644 internal/events/types/shoutout/shoutout_test.go

diff --git a/internal/events/types/shoutout/shoutout_test.go b/internal/events/types/shoutout/shoutout_test.go
new file mode 100644
index 00000000..b2407fa1
--- /dev/null
+++ b/internal/events/types/shoutout/shoutout_test.go
@@ -0,0 +1,107 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+package shoutout
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/twitchdev/twitch-cli/internal/events"
+	"github.com/twitchdev/twitch-cli/internal/models"
+	"github.com/twitchdev/twitch-cli/test_setup"
+)
+
+var fromUser = "1234"
+var toUser = "4567"
+
+func TestEventSub(t *testing.T) {
+	a := test_setup.SetupTestEnv(t)
+
+	beginParams := *&events.MockEventParameters{
+		FromUserID:         fromUser,
+		ToUserID:           toUser,
+		Transport:          models.TransportEventSub,
+		Trigger:            "shoutout-create",
+		SubscriptionStatus: "enabled",
+		Cost:               0,
+	}
+	endParams := *&events.MockEventParameters{
+		FromUserID:         fromUser,
+		ToUserID:           toUser,
+		Transport:          models.TransportEventSub,
+		Trigger:            "shoutout-received",
+		SubscriptionStatus: "enabled",
+		Cost:               0,
+	}
+
+	r1, err := Event{}.GenerateEvent(beginParams)
+	a.Nil(err)
+
+	r2, err := Event{}.GenerateEvent(endParams)
+	a.Nil(err)
+
+	var body1 models.ShoutoutCreateEventSubResponse
+	err = json.Unmarshal(r1.JSON, &body1)
+	a.Nil(err)
+
+	var body2 models.ShoutoutReceivedEventSubResponse
+	err = json.Unmarshal(r2.JSON, &body2)
+	a.Nil(err)
+}
+
+func TestFakeTransport(t *testing.T) {
+	a := test_setup.SetupTestEnv(t)
+
+	beginParams := *&events.MockEventParameters{
+		FromUserID:         fromUser,
+		ToUserID:           toUser,
+		Transport:          "fake_transport",
+		Trigger:            "shoutout-create",
+		SubscriptionStatus: "enabled",
+	}
+	endParams := *&events.MockEventParameters{
+		FromUserID:         fromUser,
+		ToUserID:           toUser,
+		Transport:          "fake_transport",
+		Trigger:            "shoutout-received",
+		SubscriptionStatus: "enabled",
+	}
+
+	r1, err1 := Event{}.GenerateEvent(beginParams)
+	r2, err2 := Event{}.GenerateEvent(endParams)
+	a.Nil(err1)
+	a.Nil(err2)
+	a.Empty(r1)
+	a.Empty(r2)
+}
+func TestValidTrigger(t *testing.T) {
+	a := test_setup.SetupTestEnv(t)
+
+	r := Event{}.ValidTrigger("shoutout-create")
+	a.Equal(true, r)
+
+	r = Event{}.ValidTrigger("shoutout-received")
+	a.Equal(true, r)
+
+	r = Event{}.ValidTrigger("notshoutout")
+	a.Equal(false, r)
+}
+
+func TestValidTransport(t *testing.T) {
+	a := test_setup.SetupTestEnv(t)
+
+	r := Event{}.ValidTransport(models.TransportEventSub)
+	a.Equal(true, r)
+
+	r = Event{}.ValidTransport("noteventsub")
+	a.Equal(false, r)
+}
+func TestGetTopic(t *testing.T) {
+	a := test_setup.SetupTestEnv(t)
+
+	r := Event{}.GetTopic(models.TransportEventSub, "shoutout-create")
+	a.NotNil(r)
+
+	r = Event{}.GetTopic(models.TransportEventSub, "shoutout-receieve")
+	a.NotNil(r)
+}