diff --git a/docs/event.md b/docs/event.md index 9a88cb1..c6cd5ed 100644 --- a/docs/event.md +++ b/docs/event.md @@ -35,53 +35,54 @@ Used to either create or send mock events for use with local webhooks testing. This command can take either the Event or Alias listed as an argument. It is preferred that you work with the Event, but for backwards compatibility Aliases still work. -| Event | Alias | Description | -|----------------------------------------------------------|-----------------------|-------------| -| `channel.ban` | `ban` | Channel ban event. | -| `channel.channel_points_custom_reward.add` | `add-reward` | Channel Points event for a Custom Reward being added. | -| `channel.channel_points_custom_reward.remove` | `remove-reward` | Channel Points event for a Custom Reward being removed. | -| `channel.channel_points_custom_reward.update` | `update-reward` | Channel Points event for a Custom Reward being updated. | -| `channel.channel_points_custom_reward_redemption.add` | `add-redemption` | Channel Points EventSub event for a redemption being performed. | -| `channel.channel_points_custom_reward_redemption.update` | `add-update` | Channel Points EventSub event for a redemption being performed. | -| `channel.charity_campaign.donate` | `charity-donate` | Charity campaign donation occurance event. | -| `channel.charity_campaign.progress` | `charity-progress` | Charity campaign progress event. | -| `channel.charity_campaign.start` | `charity-start` | Charity campaign start event. | -| `channel.charity_campaign.stop` | `charity-stop` | Charity campaign stop event. | -| `channel.cheer` | `cheer` | Channel event for receiving cheers. | -| `channel.follow` | `follow` | Channel event for receiving a follow. | -| `channel.goal.begin` | `goal-begin` | Channel creator goal start event. | -| `channel.goal.end` | `goal-end` | Channel creator goal end event. | -| `channel.goal.progress` | `goal-progress` | Channel creator goal progress event. | -| `channel.hype_train.begin` | `hype-train-begin` | Channel hype train start event. | -| `channel.hype_train.end` | `hype-train-end` | Channel hype train start event. | -| `channel.hype_train.progress` | `hype-train-progress` | Channel hype train start event. | -| `channel.moderator.add` | `add-moderator` | Channel moderator add event. | -| `channel.moderator.remove` | `remove-moderator` | Channel moderator removal event. | -| `channel.poll.begin` | `poll-begin` | Channel poll begin event. | -| `channel.poll.end` | `poll-end` | Channel poll end event. | -| `channel.poll.progress` | `poll-progress` | Channel poll progress event. | -| `channel.prediction.begin` | `prediction-begin` | Channel prediction begin event. | -| `channel.prediction.end` | `prediction-end` | Channel prediction end event. | -| `channel.prediction.lock` | `prediction-lock` | Channel prediction lock event. | -| `channel.prediction.progress` | `prediction-progress` | Channel prediction progress event. | -| `channel.raid` | `raid` | Channel raid event with a random viewer count. | -| `channel.shield_mode.begin` | `shield-mode-begin` | Channel Shield Mode activate event. | -| `channel.shield_mode.end` | `shield-mode-end` | Channel Shield Mode deactivate event. | -| `channel.shoutout.create` | `shoutout-create` | Channel shoutout created event. This is for outgoing shoutouts, from your channel to another. | -| `channel.shoutout.receive` | `shoutout-received` | Channel shoutout created event. This is for incoming shoutouts, to your channel from anothers. | -| `channel.subscribe` | `subscribe` | A standard subscription event. Triggers a basic tier 1 sub, but can be flexible with --tier | -| `channel.subscribe` | `gift` | A gifted subscription event. Triggers a basic tier 1 sub, but can be flexible with --tier | -| `channel.subscription.end` | `unsubscribe` | A standard subscription end event. Triggers a basic tier 1 sub, but can be flexible with --tier | -| `channel.subscription.gift` | `channel-gift` | Channel gifting event; not to be confused with the `gift` event. This event is a description of the number of gifts given by a user. | -| `channel.subscription.message` | `subscribe-message` | Subscription Message event. | -| `channel.unban` | `unban` | Channel unban event. | -| `channel.update` | `stream-change` | Channel update event. When a broadcaster updates channel properties. | -| `drop.entitlement.grant` | `drop` | Drop Entitlement event. | -| `extension.bits_transaction.create` | `transaction` | Bits in Extensions transactions events. | -| `stream.offline` | `streamdown` | Stream offline event. | -| `stream.online` | `streamup` | Stream online event. | -| `user.authorization.grant` | `grant` | Authorization grant event. | -| `user.authorization.revoke` | `revoke` | User authorization revoke event. Uses local Client as set in `twitch configure` or generates one randomly. | +| Event | Alias | Description | +|----------------------------------------------------------|------------------------|-------------| +| `automod.message.hold` | `automod-message-hold` | Automod caught a message for review. | +| `channel.ban` | `ban` | Channel ban event. | +| `channel.channel_points_custom_reward.add` | `add-reward` | Channel Points event for a Custom Reward being added. | +| `channel.channel_points_custom_reward.remove` | `remove-reward` | Channel Points event for a Custom Reward being removed. | +| `channel.channel_points_custom_reward.update` | `update-reward` | Channel Points event for a Custom Reward being updated. | +| `channel.channel_points_custom_reward_redemption.add` | `add-redemption` | Channel Points EventSub event for a redemption being performed. | +| `channel.channel_points_custom_reward_redemption.update` | `add-update` | Channel Points EventSub event for a redemption being performed. | +| `channel.charity_campaign.donate` | `charity-donate` | Charity campaign donation occurance event. | +| `channel.charity_campaign.progress` | `charity-progress` | Charity campaign progress event. | +| `channel.charity_campaign.start` | `charity-start` | Charity campaign start event. | +| `channel.charity_campaign.stop` | `charity-stop` | Charity campaign stop event. | +| `channel.cheer` | `cheer` | Channel event for receiving cheers. | +| `channel.follow` | `follow` | Channel event for receiving a follow. | +| `channel.goal.begin` | `goal-begin` | Channel creator goal start event. | +| `channel.goal.end` | `goal-end` | Channel creator goal end event. | +| `channel.goal.progress` | `goal-progress` | Channel creator goal progress event. | +| `channel.hype_train.begin` | `hype-train-begin` | Channel hype train start event. | +| `channel.hype_train.end` | `hype-train-end` | Channel hype train start event. | +| `channel.hype_train.progress` | `hype-train-progress` | Channel hype train start event. | +| `channel.moderator.add` | `add-moderator` | Channel moderator add event. | +| `channel.moderator.remove` | `remove-moderator` | Channel moderator removal event. | +| `channel.poll.begin` | `poll-begin` | Channel poll begin event. | +| `channel.poll.end` | `poll-end` | Channel poll end event. | +| `channel.poll.progress` | `poll-progress` | Channel poll progress event. | +| `channel.prediction.begin` | `prediction-begin` | Channel prediction begin event. | +| `channel.prediction.end` | `prediction-end` | Channel prediction end event. | +| `channel.prediction.lock` | `prediction-lock` | Channel prediction lock event. | +| `channel.prediction.progress` | `prediction-progress` | Channel prediction progress event. | +| `channel.raid` | `raid` | Channel raid event with a random viewer count. | +| `channel.shield_mode.begin` | `shield-mode-begin` | Channel Shield Mode activate event. | +| `channel.shield_mode.end` | `shield-mode-end` | Channel Shield Mode deactivate event. | +| `channel.shoutout.create` | `shoutout-create` | Channel shoutout created event. This is for outgoing shoutouts, from your channel to another. | +| `channel.shoutout.receive` | `shoutout-received` | Channel shoutout created event. This is for incoming shoutouts, to your channel from anothers. | +| `channel.subscribe` | `subscribe` | A standard subscription event. Triggers a basic tier 1 sub, but can be flexible with --tier | +| `channel.subscribe` | `gift` | A gifted subscription event. Triggers a basic tier 1 sub, but can be flexible with --tier | +| `channel.subscription.end` | `unsubscribe` | A standard subscription end event. Triggers a basic tier 1 sub, but can be flexible with --tier | +| `channel.subscription.gift` | `channel-gift` | Channel gifting event; not to be confused with the `gift` event. This event is a description of the number of gifts given by a user. | +| `channel.subscription.message` | `subscribe-message` | Subscription Message event. | +| `channel.unban` | `unban` | Channel unban event. | +| `channel.update` | `stream-change` | Channel update event. When a broadcaster updates channel properties. | +| `drop.entitlement.grant` | `drop` | Drop Entitlement event. | +| `extension.bits_transaction.create` | `transaction` | Bits in Extensions transactions events. | +| `stream.offline` | `streamdown` | Stream offline event. | +| `stream.online` | `streamup` | Stream online event. | +| `user.authorization.grant` | `grant` | Authorization grant event. | +| `user.authorization.revoke` | `revoke` | User authorization revoke event. Uses local Client as set in `twitch configure` or generates one randomly. | diff --git a/internal/events/types/automod_message_hold/automod_message_hold_event.go b/internal/events/types/automod_message_hold/automod_message_hold_event.go new file mode 100644 index 0000000..377eb54 --- /dev/null +++ b/internal/events/types/automod_message_hold/automod_message_hold_event.go @@ -0,0 +1,164 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package automod_message_hold + +import ( + "encoding/json" + "strings" + + "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.TransportWebhook: true, + models.TransportWebSocket: true, +} + +var triggerSupported = []string{"automod-message-hold"} + +var triggerMapping = map[string]map[string]string{ + models.TransportWebhook: { + "automod-message-hold": "automod.message.hold", + }, + models.TransportWebSocket: { + "automod-message-hold": "automod.message.hold", + }, +} + +type Event struct{} + +func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEventResponse, error) { + var event []byte + var err error + + switch params.Transport { + case models.TransportWebhook, models.TransportWebSocket: + body := &models.EventsubResponse{ + Subscription: models.EventsubSubscription{ + ID: params.SubscriptionID, + Status: params.SubscriptionStatus, + Type: triggerMapping[params.Transport][params.Trigger], + Version: e.SubscriptionVersion(), + Condition: models.EventsubCondition{ + BroadcasterUserID: params.ToUserID, + ModeratorUserID: params.ToUserID, + }, + Transport: models.EventsubTransport{ + Method: "webhook", + Callback: "null", + }, + Cost: 0, + CreatedAt: params.Timestamp, + }, + Event: models.AutomodMessageHoldEvent{ + UserID: params.FromUserID, + UserLogin: params.FromUserName, + UserName: params.FromUserName, + BroadcasterUserID: params.ToUserID, + BroadcasterUserLogin: params.ToUserName, + BroadcasterUserName: params.ToUserName, + MessageID: util.RandomGUID(), + Message: models.AutomodMessage{ + Text: "I am very angry SwiftRage", + Fragments: []models.AutomodMessageFragment{ + { + Type: "text", + Text: "I am very angry", + }, + { + Type: "emote", + Text: "SwiftRage", + Emote: &models.MessagePartialEmote{ + ID: "34", + EmoteSetID: "0", + }, + }, + }, + }, + HeldAt: params.Timestamp, + Reason: "blocked_term", + BlockedTerm: &models.AutomodMessageBlockedTermReason{ + TermsFound: []models.AutomodMessageFoundTerm{ + { + TermID: util.RandomGUID(), + Boundary: models.AutomodMessageBoundary{ + StartPos: 10, + EndPos: 14, + }, + OwnerBroadcasterUserID: params.ToUserID, + OwnerBroadcasterUserLogin: params.ToUserName, + OwnerBroadcasterUserName: params.ToUserName, + }, + }, + }, + }, + } + 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 any + if err := json.Unmarshal([]byte(event), &i); err != nil { + return events.MockEventResponse{}, err + } + if m, ok := i.(map[string]any); 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.EventMessageID, + JSON: event, + FromUser: params.FromUserID, + ToUser: params.ToUserID, + }, nil +} + +func (e Event) ValidTransport(t string) bool { + return transportsSupported[t] +} + +func (e Event) ValidTrigger(t string) bool { + for _, ts := range triggerSupported { + if ts == t { + return true + } + } + return false +} +func (e Event) GetTopic(transport string, trigger string) string { + return triggerMapping[transport][trigger] +} +func (e Event) GetAllTopicsByTransport(transport string) []string { + allTopics := []string{} + for _, topic := range triggerMapping[transport] { + allTopics = append(allTopics, topic) + } + return allTopics +} +func (e Event) GetEventSubAlias(t string) string { + // check for aliases + for trigger, topic := range triggerMapping[models.TransportWebhook] { + if topic == t { + return trigger + } + } + return "" +} + +func (e Event) SubscriptionVersion() string { + return "2" +} diff --git a/internal/events/types/automod_message_hold/automod_message_hold_event_test.go b/internal/events/types/automod_message_hold/automod_message_hold_event_test.go new file mode 100644 index 0000000..1908051 --- /dev/null +++ b/internal/events/types/automod_message_hold/automod_message_hold_event_test.go @@ -0,0 +1,76 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package automod_message_hold + +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) + + params := events.MockEventParameters{ + FromUserID: fromUser, + ToUserID: toUser, + Transport: models.TransportWebhook, + Trigger: "automod.message.hold", + SubscriptionStatus: "enabled", + } + + r, err := Event{}.GenerateEvent(params) + a.Nil(err) + + var body models.AutomodMessageHoldEventSubResponse + err = json.Unmarshal(r.JSON, &body) + a.Nil(err) + + a.Equal(toUser, body.Event.BroadcasterUserID, "Expected to user %v, got %v", toUser, body.Event.BroadcasterUserID) + a.Equal(fromUser, body.Event.UserID, "Expected from user %v, got %v", r.ToUser, body.Event.UserID) +} +func TestFakeTransport(t *testing.T) { + a := test_setup.SetupTestEnv(t) + + params := events.MockEventParameters{ + FromUserID: fromUser, + ToUserID: toUser, + Transport: "fake_transport", + Trigger: "automod.message.hold", + } + + r, err := Event{}.GenerateEvent(params) + a.Nil(err) + a.Empty(r) +} +func TestValidTrigger(t *testing.T) { + a := test_setup.SetupTestEnv(t) + + r := Event{}.ValidTrigger("automod-message-hold") + a.Equal(true, r) + + r = Event{}.ValidTrigger("automod") + a.Equal(false, r) +} + +func TestValidTransport(t *testing.T) { + a := test_setup.SetupTestEnv(t) + + r := Event{}.ValidTransport(models.TransportWebhook) + 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.TransportWebhook, "automod.message.hold") + a.NotNil(r) +} diff --git a/internal/events/types/types.go b/internal/events/types/types.go index 77222aa..a0a31f5 100644 --- a/internal/events/types/types.go +++ b/internal/events/types/types.go @@ -12,6 +12,7 @@ import ( "github.com/twitchdev/twitch-cli/internal/events/types/ad_break" "github.com/twitchdev/twitch-cli/internal/events/types/authorization_grant" "github.com/twitchdev/twitch-cli/internal/events/types/authorization_revoke" + "github.com/twitchdev/twitch-cli/internal/events/types/automod_message_hold" "github.com/twitchdev/twitch-cli/internal/events/types/ban" "github.com/twitchdev/twitch-cli/internal/events/types/channel_points_redemption" "github.com/twitchdev/twitch-cli/internal/events/types/channel_points_reward" @@ -46,6 +47,7 @@ func AllEvents() []events.MockEvent { ad_break.Event{}, authorization_grant.Event{}, authorization_revoke.Event{}, + automod_message_hold.Event{}, ban.Event{}, channel_points_redemption.Event{}, channel_points_reward.Event{}, diff --git a/internal/models/automod.go b/internal/models/automod.go new file mode 100644 index 0000000..9af2ef5 --- /dev/null +++ b/internal/models/automod.go @@ -0,0 +1,58 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package models + +type AutomodMessageHoldEvent struct { + BroadcasterUserID string `json:"broadcaster_user_id"` + BroadcasterUserLogin string `json:"broadcaster_user_login"` + BroadcasterUserName string `json:"broadcaster_user_name"` + UserID string `json:"user_id"` + UserLogin string `json:"user_login"` + UserName string `json:"user_name"` + MessageID string `json:"message_id"` + Message AutomodMessage `json:"message"` + HeldAt string `json:"held_at"` + Reason string `json:"reason"` + Automod *AutomodMessageAutomodReason `json:"automod"` + BlockedTerm *AutomodMessageBlockedTermReason `json:"blocked_term"` +} + +type AutomodMessageHoldEventSubResponse struct { + Subscription EventsubSubscription `json:"subscription"` + Event AutomodMessageHoldEvent `json:"event"` +} + +type AutomodMessageBoundary struct { + StartPos int `json:"start_pos"` + EndPos int `json:"end_pos"` +} + +type AutomodMessageAutomodReason struct { + Category string `json:"category"` + Level int `json:"level"` + Boundaries []AutomodMessageBoundary `json:"boundaries"` +} + +type AutomodMessageFoundTerm struct { + TermID string `json:"term_id"` + Boundary AutomodMessageBoundary `json:"boundary"` + OwnerBroadcasterUserID string `json:"owner_broadcaster_user_id"` + OwnerBroadcasterUserLogin string `json:"owner_broadcaster_user_login"` + OwnerBroadcasterUserName string `json:"owner_broadcaster_user_name"` +} + +type AutomodMessageBlockedTermReason struct { + TermsFound []AutomodMessageFoundTerm `json:"terms_found"` +} + +type AutomodMessage struct { + Text string `json:"text"` + Fragments []AutomodMessageFragment `json:"fragments"` +} + +type AutomodMessageFragment struct { + Type string `json:"type"` + Text string `json:"text"` + Emote *MessagePartialEmote `json:"emote"` + Cheermote *MessageCheermote `json:"cheermote"` +} diff --git a/internal/models/message.go b/internal/models/message.go new file mode 100644 index 0000000..6beb30a --- /dev/null +++ b/internal/models/message.go @@ -0,0 +1,14 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package models + +type MessagePartialEmote struct { + ID string `json:"id"` + EmoteSetID string `json:"emote_set_id"` +} + +type MessageCheermote struct { + Prefix string `json:"prefix"` + Bits int `json:"bits"` + Tier int `json:"tier"` +}