diff --git a/cmd/events.go b/cmd/events.go
index 53006c6c..94ba765c 100644
--- a/cmd/events.go
+++ b/cmd/events.go
@@ -10,10 +10,11 @@ import (
"github.com/spf13/cobra"
"github.com/twitchdev/twitch-cli/internal/events"
- "github.com/twitchdev/twitch-cli/internal/events/mock_wss_server"
"github.com/twitchdev/twitch-cli/internal/events/trigger"
"github.com/twitchdev/twitch-cli/internal/events/types"
"github.com/twitchdev/twitch-cli/internal/events/verify"
+ "github.com/twitchdev/twitch-cli/internal/events/websocket"
+ "github.com/twitchdev/twitch-cli/internal/events/websocket/mock_server"
"github.com/twitchdev/twitch-cli/internal/util"
)
@@ -41,9 +42,18 @@ var (
timestamp string
charityCurrentValue int
charityTargetValue int
- debug bool
- wssReconnectTest int
- sslEnabled bool
+ clientId string
+ websocketClient string
+)
+
+// websocketCmd-specific flags
+var (
+ wsDebug bool
+ wsStrict bool
+ wsClient string
+ wsSubscription string
+ wsStatus string
+ wsReason string
)
var eventCmd = &cobra.Command{
@@ -56,9 +66,9 @@ var triggerCmd = &cobra.Command{
Short: "Creates mock events that can be forwarded to a local webserver for event testing.",
Long: fmt.Sprintf(`Creates mock events that can be forwarded to a local webserver for event testing.
Supported:
- %s`, types.AllEventTopics()),
+ %s`, types.AllWebhookTopics()),
Args: cobra.MaximumNArgs(1),
- ValidArgs: types.AllEventTopics(),
+ ValidArgs: types.AllWebhookTopics(),
Run: triggerCmdRun,
Example: `twitch event trigger subscribe`,
Aliases: []string{
@@ -71,9 +81,9 @@ var verifyCmd = &cobra.Command{
Short: "Mocks the subscription verification event. Can be forwarded to a local webserver for testing.",
Long: fmt.Sprintf(`Mocks the subscription verification event that can be forwarded to a local webserver for testing.
Supported:
- %s`, types.AllEventTopics()),
+ %s`, types.AllWebhookTopics()),
Args: cobra.MaximumNArgs(1),
- ValidArgs: types.AllEventTopics(),
+ ValidArgs: types.AllWebhookTopics(),
Run: verifyCmdRun,
Example: `twitch event verify-subscription subscribe`,
Aliases: []string{
@@ -81,6 +91,24 @@ var verifyCmd = &cobra.Command{
},
}
+var websocketCmd = &cobra.Command{
+ Use: "websocket [action]",
+ Short: `Executes actions regarding the mock EventSub WebSocket server. See "twitch event websocket --help" for usage info.`,
+ Long: fmt.Sprintf(`Executes actions regarding the mock EventSub WebSocket server.`),
+ Args: cobra.MaximumNArgs(1),
+ Run: websocketCmdRun,
+ Example: fmt.Sprintf(` twitch event websocket start-server
+ twitch event websocket reconnect
+ twitch event websocket close --session=e411cc1e_a2613d4e --reason=4006
+ twitch event websocket subscription --status=user_removed --subscription=82a855-fae8-93bff0`,
+ ),
+ Aliases: []string{
+ "websockets",
+ "ws",
+ "wss",
+ },
+}
+
var retriggerCmd = &cobra.Command{
Use: "retrigger",
Short: "Refires events based on the event ID. Can be forwarded to the local webserver for event testing.",
@@ -89,24 +117,19 @@ var retriggerCmd = &cobra.Command{
}
var startWebsocketServerCmd = &cobra.Command{
- Use: "start-websocket-server",
- Short: `Starts a local websocket server at "ws://localhost:8080/eventsub" or at another preferred port.`,
- Run: startWebsocketServerCmdRun,
- Example: `twitch event start-websocket-server`,
- Aliases: []string{
- "ws",
- "wss",
- },
+ Use: "start-websocket-server",
+ Deprecated: `use "twitch event websocket start-server" instead.`,
}
func init() {
rootCmd.AddCommand(eventCmd)
- eventCmd.AddCommand(triggerCmd, retriggerCmd, verifyCmd, startWebsocketServerCmd)
+
+ eventCmd.AddCommand(triggerCmd, retriggerCmd, verifyCmd, websocketCmd, startWebsocketServerCmd)
// trigger flags
//// flags for forwarding functionality/changing payloads
- triggerCmd.Flags().StringVarP(&forwardAddress, "forward-address", "F", "", "Forward address for mock event.")
- triggerCmd.Flags().StringVarP(&transport, "transport", "T", "eventsub", fmt.Sprintf("Preferred transport method for event. Defaults to /EventSub.\nSupported values: %s", events.ValidTransports()))
+ triggerCmd.Flags().StringVarP(&forwardAddress, "forward-address", "F", "", "Forward address for mock event (webhook only).")
+ triggerCmd.Flags().StringVarP(&transport, "transport", "T", "webhook", fmt.Sprintf("Preferred transport method for event. Defaults to /EventSub.\nSupported values: %s", events.ValidTransports()))
triggerCmd.Flags().StringVarP(&secret, "secret", "s", "", "Webhook secret. If defined, signs all forwarded events with the SHA256 HMAC and must be 10-100 characters in length.")
// trigger flags
@@ -128,26 +151,35 @@ func init() {
triggerCmd.Flags().StringVar(×tamp, "timestamp", "", "Sets the timestamp to be used in payloads and headers. Must be in RFC3339Nano format.")
triggerCmd.Flags().IntVar(&charityCurrentValue, "charity-current-value", 0, "Only used for \"charity-*\" events. Manually set the current dollar value for charity events.")
triggerCmd.Flags().IntVar(&charityTargetValue, "charity-target-value", 1500000, "Only used for \"charity-*\" events. Manually set the target dollar value for charity events.")
+ triggerCmd.Flags().StringVar(&clientId, "client-id", "", "Manually set the Client ID used in revoke, grant, and bits transaction events.")
+ triggerCmd.Flags().StringVar(&websocketClient, "session", "", "Defines a specific websocket client/session to forward an event to. Used only with \"websocket\" transport.")
// retrigger flags
- retriggerCmd.Flags().StringVarP(&forwardAddress, "forward-address", "F", "", "Forward address for mock event.")
+ retriggerCmd.Flags().StringVarP(&forwardAddress, "forward-address", "F", "", "Forward address for mock event (webhook only).")
retriggerCmd.Flags().StringVarP(&eventID, "id", "i", "", "ID of the event to be refired.")
retriggerCmd.Flags().StringVarP(&secret, "secret", "s", "", "Webhook secret. If defined, signs all forwarded events with the SHA256 HMAC and must be 10-100 characters in length.")
retriggerCmd.MarkFlagRequired("id")
// verify-subscription flags
- verifyCmd.Flags().StringVarP(&forwardAddress, "forward-address", "F", "", "Forward address for mock event.")
- verifyCmd.Flags().StringVarP(&transport, "transport", "T", "eventsub", fmt.Sprintf("Preferred transport method for event. Defaults to EventSub.\nSupported values: %s", events.ValidTransports()))
+ verifyCmd.Flags().StringVarP(&forwardAddress, "forward-address", "F", "", "Forward address for mock event (webhook only).")
+ verifyCmd.Flags().StringVarP(&transport, "transport", "T", "webhook", fmt.Sprintf("Preferred transport method for event. Defaults to EventSub.\nSupported values: %s", events.ValidTransports()))
verifyCmd.Flags().StringVarP(&secret, "secret", "s", "", "Webhook secret. If defined, signs all forwarded events with the SHA256 HMAC and must be 10-100 characters in length.")
verifyCmd.Flags().StringVar(×tamp, "timestamp", "", "Sets the timestamp to be used in payloads and headers. Must be in RFC3339Nano format.")
verifyCmd.Flags().StringVarP(&eventID, "subscription-id", "u", "", "Manually set the subscription/event ID of the event itself.") // TODO: This description will need to change with https://github.com/twitchdev/twitch-cli/issues/184
verifyCmd.MarkFlagRequired("forward-address")
- // start-websocket-server flags
- startWebsocketServerCmd.Flags().IntVarP(&port, "port", "p", 8080, "Defines the port that the mock EventSub websocket server will run on.")
- startWebsocketServerCmd.Flags().BoolVar(&debug, "debug", false, "Set on/off for debug messages for the EventSub WebSocket server.")
- startWebsocketServerCmd.Flags().BoolVar(&sslEnabled, "ssl", false, "Sets on/off for SSL. Recommended to keep 'false', as most testing does not require this.")
- startWebsocketServerCmd.Flags().IntVarP(&wssReconnectTest, "reconnect", "r", -1, "Used to test WebSocket Reconnect message. Sets delay (in seconds) from first client connection until the reconnect occurs.")
+ // websocket flags
+ /// flags for start-server
+ websocketCmd.Flags().IntVarP(&port, "port", "p", 8080, "Defines the port that the mock EventSub websocket server will run on.")
+ websocketCmd.Flags().BoolVar(&wsDebug, "debug", false, "Set on/off for debug messages for the EventSub WebSocket server.")
+ websocketCmd.Flags().BoolVarP(&wsStrict, "require-subscription", "S", false, "Requires subscriptions for all events, and activates 10 second subscription requirement.")
+
+ // websocket flags
+ /// flags for everything else
+ websocketCmd.Flags().StringVarP(&wsClient, "session", "s", "", "WebSocket client/session to target with your server command. Used in multiple commands.")
+ websocketCmd.Flags().StringVar(&wsSubscription, "subscription", "", `Subscription to target with your server command. Used with "websocket subscription".`)
+ websocketCmd.Flags().StringVar(&wsStatus, "status", "", `Changes the status of an existing subscription. Used with "websocket subscription".`)
+ websocketCmd.Flags().StringVar(&wsReason, "reason", "", `Sets the close reason when sending a Close message to the client. Used with "websocket close".`)
}
func triggerCmdRun(cmd *cobra.Command, args []string) {
@@ -197,6 +229,8 @@ func triggerCmdRun(cmd *cobra.Command, args []string) {
Timestamp: timestamp,
CharityCurrentValue: charityCurrentValue,
CharityTargetValue: charityTargetValue,
+ ClientID: clientId,
+ WebSocketClient: websocketClient,
})
if err != nil {
@@ -286,13 +320,22 @@ https://dev.twitch.tv/docs/eventsub/handling-webhook-events#processing-an-event`
}
}
-func startWebsocketServerCmdRun(cmd *cobra.Command, args []string) {
- wsStr := "ws"
- if sslEnabled {
- wsStr = "wss"
+func websocketCmdRun(cmd *cobra.Command, args []string) {
+ if len(args) == 0 {
+ cmd.Help()
+ return
}
- log.Printf("Starting mock EventSub WebSocket servers on %v://localhost:%v/eventsub (alternate on port %v)", wsStr, port, port+1)
- log.Printf("`Ctrl + C` to exit mock servers.")
- mock_wss_server.StartServer(port, debug, wssReconnectTest, sslEnabled)
+ if args[0] == "start-server" || args[0] == "start" {
+ log.Printf("`Ctrl + C` to exit mock WebSocket servers.")
+ mock_server.StartWebsocketServer(wsDebug, port, wsStrict)
+ } else {
+ // Forward all other commands via RPC
+ websocket.ForwardWebsocketCommand(args[0], websocket.WebsocketCommandParameters{
+ Client: wsClient,
+ Subscription: wsSubscription,
+ SubscriptionStatus: wsStatus,
+ CloseReason: wsReason,
+ })
+ }
}
diff --git a/docs/event.md b/docs/event.md
index bed8be73..f544b0f1 100644
--- a/docs/event.md
+++ b/docs/event.md
@@ -5,6 +5,7 @@
- [Trigger](#trigger)
- [Retrigger](#retrigger)
- [Verify-Subscription](#verify-subscription)
+ - [Websocket](#websocket)
## Description
@@ -16,68 +17,85 @@ Used to either create or send mock events for use with local webhooks testing.
**Args**
-| Argument | Description |
-|-----------------------|--------------------------------------------------------------------------------------------------------------------------------------|
-| `add-moderator` | Channel moderator add event. |
-| `add-redemption` | Channel Points EventSub event for a redemption being performed. |
-| `add-reward` | Channel Points EventSub event for a Custom Reward being added. |
-| `ban` | Channel ban event. |
-| `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. |
-| `cheer` | Only usable with the `eventsub` transport |
-| `drop` | Drops Entitlement event. |
-| `gift` | A gifted subscription event. Triggers a basic tier 1 sub. |
-| `goal-begin` | Channel creator goal start event. |
-| `goal-end` | Channel creator goal end event. |
-| `goal-progress` | Channel creator goal progress event. |
-| `grant` | Authorization grant event. |
-| `hype-train-begin` | Channel hype train start event. |
-| `hype-train-end` | Channel hype train end event. |
-| `hype-train-progress` | Channel hype train progress event. |
-| `poll-begin` | Channel poll begin event. |
-| `poll-end` | Channel poll end event. |
-| `poll-progress` | Channel poll progress event. |
-| `prediction-begin` | Channel prediction begin event. |
-| `prediction-end` | Channel prediction end event. |
-| `prediction-lock` | Channel prediction lock event. |
-| `prediction-progress` | Channel prediction progress event. |
-| `raid` | Channel Raid event with a random viewer count. |
-| `remove-moderator` | Channel moderator removal event. |
-| `remove-reward` | Channel Points EventSub event for a Custom Reward being removed. |
-| `revoke` | User authorization revoke event. Uses local Client as set in `twitch configure` or generates one randomly. |
-| `stream-change` | Stream Changed event. |
-| `streamdown` | Stream offline event. |
-| `streamup` | Stream online event. |
-| `subscribe-message` | Subscription Message event. |
-| `subscribe` | A standard subscription event. Triggers a basic tier 1 sub. |
-| `transaction` | Bits in Extensions transactions events. |
-| `unban` | Channel unban event. |
-| `unsubscribe` | A standard unsubscribe event. Triggers a basic tier 1 sub. |
-| `update-redemption` | Channel Points EventSub event for a redemption being updated. |
-| `update-reward` | Channel Points EventSub event for a Custom Reward being updated. |
-| `user.update` | A user updates their account. [User Update](https://dev.twitch.tv/docs/eventsub/eventsub-subscription-types#user-update) |
+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. |
**Flags**
-| Flag | Shorthand | Description | Example | Required? (Y/N) |
-|---------------------|-----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------|-----------------|
-| `--forward-address` | `-F` | Web server address for where to send mock events. | `-F https://localhost:8080` | N |
-| `--transport` | `-T` | The method used to send events. Default is `eventsub`. | `-T eventsub` | N |
-| `--to-user` | `-t` | Denotes the receiver's TUID of the event, usually the broadcaster. | `-t 44635596` | N |
-| `--from-user` | `-f` | Denotes the sender's TUID of the event, for example the user that follows another user or the subscriber to a broadcaster. | `-f 44635596` | N |
-| `--gift-user` | `-g` | Used only for subcription-based events, denotes the gifting user ID | `-g 44635596` | N |
-| `--secret` | `-s` | Webhook secret. If defined, signs all forwarded events with the SHA256 HMAC and must be 10-100 characters in length. | `-s testsecret` | N |
-| `--count` | `-c` | Count of events to fire. This can be used to simulate an influx of events. | `-c 100` | N |
-| `--anonymous` | `-a` | If the event is anonymous. Only applies to `gift` and `cheer` events. | `-a` | N |
-| `--status` | `-S` | Status of the event object, currently applies to channel points redemptions. | `-S fulfilled` | N |
-| `--item-id` | `-i` | Manually set the ID of the event payload item (for example the reward ID in redemption events or game in stream events). | `-i 032e4a6c-4aef-11eb-a9f5-1f703d1f0b92` | N |
-| `--item-name` | `-n` | Manually set the name of the event payload item (for example the reward ID in redemption events or game name in stream events). | `-n "Science & Technology"` | N |
-| `--cost` | `-C` | Amount of bits or channel points redeemed/used in the event. | `-C 250` | N |
-| `--description` | `-d` | Title the stream should be updated/started with. | `-d Awesome new title!` | N |
-| `--game-id` | `-G` | Game ID for Drop or other relevant events. | `-G 1234` | N |
-| `--tier` | | Tier of the subscription. | `--tier 3000` | N |
+| Flag | Shorthand | Description | Example | Required? (Y/N) |
+|---------------------------|-----------|---------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------|-----------------|
+| `--anonymous` | `-a` | Denotes if the event is anonymous. Only applies to Gift and Sub events. | `-a` | N |
+| `--charity-current-value` | | For charity events, manually set the charity dollar value. | `--charity-current-value 11000` | N |
+| `--charity-target-value` | | For charity events, manually set the charity dollar value. | `--charity-current-value 23400` | N |
+| `--client-id` | | Manually set the Client ID used for revoke, grant, and bits transactions. | `--client-id 4ofh8m0706jqpholgk00u3xvb4spct` | N |
+| `--cost` | `-C` | Amount of bits or channel points redeemed/used in the event. | `-C 250` | N |
+| `--count` | `-c` | Count of events to fire. This can be used to simulate an influx of events. | `-c 100` | N |
+| `--description` | `-d` | Title the stream should be updated/started with. | `-d Awesome new title!` | N |
+| `--event-status` | `-S` | Status of the Event object (.event.status in JSON); Currently applies to channel points redemptions. | `-S fulfilled` | N |
+| `--forward-address` | `-F` | Web server address for where to send mock events. | `-F https://localhost:8080` | N |
+| `--from-user` | `-f` | Denotes the sender's TUID of the event, for example the user that follows another user or the subscriber to a broadcaster. | `-f 44635596` | N |
+| `--game-id` | `-G` | Game ID for Drop or other relevant events. | `-G 1234` | N |
+| `--gift-user` | `-g` | Used only for subcription-based events, denotes the gifting user ID. | `-g 44635596` | N |
+| `--item-id` | `-i` | Manually set the ID of the event payload item (for example the reward ID in redemption events or game in stream events). | `-i 032e4a6c-4aef-11eb-a9f5-1f703d1f0b92` | N |
+| `--item-name` | `-n` | Manually set the name of the event payload item (for example the reward ID in redemption events or game name in stream events). | `-n "Science & Technology"` | N |
+| `--secret` | `-s` | Webhook secret. If defined, signs all forwarded events with the SHA256 HMAC and must be 10-100 characters in length. | `-s testsecret` | N |
+| `--session` | | WebSocket session to target. Only used when forwarding to WebSocket servers with --transport=websocket | `--session e411cc1e_a2613d4e` | N |
+| `--subscription-id` | `-u` | Manually set the subscription/event ID of the event itself. | `-u 5d3aed06-d019-11ed-afa1-0242ac120002` | N |
+| `--subscription-status` | `-r` | Status of the Subscription object (.subscription.status in JSON). Defaults to "enabled" | `-r revoked` | N |
+| `--tier` | | Tier of the subscription. | `--tier 3000` | N |
+| `--timestamp` | | Sets the timestamp to be used in payloads and headers. Must be in RFC3339Nano format. | `--timestamp 2017-04-13T14:34:23` | N |
+| `--to-user` | `-t` | Denotes the receiver's TUID of the event, usually the broadcaster. | `-t 44635596` | N |
+| `--transport` | `-T` | The method used to send events. Can either be `webhook` or `websocket`. Default is `webhook`. | `-T webhook` | N |
```sh
@@ -126,45 +144,7 @@ Allows you to test if your webserver responds to subscription requests properly.
**Args**
-| Argument | Description |
-|-----------------------|--------------------------------------------------------------------------------------------------------------------------------------|
-| `add-moderator` | Channel moderator add event. |
-| `add-redemption` | Channel Points EventSub event for a redemption being performed. |
-| `add-reward` | Channel Points EventSub event for a Custom Reward being added. |
-| `ban` | Channel ban event. |
-| `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. |
-| `cheer` | Only usable with the `eventsub` transport |
-| `drop` | Drops Entitlement event. |
-| `gift` | A gifted subscription event. Triggers a basic tier 1 sub. |
-| `goal-begin` | Channel creator goal start event. |
-| `goal-end` | Channel creator goal end event. |
-| `goal-progress` | Channel creator goal progress event. |
-| `grant` | Authorization grant event. |
-| `hype-train-begin` | Channel hype train start event. |
-| `hype-train-end` | Channel hype train end event. |
-| `hype-train-progress` | Channel hype train progress event. |
-| `poll-begin` | Channel poll begin event. |
-| `poll-end` | Channel poll end event. |
-| `poll-progress` | Channel poll progress event. |
-| `prediction-begin` | Channel prediction begin event. |
-| `prediction-end` | Channel prediction end event. |
-| `prediction-lock` | Channel prediction lock event. |
-| `prediction-progress` | Channel prediction progress event. |
-| `raid` | Channel Raid event with a random viewer count. |
-| `remove-moderator` | Channel moderator removal event. |
-| `remove-reward` | Channel Points EventSub event for a Custom Reward being removed. |
-| `revoke` | User authorization revoke event. Uses local Client as set in `twitch configure` or generates one randomly. |
-| `stream-change` | Stream Changed event. |
-| `streamdown` | Stream offline event. |
-| `streamup` | Stream online event. |
-| `subscribe-message` | Subscription Message event. |
-| `subscribe` | A standard subscription event. Triggers a basic tier 1 sub. |
-| `transaction` | Bits in Extensions transactions events. |
-| `unban` | Channel unban event. |
-| `unsubscribe` | A standard unsubscribe event. Triggers a basic tier 1 sub. |
-| `update-redemption` | Channel Points EventSub event for a redemption being updated. |
-| `update-reward` | Channel Points EventSub event for a Custom Reward being updated. |
-
+This command takes the same arguments as [Trigger](#trigger).
**Flags**
@@ -174,10 +154,45 @@ Allows you to test if your webserver responds to subscription requests properly.
| `--secret` | `-s` | Webhook secret. If defined, signs all forwarded events with the SHA256 HMAC and must be 10-100 characters in length. | `-s testsecret` | N |
| `--transport` | `-T` | The method used to send events. Default is `eventsub`. | `-T eventsub` | N |
-
-
**Examples**
```sh
twitch event verify-subscription cheer -F https://localhost:8080/ // triggers a fake "cheer" EventSub subscription and validates if localhost responds properly
```
+
+## WebSocket
+
+Provides access to a mock EventSub WebSocket server. More information can be found on [Twitch Developers documentation](https://dev.twitch.tv/docs/cli/websocket-event-command/).
+
+**Args**
+
+| Arg | Description |
+|--------------|-------------|
+| start-server | Attempts to start the websocket sever. Default port is 8080. |
+| reconnect | Server command. Starts reconnect testing on the active WebSocket server. See documentation for more info. |
+| close | Server command. Closes a specific client connection with the provided WebSocket close code. |
+| subscription | Server command. Modifies an existing subscription on the WebSocket server. |
+
+**Flags used with start-server**
+| Flag | Shorthand | Description | Example |
+|--------------------------|-----------|--------------------------------------------------------------------------------------|---------------|
+| `--port` | `-p` | Use to specify the port number to use in the localhost address. The default is 8080. | `--port=8080` |
+| `--require-subscription` | `-S` | Prevents the server from allowing subscriptions to be forwarded unless they have a subscription created. Also enables 10 second subscription requirement when a client connects. | `-S` |
+
+
+**Flags used with all other sub-commands**
+| Flag | Shorthand | Description | Example |
+|------------------|-----------|------------------------------------------------------------------------------------------------------------------------------|---------|
+| `--session` | `-s` | Targets a specific client by the session_id given during its Welcome message. | `twitch event websocket close --session=e411cc1e_a2613d4e` |
+| `--reason` | | Specifies the Close message code you wish to close a client’s connection with. Only used with "twitch websocket close" | `twitch event websocket close --reason=4006` |
+| `--status` | | Specifies the Status code you wish to override an existing subscription’s status to. Only used with "twitch websocket close" | `twitch event websocket subscription --status=user_removed` |
+| `--subscription` | | Specifies the subscription ID you wish to target. Only used with “twitch websocket subscription”. | `twitch event websocket subscription --subscription=48d3-b9a-f84c` |
+
+**Examples**
+
+```sh
+twitch event websocket start-server
+twitch event websocket reconnect
+twitch event websocket close --session=e411cc1e_a2613d4e --reason=4006
+twitch event websocket subscription --status=user_removed --subscription=82a855-fae8-93bff0
+```
\ No newline at end of file
diff --git a/go.mod b/go.mod
index 5e724e82..bcf0f2d7 100755
--- a/go.mod
+++ b/go.mod
@@ -4,7 +4,7 @@ go 1.18
require (
github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2
- github.com/fatih/color v1.12.0
+ github.com/fatih/color v1.15.0
github.com/gorilla/websocket v1.5.0
github.com/jmoiron/sqlx v1.3.4
github.com/manifoldco/promptui v0.8.0
@@ -25,8 +25,8 @@ require (
github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a // indirect
github.com/lunixbochs/vtclean v1.0.0 // indirect
github.com/magiconair/properties v1.8.5 // indirect
- github.com/mattn/go-colorable v0.1.8 // indirect
- github.com/mattn/go-isatty v0.0.13 // indirect
+ github.com/mattn/go-colorable v0.1.13 // indirect
+ github.com/mattn/go-isatty v0.0.18 // indirect
github.com/mitchellh/mapstructure v1.4.1 // indirect
github.com/pelletier/go-toml v1.9.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
@@ -35,7 +35,7 @@ require (
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.2.0 // indirect
- golang.org/x/sys v0.5.0 // indirect
+ golang.org/x/sys v0.6.0 // indirect
golang.org/x/text v0.7.0 // indirect
gopkg.in/ini.v1 v1.62.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
diff --git a/go.sum b/go.sum
index 2dc1b3dd..46af993f 100644
--- a/go.sum
+++ b/go.sum
@@ -47,6 +47,8 @@ github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.12.0 h1:mRhaKNwANqRgUBGKmnI5ZxEk7QXmjQeCcuYFMX2bfcc=
github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
+github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
+github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
@@ -144,11 +146,16 @@ github.com/manifoldco/promptui v0.8.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEX
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
+github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA=
github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
+github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.7 h1:fxWBnXkxfM6sRiuH3bqJ4CfzZojMOLVc0UTsTglEghA=
github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
@@ -299,8 +306,11 @@ golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
diff --git a/internal/events/event.go b/internal/events/event.go
index dc38b955..32bb35d5 100644
--- a/internal/events/event.go
+++ b/internal/events/event.go
@@ -26,6 +26,7 @@ type MockEventParameters struct {
Timestamp string
CharityCurrentValue int
CharityTargetValue int
+ ClientID string
}
type MockEventResponse struct {
diff --git a/internal/events/mock_wss_server/server.go b/internal/events/mock_wss_server/server.go
deleted file mode 100644
index 73df82f5..00000000
--- a/internal/events/mock_wss_server/server.go
+++ /dev/null
@@ -1,557 +0,0 @@
-package mock_wss_server
-
-import (
- "context"
- "encoding/json"
- "fmt"
- "log"
- "net"
- "net/http"
- "os"
- "os/signal"
- "path/filepath"
- "strconv"
- "sync"
- "text/template"
- "time"
-
- "github.com/gorilla/websocket"
-
- "github.com/twitchdev/twitch-cli/internal/database"
- "github.com/twitchdev/twitch-cli/internal/mock_api/generate"
- "github.com/twitchdev/twitch-cli/internal/util"
-)
-
-/* Minimum time between messages before the server disconnects a client. AKA, "timeout period"
- * This is a const for now, but it may need to be a flag in the future. */
-const KEEPALIVE_TIMEOUT_SECONDS = 10
-
-var upgrader = websocket.Upgrader{}
-var debug = false
-
-// List of websocket servers. Limited to 2 for now, as allowing more would require rewriting reconnect stuff.
-var wsServers = [2]*WebsocketServer{}
-
-type WebsocketServer struct {
- serverId int // Int representing the ID of the server (0, 1, 2, ...)
- websocketId string // UUID of the websocket. Used for subscribing via EventSub
- connectionUrl string // URL used to connect to the websocket. Used for reconnect messages
- connections []*WebsocketConnection // Current clients connected to this websocket
- deactivatedStatus bool // Boolean used for preventing connections/messages during deactivation; Used for reconnect testing
- reconnectTestTimeout int // Timeout for reconnect testing after first client connects; 0 if reconnect testing not enabled.
- firstClientConnected bool // Whether or not the first client has connected (used for reconnect testing)
-}
-
-type WebsocketConnection struct {
- clientId string
- Conn *websocket.Conn
- mu sync.Mutex
- connectedAtTimestamp string
- pingLoopChan chan struct{}
- kaLoopChan chan struct{}
- closed bool
-}
-
-func (wc *WebsocketConnection) SendMessage(messageType int, data []byte) error {
- wc.mu.Lock()
- defer wc.mu.Unlock()
- return wc.Conn.WriteMessage(messageType, data)
-}
-
-func eventsubHandle(w http.ResponseWriter, r *http.Request) {
- // This next line is required to disable CORS checking.
- // If we weren't returning "true", the the /debug page on the default port wouldn't be able to make calls to port+1
- upgrader.CheckOrigin = func(r *http.Request) bool { return true }
-
- conn, err := upgrader.Upgrade(w, r, nil)
- if err != nil {
- log.Print("[[websocket upgrade err]] ", err)
- return
- }
- defer conn.Close()
-
- // Connection sucessful. WebSocket is open.
-
- serverId, _ := strconv.Atoi(r.Context().Value("serverId").(string))
- wsSrv := wsServers[serverId]
-
- // Or is it? Check for websocket set to deactivated (due to reconnect), and kick them out if so
- if wsSrv.deactivatedStatus {
- log.Printf("Client trying to connect while websocket in reconnect timeout phase. Disconnecting them.")
- conn.Close()
- return
- }
-
- // Activate reconnect testing upon first client connection
- if !wsSrv.firstClientConnected {
- wsSrv.firstClientConnected = true
-
- if wsSrv.reconnectTestTimeout >= 0 {
- go func() {
- log.Printf("First client connected; Reconnect testing enabled. Notices will be sent in %d seconds.", wsSrv.reconnectTestTimeout)
- duration := time.Second * time.Duration(wsSrv.reconnectTestTimeout)
- if duration == 0 {
- duration = time.Second * 1
- }
-
- select {
- case <-time.After(time.Second * time.Duration(wsSrv.reconnectTestTimeout)):
- activateReconnectTest(r.Context())
- }
- }()
- }
- }
-
- // TODO: Decline websocket if it reached 100 connections from the same application access token
-
- // RFC3339Nano = "2022-10-04T12:38:15.548912638Z07" ; This is used by Twitch in production
- connectedAtTimestamp := time.Now().UTC().Format(time.RFC3339Nano)
- conn.SetReadDeadline(time.Now().Add(time.Second * KEEPALIVE_TIMEOUT_SECONDS))
-
- // Add to websocket connection list.
- wc := &WebsocketConnection{
- clientId: util.RandomGUID(),
- Conn: conn,
- connectedAtTimestamp: connectedAtTimestamp,
- closed: false,
- }
- wsSrv.connections = append(wsSrv.connections, wc)
- printConnections(wsSrv.serverId)
-
- // Send "websocket_welcome" message
- welcomeMsg, _ := json.Marshal(
- WelcomeMessage{
- Metadata: MessageMetadata{
- MessageID: util.RandomGUID(),
- MessageType: "session_welcome",
- MessageTimestamp: time.Now().UTC().Format(time.RFC3339Nano),
- },
- Payload: WelcomeMessagePayload{
- Session: WelcomeMessagePayloadSession{
- ID: wsSrv.websocketId,
- Status: "connected",
- KeepaliveTimeoutSeconds: KEEPALIVE_TIMEOUT_SECONDS,
- ReconnectUrl: nil,
- ConnectedAt: connectedAtTimestamp,
- },
- },
- },
- )
- wc.SendMessage(websocket.TextMessage, welcomeMsg)
- if debug {
- log.Printf("[DEBUG] Write: %s", welcomeMsg)
- }
-
- // TODO: Look to implement a way to shut off pings. This would be used specifically for testing the timeout feature.
- // Set up ping/pong handling
- pingTicker := time.NewTicker(5 * time.Second)
- wc.pingLoopChan = make(chan struct{}) // Also used for keepalive
- go func() {
- // Set pong handler
- // Weirdly, pongs are not seen as messages read by conn.ReadMessage, so we have to reset the deadline manually
- conn.SetPongHandler(func(string) error {
- conn.SetReadDeadline(time.Now().Add(time.Second * KEEPALIVE_TIMEOUT_SECONDS))
- return nil
- })
-
- // Ping loop
- for {
- select {
- case <-wc.pingLoopChan:
- pingTicker.Stop()
- return
- case <-pingTicker.C:
- err := wc.SendMessage(websocket.PingMessage, []byte{})
- if err != nil {
- onCloseConnection(wsSrv.serverId, wc)
- }
- }
- }
- }()
-
- // Set up keepalive loop
- kaTicker := time.NewTicker(10 * time.Second)
- wc.kaLoopChan = make(chan struct{})
- go func() {
- for {
- select {
- case <-wc.kaLoopChan:
- kaTicker.Stop()
- case <-kaTicker.C:
- keepAliveMsg, _ := json.Marshal(
- KeepaliveMessage{
- Metadata: MessageMetadata{
- MessageID: util.RandomGUID(),
- MessageType: "session_keepalive",
- MessageTimestamp: time.Now().UTC().Format(time.RFC3339Nano),
- },
- Payload: KeepaliveMessagePayload{},
- },
- )
- err := wc.SendMessage(websocket.TextMessage, keepAliveMsg)
- if err != nil {
- onCloseConnection(wsSrv.serverId, wc)
- }
-
- if debug {
- log.Printf("[DEBUG] Write: %s", keepAliveMsg)
- }
- }
- }
- }()
-
- // TODO: Read messages
- for {
- conn.SetReadDeadline(time.Now().Add(time.Second * KEEPALIVE_TIMEOUT_SECONDS))
- mt, message, err := conn.ReadMessage()
- if err != nil {
- log.Println("read:", err)
- onCloseConnection(wsSrv.serverId, wc)
- break
- }
- if debug {
- log.Printf("recv: [%d] %s", mt, message)
- }
- /*err = wc.SendMessage(mt, message)
- if err != nil {
- log.Println("write:", err)
- onCloseConnection(websocketId)
- break
- }*/
- }
-}
-
-func onCloseConnection(serverId int, wc *WebsocketConnection) {
- // Close ping loop chan
- if !wc.closed {
- close(wc.pingLoopChan)
- close(wc.kaLoopChan)
- }
- wc.closed = true
-
- wsSrv := wsServers[serverId]
-
- // Remove from list
- c := 0
- for i := 0; i < len(wsSrv.connections); i++ {
- if wsSrv.connections[i].Conn == wsSrv.connections[i].Conn {
- log.Printf("Disconnected websocket %s", wsSrv.connections[i].clientId)
- c = i
- break
- }
- }
- wsSrv.connections = append(wsSrv.connections[:c], wsSrv.connections[c+1:]...)
-
- printConnections(wsSrv.serverId)
-}
-
-func printConnections(serverId int) {
- currentConnections := ""
- wsSrv := wsServers[serverId]
- for _, s := range wsSrv.connections {
- currentConnections += s.clientId + ", "
- }
- if currentConnections != "" {
- currentConnections = string(currentConnections[:len(currentConnections)-2])
- }
- log.Printf("[Server %v] Connections: (%d) [ %s ]", serverId, len(wsSrv.connections), currentConnections)
-}
-
-func activateReconnectTest(ctx context.Context) {
- timer := 30 // 30 seconds, as used by Twitch
-
- serverId, _ := strconv.Atoi(ctx.Value("serverId").(string))
- wsSrv := wsServers[serverId]
-
- log.Printf("Terminating server %v with reconnect notice. Disallowing connections in 30 seconds.", serverId)
-
- var wsAltSrv *WebsocketServer
- if serverId == 0 {
- wsAltSrv = wsServers[1]
- } else {
- wsAltSrv = wsServers[0]
- }
-
- // Stop processing new messages
- wsSrv.deactivatedStatus = true // This server
- wsAltSrv.deactivatedStatus = false // Other server; We gotta turn it on to accept connections and whatnot
- log.Printf("Server \"not accepting connections\" status: [Server 0: %v, Server 1: %v]", wsServers[0].deactivatedStatus, wsServers[1].deactivatedStatus)
-
- if debug {
- log.Printf("Connections at time of close: %v", len(wsSrv.connections))
- }
-
- // Send reconnect notices
- for _, c := range wsSrv.connections {
- reconnectMsg, _ := json.Marshal(
- ReconnectMessage{
- Metadata: MessageMetadata{
- MessageID: util.RandomGUID(),
- MessageType: "session_reconnect",
- MessageTimestamp: time.Now().UTC().Format(time.RFC3339Nano),
- },
- Payload: ReconnectMessagePayload{
- Session: ReconnectMessagePayloadSession{
- ID: wsSrv.websocketId,
- Status: "reconnecting",
- KeepaliveTimeoutSeconds: nil,
- ReconnectUrl: wsAltSrv.connectionUrl,
- ConnectedAt: c.connectedAtTimestamp,
- },
- },
- },
- )
-
- err := c.SendMessage(websocket.TextMessage, reconnectMsg)
- if err != nil {
- log.Printf("ERROR (clientId %v): %v", c.clientId, err)
- } else {
- log.Printf("Sent reconnect notice to %v", c.clientId)
- }
- }
-
- log.Printf("Reconnect notices sent for server %v", serverId)
- log.Printf("Use this new URL for connections: %v", wsAltSrv.connectionUrl)
-
- // TODO: Transfer subscriptions to the other websocket server.
-
- // Wait 30 seconds to close out, just like Twitch production EventSub websockets
- select {
- case <-time.After(time.Second * time.Duration(timer)):
- for _, c := range wsSrv.connections {
- c.Conn.Close()
- }
-
- if debug {
- log.Printf("[DEBUG] Resetting websocket ID on server %v", wsSrv.websocketId)
- }
- wsSrv.websocketId = util.RandomGUID() // Change websocket ID
- }
-}
-
-func StartServer(port int, enableDebug bool, reconnectTestTimer int, sslEnabled bool) {
- debug = enableDebug
-
- m := http.NewServeMux()
-
- // Server IDs are 0-index, so we don't have to do any math when calling their referenced arrays
- ctx1 := context.Background()
- ctx1 = context.WithValue(ctx1, "serverId", "0")
- ctx2 := context.Background()
- ctx2 = context.WithValue(ctx2, "serverId", "1")
-
- db, err := database.NewConnection()
- if err != nil {
- log.Fatalf("Error connecting to database: %v", err.Error())
- return
- }
-
- firstTime := db.IsFirstRun()
-
- if firstTime {
- err := generate.Generate(25)
- if err != nil {
- log.Fatal(err)
- }
- }
-
- ctx1 = context.WithValue(ctx1, "db", db)
- ctx2 = context.WithValue(ctx2, "db", db)
-
- wsSrv1Url := fmt.Sprintf("ws://localhost:%v/eventsub", port)
- wsSrv2Url := fmt.Sprintf("ws://localhost:%v/eventsub", port+1)
-
- // Change to WSS if SSL is enabled via flag
- if sslEnabled {
- wsSrv1Url = fmt.Sprintf("wss://localhost:%v/eventsub", port)
- wsSrv2Url = fmt.Sprintf("wss://localhost:%v/eventsub", port+1)
- }
-
- wsServers[0] = &WebsocketServer{
- serverId: 0,
- websocketId: util.RandomGUID(),
- connectionUrl: wsSrv1Url,
- connections: []*WebsocketConnection{},
- deactivatedStatus: false,
- reconnectTestTimeout: reconnectTestTimer,
- firstClientConnected: false,
- }
- wsServers[1] = &WebsocketServer{
- serverId: 1,
- websocketId: util.RandomGUID(),
- connectionUrl: wsSrv2Url,
- connections: []*WebsocketConnection{},
- deactivatedStatus: true, // 2nd server is deactivated by default. Will reactivate for reconnect testing.
- reconnectTestTimeout: -1, // No reconnect testing
- firstClientConnected: false,
- }
-
- RegisterHandlers(m)
-
- stop := make(chan os.Signal)
- signal.Notify(stop, os.Interrupt)
-
- s1 := StartIndividualServer(port, sslEnabled, m, ctx1, false)
- s2 := StartIndividualServer(port+1, sslEnabled, m, ctx2, true) // Start second server, at a port above.
-
- <-stop // Wait for ctrl + c
-
- log.Print("Shutting down EventSub WebSocket server ...\n")
- db.DB.Close()
- ctx, cancel := context.WithTimeout(context.Background(), time.Duration(time.Second*5))
- defer cancel()
-
- if err1 := s1.Shutdown(ctx); err1 != nil {
- log.Fatal(err1)
- }
-
- if err2 := s2.Shutdown(ctx); err2 != nil {
- log.Fatal(err2)
- }
-}
-
-func StartIndividualServer(port int, sslEnabled bool, m *http.ServeMux, ctx context.Context, alternateServer bool) http.Server {
- s := http.Server{
- Addr: fmt.Sprintf(":%v", port),
- Handler: m,
- BaseContext: func(l net.Listener) context.Context {
- return ctx
- },
- }
- stop := make(chan os.Signal)
- signal.Notify(stop, os.Interrupt)
-
- go func() {
- if !alternateServer {
- log.Printf("Mock EventSub websocket server started on port %d", port)
- } else {
- log.Printf("Started alternate EventSub websocket server on port %d. This one is used for reconnect testing.", port)
- }
-
- if sslEnabled { // Open HTTP server with HTTPS support
- home, _ := util.GetApplicationDir()
- crtFile := filepath.Join(home, "localhost.crt")
- keyFile := filepath.Join(home, "localhost.key")
-
- if err := s.ListenAndServeTLS(crtFile, keyFile); err != nil {
- if err != http.ErrServerClosed {
- log.Fatalf(`%v
- ** You need to generate localhost.crt and localhost.key for this to work **
- ** Please run these commands (Note: you'll have a cert error in your web browser, but it'll still start): **
- openssl genrsa -out "%v" 2048
- openssl req -new -x509 -sha256 -key "%v" -out "%v" -days 3650`,
- err, keyFile, keyFile, crtFile)
- }
- }
- } else { // Open HTTP server without HTTPS support
- if err := s.ListenAndServe(); err != nil {
- if err != http.ErrServerClosed {
- log.Fatalf("%v", err)
- }
- }
- }
- }()
-
- return s
-}
-
-func RegisterHandlers(m *http.ServeMux) {
- m.HandleFunc("/debug", debugPageHandler)
- m.HandleFunc("/eventsub", eventsubHandle)
-}
-
-func debugPageHandler(w http.ResponseWriter, r *http.Request) {
- debugTemplate.Execute(w, DebugServerButtons{Server1: wsServers[0].connectionUrl, Server2: wsServers[1].connectionUrl})
-}
-
-// Used for template weirdness
-type DebugServerButtons struct {
- Server1 string
- Server2 string
-}
-
-var debugTemplate = template.Must(template.New("").Parse(`
-
-
-
-
-
-
-
-
-
- Click "Open" to create a connection to the server ({{.Server1}}),
-"Send" to send a message to the server and "Close" to close the connection.
-You can change the message and send multiple times.
-
- For testing the higher port ({{.Server2}}), hold ctrl OR alt and click "Open"
-
- |
-
- |
-
-
-`))
diff --git a/internal/events/models.go b/internal/events/models.go
index afadc548..9b769607 100644
--- a/internal/events/models.go
+++ b/internal/events/models.go
@@ -3,7 +3,7 @@
package events
var transportSupported = map[string]bool{
- "websub": false,
- "eventsub": true,
- "websockets": false,
+ "websub": false,
+ "webhook": true,
+ "websocket": true,
}
diff --git a/internal/events/trigger/forward_event.go b/internal/events/trigger/forward_event.go
index dcf94c6e..1529fedd 100644
--- a/internal/events/trigger/forward_event.go
+++ b/internal/events/trigger/forward_event.go
@@ -39,7 +39,7 @@ const (
)
var notificationHeaders = map[string][]header{
- models.TransportEventSub: {
+ models.TransportWebhook: {
{
HeaderName: `Twitch-Eventsub-Message-Retry`,
HeaderValue: `0`,
@@ -64,7 +64,7 @@ func ForwardEvent(p ForwardParamters) (*http.Response, error) {
}
switch p.Transport {
- case models.TransportEventSub:
+ case models.TransportWebhook:
req.Header.Set("Twitch-Eventsub-Message-Id", p.ID)
req.Header.Set("Twitch-Eventsub-Subscription-Type", p.Event)
req.Header.Set("Twitch-Eventsub-Subscription-Version", p.SubscriptionVersion)
@@ -101,7 +101,7 @@ func getSignatureHeader(req *http.Request, id string, secret string, transport s
ts, _ := time.Parse(time.RFC3339Nano, timestamp)
switch transport {
- case models.TransportEventSub:
+ case models.TransportWebhook:
req.Header.Set("Twitch-Eventsub-Message-Timestamp", timestamp)
prefix := ts.AppendFormat([]byte(id), time.RFC3339Nano)
mac.Write(prefix)
diff --git a/internal/events/trigger/retrigger_event_test.go b/internal/events/trigger/retrigger_event_test.go
index 3ed1d5a1..a634721e 100644
--- a/internal/events/trigger/retrigger_event_test.go
+++ b/internal/events/trigger/retrigger_event_test.go
@@ -26,7 +26,7 @@ func TestRefireEvent(t *testing.T) {
params := *&TriggerParameters{
Event: "gift",
- Transport: models.TransportEventSub,
+ Transport: models.TransportWebhook,
IsAnonymous: false,
FromUser: "",
ToUser: "",
diff --git a/internal/events/trigger/trigger_event.go b/internal/events/trigger/trigger_event.go
index 0dfa5a20..fe20affc 100644
--- a/internal/events/trigger/trigger_event.go
+++ b/internal/events/trigger/trigger_event.go
@@ -3,16 +3,21 @@
package trigger
import (
+ "encoding/json"
+ "errors"
"fmt"
"io/ioutil"
+ "net/rpc"
"strings"
"time"
"github.com/fatih/color"
+ "github.com/spf13/viper"
"github.com/twitchdev/twitch-cli/internal/database"
"github.com/twitchdev/twitch-cli/internal/events"
"github.com/twitchdev/twitch-cli/internal/events/types"
"github.com/twitchdev/twitch-cli/internal/models"
+ rpc_handler "github.com/twitchdev/twitch-cli/internal/rpc"
"github.com/twitchdev/twitch-cli/internal/util"
)
@@ -40,6 +45,8 @@ type TriggerParameters struct {
EventID string // Also serves as subscription ID. See https://github.com/twitchdev/twitch-cli/issues/184
CharityCurrentValue int
CharityTargetValue int
+ ClientID string
+ WebSocketClient string
}
type TriggerResponse struct {
@@ -55,6 +62,16 @@ func Fire(p TriggerParameters) (string, error) {
var resp events.MockEventResponse
var err error
+ if p.ClientID == "" {
+ p.ClientID = viper.GetString("ClientID") // Get from config
+
+ if p.ClientID == "" {
+ // --client-id wasn't used, and config file doesn't have a Client ID set.
+ // Generate a randomized one
+ p.ClientID = util.RandomClientID()
+ }
+ }
+
if p.ToUser == "" {
p.ToUser = util.RandomUserID()
}
@@ -74,8 +91,8 @@ func Fire(p TriggerParameters) (string, error) {
// do nothing, these are valid values
default:
return "", fmt.Errorf(
- `Discarding event: Invalid tier provided.
- Valid values are 1000, 2000 or 3000`)
+ "Discarding event: Invalid tier provided.\n" +
+ "Valid values are 1000, 2000 or 3000")
}
if p.EventID == "" {
@@ -115,6 +132,7 @@ https://dev.twitch.tv/docs/eventsub/handling-webhook-events#processing-an-event`
Timestamp: p.Timestamp,
CharityCurrentValue: p.CharityCurrentValue,
CharityTargetValue: p.CharityTargetValue,
+ ClientID: p.ClientID,
}
e, err := types.GetByTriggerAndTransport(p.Event, p.Transport)
@@ -122,11 +140,9 @@ https://dev.twitch.tv/docs/eventsub/handling-webhook-events#processing-an-event`
return "", err
}
- if eventParamaters.Transport == models.TransportEventSub {
- newTrigger := e.GetEventSubAlias(p.Event)
- if newTrigger != "" {
- eventParamaters.Trigger = newTrigger // overwrite the existing trigger with the "correct" one
- }
+ newTrigger := e.GetEventSubAlias(p.Event)
+ if newTrigger != "" {
+ eventParamaters.Trigger = newTrigger // overwrite the existing trigger with the "correct" one
}
resp, err = e.GenerateEvent(eventParamaters)
@@ -163,7 +179,7 @@ https://dev.twitch.tv/docs/eventsub/handling-webhook-events#processing-an-event`
messageType = EventSubMessageTypeRevocation
}
- if p.ForwardAddress != "" {
+ if p.ForwardAddress != "" && strings.EqualFold(p.Transport, "webhook") { // Forwarding to an address requires Webhook, as its done via HTTP
resp, err := ForwardEvent(ForwardParamters{
ID: resp.ID,
Transport: p.Transport,
@@ -195,5 +211,51 @@ https://dev.twitch.tv/docs/eventsub/handling-webhook-events#processing-an-event`
}
}
+ // Forward to WebSocket server via RPC
+ if strings.EqualFold(p.Transport, "websocket") {
+ client, err := rpc.DialHTTP("tcp", ":44747")
+ if err != nil {
+ return "", errors.New("Failed to dial RPC handler for WebSocket server. Is it online?\nError: " + err.Error())
+ }
+
+ var reply rpc_handler.RPCResponse
+
+ // Modify transport
+ modifiedTransportJSON := models.EventsubResponse{}
+ err = json.Unmarshal([]byte(resp.JSON), &modifiedTransportJSON)
+ if err != nil {
+ return "", errors.New("Unexpected error unmarshling JSON before forwarding to WebSocket server: " + err.Error())
+ }
+ modifiedTransportJSON.Subscription.Transport.Method = "websocket"
+ modifiedTransportJSON.Subscription.Transport.Callback = ""
+ modifiedTransportJSON.Subscription.Transport.SessionID = "WebSocket-Server-Will-Set"
+ rawModifiedTransportJSON, _ := json.Marshal(modifiedTransportJSON)
+ resp.JSON = rawModifiedTransportJSON
+
+ // Trigger any EventSub subscription that's available over 1st party WebSocket connections
+ variables := make(map[string]string)
+ variables["ClientName"] = p.WebSocketClient
+
+ args := &rpc_handler.RPCArgs{
+ RPCName: "EventSubWebSocketForwardEvent",
+ Body: string(resp.JSON),
+ Variables: variables,
+ }
+
+ err = client.Call("RPCHandler.ExecuteGenericRPC", args, &reply)
+
+ // Error checking for RPC internals
+ if err != nil {
+ return "", errors.New("Failed to send via RPC to WebSocket server: " + err.Error())
+ }
+
+ // Error checking for everything else
+ if reply.ResponseCode == 0 { // Zero will always be success
+ color.New().Add(color.FgGreen).Println(fmt.Sprintf(`✔ Forwarded for use in mock EventSub WebSocket server`))
+ } else {
+ color.New().Add(color.FgRed).Println(fmt.Sprintf(`✗ EventSub WebSocket server failed to process event: [%v] %v`, reply.DetailedInfo, reply.DetailedInfo))
+ }
+ }
+
return string(resp.JSON), nil
}
diff --git a/internal/events/trigger/trigger_event_test.go b/internal/events/trigger/trigger_event_test.go
index 0a90f1c8..4b0dd29f 100644
--- a/internal/events/trigger/trigger_event_test.go
+++ b/internal/events/trigger/trigger_event_test.go
@@ -24,7 +24,7 @@ func TestFire(t *testing.T) {
params := *&TriggerParameters{
Event: "gift",
- Transport: models.TransportEventSub,
+ Transport: models.TransportWebhook,
IsAnonymous: false,
FromUser: "",
ToUser: "",
@@ -44,7 +44,7 @@ func TestFire(t *testing.T) {
params = *&TriggerParameters{
Event: "cheer",
- Transport: models.TransportEventSub,
+ Transport: models.TransportWebhook,
IsAnonymous: false,
FromUser: "",
ToUser: "",
@@ -63,7 +63,7 @@ func TestFire(t *testing.T) {
params = *&TriggerParameters{
Event: "follow",
- Transport: models.TransportEventSub,
+ Transport: models.TransportWebhook,
IsAnonymous: false,
FromUser: "",
ToUser: "",
@@ -82,7 +82,7 @@ func TestFire(t *testing.T) {
params = *&TriggerParameters{
Event: "cheer",
- Transport: models.TransportEventSub,
+ Transport: models.TransportWebhook,
IsAnonymous: false,
FromUser: "",
ToUser: "",
@@ -101,7 +101,7 @@ func TestFire(t *testing.T) {
params = *&TriggerParameters{
Event: "add-redemption",
- Transport: models.TransportEventSub,
+ Transport: models.TransportWebhook,
IsAnonymous: false,
FromUser: "",
ToUser: "",
@@ -120,7 +120,7 @@ func TestFire(t *testing.T) {
params = *&TriggerParameters{
Event: "add-reward",
- Transport: models.TransportEventSub,
+ Transport: models.TransportWebhook,
IsAnonymous: false,
FromUser: "",
ToUser: "",
@@ -139,7 +139,7 @@ func TestFire(t *testing.T) {
params = *&TriggerParameters{
Event: "transaction",
- Transport: models.TransportEventSub,
+ Transport: models.TransportWebhook,
IsAnonymous: false,
FromUser: "",
ToUser: "",
diff --git a/internal/events/types/_template/_event_name.go b/internal/events/types/_template/_event_name.go
index b2c92c7d..cab2638d 100644
--- a/internal/events/types/_template/_event_name.go
+++ b/internal/events/types/_template/_event_name.go
@@ -4,19 +4,24 @@ package event_name
import (
"encoding/json"
+ "strings"
"github.com/twitchdev/twitch-cli/internal/events"
"github.com/twitchdev/twitch-cli/internal/models"
)
var transportsSupported = map[string]bool{
- models.TransportEventSub: true,
+ models.TransportWebhook: true,
+ models.TransportWebSocket: true,
}
var triggerSupported = []string{"trigger_keyword"}
var triggerMapping = map[string]map[string]string{
- models.TransportEventSub: {
+ models.TransportWebhook: {
+ "trigger_keyword": "topic_name_es",
+ },
+ models.TransportWebSocket: {
"trigger_keyword": "topic_name_es",
},
}
@@ -28,7 +33,7 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven
var err error
switch params.Transport {
- case models.TransportEventSub:
+ case models.TransportWebhook, models.TransportWebSocket:
body := &models.EventsubResponse{
// make the eventsub response (if supported)
}
@@ -36,6 +41,22 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven
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
}
@@ -65,7 +86,7 @@ func (e Event) GetTopic(transport string, trigger string) string {
}
func (e Event) GetEventSubAlias(t string) string {
// check for aliases
- for trigger, topic := range triggerMapping[models.TransportEventSub] {
+ for trigger, topic := range triggerMapping[models.TransportWebhook] {
if topic == t {
return trigger
}
diff --git a/internal/events/types/_template/_event_name_test.go b/internal/events/types/_template/_event_name_test.go
index 9579b64f..79436123 100644
--- a/internal/events/types/_template/_event_name_test.go
+++ b/internal/events/types/_template/_event_name_test.go
@@ -20,7 +20,7 @@ func TestEventSub(t *testing.T) {
params := *&events.MockEventParameters{
FromUserID: fromUser,
ToUserID: toUser,
- Transport: models.TransportEventSub,
+ Transport: models.TransportWebhook,
Trigger: "subscribe",
}
@@ -60,7 +60,7 @@ func TestValidTrigger(t *testing.T) {
func TestValidTransport(t *testing.T) {
a := test_setup.SetupTestEnv(t)
- r := Event{}.ValidTransport(models.TransportEventSub)
+ r := Event{}.ValidTransport(models.TransportWebhook)
a.Equal(true, r)
r = Event{}.ValidTransport("noteventsub")
@@ -69,6 +69,6 @@ func TestValidTransport(t *testing.T) {
func TestGetTopic(t *testing.T) {
a := test_setup.SetupTestEnv(t)
- r := Event{}.GetTopic(models.TransportEventSub, "trigger_keyword")
+ r := Event{}.GetTopic(models.TransportWebhook, "trigger_keyword")
a.NotNil(r)
}
diff --git a/internal/events/types/authorization_grant/grant.go b/internal/events/types/authorization_grant/grant.go
new file mode 100644
index 00000000..f51c2f0b
--- /dev/null
+++ b/internal/events/types/authorization_grant/grant.go
@@ -0,0 +1,123 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+package authorization_grant
+
+import (
+ "encoding/json"
+ "strings"
+
+ "github.com/twitchdev/twitch-cli/internal/events"
+ "github.com/twitchdev/twitch-cli/internal/models"
+)
+
+var transportsSupported = map[string]bool{
+ models.TransportWebhook: true,
+ models.TransportWebSocket: false,
+}
+
+var triggerSupported = []string{"grant"}
+
+var triggerMapping = map[string]map[string]string{
+ models.TransportWebhook: {
+ "grant": "user.authorization.grant",
+ },
+}
+
+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:
+ body := &models.AuthorizationRevokeEventSubResponse{
+ Subscription: models.EventsubSubscription{
+ ID: params.ID,
+ Status: params.SubscriptionStatus,
+ Type: triggerMapping[params.Transport][params.Trigger],
+ Version: e.SubscriptionVersion(),
+ Condition: models.EventsubCondition{
+ ClientID: params.ClientID,
+ },
+ Transport: models.EventsubTransport{
+ Method: "webhook",
+ Callback: "null",
+ },
+ Cost: 1,
+ CreatedAt: params.Timestamp,
+ },
+ Event: &models.AuthorizationRevokeEvent{
+ ClientID: params.ClientID,
+ UserID: params.FromUserID,
+ UserLogin: params.FromUserName,
+ UserName: params.FromUserName,
+ },
+ }
+ 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,
+ 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 "1"
+}
diff --git a/internal/events/types/authorization_grant/grant_test.go b/internal/events/types/authorization_grant/grant_test.go
new file mode 100644
index 00000000..8eb414bb
--- /dev/null
+++ b/internal/events/types/authorization_grant/grant_test.go
@@ -0,0 +1,79 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+package authorization_grant
+
+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: "subscribe",
+ SubscriptionStatus: "enabled",
+ ClientID: "1234",
+ }
+
+ r, err := Event{}.GenerateEvent(params)
+ a.Nil(err)
+
+ var body models.AuthorizationRevokeEventSubResponse
+ err = json.Unmarshal(r.JSON, &body)
+ a.Nil(err)
+
+ a.NotEmpty(body.Event.ClientID)
+ a.Equal(body.Event.ClientID, body.Subscription.Condition.ClientID)
+ a.Equal("1234", body.Event.ClientID)
+}
+func TestFakeTransport(t *testing.T) {
+ a := test_setup.SetupTestEnv(t)
+
+ params := *&events.MockEventParameters{
+ FromUserID: fromUser,
+ ToUserID: toUser,
+ Transport: "fake_transport",
+ Trigger: "unsubscribe",
+ SubscriptionStatus: "enabled",
+ }
+
+ r, err := Event{}.GenerateEvent(params)
+ a.Nil(err)
+ a.Empty(r)
+}
+func TestValidTrigger(t *testing.T) {
+ a := test_setup.SetupTestEnv(t)
+
+ r := Event{}.ValidTrigger("grant")
+ a.Equal(true, r)
+
+ r = Event{}.ValidTrigger("fake_grant")
+ 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, "grant")
+ a.NotNil(r)
+}
diff --git a/internal/events/types/authorization/authorization.go b/internal/events/types/authorization_revoke/revoke.go
similarity index 81%
rename from internal/events/types/authorization/authorization.go
rename to internal/events/types/authorization_revoke/revoke.go
index d683ad56..d0da37d4 100644
--- a/internal/events/types/authorization/authorization.go
+++ b/internal/events/types/authorization_revoke/revoke.go
@@ -1,27 +1,28 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
-package authorization
+package authorization_revoke
import (
"encoding/json"
"strings"
- "github.com/spf13/viper"
"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,
+ models.TransportWebhook: true,
+ models.TransportWebSocket: true,
}
-var triggerSupported = []string{"revoke", "grant"}
+var triggerSupported = []string{"revoke"}
var triggerMapping = map[string]map[string]string{
- models.TransportEventSub: {
+ models.TransportWebhook: {
+ "revoke": "user.authorization.revoke",
+ },
+ models.TransportWebSocket: {
"revoke": "user.authorization.revoke",
- "grant": "user.authorization.grant",
},
}
@@ -30,14 +31,9 @@ type Event struct{}
func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEventResponse, error) {
var event []byte
var err error
- clientID := viper.GetString("ClientID")
- // if not configured, generate a random one
- if clientID == "" {
- clientID = util.RandomClientID()
- }
switch params.Transport {
- case models.TransportEventSub:
+ case models.TransportWebhook, models.TransportWebSocket:
body := &models.AuthorizationRevokeEventSubResponse{
Subscription: models.EventsubSubscription{
ID: params.ID,
@@ -45,7 +41,7 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven
Type: triggerMapping[params.Transport][params.Trigger],
Version: e.SubscriptionVersion(),
Condition: models.EventsubCondition{
- ClientID: clientID,
+ ClientID: params.ClientID,
},
Transport: models.EventsubTransport{
Method: "webhook",
@@ -54,13 +50,18 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven
Cost: 1,
CreatedAt: params.Timestamp,
},
- Event: models.AuthorizationRevokeEvent{
- ClientID: clientID,
+ Event: &models.AuthorizationRevokeEvent{
+ ClientID: params.ClientID,
UserID: params.FromUserID,
UserLogin: params.FromUserName,
UserName: params.FromUserName,
},
}
+
+ if params.Transport == models.TransportWebSocket {
+ body.Event = nil
+ }
+
event, err = json.Marshal(body)
if err != nil {
return events.MockEventResponse{}, err
@@ -117,7 +118,7 @@ func (e Event) GetAllTopicsByTransport(transport string) []string {
}
func (e Event) GetEventSubAlias(t string) string {
// check for aliases
- for trigger, topic := range triggerMapping[models.TransportEventSub] {
+ for trigger, topic := range triggerMapping[models.TransportWebhook] {
if topic == t {
return trigger
}
diff --git a/internal/events/types/authorization/authorization_test.go b/internal/events/types/authorization_revoke/revoke_test.go
similarity index 87%
rename from internal/events/types/authorization/authorization_test.go
rename to internal/events/types/authorization_revoke/revoke_test.go
index abcd5e0b..d91f77c0 100644
--- a/internal/events/types/authorization/authorization_test.go
+++ b/internal/events/types/authorization_revoke/revoke_test.go
@@ -1,12 +1,11 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
-package authorization
+package authorization_revoke
import (
"encoding/json"
"testing"
- "github.com/spf13/viper"
"github.com/twitchdev/twitch-cli/internal/events"
"github.com/twitchdev/twitch-cli/internal/models"
"github.com/twitchdev/twitch-cli/test_setup"
@@ -18,13 +17,13 @@ var toUser = "4567"
func TestEventSub(t *testing.T) {
a := test_setup.SetupTestEnv(t)
- viper.Set("ClientID", "1234")
params := *&events.MockEventParameters{
FromUserID: fromUser,
ToUserID: toUser,
- Transport: models.TransportEventSub,
+ Transport: models.TransportWebhook,
Trigger: "subscribe",
SubscriptionStatus: "enabled",
+ ClientID: "1234",
}
r, err := Event{}.GenerateEvent(params)
@@ -66,7 +65,7 @@ func TestValidTrigger(t *testing.T) {
func TestValidTransport(t *testing.T) {
a := test_setup.SetupTestEnv(t)
- r := Event{}.ValidTransport(models.TransportEventSub)
+ r := Event{}.ValidTransport(models.TransportWebhook)
a.Equal(true, r)
r = Event{}.ValidTransport("noteventsub")
@@ -75,6 +74,6 @@ func TestValidTransport(t *testing.T) {
func TestGetTopic(t *testing.T) {
a := test_setup.SetupTestEnv(t)
- r := Event{}.GetTopic(models.TransportEventSub, "revoke")
+ r := Event{}.GetTopic(models.TransportWebhook, "revoke")
a.NotNil(r)
}
diff --git a/internal/events/types/ban/ban.go b/internal/events/types/ban/ban.go
index 1de92315..fd0e012c 100644
--- a/internal/events/types/ban/ban.go
+++ b/internal/events/types/ban/ban.go
@@ -13,13 +13,18 @@ import (
)
var transportsSupported = map[string]bool{
- models.TransportEventSub: true,
+ models.TransportWebhook: true,
+ models.TransportWebSocket: true,
}
var triggerSupported = []string{"ban", "unban"}
var triggerMapping = map[string]map[string]string{
- models.TransportEventSub: {
+ models.TransportWebhook: {
+ "ban": "channel.ban",
+ "unban": "channel.unban",
+ },
+ models.TransportWebSocket: {
"ban": "channel.ban",
"unban": "channel.unban",
},
@@ -32,7 +37,7 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven
var err error
switch params.Transport {
- case models.TransportEventSub:
+ case models.TransportWebhook, models.TransportWebSocket:
ban := models.BanEventSubEvent{
UserID: params.FromUserID,
UserLogin: params.FromUserName,
@@ -140,7 +145,7 @@ func (e Event) GetAllTopicsByTransport(transport string) []string {
}
func (e Event) GetEventSubAlias(t string) string {
// check for aliases
- for trigger, topic := range triggerMapping[models.TransportEventSub] {
+ for trigger, topic := range triggerMapping[models.TransportWebhook] {
if topic == t {
return trigger
}
diff --git a/internal/events/types/ban/ban_test.go b/internal/events/types/ban/ban_test.go
index bd688f04..45d4dfd8 100644
--- a/internal/events/types/ban/ban_test.go
+++ b/internal/events/types/ban/ban_test.go
@@ -19,7 +19,7 @@ func TestEventSubBan(t *testing.T) {
params := events.MockEventParameters{
FromUserID: fromUser,
ToUserID: toUser,
- Transport: models.TransportEventSub,
+ Transport: models.TransportWebhook,
Trigger: "ban",
SubscriptionStatus: "enabled",
}
@@ -39,7 +39,7 @@ func TestEventSubBan(t *testing.T) {
params = events.MockEventParameters{
FromUserID: fromUser,
ToUserID: toUser,
- Transport: models.TransportEventSub,
+ Transport: models.TransportWebhook,
Trigger: "unban",
SubscriptionStatus: "enabled",
}
@@ -85,7 +85,7 @@ func TestValidTrigger(t *testing.T) {
func TestValidTransport(t *testing.T) {
a := test_setup.SetupTestEnv(t)
- r := Event{}.ValidTransport(models.TransportEventSub)
+ r := Event{}.ValidTransport(models.TransportWebhook)
a.Equal(true, r)
r = Event{}.ValidTransport("noteventsub")
@@ -95,6 +95,6 @@ func TestValidTransport(t *testing.T) {
func TestGetTopic(t *testing.T) {
a := test_setup.SetupTestEnv(t)
- r := Event{}.GetTopic(models.TransportEventSub, "ban")
+ r := Event{}.GetTopic(models.TransportWebhook, "ban")
a.NotNil(r)
}
diff --git a/internal/events/types/channel_points_redemption/redemption_event.go b/internal/events/types/channel_points_redemption/redemption_event.go
index c5e5174d..89149017 100644
--- a/internal/events/types/channel_points_redemption/redemption_event.go
+++ b/internal/events/types/channel_points_redemption/redemption_event.go
@@ -12,13 +12,18 @@ import (
)
var transportsSupported = map[string]bool{
- models.TransportEventSub: true,
+ models.TransportWebhook: true,
+ models.TransportWebSocket: true,
}
var triggerSupported = []string{"add-redemption", "update-redemption"}
var triggerMapping = map[string]map[string]string{
- models.TransportEventSub: {
+ models.TransportWebhook: {
+ "add-redemption": "channel.channel_points_custom_reward_redemption.add",
+ "update-redemption": "channel.channel_points_custom_reward_redemption.update",
+ },
+ models.TransportWebSocket: {
"add-redemption": "channel.channel_points_custom_reward_redemption.add",
"update-redemption": "channel.channel_points_custom_reward_redemption.update",
},
@@ -47,7 +52,7 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven
}
switch params.Transport {
- case models.TransportEventSub:
+ case models.TransportWebhook, models.TransportWebSocket:
body := *&models.RedemptionEventSubResponse{
Subscription: models.EventsubSubscription{
ID: params.ID,
@@ -140,7 +145,7 @@ func (e Event) GetAllTopicsByTransport(transport string) []string {
}
func (e Event) GetEventSubAlias(t string) string {
// check for aliases
- for trigger, topic := range triggerMapping[models.TransportEventSub] {
+ for trigger, topic := range triggerMapping[models.TransportWebhook] {
if topic == t {
return trigger
}
diff --git a/internal/events/types/channel_points_redemption/redemption_event_test.go b/internal/events/types/channel_points_redemption/redemption_event_test.go
index f2adc22a..619c3708 100644
--- a/internal/events/types/channel_points_redemption/redemption_event_test.go
+++ b/internal/events/types/channel_points_redemption/redemption_event_test.go
@@ -18,7 +18,7 @@ func TestEventSub(t *testing.T) {
a := test_setup.SetupTestEnv(t)
params := events.MockEventParameters{
- Transport: models.TransportEventSub,
+ Transport: models.TransportWebhook,
Trigger: "add-redemption",
SubscriptionStatus: "enabled",
ToUserID: toUser,
@@ -44,7 +44,7 @@ func TestEventSub(t *testing.T) {
a.Equal(params.ItemName, body.Event.Reward.Title)
params = events.MockEventParameters{
- Transport: models.TransportEventSub,
+ Transport: models.TransportWebhook,
ToUserID: toUser,
FromUserID: fromUser,
}
@@ -88,7 +88,7 @@ func TestValidTrigger(t *testing.T) {
func TestValidTransport(t *testing.T) {
a := test_setup.SetupTestEnv(t)
- r := Event{}.ValidTransport(models.TransportEventSub)
+ r := Event{}.ValidTransport(models.TransportWebhook)
a.Equal(true, r)
r = Event{}.ValidTransport("noteventsub")
@@ -97,6 +97,6 @@ func TestValidTransport(t *testing.T) {
func TestGetTopic(t *testing.T) {
a := test_setup.SetupTestEnv(t)
- r := Event{}.GetTopic(models.TransportEventSub, "add-redemption")
+ r := Event{}.GetTopic(models.TransportWebhook, "add-redemption")
a.NotNil(r)
}
diff --git a/internal/events/types/channel_points_reward/reward_event.go b/internal/events/types/channel_points_reward/reward_event.go
index 5e952598..589ed815 100644
--- a/internal/events/types/channel_points_reward/reward_event.go
+++ b/internal/events/types/channel_points_reward/reward_event.go
@@ -11,13 +11,19 @@ import (
)
var transportsSupported = map[string]bool{
- models.TransportEventSub: true,
+ models.TransportWebhook: true,
+ models.TransportWebSocket: true,
}
var triggerSupported = []string{"add-reward", "update-reward", "remove-reward"}
var triggerMapping = map[string]map[string]string{
- models.TransportEventSub: {
+ models.TransportWebhook: {
+ "add-reward": "channel.channel_points_custom_reward.add",
+ "update-reward": "channel.channel_points_custom_reward.update",
+ "remove-reward": "channel.channel_points_custom_reward.remove",
+ },
+ models.TransportWebSocket: {
"add-reward": "channel.channel_points_custom_reward.add",
"update-reward": "channel.channel_points_custom_reward.update",
"remove-reward": "channel.channel_points_custom_reward.remove",
@@ -39,7 +45,7 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven
}
switch params.Transport {
- case models.TransportEventSub:
+ case models.TransportWebhook, models.TransportWebSocket:
body := models.EventsubResponse{
Subscription: models.EventsubSubscription{
ID: params.ID,
@@ -153,7 +159,7 @@ func (e Event) GetAllTopicsByTransport(transport string) []string {
}
func (e Event) GetEventSubAlias(t string) string {
// check for aliases
- for trigger, topic := range triggerMapping[models.TransportEventSub] {
+ for trigger, topic := range triggerMapping[models.TransportWebhook] {
if topic == t {
return trigger
}
diff --git a/internal/events/types/channel_points_reward/reward_event_test.go b/internal/events/types/channel_points_reward/reward_event_test.go
index 8c7ae31d..25ad48b4 100644
--- a/internal/events/types/channel_points_reward/reward_event_test.go
+++ b/internal/events/types/channel_points_reward/reward_event_test.go
@@ -18,7 +18,7 @@ func TestEventSub(t *testing.T) {
a := test_setup.SetupTestEnv(t)
params := events.MockEventParameters{
- Transport: models.TransportEventSub,
+ Transport: models.TransportWebhook,
Trigger: "add-redemption",
SubscriptionStatus: "enabled",
ToUserID: toUser,
@@ -70,7 +70,7 @@ func TestValidTrigger(t *testing.T) {
func TestValidTransport(t *testing.T) {
a := test_setup.SetupTestEnv(t)
- r := Event{}.ValidTransport(models.TransportEventSub)
+ r := Event{}.ValidTransport(models.TransportWebhook)
a.Equal(true, r)
r = Event{}.ValidTransport("noteventsub")
@@ -79,6 +79,6 @@ func TestValidTransport(t *testing.T) {
func TestGetTopic(t *testing.T) {
a := test_setup.SetupTestEnv(t)
- r := Event{}.GetTopic(models.TransportEventSub, "add-reward")
+ r := Event{}.GetTopic(models.TransportWebhook, "add-reward")
a.NotNil(r)
}
diff --git a/internal/events/types/charity/charity_event.go b/internal/events/types/charity/charity_event.go
index 15fc1bbb..48dea525 100644
--- a/internal/events/types/charity/charity_event.go
+++ b/internal/events/types/charity/charity_event.go
@@ -12,12 +12,19 @@ import (
)
var transportsSupported = map[string]bool{
- models.TransportEventSub: true,
+ models.TransportWebhook: true,
+ models.TransportWebSocket: true,
}
var triggers = []string{"charity-donate", "charity-start", "charity-progress", "charity-stop"}
var triggerMapping = map[string]map[string]string{
- models.TransportEventSub: {
+ models.TransportWebhook: {
+ "charity-donate": "channel.charity_campaign.donate",
+ "charity-start": "channel.charity_campaign.start",
+ "charity-progress": "channel.charity_campaign.progress",
+ "charity-stop": "channel.charity_campaign.stop",
+ },
+ models.TransportWebSocket: {
"charity-donate": "channel.charity_campaign.donate",
"charity-start": "channel.charity_campaign.start",
"charity-progress": "channel.charity_campaign.progress",
@@ -135,7 +142,7 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven
}
switch params.Transport {
- case models.TransportEventSub:
+ case models.TransportWebhook, models.TransportWebSocket:
body := models.EventsubResponse{
Subscription: models.EventsubSubscription{
ID: params.ID,
@@ -228,7 +235,7 @@ func (e Event) GetAllTopicsByTransport(transport string) []string {
}
func (e Event) GetEventSubAlias(t string) string {
// check for aliases
- for trigger, topic := range triggerMapping[models.TransportEventSub] {
+ for trigger, topic := range triggerMapping[models.TransportWebhook] {
if topic == t {
return trigger
}
diff --git a/internal/events/types/charity/charity_event_test.go b/internal/events/types/charity/charity_event_test.go
index 85e02e7d..84823df5 100644
--- a/internal/events/types/charity/charity_event_test.go
+++ b/internal/events/types/charity/charity_event_test.go
@@ -26,7 +26,7 @@ func testEventSubCharity(t *testing.T, trigger string) {
params := events.MockEventParameters{
FromUserID: fromUser,
ToUserID: toUser,
- Transport: models.TransportEventSub,
+ Transport: models.TransportWebhook,
Trigger: trigger,
SubscriptionStatus: "enabled",
}
@@ -83,7 +83,7 @@ func TestValidTrigger(t *testing.T) {
func TestValidTransport(t *testing.T) {
a := test_setup.SetupTestEnv(t)
- r := Event{}.ValidTransport(models.TransportEventSub)
+ r := Event{}.ValidTransport(models.TransportWebhook)
a.Equal(true, r)
r = Event{}.ValidTransport("noteventsub")
@@ -93,15 +93,15 @@ func TestValidTransport(t *testing.T) {
func TestGetTopic(t *testing.T) {
a := test_setup.SetupTestEnv(t)
- r := Event{}.GetTopic(models.TransportEventSub, "charity-donate")
+ r := Event{}.GetTopic(models.TransportWebhook, "charity-donate")
a.NotNil(r)
- r = Event{}.GetTopic(models.TransportEventSub, "charity-start")
+ r = Event{}.GetTopic(models.TransportWebhook, "charity-start")
a.NotNil(r)
- r = Event{}.GetTopic(models.TransportEventSub, "charity-progress")
+ r = Event{}.GetTopic(models.TransportWebhook, "charity-progress")
a.NotNil(r)
- r = Event{}.GetTopic(models.TransportEventSub, "charity-stop")
+ r = Event{}.GetTopic(models.TransportWebhook, "charity-stop")
a.NotNil(r)
}
diff --git a/internal/events/types/cheer/cheer_event.go b/internal/events/types/cheer/cheer_event.go
index f42d27d3..8d5dfe10 100644
--- a/internal/events/types/cheer/cheer_event.go
+++ b/internal/events/types/cheer/cheer_event.go
@@ -11,13 +11,17 @@ import (
)
var transportsSupported = map[string]bool{
- models.TransportEventSub: true,
+ models.TransportWebhook: true,
+ models.TransportWebSocket: true,
}
var triggerSupported = []string{"cheer"}
var triggerMapping = map[string]map[string]string{
- models.TransportEventSub: {
+ models.TransportWebhook: {
+ "cheer": "channel.cheer",
+ },
+ models.TransportWebSocket: {
"cheer": "channel.cheer",
},
}
@@ -38,7 +42,7 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven
}
switch params.Transport {
- case models.TransportEventSub:
+ case models.TransportWebhook, models.TransportWebSocket:
body := *&models.EventsubResponse{
Subscription: models.EventsubSubscription{
ID: params.ID,
@@ -125,7 +129,7 @@ func (e Event) GetAllTopicsByTransport(transport string) []string {
}
func (e Event) GetEventSubAlias(t string) string {
// check for aliases
- for trigger, topic := range triggerMapping[models.TransportEventSub] {
+ for trigger, topic := range triggerMapping[models.TransportWebhook] {
if topic == t {
return trigger
}
diff --git a/internal/events/types/cheer/cheer_event_test.go b/internal/events/types/cheer/cheer_event_test.go
index 33900764..da877981 100644
--- a/internal/events/types/cheer/cheer_event_test.go
+++ b/internal/events/types/cheer/cheer_event_test.go
@@ -19,7 +19,7 @@ func TestEventsubCheer(t *testing.T) {
params := events.MockEventParameters{
FromUserID: fromUser,
ToUserID: toUser,
- Transport: models.TransportEventSub,
+ Transport: models.TransportWebhook,
Trigger: "cheer",
SubscriptionStatus: "enabled",
}
@@ -39,7 +39,7 @@ func TestEventsubCheer(t *testing.T) {
params = events.MockEventParameters{
FromUserID: fromUser,
ToUserID: toUser,
- Transport: models.TransportEventSub,
+ Transport: models.TransportWebhook,
IsAnonymous: true,
Trigger: "cheer",
SubscriptionStatus: "enabled",
@@ -83,7 +83,7 @@ func TestValidTrigger(t *testing.T) {
func TestValidTransport(t *testing.T) {
a := test_setup.SetupTestEnv(t)
- r := Event{}.ValidTransport(models.TransportEventSub)
+ r := Event{}.ValidTransport(models.TransportWebhook)
a.Equal(true, r)
r = Event{}.ValidTransport("noteventsub")
@@ -92,6 +92,6 @@ func TestValidTransport(t *testing.T) {
func TestGetTopic(t *testing.T) {
a := test_setup.SetupTestEnv(t)
- r := Event{}.GetTopic(models.TransportEventSub, "cheer")
+ r := Event{}.GetTopic(models.TransportWebhook, "cheer")
a.NotNil(r)
}
diff --git a/internal/events/types/drop/drop.go b/internal/events/types/drop/drop.go
index 4b5a2543..afa3bd98 100644
--- a/internal/events/types/drop/drop.go
+++ b/internal/events/types/drop/drop.go
@@ -13,13 +13,13 @@ import (
)
var transportsSupported = map[string]bool{
- models.TransportEventSub: true,
+ models.TransportWebhook: true,
}
var triggerSupported = []string{"drop"}
var triggerMapping = map[string]map[string]string{
- models.TransportEventSub: {
+ models.TransportWebhook: {
"drop": "drop.entitlement.grant",
},
}
@@ -38,7 +38,7 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven
params.Description = fmt.Sprintf("%v", util.RandomInt(1000))
}
switch params.Transport {
- case models.TransportEventSub:
+ case models.TransportWebhook:
campaignId := util.RandomGUID()
body := &models.DropsEntitlementEventSubResponse{
@@ -133,7 +133,7 @@ func (e Event) GetAllTopicsByTransport(transport string) []string {
}
func (e Event) GetEventSubAlias(t string) string {
// check for aliases
- for trigger, topic := range triggerMapping[models.TransportEventSub] {
+ for trigger, topic := range triggerMapping[models.TransportWebhook] {
if topic == t {
return trigger
}
diff --git a/internal/events/types/drop/drop_test.go b/internal/events/types/drop/drop_test.go
index 40cdd488..75ff7dad 100644
--- a/internal/events/types/drop/drop_test.go
+++ b/internal/events/types/drop/drop_test.go
@@ -20,7 +20,7 @@ func TestEventSub(t *testing.T) {
params := *&events.MockEventParameters{
FromUserID: fromUser,
ToUserID: toUser,
- Transport: models.TransportEventSub,
+ Transport: models.TransportWebhook,
Trigger: "drop",
SubscriptionStatus: "enabled",
}
@@ -63,7 +63,7 @@ func TestValidTrigger(t *testing.T) {
func TestValidTransport(t *testing.T) {
a := test_setup.SetupTestEnv(t)
- r := Event{}.ValidTransport(models.TransportEventSub)
+ r := Event{}.ValidTransport(models.TransportWebhook)
a.Equal(true, r)
r = Event{}.ValidTransport("noteventsub")
@@ -72,6 +72,6 @@ func TestValidTransport(t *testing.T) {
func TestGetTopic(t *testing.T) {
a := test_setup.SetupTestEnv(t)
- r := Event{}.GetTopic(models.TransportEventSub, "drop")
+ r := Event{}.GetTopic(models.TransportWebhook, "drop")
a.NotNil(r)
}
diff --git a/internal/events/types/extension_transaction/transaction_event.go b/internal/events/types/extension_transaction/transaction_event.go
index e1d63747..66ff267d 100644
--- a/internal/events/types/extension_transaction/transaction_event.go
+++ b/internal/events/types/extension_transaction/transaction_event.go
@@ -6,20 +6,18 @@ import (
"encoding/json"
"strings"
- "github.com/spf13/viper"
"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,
+ models.TransportWebhook: true,
}
var triggerSupported = []string{"transaction"}
var triggerMapping = map[string]map[string]string{
- models.TransportEventSub: {
+ models.TransportWebhook: {
"transaction": "extension.bits_transaction.create",
},
}
@@ -30,13 +28,6 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven
var event []byte
var err error
- clientID := viper.GetString("clientId")
-
- // if not configured, generate a random one
- if clientID == "" {
- clientID = util.RandomClientID()
- }
-
if params.Cost == 0 {
params.Cost = 100
}
@@ -50,7 +41,7 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven
}
switch params.Transport {
- case models.TransportEventSub:
+ case models.TransportWebhook:
body := &models.TransactionEventSubResponse{
Subscription: models.EventsubSubscription{
ID: params.ID,
@@ -58,7 +49,7 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven
Type: triggerMapping[params.Transport][params.Trigger],
Version: e.SubscriptionVersion(),
Condition: models.EventsubCondition{
- ExtensionClientID: clientID,
+ ExtensionClientID: params.ClientID,
},
Transport: models.EventsubTransport{
Method: "webhook",
@@ -69,7 +60,7 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven
},
Event: models.TransactionEventSubEvent{
ID: params.ID,
- ExtensionClientID: clientID,
+ ExtensionClientID: params.ClientID,
BroadcasterUserID: params.ToUserID,
BroadcasterUserLogin: "testBroadcaster",
BroadcasterUserName: "testBroadcaster",
@@ -140,7 +131,7 @@ func (e Event) GetAllTopicsByTransport(transport string) []string {
}
func (e Event) GetEventSubAlias(t string) string {
// check for aliases
- for trigger, topic := range triggerMapping[models.TransportEventSub] {
+ for trigger, topic := range triggerMapping[models.TransportWebhook] {
if topic == t {
return trigger
}
diff --git a/internal/events/types/extension_transaction/transaction_event_test.go b/internal/events/types/extension_transaction/transaction_event_test.go
index f36f8c1e..6c197443 100644
--- a/internal/events/types/extension_transaction/transaction_event_test.go
+++ b/internal/events/types/extension_transaction/transaction_event_test.go
@@ -6,7 +6,6 @@ import (
"encoding/json"
"testing"
- "github.com/spf13/viper"
"github.com/twitchdev/twitch-cli/internal/events"
"github.com/twitchdev/twitch-cli/internal/models"
"github.com/twitchdev/twitch-cli/test_setup"
@@ -22,13 +21,12 @@ func TestEventsubTransaction(t *testing.T) {
params := events.MockEventParameters{
FromUserID: fromUser,
ToUserID: toUser,
- Transport: models.TransportEventSub,
+ Transport: models.TransportWebhook,
Trigger: "transaction",
SubscriptionStatus: "enabled",
+ ClientID: clientId,
}
- viper.Set("clientId", clientId)
-
r, err := Event{}.GenerateEvent(params)
a.Nil(err)
@@ -70,7 +68,7 @@ func TestValidTrigger(t *testing.T) {
func TestValidTransport(t *testing.T) {
a := test_setup.SetupTestEnv(t)
- r := Event{}.ValidTransport(models.TransportEventSub)
+ r := Event{}.ValidTransport(models.TransportWebhook)
a.Equal(true, r)
r = Event{}.ValidTransport("noteventsub")
@@ -79,6 +77,6 @@ func TestValidTransport(t *testing.T) {
func TestGetTopic(t *testing.T) {
a := test_setup.SetupTestEnv(t)
- r := Event{}.GetTopic(models.TransportEventSub, "transaction")
+ r := Event{}.GetTopic(models.TransportWebhook, "transaction")
a.NotNil(r)
}
diff --git a/internal/events/types/follow/follow_event.go b/internal/events/types/follow/follow_event.go
index 98d7f8e5..bd998ba8 100644
--- a/internal/events/types/follow/follow_event.go
+++ b/internal/events/types/follow/follow_event.go
@@ -11,12 +11,16 @@ import (
)
var transportsSupported = map[string]bool{
- models.TransportEventSub: true,
+ models.TransportWebhook: true,
+ models.TransportWebSocket: true,
}
var triggers = []string{"follow"}
var triggerMapping = map[string]map[string]string{
- models.TransportEventSub: {
+ models.TransportWebhook: {
+ "follow": "channel.follow",
+ },
+ models.TransportWebSocket: {
"follow": "channel.follow",
},
}
@@ -28,7 +32,7 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven
var err error
switch params.Transport {
- case models.TransportEventSub:
+ case models.TransportWebhook, models.TransportWebSocket:
body := models.EventsubResponse{
Subscription: models.EventsubSubscription{
ID: params.ID,
@@ -112,7 +116,7 @@ func (e Event) GetAllTopicsByTransport(transport string) []string {
}
func (e Event) GetEventSubAlias(t string) string {
// check for aliases
- for trigger, topic := range triggerMapping[models.TransportEventSub] {
+ for trigger, topic := range triggerMapping[models.TransportWebhook] {
if topic == t {
return trigger
}
diff --git a/internal/events/types/follow/follow_event_test.go b/internal/events/types/follow/follow_event_test.go
index bfb7ba31..e8ede722 100644
--- a/internal/events/types/follow/follow_event_test.go
+++ b/internal/events/types/follow/follow_event_test.go
@@ -20,7 +20,7 @@ func TestEventSub(t *testing.T) {
params := *&events.MockEventParameters{
FromUserID: fromUser,
ToUserID: toUser,
- Transport: models.TransportEventSub,
+ Transport: models.TransportWebhook,
Trigger: "subscribe",
SubscriptionStatus: "enabled",
}
@@ -64,7 +64,7 @@ func TestValidTrigger(t *testing.T) {
func TestValidTransport(t *testing.T) {
a := test_setup.SetupTestEnv(t)
- r := Event{}.ValidTransport(models.TransportEventSub)
+ r := Event{}.ValidTransport(models.TransportWebhook)
a.Equal(true, r)
r = Event{}.ValidTransport("noteventsub")
@@ -73,6 +73,6 @@ func TestValidTransport(t *testing.T) {
func TestGetTopic(t *testing.T) {
a := test_setup.SetupTestEnv(t)
- r := Event{}.GetTopic(models.TransportEventSub, "follow")
+ r := Event{}.GetTopic(models.TransportWebhook, "follow")
a.NotNil(r)
}
diff --git a/internal/events/types/gift/channel_gift.go b/internal/events/types/gift/channel_gift.go
index 68e6d721..7f3a5cba 100644
--- a/internal/events/types/gift/channel_gift.go
+++ b/internal/events/types/gift/channel_gift.go
@@ -12,13 +12,17 @@ import (
)
var transportsSupported = map[string]bool{
- models.TransportEventSub: true,
+ models.TransportWebhook: true,
+ models.TransportWebSocket: true,
}
var triggerSupported = []string{"channel-gift"}
var triggerMapping = map[string]map[string]string{
- models.TransportEventSub: {
+ models.TransportWebhook: {
+ "channel-gift": "channel.subscription.gift",
+ },
+ models.TransportWebSocket: {
"channel-gift": "channel.subscription.gift",
},
}
@@ -41,7 +45,7 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven
}
switch params.Transport {
- case models.TransportEventSub:
+ case models.TransportWebhook, models.TransportWebSocket:
body := &models.GiftEventSubResponse{
Subscription: models.EventsubSubscription{
ID: params.ID,
@@ -130,7 +134,7 @@ func (e Event) GetAllTopicsByTransport(transport string) []string {
}
func (e Event) GetEventSubAlias(t string) string {
// check for aliases
- for trigger, topic := range triggerMapping[models.TransportEventSub] {
+ for trigger, topic := range triggerMapping[models.TransportWebhook] {
if topic == t {
return trigger
}
diff --git a/internal/events/types/gift/channel_gift_test.go b/internal/events/types/gift/channel_gift_test.go
index 6617bc4c..551fddc3 100644
--- a/internal/events/types/gift/channel_gift_test.go
+++ b/internal/events/types/gift/channel_gift_test.go
@@ -20,7 +20,7 @@ func TestEventSub(t *testing.T) {
params := *&events.MockEventParameters{
FromUserID: fromUser,
ToUserID: toUser,
- Transport: models.TransportEventSub,
+ Transport: models.TransportWebhook,
Trigger: "channel-gift",
SubscriptionStatus: "enabled",
Cost: 0,
@@ -67,7 +67,7 @@ func TestValidTrigger(t *testing.T) {
func TestValidTransport(t *testing.T) {
a := test_setup.SetupTestEnv(t)
- r := Event{}.ValidTransport(models.TransportEventSub)
+ r := Event{}.ValidTransport(models.TransportWebhook)
a.Equal(true, r)
r = Event{}.ValidTransport("noteventsub")
@@ -76,6 +76,6 @@ func TestValidTransport(t *testing.T) {
func TestGetTopic(t *testing.T) {
a := test_setup.SetupTestEnv(t)
- r := Event{}.GetTopic(models.TransportEventSub, "channel-gift")
+ r := Event{}.GetTopic(models.TransportWebhook, "channel-gift")
a.NotNil(r)
}
diff --git a/internal/events/types/goal/goal_event.go b/internal/events/types/goal/goal_event.go
index d7222260..78e1782a 100644
--- a/internal/events/types/goal/goal_event.go
+++ b/internal/events/types/goal/goal_event.go
@@ -13,13 +13,19 @@ import (
)
var transportsSupported = map[string]bool{
- models.TransportEventSub: true,
+ models.TransportWebhook: true,
+ models.TransportWebSocket: true,
}
var triggerSupported = []string{"goal-begin", "goal-progress", "goal-end"}
var triggerMapping = map[string]map[string]string{
- models.TransportEventSub: {
+ models.TransportWebhook: {
+ "goal-progress": "channel.goal.progress",
+ "goal-begin": "channel.goal.begin",
+ "goal-end": "channel.goal.end",
+ },
+ models.TransportWebSocket: {
"goal-progress": "channel.goal.progress",
"goal-begin": "channel.goal.begin",
"goal-end": "channel.goal.end",
@@ -61,7 +67,7 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven
}
switch params.Transport {
- case models.TransportEventSub:
+ case models.TransportWebhook, models.TransportWebSocket:
body := *&models.EventsubResponse{
Subscription: models.EventsubSubscription{
@@ -153,7 +159,7 @@ func (e Event) GetAllTopicsByTransport(transport string) []string {
func (e Event) GetEventSubAlias(t string) string {
// check for aliases
- for trigger, topic := range triggerMapping[models.TransportEventSub] {
+ for trigger, topic := range triggerMapping[models.TransportWebhook] {
if topic == t {
return trigger
}
diff --git a/internal/events/types/goal/goal_event_test.go b/internal/events/types/goal/goal_event_test.go
index c213dc2f..8b8d8630 100644
--- a/internal/events/types/goal/goal_event_test.go
+++ b/internal/events/types/goal/goal_event_test.go
@@ -19,7 +19,7 @@ func TestEventSub(t *testing.T) {
params := *&events.MockEventParameters{
ToUserID: user,
Description: "Twitch Subscriber Goal",
- Transport: models.TransportEventSub,
+ Transport: models.TransportWebhook,
Trigger: "goal-begin",
SubscriptionStatus: "enabled",
EventStatus: "subscriber",
@@ -39,7 +39,7 @@ func TestEventSub(t *testing.T) {
params = *&events.MockEventParameters{
ToUserID: user,
Description: "Twitch Follower Goal",
- Transport: models.TransportEventSub,
+ Transport: models.TransportWebhook,
Trigger: "goal-progress",
SubscriptionStatus: "enabled",
}
@@ -56,7 +56,7 @@ func TestEventSub(t *testing.T) {
params = *&events.MockEventParameters{
ToUserID: user,
Description: "Twitch Follower Goal",
- Transport: models.TransportEventSub,
+ Transport: models.TransportWebhook,
Trigger: "goal-end",
SubscriptionStatus: "enabled",
}
@@ -100,7 +100,7 @@ func TestValidTrigger(t *testing.T) {
func TestValidTransport(t *testing.T) {
a := test_setup.SetupTestEnv(t)
- r := Event{}.ValidTransport(models.TransportEventSub)
+ r := Event{}.ValidTransport(models.TransportWebhook)
a.Equal(true, r)
r = Event{}.ValidTransport("noteventsub")
@@ -110,6 +110,6 @@ func TestValidTransport(t *testing.T) {
func TestGetTopic(t *testing.T) {
a := test_setup.SetupTestEnv(t)
- r := Event{}.GetTopic(models.TransportEventSub, "goal-progress")
+ r := Event{}.GetTopic(models.TransportWebhook, "goal-progress")
a.NotNil(r)
}
diff --git a/internal/events/types/hype_train/hype_train_event.go b/internal/events/types/hype_train/hype_train_event.go
index 2c21c5b2..266c2e00 100644
--- a/internal/events/types/hype_train/hype_train_event.go
+++ b/internal/events/types/hype_train/hype_train_event.go
@@ -13,11 +13,17 @@ import (
)
var transportsSupported = map[string]bool{
- models.TransportEventSub: true,
+ models.TransportWebhook: true,
+ models.TransportWebSocket: true,
}
var triggerSupported = []string{"hype-train-begin", "hype-train-progress", "hype-train-end"}
var triggerMapping = map[string]map[string]string{
- models.TransportEventSub: {
+ models.TransportWebhook: {
+ "hype-train-progress": "channel.hype_train.progress",
+ "hype-train-begin": "channel.hype_train.begin",
+ "hype-train-end": "channel.hype_train.end",
+ },
+ models.TransportWebSocket: {
"hype-train-progress": "channel.hype_train.progress",
"hype-train-begin": "channel.hype_train.begin",
"hype-train-end": "channel.hype_train.end",
@@ -42,7 +48,7 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven
tNow, _ := time.Parse(time.RFC3339Nano, params.Timestamp)
switch params.Transport {
- case models.TransportEventSub:
+ case models.TransportWebhook, models.TransportWebSocket:
body := models.HypeTrainEventSubResponse{
Subscription: models.EventsubSubscription{
ID: params.ID,
@@ -159,7 +165,7 @@ func (e Event) GetAllTopicsByTransport(transport string) []string {
}
func (e Event) GetEventSubAlias(t string) string {
// check for aliases
- for trigger, topic := range triggerMapping[models.TransportEventSub] {
+ for trigger, topic := range triggerMapping[models.TransportWebhook] {
if topic == t {
return trigger
}
diff --git a/internal/events/types/hype_train/hype_train_event_test.go b/internal/events/types/hype_train/hype_train_event_test.go
index ee755f20..bb028b4e 100644
--- a/internal/events/types/hype_train/hype_train_event_test.go
+++ b/internal/events/types/hype_train/hype_train_event_test.go
@@ -18,7 +18,7 @@ func TestEventSub(t *testing.T) {
params := *&events.MockEventParameters{
ToUserID: toUser,
- Transport: models.TransportEventSub,
+ Transport: models.TransportWebhook,
Trigger: "hype-train-begin",
SubscriptionStatus: "enabled",
}
@@ -35,7 +35,7 @@ func TestEventSub(t *testing.T) {
params = *&events.MockEventParameters{
ToUserID: toUser,
- Transport: models.TransportEventSub,
+ Transport: models.TransportWebhook,
Trigger: "hype-train-progress",
SubscriptionStatus: "enabled",
}
@@ -52,7 +52,7 @@ func TestEventSub(t *testing.T) {
params = *&events.MockEventParameters{
ToUserID: toUser,
- Transport: models.TransportEventSub,
+ Transport: models.TransportWebhook,
Trigger: "hype-train-end",
}
@@ -98,13 +98,13 @@ func TestValidTrigger(t *testing.T) {
func TestValidTransport(t *testing.T) {
a := test_setup.SetupTestEnv(t)
- r := Event{}.ValidTransport(models.TransportEventSub)
+ r := Event{}.ValidTransport(models.TransportWebhook)
a.Equal(true, r)
}
func TestGetTopic(t *testing.T) {
a := test_setup.SetupTestEnv(t)
- r := Event{}.GetTopic(models.TransportEventSub, "hype-train-progress")
+ r := Event{}.GetTopic(models.TransportWebhook, "hype-train-progress")
a.Equal("channel.hype_train.progress", r, "Expected %v, got %v", "channel.hype_train.progress", r)
}
diff --git a/internal/events/types/moderator_change/moderator_change_event.go b/internal/events/types/moderator_change/moderator_change_event.go
index 360d7548..9b4aaf45 100644
--- a/internal/events/types/moderator_change/moderator_change_event.go
+++ b/internal/events/types/moderator_change/moderator_change_event.go
@@ -12,13 +12,18 @@ import (
)
var transportsSupported = map[string]bool{
- models.TransportEventSub: true,
+ models.TransportWebhook: true,
+ models.TransportWebSocket: true,
}
var triggerSupported = []string{"add-moderator", "remove-moderator"}
var triggerMapping = map[string]map[string]string{
- models.TransportEventSub: {
+ models.TransportWebhook: {
+ "add-moderator": "channel.moderator.add",
+ "remove-moderator": "channel.moderator.remove",
+ },
+ models.TransportWebSocket: {
"add-moderator": "channel.moderator.add",
"remove-moderator": "channel.moderator.remove",
},
@@ -31,7 +36,7 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven
var err error
switch params.Transport {
- case models.TransportEventSub:
+ case models.TransportWebhook, models.TransportWebSocket:
body := *&models.EventsubResponse{
Subscription: models.EventsubSubscription{
ID: params.ID,
@@ -114,7 +119,7 @@ func (e Event) GetAllTopicsByTransport(transport string) []string {
}
func (e Event) GetEventSubAlias(t string) string {
// check for aliases
- for trigger, topic := range triggerMapping[models.TransportEventSub] {
+ for trigger, topic := range triggerMapping[models.TransportWebhook] {
if topic == t {
return trigger
}
diff --git a/internal/events/types/moderator_change/moderator_change_event_test.go b/internal/events/types/moderator_change/moderator_change_event_test.go
index 4474c385..4543920f 100644
--- a/internal/events/types/moderator_change/moderator_change_event_test.go
+++ b/internal/events/types/moderator_change/moderator_change_event_test.go
@@ -20,7 +20,7 @@ func TestEventSub(t *testing.T) {
params := *&events.MockEventParameters{
FromUserID: fromUser,
ToUserID: toUser,
- Transport: models.TransportEventSub,
+ Transport: models.TransportWebhook,
Trigger: "add-moderator",
SubscriptionStatus: "enabled",
}
@@ -68,13 +68,13 @@ func TestValidTrigger(t *testing.T) {
func TestValidTransport(t *testing.T) {
a := test_setup.SetupTestEnv(t)
- r := Event{}.ValidTransport(models.TransportEventSub)
+ r := Event{}.ValidTransport(models.TransportWebhook)
a.Equal(true, r)
}
func TestGetTopic(t *testing.T) {
a := test_setup.SetupTestEnv(t)
- r := Event{}.GetTopic(models.TransportEventSub, "remove-moderator")
+ r := Event{}.GetTopic(models.TransportWebhook, "remove-moderator")
a.Equal("channel.moderator.remove", r, "Expected %v, got %v", "channel.moderator.remove", r)
}
diff --git a/internal/events/types/poll/poll.go b/internal/events/types/poll/poll.go
index 961a2ec9..6dc6d918 100644
--- a/internal/events/types/poll/poll.go
+++ b/internal/events/types/poll/poll.go
@@ -14,13 +14,19 @@ import (
)
var transportsSupported = map[string]bool{
- models.TransportEventSub: true,
+ models.TransportWebhook: true,
+ models.TransportWebSocket: true,
}
var triggerSupported = []string{"poll-begin", "poll-progress", "poll-end"}
var triggerMapping = map[string]map[string]string{
- models.TransportEventSub: {
+ models.TransportWebhook: {
+ "poll-begin": "channel.poll.begin",
+ "poll-progress": "channel.poll.progress",
+ "poll-end": "channel.poll.end",
+ },
+ models.TransportWebSocket: {
"poll-begin": "channel.poll.begin",
"poll-progress": "channel.poll.progress",
"poll-end": "channel.poll.end",
@@ -38,7 +44,7 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven
}
switch params.Transport {
- case models.TransportEventSub:
+ case models.TransportWebhook, models.TransportWebSocket:
choices := []models.PollEventSubEventChoice{}
for i := 1; i < 5; i++ {
c := models.PollEventSubEventChoice{
@@ -157,7 +163,7 @@ func intPointer(i int) *int {
}
func (e Event) GetEventSubAlias(t string) string {
// check for aliases
- for trigger, topic := range triggerMapping[models.TransportEventSub] {
+ for trigger, topic := range triggerMapping[models.TransportWebhook] {
if topic == t {
return trigger
}
diff --git a/internal/events/types/poll/poll_test.go b/internal/events/types/poll/poll_test.go
index c61eb36b..a4c8c287 100644
--- a/internal/events/types/poll/poll_test.go
+++ b/internal/events/types/poll/poll_test.go
@@ -20,7 +20,7 @@ func TestEventSub(t *testing.T) {
params := *&events.MockEventParameters{
FromUserID: fromUser,
ToUserID: toUser,
- Transport: models.TransportEventSub,
+ Transport: models.TransportWebhook,
Trigger: "poll-begin",
SubscriptionStatus: "enabled",
}
@@ -37,7 +37,7 @@ func TestEventSub(t *testing.T) {
params = *&events.MockEventParameters{
FromUserID: fromUser,
ToUserID: toUser,
- Transport: models.TransportEventSub,
+ Transport: models.TransportWebhook,
Trigger: "poll-progress",
SubscriptionStatus: "enabled",
}
@@ -54,7 +54,7 @@ func TestEventSub(t *testing.T) {
params = *&events.MockEventParameters{
FromUserID: fromUser,
ToUserID: toUser,
- Transport: models.TransportEventSub,
+ Transport: models.TransportWebhook,
Trigger: "poll-end",
SubscriptionStatus: "enabled",
}
@@ -96,7 +96,7 @@ func TestValidTrigger(t *testing.T) {
func TestValidTransport(t *testing.T) {
a := test_setup.SetupTestEnv(t)
- r := Event{}.ValidTransport(models.TransportEventSub)
+ r := Event{}.ValidTransport(models.TransportWebhook)
a.Equal(true, r)
r = Event{}.ValidTransport("noteventsub")
@@ -105,6 +105,6 @@ func TestValidTransport(t *testing.T) {
func TestGetTopic(t *testing.T) {
a := test_setup.SetupTestEnv(t)
- r := Event{}.GetTopic(models.TransportEventSub, "poll-begin")
+ r := Event{}.GetTopic(models.TransportWebhook, "poll-begin")
a.NotNil(r)
}
diff --git a/internal/events/types/prediction/prediction.go b/internal/events/types/prediction/prediction.go
index 761ba1e4..4b8b7e1d 100644
--- a/internal/events/types/prediction/prediction.go
+++ b/internal/events/types/prediction/prediction.go
@@ -13,13 +13,20 @@ import (
)
var transportsSupported = map[string]bool{
- models.TransportEventSub: true,
+ models.TransportWebhook: true,
+ models.TransportWebSocket: true,
}
var triggerSupported = []string{"prediction-begin", "prediction-progress", "prediction-end", "prediction-lock"}
var triggerMapping = map[string]map[string]string{
- models.TransportEventSub: {
+ models.TransportWebhook: {
+ "prediction-begin": "channel.prediction.begin",
+ "prediction-progress": "channel.prediction.progress",
+ "prediction-lock": "channel.prediction.lock",
+ "prediction-end": "channel.prediction.end",
+ },
+ models.TransportWebSocket: {
"prediction-begin": "channel.prediction.begin",
"prediction-progress": "channel.prediction.progress",
"prediction-lock": "channel.prediction.lock",
@@ -37,7 +44,7 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven
}
switch params.Transport {
- case models.TransportEventSub:
+ case models.TransportWebhook, models.TransportWebSocket:
var outcomes []models.PredictionEventSubEventOutcomes
for i := 0; i < 2; i++ {
color := "blue"
@@ -182,7 +189,7 @@ func intPointer(i int) *int {
}
func (e Event) GetEventSubAlias(t string) string {
// check for aliases
- for trigger, topic := range triggerMapping[models.TransportEventSub] {
+ for trigger, topic := range triggerMapping[models.TransportWebhook] {
if topic == t {
return trigger
}
diff --git a/internal/events/types/prediction/prediction_test.go b/internal/events/types/prediction/prediction_test.go
index 85975865..91b993bb 100644
--- a/internal/events/types/prediction/prediction_test.go
+++ b/internal/events/types/prediction/prediction_test.go
@@ -20,7 +20,7 @@ func TestEventSub(t *testing.T) {
params := *&events.MockEventParameters{
FromUserID: fromUser,
ToUserID: toUser,
- Transport: models.TransportEventSub,
+ Transport: models.TransportWebhook,
Trigger: "prediction-begin",
SubscriptionStatus: "enabled",
}
@@ -35,7 +35,7 @@ func TestEventSub(t *testing.T) {
params = *&events.MockEventParameters{
FromUserID: fromUser,
ToUserID: toUser,
- Transport: models.TransportEventSub,
+ Transport: models.TransportWebhook,
Trigger: "prediction-progress",
SubscriptionStatus: "enabled",
}
@@ -50,7 +50,7 @@ func TestEventSub(t *testing.T) {
params = *&events.MockEventParameters{
FromUserID: fromUser,
ToUserID: toUser,
- Transport: models.TransportEventSub,
+ Transport: models.TransportWebhook,
Trigger: "prediction-lock",
}
@@ -64,7 +64,7 @@ func TestEventSub(t *testing.T) {
params = *&events.MockEventParameters{
FromUserID: fromUser,
ToUserID: toUser,
- Transport: models.TransportEventSub,
+ Transport: models.TransportWebhook,
Trigger: "prediction-end",
}
@@ -103,7 +103,7 @@ func TestValidTrigger(t *testing.T) {
func TestValidTransport(t *testing.T) {
a := test_setup.SetupTestEnv(t)
- r := Event{}.ValidTransport(models.TransportEventSub)
+ r := Event{}.ValidTransport(models.TransportWebhook)
a.Equal(true, r)
r = Event{}.ValidTransport("noteventsub")
@@ -112,6 +112,6 @@ func TestValidTransport(t *testing.T) {
func TestGetTopic(t *testing.T) {
a := test_setup.SetupTestEnv(t)
- r := Event{}.GetTopic(models.TransportEventSub, "prediction-begin")
+ r := Event{}.GetTopic(models.TransportWebhook, "prediction-begin")
a.NotNil(r)
}
diff --git a/internal/events/types/raid/raid.go b/internal/events/types/raid/raid.go
index 1215fdb4..21bdbe6f 100644
--- a/internal/events/types/raid/raid.go
+++ b/internal/events/types/raid/raid.go
@@ -12,13 +12,17 @@ import (
)
var transportsSupported = map[string]bool{
- models.TransportEventSub: true,
+ models.TransportWebhook: true,
+ models.TransportWebSocket: true,
}
var triggerSupported = []string{"raid"}
var triggerMapping = map[string]map[string]string{
- models.TransportEventSub: {
+ models.TransportWebhook: {
+ "raid": "channel.raid",
+ },
+ models.TransportWebSocket: {
"raid": "channel.raid",
},
}
@@ -30,7 +34,7 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven
var err error
switch params.Transport {
- case models.TransportEventSub:
+ case models.TransportWebhook, models.TransportWebSocket:
body := &models.RaidEventSubResponse{
Subscription: models.EventsubSubscription{
ID: params.ID,
@@ -113,7 +117,7 @@ func (e Event) GetAllTopicsByTransport(transport string) []string {
}
func (e Event) GetEventSubAlias(t string) string {
// check for aliases
- for trigger, topic := range triggerMapping[models.TransportEventSub] {
+ for trigger, topic := range triggerMapping[models.TransportWebhook] {
if topic == t {
return trigger
}
diff --git a/internal/events/types/raid/raid_test.go b/internal/events/types/raid/raid_test.go
index 73e77fcb..80f54b4a 100644
--- a/internal/events/types/raid/raid_test.go
+++ b/internal/events/types/raid/raid_test.go
@@ -20,7 +20,7 @@ func TestEventSub(t *testing.T) {
params := *&events.MockEventParameters{
FromUserID: fromUser,
ToUserID: toUser,
- Transport: models.TransportEventSub,
+ Transport: models.TransportWebhook,
Trigger: "raid",
SubscriptionStatus: "enabled",
}
@@ -63,7 +63,7 @@ func TestValidTrigger(t *testing.T) {
func TestValidTransport(t *testing.T) {
a := test_setup.SetupTestEnv(t)
- r := Event{}.ValidTransport(models.TransportEventSub)
+ r := Event{}.ValidTransport(models.TransportWebhook)
a.Equal(true, r)
r = Event{}.ValidTransport("noteventsub")
@@ -72,6 +72,6 @@ func TestValidTransport(t *testing.T) {
func TestGetTopic(t *testing.T) {
a := test_setup.SetupTestEnv(t)
- r := Event{}.GetTopic(models.TransportEventSub, "trigger_keyword")
+ r := Event{}.GetTopic(models.TransportWebhook, "trigger_keyword")
a.NotNil(r)
}
diff --git a/internal/events/types/shield_mode/shield_mode.go b/internal/events/types/shield_mode/shield_mode.go
index bccaf603..b87e59cc 100644
--- a/internal/events/types/shield_mode/shield_mode.go
+++ b/internal/events/types/shield_mode/shield_mode.go
@@ -13,12 +13,17 @@ import (
)
var transportsSupported = map[string]bool{
- models.TransportEventSub: true,
+ models.TransportWebhook: true,
+ models.TransportWebSocket: true,
}
var triggers = []string{"shield-mode-begin", "shield-mode-end"}
var triggerMapping = map[string]map[string]string{
- models.TransportEventSub: {
+ models.TransportWebhook: {
+ "shield-mode-begin": "channel.shield_mode.begin",
+ "shield-mode-end": "channel.shield_mode.end",
+ },
+ models.TransportWebSocket: {
"shield-mode-begin": "channel.shield_mode.begin",
"shield-mode-end": "channel.shield_mode.end",
},
@@ -31,7 +36,7 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven
var err error
switch params.Transport {
- case models.TransportEventSub:
+ case models.TransportWebhook, models.TransportWebSocket:
eventBody := models.ShieldModeEventSubEvent{
BroadcasterUserID: params.ToUserID,
BroadcasterUserName: params.ToUserName,
@@ -123,7 +128,7 @@ func (e Event) GetAllTopicsByTransport(transport string) []string {
}
func (e Event) GetEventSubAlias(t string) string {
// check for aliases
- for trigger, topic := range triggerMapping[models.TransportEventSub] {
+ for trigger, topic := range triggerMapping[models.TransportWebhook] {
if topic == t {
return trigger
}
diff --git a/internal/events/types/shield_mode/shield_mode_test.go b/internal/events/types/shield_mode/shield_mode_test.go
index 1b33435e..3ca0beeb 100644
--- a/internal/events/types/shield_mode/shield_mode_test.go
+++ b/internal/events/types/shield_mode/shield_mode_test.go
@@ -20,7 +20,7 @@ func TestEventSub(t *testing.T) {
beginParams := *&events.MockEventParameters{
FromUserID: fromUser,
ToUserID: toUser,
- Transport: models.TransportEventSub,
+ Transport: models.TransportWebhook,
Trigger: "shield-mode-begin",
SubscriptionStatus: "enabled",
Cost: 0,
@@ -28,7 +28,7 @@ func TestEventSub(t *testing.T) {
endParams := *&events.MockEventParameters{
FromUserID: fromUser,
ToUserID: toUser,
- Transport: models.TransportEventSub,
+ Transport: models.TransportWebhook,
Trigger: "shield-mode-end",
SubscriptionStatus: "enabled",
Cost: 0,
@@ -89,7 +89,7 @@ func TestValidTrigger(t *testing.T) {
func TestValidTransport(t *testing.T) {
a := test_setup.SetupTestEnv(t)
- r := Event{}.ValidTransport(models.TransportEventSub)
+ r := Event{}.ValidTransport(models.TransportWebhook)
a.Equal(true, r)
r = Event{}.ValidTransport("noteventsub")
@@ -98,9 +98,9 @@ func TestValidTransport(t *testing.T) {
func TestGetTopic(t *testing.T) {
a := test_setup.SetupTestEnv(t)
- r := Event{}.GetTopic(models.TransportEventSub, "shield-mode-begin")
+ r := Event{}.GetTopic(models.TransportWebhook, "shield-mode-begin")
a.NotNil(r)
- r = Event{}.GetTopic(models.TransportEventSub, "shield-mode-end")
+ r = Event{}.GetTopic(models.TransportWebhook, "shield-mode-end")
a.NotNil(r)
}
diff --git a/internal/events/types/shoutout/shoutout.go b/internal/events/types/shoutout/shoutout.go
index e0c673e8..39c2e3b7 100644
--- a/internal/events/types/shoutout/shoutout.go
+++ b/internal/events/types/shoutout/shoutout.go
@@ -13,12 +13,17 @@ import (
)
var transportsSupported = map[string]bool{
- models.TransportEventSub: true,
+ models.TransportWebhook: true,
+ models.TransportWebSocket: true,
}
var triggers = []string{"shoutout-create", "shoutout-received"}
var triggerMapping = map[string]map[string]string{
- models.TransportEventSub: {
+ models.TransportWebhook: {
+ "shoutout-create": "channel.shoutout.create",
+ "shoutout-received": "channel.shoutout.receive",
+ },
+ models.TransportWebSocket: {
"shoutout-create": "channel.shoutout.create",
"shoutout-received": "channel.shoutout.receive",
},
@@ -31,7 +36,7 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven
var err error
switch params.Transport {
- case models.TransportEventSub:
+ case models.TransportWebhook, models.TransportWebSocket:
viewerCount := util.RandomInt(2000)
startedAt := util.GetTimestamp()
@@ -163,7 +168,7 @@ func (e Event) GetAllTopicsByTransport(transport string) []string {
}
func (e Event) GetEventSubAlias(t string) string {
// check for aliases
- for trigger, topic := range triggerMapping[models.TransportEventSub] {
+ for trigger, topic := range triggerMapping[models.TransportWebhook] {
if topic == t {
return trigger
}
diff --git a/internal/events/types/shoutout/shoutout_test.go b/internal/events/types/shoutout/shoutout_test.go
index b2407fa1..3ce11898 100644
--- a/internal/events/types/shoutout/shoutout_test.go
+++ b/internal/events/types/shoutout/shoutout_test.go
@@ -20,7 +20,7 @@ func TestEventSub(t *testing.T) {
beginParams := *&events.MockEventParameters{
FromUserID: fromUser,
ToUserID: toUser,
- Transport: models.TransportEventSub,
+ Transport: models.TransportWebhook,
Trigger: "shoutout-create",
SubscriptionStatus: "enabled",
Cost: 0,
@@ -28,7 +28,7 @@ func TestEventSub(t *testing.T) {
endParams := *&events.MockEventParameters{
FromUserID: fromUser,
ToUserID: toUser,
- Transport: models.TransportEventSub,
+ Transport: models.TransportWebhook,
Trigger: "shoutout-received",
SubscriptionStatus: "enabled",
Cost: 0,
@@ -90,7 +90,7 @@ func TestValidTrigger(t *testing.T) {
func TestValidTransport(t *testing.T) {
a := test_setup.SetupTestEnv(t)
- r := Event{}.ValidTransport(models.TransportEventSub)
+ r := Event{}.ValidTransport(models.TransportWebhook)
a.Equal(true, r)
r = Event{}.ValidTransport("noteventsub")
@@ -99,9 +99,9 @@ func TestValidTransport(t *testing.T) {
func TestGetTopic(t *testing.T) {
a := test_setup.SetupTestEnv(t)
- r := Event{}.GetTopic(models.TransportEventSub, "shoutout-create")
+ r := Event{}.GetTopic(models.TransportWebhook, "shoutout-create")
a.NotNil(r)
- r = Event{}.GetTopic(models.TransportEventSub, "shoutout-receieve")
+ r = Event{}.GetTopic(models.TransportWebhook, "shoutout-receieve")
a.NotNil(r)
}
diff --git a/internal/events/types/stream_change/stream_change_event.go b/internal/events/types/stream_change/stream_change_event.go
index 57add539..4488b963 100644
--- a/internal/events/types/stream_change/stream_change_event.go
+++ b/internal/events/types/stream_change/stream_change_event.go
@@ -11,13 +11,17 @@ import (
)
var transportsSupported = map[string]bool{
- models.TransportEventSub: true,
+ models.TransportWebhook: true,
+ models.TransportWebSocket: true,
}
var triggerSupported = []string{"stream-change"}
var triggerMapping = map[string]map[string]string{
- models.TransportEventSub: {
+ models.TransportWebhook: {
+ "stream-change": "channel.update",
+ },
+ models.TransportWebSocket: {
"stream-change": "channel.update",
},
}
@@ -41,7 +45,7 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven
}
switch params.Transport {
- case models.TransportEventSub:
+ case models.TransportWebhook, models.TransportWebSocket:
body := &models.EventsubResponse{
// make the eventsub response (if supported)
Subscription: models.EventsubSubscription{
@@ -127,7 +131,7 @@ func (e Event) GetAllTopicsByTransport(transport string) []string {
}
func (e Event) GetEventSubAlias(t string) string {
// check for aliases
- for trigger, topic := range triggerMapping[models.TransportEventSub] {
+ for trigger, topic := range triggerMapping[models.TransportWebhook] {
if topic == t {
return trigger
}
diff --git a/internal/events/types/stream_change/stream_change_event_test.go b/internal/events/types/stream_change/stream_change_event_test.go
index 166d341a..c55ea8b4 100644
--- a/internal/events/types/stream_change/stream_change_event_test.go
+++ b/internal/events/types/stream_change/stream_change_event_test.go
@@ -20,7 +20,7 @@ func TestEventSub(t *testing.T) {
params := *&events.MockEventParameters{
FromUserID: fromUser,
ToUserID: toUser,
- Transport: models.TransportEventSub,
+ Transport: models.TransportWebhook,
Trigger: "stream-change",
SubscriptionStatus: "enabled",
}
@@ -39,7 +39,7 @@ func TestEventSub(t *testing.T) {
params = events.MockEventParameters{
FromUserID: fromUser,
ToUserID: toUser,
- Transport: models.TransportEventSub,
+ Transport: models.TransportWebhook,
Trigger: "stream_change",
SubscriptionStatus: "enabled",
GameID: "1234",
@@ -84,7 +84,7 @@ func TestValidTrigger(t *testing.T) {
func TestValidTransport(t *testing.T) {
a := test_setup.SetupTestEnv(t)
- r := Event{}.ValidTransport(models.TransportEventSub)
+ r := Event{}.ValidTransport(models.TransportWebhook)
a.Equal(true, r)
r = Event{}.ValidTransport("noteventsub")
@@ -93,6 +93,6 @@ func TestValidTransport(t *testing.T) {
func TestGetTopic(t *testing.T) {
a := test_setup.SetupTestEnv(t)
- r := Event{}.GetTopic(models.TransportEventSub, "stream-change")
+ r := Event{}.GetTopic(models.TransportWebhook, "stream-change")
a.NotNil(r)
}
diff --git a/internal/events/types/streamdown/streamdown.go b/internal/events/types/streamdown/streamdown.go
index d8f55007..c84bab22 100644
--- a/internal/events/types/streamdown/streamdown.go
+++ b/internal/events/types/streamdown/streamdown.go
@@ -11,13 +11,17 @@ import (
)
var transportsSupported = map[string]bool{
- models.TransportEventSub: true,
+ models.TransportWebhook: true,
+ models.TransportWebSocket: true,
}
var triggerSupported = []string{"streamdown"}
var triggerMapping = map[string]map[string]string{
- models.TransportEventSub: {
+ models.TransportWebhook: {
+ "streamdown": "stream.offline",
+ },
+ models.TransportWebSocket: {
"streamdown": "stream.offline",
},
}
@@ -29,7 +33,7 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven
var err error
switch params.Transport {
- case models.TransportEventSub:
+ case models.TransportWebhook, models.TransportWebSocket:
body := &models.EventsubResponse{
Subscription: models.EventsubSubscription{
ID: params.ID,
@@ -108,7 +112,7 @@ func (e Event) GetAllTopicsByTransport(transport string) []string {
}
func (e Event) GetEventSubAlias(t string) string {
// check for aliases
- for trigger, topic := range triggerMapping[models.TransportEventSub] {
+ for trigger, topic := range triggerMapping[models.TransportWebhook] {
if topic == t {
return trigger
}
diff --git a/internal/events/types/streamdown/streamdown_test.go b/internal/events/types/streamdown/streamdown_test.go
index 82c186cd..f28270db 100644
--- a/internal/events/types/streamdown/streamdown_test.go
+++ b/internal/events/types/streamdown/streamdown_test.go
@@ -20,7 +20,7 @@ func TestEventSub(t *testing.T) {
params := *&events.MockEventParameters{
FromUserID: fromUser,
ToUserID: toUser,
- Transport: models.TransportEventSub,
+ Transport: models.TransportWebhook,
Trigger: "streamdown",
SubscriptionStatus: "enabled",
}
@@ -63,7 +63,7 @@ func TestValidTrigger(t *testing.T) {
func TestValidTransport(t *testing.T) {
a := test_setup.SetupTestEnv(t)
- r := Event{}.ValidTransport(models.TransportEventSub)
+ r := Event{}.ValidTransport(models.TransportWebhook)
a.Equal(true, r)
r = Event{}.ValidTransport("noteventsub")
@@ -72,6 +72,6 @@ func TestValidTransport(t *testing.T) {
func TestGetTopic(t *testing.T) {
a := test_setup.SetupTestEnv(t)
- r := Event{}.GetTopic(models.TransportEventSub, "streamdown")
+ r := Event{}.GetTopic(models.TransportWebhook, "streamdown")
a.NotNil(r)
}
diff --git a/internal/events/types/streamup/streamup.go b/internal/events/types/streamup/streamup.go
index 63dced19..991b5520 100644
--- a/internal/events/types/streamup/streamup.go
+++ b/internal/events/types/streamup/streamup.go
@@ -12,13 +12,17 @@ import (
)
var transportsSupported = map[string]bool{
- models.TransportEventSub: true,
+ models.TransportWebhook: true,
+ models.TransportWebSocket: true,
}
var triggerSupported = []string{"streamup"}
var triggerMapping = map[string]map[string]string{
- models.TransportEventSub: {
+ models.TransportWebhook: {
+ "streamup": "stream.online",
+ },
+ models.TransportWebSocket: {
"streamup": "stream.online",
},
}
@@ -38,7 +42,7 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven
}
switch params.Transport {
- case models.TransportEventSub:
+ case models.TransportWebhook, models.TransportWebSocket:
body := &models.EventsubResponse{
Subscription: models.EventsubSubscription{
ID: params.ID,
@@ -120,7 +124,7 @@ func (e Event) GetAllTopicsByTransport(transport string) []string {
}
func (e Event) GetEventSubAlias(t string) string {
// check for aliases
- for trigger, topic := range triggerMapping[models.TransportEventSub] {
+ for trigger, topic := range triggerMapping[models.TransportWebhook] {
if topic == t {
return trigger
}
diff --git a/internal/events/types/streamup/streamup_test.go b/internal/events/types/streamup/streamup_test.go
index 26d97c94..e7ff10b2 100644
--- a/internal/events/types/streamup/streamup_test.go
+++ b/internal/events/types/streamup/streamup_test.go
@@ -20,7 +20,7 @@ func TestEventSub(t *testing.T) {
params := *&events.MockEventParameters{
FromUserID: fromUser,
ToUserID: toUser,
- Transport: models.TransportEventSub,
+ Transport: models.TransportWebhook,
Trigger: "streamup",
SubscriptionStatus: "enabled",
}
@@ -63,7 +63,7 @@ func TestValidTrigger(t *testing.T) {
func TestValidTransport(t *testing.T) {
a := test_setup.SetupTestEnv(t)
- r := Event{}.ValidTransport(models.TransportEventSub)
+ r := Event{}.ValidTransport(models.TransportWebhook)
a.Equal(true, r)
r = Event{}.ValidTransport("noteventsub")
@@ -72,6 +72,6 @@ func TestValidTransport(t *testing.T) {
func TestGetTopic(t *testing.T) {
a := test_setup.SetupTestEnv(t)
- r := Event{}.GetTopic(models.TransportEventSub, "streamup")
+ r := Event{}.GetTopic(models.TransportWebhook, "streamup")
a.NotNil(r)
}
diff --git a/internal/events/types/subscribe/sub_event.go b/internal/events/types/subscribe/sub_event.go
index 549f1f1d..6dff04c8 100644
--- a/internal/events/types/subscribe/sub_event.go
+++ b/internal/events/types/subscribe/sub_event.go
@@ -11,13 +11,20 @@ import (
)
var transportsSupported = map[string]bool{
- models.TransportEventSub: true,
+ models.TransportWebhook: true,
+ models.TransportWebSocket: true,
}
var triggerSupported = []string{"subscribe", "gift", "unsubscribe", "subscribe-end"}
var triggerMapping = map[string]map[string]string{
- models.TransportEventSub: {
+ models.TransportWebhook: {
+ "subscribe": "channel.subscribe",
+ "unsubscribe": "channel.subscription.end",
+ "gift": "channel.subscribe",
+ "subscribe-end": "channel.subscription.end",
+ },
+ models.TransportWebSocket: {
"subscribe": "channel.subscribe",
"unsubscribe": "channel.subscription.end",
"gift": "channel.subscribe",
@@ -36,7 +43,7 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven
}
switch params.Transport {
- case models.TransportEventSub:
+ case models.TransportWebhook, models.TransportWebSocket:
body := *&models.EventsubResponse{
Subscription: models.EventsubSubscription{
ID: params.ID,
@@ -122,7 +129,7 @@ func (e Event) GetAllTopicsByTransport(transport string) []string {
}
func (e Event) GetEventSubAlias(t string) string {
// check for aliases
- for trigger, topic := range triggerMapping[models.TransportEventSub] {
+ for trigger, topic := range triggerMapping[models.TransportWebhook] {
if topic == t {
return trigger
}
diff --git a/internal/events/types/subscribe/sub_event_test.go b/internal/events/types/subscribe/sub_event_test.go
index 153a7ead..3382a34c 100644
--- a/internal/events/types/subscribe/sub_event_test.go
+++ b/internal/events/types/subscribe/sub_event_test.go
@@ -21,7 +21,7 @@ func TestEventSub(t *testing.T) {
params := *&events.MockEventParameters{
FromUserID: fromUser,
ToUserID: toUser,
- Transport: models.TransportEventSub,
+ Transport: models.TransportWebhook,
SubscriptionStatus: "enabled",
Trigger: "subscribe",
Tier: tierTwo,
@@ -67,7 +67,7 @@ func TestValidTrigger(t *testing.T) {
func TestValidTransport(t *testing.T) {
a := test_setup.SetupTestEnv(t)
- r := Event{}.ValidTransport(models.TransportEventSub)
+ r := Event{}.ValidTransport(models.TransportWebhook)
a.Equal(true, r)
r = Event{}.ValidTransport("noteventsub")
@@ -77,6 +77,6 @@ func TestValidTransport(t *testing.T) {
func TestGetTopic(t *testing.T) {
a := test_setup.SetupTestEnv(t)
- r := Event{}.GetTopic(models.TransportEventSub, "subscribe")
+ r := Event{}.GetTopic(models.TransportWebhook, "subscribe")
a.NotNil(r)
}
diff --git a/internal/events/types/subscription_message/subscription_message.go b/internal/events/types/subscription_message/subscription_message.go
index 789d9182..bb75dc99 100644
--- a/internal/events/types/subscription_message/subscription_message.go
+++ b/internal/events/types/subscription_message/subscription_message.go
@@ -12,13 +12,17 @@ import (
)
var transportsSupported = map[string]bool{
- models.TransportEventSub: true,
+ models.TransportWebhook: true,
+ models.TransportWebSocket: true,
}
var triggerSupported = []string{"subscribe-message"}
var triggerMapping = map[string]map[string]string{
- models.TransportEventSub: {
+ models.TransportWebhook: {
+ "subscribe-message": "channel.subscription.message",
+ },
+ models.TransportWebSocket: {
"subscribe-message": "channel.subscription.message",
},
}
@@ -34,7 +38,7 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven
}
switch params.Transport {
- case models.TransportEventSub:
+ case models.TransportWebhook, models.TransportWebSocket:
body := &models.SubscribeMessageEventSubResponse{
Subscription: models.EventsubSubscription{
ID: params.ID,
@@ -134,7 +138,7 @@ func (e Event) GetAllTopicsByTransport(transport string) []string {
}
func (e Event) GetEventSubAlias(t string) string {
// check for aliases
- for trigger, topic := range triggerMapping[models.TransportEventSub] {
+ for trigger, topic := range triggerMapping[models.TransportWebhook] {
if topic == t {
return trigger
}
diff --git a/internal/events/types/subscription_message/subscription_message_test.go b/internal/events/types/subscription_message/subscription_message_test.go
index c286f9e7..98793811 100644
--- a/internal/events/types/subscription_message/subscription_message_test.go
+++ b/internal/events/types/subscription_message/subscription_message_test.go
@@ -22,7 +22,7 @@ func TestEventSub(t *testing.T) {
params := *&events.MockEventParameters{
FromUserID: fromUser,
ToUserID: toUser,
- Transport: models.TransportEventSub,
+ Transport: models.TransportWebhook,
Trigger: "subscribe-message",
SubscriptionStatus: "enabled",
Cost: int64(ten),
@@ -40,7 +40,7 @@ func TestEventSub(t *testing.T) {
params = *&events.MockEventParameters{
FromUserID: fromUser,
ToUserID: toUser,
- Transport: models.TransportEventSub,
+ Transport: models.TransportWebhook,
Trigger: "subscribe-message",
SubscriptionStatus: "enabled",
Cost: int64(ten),
@@ -85,7 +85,7 @@ func TestValidTrigger(t *testing.T) {
func TestValidTransport(t *testing.T) {
a := test_setup.SetupTestEnv(t)
- r := Event{}.ValidTransport(models.TransportEventSub)
+ r := Event{}.ValidTransport(models.TransportWebhook)
a.Equal(true, r)
r = Event{}.ValidTransport("noteventsub")
@@ -94,6 +94,6 @@ func TestValidTransport(t *testing.T) {
func TestGetTopic(t *testing.T) {
a := test_setup.SetupTestEnv(t)
- r := Event{}.GetTopic(models.TransportEventSub, "subscribe-message")
+ r := Event{}.GetTopic(models.TransportWebhook, "subscribe-message")
a.NotNil(r)
}
diff --git a/internal/events/types/types.go b/internal/events/types/types.go
index dac1fb2a..f0999075 100644
--- a/internal/events/types/types.go
+++ b/internal/events/types/types.go
@@ -5,9 +5,11 @@ package types
import (
"errors"
"sort"
+ "strings"
"github.com/twitchdev/twitch-cli/internal/events"
- "github.com/twitchdev/twitch-cli/internal/events/types/authorization"
+ "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/ban"
"github.com/twitchdev/twitch-cli/internal/events/types/channel_points_redemption"
"github.com/twitchdev/twitch-cli/internal/events/types/channel_points_reward"
@@ -36,7 +38,8 @@ import (
func AllEvents() []events.MockEvent {
return []events.MockEvent{
- authorization.Event{},
+ authorization_grant.Event{},
+ authorization_revoke.Event{},
ban.Event{},
channel_points_redemption.Event{},
channel_points_reward.Event{},
@@ -63,12 +66,34 @@ func AllEvents() []events.MockEvent {
}
}
-func AllEventTopics() []string {
+func AllWebhookTopics() []string {
allEvents := []string{}
+ allEventsMap := make(map[string]int)
for _, e := range AllEvents() {
- for _, topic := range e.GetAllTopicsByTransport(models.TransportEventSub) {
- allEvents = append(allEvents, topic)
+ for _, topic := range e.GetAllTopicsByTransport(models.TransportWebhook) {
+ _, duplicate := allEventsMap[topic]
+ if !duplicate {
+ allEvents = append(allEvents, topic)
+ allEventsMap[topic] = 1
+ }
+ }
+ }
+
+ // Sort the topics alphabetically
+ sort.Strings(allEvents)
+
+ return allEvents
+}
+
+func WebSocketCommandTopics() []string {
+ allEvents := []string{}
+
+ for _, e := range AllEvents() {
+ for _, topic := range e.GetAllTopicsByTransport(models.TransportWebSocket) {
+ if strings.HasPrefix(topic, "websocket") {
+ allEvents = append(allEvents, topic)
+ }
}
}
@@ -80,7 +105,7 @@ func AllEventTopics() []string {
func GetByTriggerAndTransport(trigger string, transport string) (events.MockEvent, error) {
for _, e := range AllEvents() {
- if transport == models.TransportEventSub {
+ if transport == models.TransportWebhook || transport == models.TransportWebSocket {
newTrigger := e.GetEventSubAlias(trigger)
if newTrigger != "" {
trigger = newTrigger
@@ -91,5 +116,11 @@ func GetByTriggerAndTransport(trigger string, transport string) (events.MockEven
}
}
+ // Different error for websocket transport
+ if strings.EqualFold(transport, "websocket") {
+ return nil, errors.New("Invalid event, or this event is not available via WebSockets.")
+ }
+
+ // Default error
return nil, errors.New("Invalid event")
}
diff --git a/internal/events/types/user/user_update.go b/internal/events/types/user/user_update.go
index bcd9af56..2f4528d9 100644
--- a/internal/events/types/user/user_update.go
+++ b/internal/events/types/user/user_update.go
@@ -11,12 +11,16 @@ import (
)
var transportsSupported = map[string]bool{
- models.TransportEventSub: true,
+ models.TransportWebhook: true,
+ models.TransportWebSocket: true,
}
var triggers = []string{"user-update"}
var triggerMapping = map[string]map[string]string{
- models.TransportEventSub: {
+ models.TransportWebhook: {
+ "user-update": "user.update",
+ },
+ models.TransportWebSocket: {
"user-update": "user.update",
},
}
@@ -28,7 +32,7 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven
var err error
switch params.Transport {
- case models.TransportEventSub:
+ case models.TransportWebhook, models.TransportWebSocket:
body := models.EventsubResponse{
Subscription: models.EventsubSubscription{
ID: params.ID,
@@ -110,7 +114,7 @@ func (e Event) GetAllTopicsByTransport(transport string) []string {
}
func (e Event) GetEventSubAlias(t string) string {
// check for aliases
- for trigger, topic := range triggerMapping[models.TransportEventSub] {
+ for trigger, topic := range triggerMapping[models.TransportWebhook] {
if topic == t {
return trigger
}
diff --git a/internal/events/types/user/user_update_test.go b/internal/events/types/user/user_update_test.go
index 92046290..f90a459f 100644
--- a/internal/events/types/user/user_update_test.go
+++ b/internal/events/types/user/user_update_test.go
@@ -18,7 +18,7 @@ func TestEventSub(t *testing.T) {
params := *&events.MockEventParameters{
ToUserID: toUser,
- Transport: models.TransportEventSub,
+ Transport: models.TransportWebhook,
Trigger: "user-update",
}
@@ -58,7 +58,7 @@ func TestValidTrigger(t *testing.T) {
func TestValidTransport(t *testing.T) {
a := test_setup.SetupTestEnv(t)
- r := Event{}.ValidTransport(models.TransportEventSub)
+ r := Event{}.ValidTransport(models.TransportWebhook)
a.Equal(true, r)
r = Event{}.ValidTransport("noteventsub")
@@ -67,6 +67,6 @@ func TestValidTransport(t *testing.T) {
func TestGetTopic(t *testing.T) {
a := test_setup.SetupTestEnv(t)
- r := Event{}.GetTopic(models.TransportEventSub, "user-update")
+ r := Event{}.GetTopic(models.TransportWebhook, "user-update")
a.NotNil(r)
}
diff --git a/internal/events/verify/subscription_verify.go b/internal/events/verify/subscription_verify.go
index 67d5967e..f1b4ab91 100644
--- a/internal/events/verify/subscription_verify.go
+++ b/internal/events/verify/subscription_verify.go
@@ -43,7 +43,7 @@ func VerifyWebhookSubscription(p VerifyParameters) (VerifyResponse, error) {
return VerifyResponse{}, err
}
- if p.Transport == models.TransportEventSub {
+ if p.Transport == models.TransportWebhook {
newTrigger := event.GetEventSubAlias(p.Event)
if newTrigger != "" {
p.Event = newTrigger
@@ -137,7 +137,7 @@ func generateWebhookSubscriptionBody(transport string, eventID string, event str
var err error
ts := util.GetTimestamp().Format(time.RFC3339Nano)
switch transport {
- case models.TransportEventSub:
+ case models.TransportWebhook:
body := models.EventsubSubscriptionVerification{
Challenge: challenge,
Subscription: models.EventsubSubscription{
diff --git a/internal/events/verify/subscription_verify_test.go b/internal/events/verify/subscription_verify_test.go
index 37ea48e5..4d015726 100644
--- a/internal/events/verify/subscription_verify_test.go
+++ b/internal/events/verify/subscription_verify_test.go
@@ -44,7 +44,7 @@ func TestSubscriptionVerify(t *testing.T) {
defer ts.Close()
p := VerifyParameters{
- Transport: models.TransportEventSub,
+ Transport: models.TransportWebhook,
Event: "subscribe",
ForwardAddress: ts.URL,
Secret: "potatoes",
@@ -59,7 +59,7 @@ func TestSubscriptionVerify(t *testing.T) {
p = VerifyParameters{
ForwardAddress: ts.URL + "/badendpoint",
- Transport: models.TransportEventSub,
+ Transport: models.TransportWebhook,
Event: "subscribe",
Secret: "potatoes",
}
diff --git a/internal/events/websocket/mock_server/client.go b/internal/events/websocket/mock_server/client.go
new file mode 100644
index 00000000..d108c2b3
--- /dev/null
+++ b/internal/events/websocket/mock_server/client.go
@@ -0,0 +1,42 @@
+package mock_server
+
+import (
+ "sync"
+ "time"
+
+ "github.com/gorilla/websocket"
+)
+
+type Client struct {
+ clientName string // Unique name for the client. Not the Client ID.
+ conn *websocket.Conn
+ mutex sync.Mutex
+ ConnectedAtTimestamp string
+ connectionUrl string
+
+ mustSubscribeTimer *time.Timer
+ keepAliveChanOpen bool
+ keepAliveLoopChan chan struct{}
+ keepAliveTimer *time.Ticker
+ pingChanOpen bool
+ pingLoopChan chan struct{}
+ pingTimer *time.Ticker
+}
+
+func (c *Client) SendMessage(messageType int, data []byte) error {
+ c.mutex.Lock()
+ defer c.mutex.Unlock()
+ return c.conn.WriteMessage(messageType, data)
+}
+
+func (c *Client) CloseWithReason(reason *CloseMessage) {
+ c.conn.WriteControl(
+ websocket.CloseMessage,
+ websocket.FormatCloseMessage(reason.code, reason.message),
+ time.Now().Add(2*time.Second),
+ )
+}
+
+func (c *Client) CloseDirty() {
+ c.conn.Close()
+}
diff --git a/internal/events/websocket/mock_server/close_messages.go b/internal/events/websocket/mock_server/close_messages.go
new file mode 100644
index 00000000..3867595e
--- /dev/null
+++ b/internal/events/websocket/mock_server/close_messages.go
@@ -0,0 +1,71 @@
+package mock_server
+
+type CloseMessage struct {
+ code int
+ message string
+}
+
+var (
+ closeInternalServerError = &CloseMessage{
+ code: 4000,
+ message: "internal server error",
+ }
+
+ closeClientSentInboundTraffic = &CloseMessage{
+ code: 4001,
+ message: "sent inbound traffic",
+ }
+
+ closeClientFailedPingPong = &CloseMessage{
+ code: 4002,
+ message: "failed ping pong",
+ }
+
+ closeConnectionUnused = &CloseMessage{
+ code: 4003,
+ message: "connection unused",
+ }
+
+ closeReconnectGraceTimeExpired = &CloseMessage{
+ code: 4004,
+ message: "client reconnect grace time expired",
+ }
+
+ closeNetworkTimeout = &CloseMessage{
+ code: 4005,
+ message: "network timeout",
+ }
+
+ closeNetworkError = &CloseMessage{
+ code: 4006,
+ message: "network error",
+ }
+
+ closeInvalidReconnect = &CloseMessage{
+ code: 4007,
+ message: "invalid reconnect attempt",
+ }
+)
+
+func GetCloseMessageFromCode(code int) *CloseMessage {
+ switch code {
+ case 4000:
+ return closeInternalServerError
+ case 4001:
+ return closeClientSentInboundTraffic
+ case 4002:
+ return closeClientFailedPingPong
+ case 4003:
+ return closeConnectionUnused
+ case 4004:
+ return closeNetworkTimeout
+ case 4005:
+ return closeNetworkTimeout
+ case 4006:
+ return closeNetworkError
+ case 4007:
+ return closeInvalidReconnect
+ default:
+ return nil
+ }
+}
diff --git a/internal/events/websocket/mock_server/manager.go b/internal/events/websocket/mock_server/manager.go
new file mode 100644
index 00000000..ea497462
--- /dev/null
+++ b/internal/events/websocket/mock_server/manager.go
@@ -0,0 +1,478 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+package mock_server
+
+import (
+ "encoding/json"
+ "fmt"
+ "log"
+ "net"
+ "net/http"
+ "os"
+ "os/signal"
+ "strings"
+ "time"
+
+ "github.com/fatih/color"
+ "github.com/gorilla/websocket"
+ "github.com/twitchdev/twitch-cli/internal/events/types"
+ rpc_handler "github.com/twitchdev/twitch-cli/internal/rpc"
+ "github.com/twitchdev/twitch-cli/internal/util"
+)
+
+type ServerManager struct {
+ serverList *util.List[WebSocketServer]
+ reconnectTesting bool
+ primaryServer string
+ port int
+ debugEnabled bool
+ strictMode bool
+}
+
+var serverManager *ServerManager
+
+func StartWebsocketServer(enableDebug bool, port int, strictMode bool) {
+ serverManager = &ServerManager{
+ serverList: &util.List[WebSocketServer]{
+ Elements: make(map[string]*WebSocketServer),
+ },
+ port: port,
+ reconnectTesting: false,
+ strictMode: strictMode,
+ }
+
+ serverManager.debugEnabled = enableDebug
+
+ // Start initial websocket server
+ initialServer := &WebSocketServer{
+ ServerId: util.RandomGUID()[:8],
+ Status: 2,
+ Clients: &util.List[Client]{
+ Elements: make(map[string]*Client),
+ },
+ Upgrader: websocket.Upgrader{},
+ DebugEnabled: serverManager.debugEnabled,
+ Subscriptions: make(map[string][]Subscription),
+ StrictMode: serverManager.strictMode,
+
+ ReconnectClients: &util.List[[]Subscription]{ // Empty and irrelevant at this point, but needed to avoid panic
+ Elements: make(map[string]*[]Subscription),
+ },
+ }
+ serverManager.serverList.Put(initialServer.ServerId, initialServer)
+ serverManager.primaryServer = initialServer.ServerId
+
+ // Allow exit with Ctrl + C
+ stop := make(chan os.Signal)
+ signal.Notify(stop, os.Interrupt)
+
+ m := http.NewServeMux()
+
+ // Register URL handler
+ m.HandleFunc("/ws", wsPageHandler)
+ m.HandleFunc("/eventsub/subscriptions", subscriptionPageHandler)
+
+ // Start HTTP server
+ go func() {
+ // Listen to port
+ listen, err := net.Listen("tcp", fmt.Sprintf(":%v", port))
+ if err != nil {
+ log.Fatalf("Cannot start HTTP server: %v", err)
+ return
+ }
+
+ lightBlue := color.New(color.FgHiBlue).SprintFunc()
+ lightGreen := color.New(color.FgHiGreen).SprintFunc()
+ lightYellow := color.New(color.FgHiYellow).SprintFunc()
+ yellow := color.New(color.FgYellow).SprintFunc()
+
+ log.Printf(lightBlue("Started WebSocket server on 127.0.0.1:%v"), port)
+ if serverManager.strictMode {
+ log.Printf(lightBlue("--require-subscription enabled. Clients will have 10 seconds to subscribe before being disconnected."))
+ }
+
+ fmt.Println()
+
+ log.Printf(yellow("Simulate subscribing to events at: http://127.0.0.1:%v/eventsub/subscriptions"), port)
+ log.Printf(yellow("POST, GET, and DELETE are supported"))
+ log.Printf(yellow("For more info: https://dev.twitch.tv/docs/cli/websocket-event-command/#simulate-subscribing-to-mock-eventsub"))
+
+ fmt.Println()
+
+ log.Printf(lightYellow("Events can be forwarded to this server from another terminal with --transport=websocket\nExample: \"twitch event trigger channel.ban --transport=websocket\""))
+ fmt.Println()
+ log.Printf(lightYellow("You can send to a specific client after its connected with --session\nExample: \"twitch event trigger channel.ban --transport=websocket --session=e411cc1e_a2613d4e\""))
+
+ fmt.Println()
+ log.Printf(lightGreen("For further usage information, please see our official documentation:\nhttps://dev.twitch.tv/docs/cli/websocket-event-command/"))
+ fmt.Println()
+
+ log.Printf(lightBlue("Connect to the WebSocket server at: ")+"ws://127.0.0.1:%v/ws", port)
+
+ // Serve HTTP server
+ if err := http.Serve(listen, m); err != nil {
+ log.Fatalf("Cannot start HTTP server: %v", err)
+ return
+ }
+ }()
+
+ // Initalize RPC handler, to accept EventSub transports
+ rpc := rpc_handler.RPCHandler{
+ Port: 44747,
+ Handlers: make(map[string]rpc_handler.HandlerCallback),
+ }
+
+ rpc.RegisterHandler("EventSubWebSocketReconnect", RPCReconnectHandler)
+ rpc.RegisterHandler("EventSubWebSocketForwardEvent", RPCFireEventSubHandler)
+ rpc.RegisterHandler("EventSubWebSocketCloseClient", RPCCloseHandler)
+ rpc.RegisterHandler("EventSubWebSocketSubscription", RPCSubscriptionHandler)
+ rpc.StartBackgroundServer()
+
+ // TODO: Interactive shell maybe?
+
+ <-stop // Wait for Ctrl + C
+}
+
+func wsPageHandler(w http.ResponseWriter, r *http.Request) {
+ server, ok := serverManager.serverList.Get(serverManager.primaryServer)
+ if !ok {
+ log.Printf("Failed to find primary server [%v] when new client was accessing ws://127.0.0.1:%v/ws -- Aborting...", serverManager.primaryServer, serverManager.port)
+ return
+ }
+
+ server.WsPageHandler(w, r)
+}
+
+func subscriptionPageHandler(w http.ResponseWriter, r *http.Request) {
+ method := strings.ToUpper(r.Method)
+
+ // OPTIONS method
+ if method == "OPTIONS" {
+ w.Header().Set("Access-Control-Allow-Headers", "Accept, Accept-Language, Authorization, Client-Id, Twitch-Api-Token, X-Forwarded-Proto, X-Requested-With, X-Csrf-Token, Content-Type, X-Device-Id, X-Twitch-Vhscf, X-Forwarded-Ip")
+ w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE")
+ w.Header().Set("Access-Control-Allow-Origin", "*")
+ w.Header().Set("Access-Control-Max-Age", "600")
+ w.WriteHeader(http.StatusOK)
+ return
+ }
+
+ // GET method
+ if method == "GET" {
+ subscriptionPageHandlerGet(w, r)
+ return
+ }
+
+ // POST method
+ if method == "POST" {
+ subscriptionPageHandlerPost(w, r)
+ return
+ }
+
+ // DELETE method
+ if method == "DELETE" {
+ subscriptionPageHandlerDelete(w, r)
+ return
+ }
+
+ // Fallback
+ w.WriteHeader(http.StatusMethodNotAllowed)
+}
+
+func subscriptionPageHandlerGet(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json; charset=utf-8")
+ w.Header().Set("Access-Control-Allow-Origin", "*")
+ w.Header().Set("ratelimit-limit", "800")
+ w.Header().Set("ratelimit-remaining", "799")
+ w.Header().Set("ratelimit-reset", fmt.Sprintf("%d", time.Now().Unix()+1)) // 1 second from now
+
+ // Basic error checking
+ clientID := r.Header.Get("client-id")
+ if clientID == "" {
+ handlerResponseErrorUnauthorized(w, "Client-Id header required")
+ return
+ }
+
+ server, ok := serverManager.serverList.Get(serverManager.primaryServer)
+ if !ok {
+ handlerResponseErrorInternalServerError(w, "Primary server not found in server list.")
+ return
+ }
+
+ allSubscriptions := []SubscriptionPostSuccessResponseBody{}
+
+ server.muSubscriptions.Lock()
+
+ for clientName, clientSubscriptions := range server.Subscriptions {
+ for _, subscription := range clientSubscriptions {
+ if clientID == "debug" || subscription.ClientID == clientID {
+ allSubscriptions = append(allSubscriptions, SubscriptionPostSuccessResponseBody{
+ ID: subscription.ClientID,
+ Status: subscription.Status,
+ Type: subscription.Type,
+ Version: subscription.Version,
+ Condition: EmptyStruct{},
+ CreatedAt: subscription.CreatedAt,
+ Transport: SubscriptionTransport{
+ Method: "websocket",
+ SessionID: fmt.Sprintf("%v_%v", server.ServerId, clientName),
+ },
+ Cost: 0,
+ })
+ }
+ }
+ }
+
+ server.muSubscriptions.Unlock()
+
+ w.WriteHeader(http.StatusOK)
+ json.NewEncoder(w).Encode(&SubscriptionGetSuccessResponse{
+ Total: len(allSubscriptions),
+ Data: allSubscriptions,
+ TotalCost: 0,
+ MaxTotalCost: 10,
+ Pagination: EmptyStruct{},
+ })
+}
+
+func subscriptionPageHandlerPost(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json; charset=utf-8")
+ w.Header().Set("Access-Control-Allow-Origin", "*")
+ w.Header().Set("ratelimit-limit", "800")
+ w.Header().Set("ratelimit-remaining", "799")
+ w.Header().Set("ratelimit-reset", fmt.Sprintf("%d", time.Now().Unix()+1)) // 1 second from now
+
+ var body SubscriptionPostRequest
+
+ err := json.NewDecoder(r.Body).Decode(&body)
+ if err != nil {
+ handlerResponseErrorBadRequest(w, "error validating json")
+ return
+ }
+
+ // Basic error checking
+ if r.Header.Get("client-id") == "" {
+ handlerResponseErrorUnauthorized(w, "Client-Id header required")
+ return
+ }
+ if !strings.EqualFold(body.Transport.Method, "websocket") {
+ handlerResponseErrorBadRequest(w, "The value specified in the 'method' field is not valid")
+ return
+ }
+ if !sessionRegex.MatchString(body.Transport.SessionID) {
+ handlerResponseErrorBadRequest(w, "The value specified in the 'session_id' field is not valid")
+ return
+ }
+ if body.Type == "" {
+ handlerResponseErrorBadRequest(w, "The value specified in the 'type' field is not valid")
+ return
+ }
+ if body.Version == "" {
+ handlerResponseErrorBadRequest(w, "The value specified in the 'version' field is not valid")
+ return
+ }
+
+ // Check if the topic exists
+ _, err = types.GetByTriggerAndTransport(body.Type, body.Transport.Method)
+ if err != nil {
+ handlerResponseErrorBadRequest(w, "The combination of values in the type and version fields is not valid")
+ return
+ }
+
+ sessionRegexExec := sessionRegex.FindAllStringSubmatch(body.Transport.SessionID, -1)
+ clientName := sessionRegexExec[0][2]
+
+ // Get client and server
+ server, ok := serverManager.serverList.Get(sessionRegexExec[0][1])
+ if !ok {
+ handlerResponseErrorBadRequest(w, "non-existent session_id")
+ return
+ }
+ client, ok := server.Clients.Get(clientName)
+ if !ok {
+ handlerResponseErrorBadRequest(w, "non-existent session_id")
+ return
+ }
+
+ server.muSubscriptions.Lock()
+
+ // Check for duplicate subscription
+ for _, s := range server.Subscriptions[clientName] {
+ if s.ClientID == r.Header.Get("client-id") && s.Type == body.Type && s.Version == body.Version {
+ handlerResponseErrorConflict(w, "Subscription by the specified type and version combination for the specified Client ID already exists")
+ server.muSubscriptions.Unlock()
+ return
+ }
+ }
+
+ if len(server.Subscriptions[clientName]) >= 100 {
+ handlerResponseErrorBadRequest(w, "You may only create 100 subscriptions within a single WebSocket connection")
+ server.muSubscriptions.Unlock()
+ return
+ }
+
+ // Add subscription
+ subscription := Subscription{
+ SubscriptionID: util.RandomGUID(),
+ ClientID: r.Header.Get("client-id"),
+ Type: body.Type,
+ Version: body.Version,
+ CreatedAt: time.Now().UTC().Format(time.RFC3339Nano),
+ Status: STATUS_ENABLED, // https://dev.twitch.tv/docs/api/reference/#get-eventsub-subscriptions
+ SessionClientName: clientName,
+ }
+
+ var subs []Subscription
+ existingList, ok := server.Subscriptions[clientName]
+ if ok {
+ subs = existingList
+ } else {
+ subs = []Subscription{}
+ }
+
+ subs = append(subs, subscription)
+ server.Subscriptions[clientName] = subs
+
+ server.muSubscriptions.Unlock()
+
+ // Return 202 status code and response body
+ w.WriteHeader(http.StatusAccepted)
+
+ json.NewEncoder(w).Encode(&SubscriptionPostSuccessResponse{
+ Body: SubscriptionPostSuccessResponseBody{
+ ID: subscription.SubscriptionID,
+ Status: subscription.Status,
+ Type: subscription.Type,
+ Version: subscription.Version,
+ Condition: EmptyStruct{},
+ CreatedAt: subscription.CreatedAt,
+ Transport: SubscriptionTransport{
+ Method: "websocket",
+ SessionID: fmt.Sprintf("%v_%v", server.ServerId, clientName),
+ ConnectedAt: client.ConnectedAtTimestamp,
+ },
+ Cost: 0,
+ },
+ Total: 0,
+ MaxTotalCost: 10,
+ TotalCost: 0,
+ })
+
+ if serverManager.debugEnabled {
+ log.Printf(
+ "Client ID [%v] created subscription [%v/%v] at subscription ID [%v]",
+ r.Header.Get("client-id"),
+ subscription.Type,
+ subscription.Version,
+ subscription.SubscriptionID,
+ )
+ }
+
+ return
+}
+
+func subscriptionPageHandlerDelete(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json; charset=utf-8")
+ w.Header().Set("Access-Control-Allow-Origin", "*")
+ w.Header().Set("ratelimit-limit", "800")
+ w.Header().Set("ratelimit-remaining", "799")
+ w.Header().Set("ratelimit-reset", fmt.Sprintf("%d", time.Now().Unix()+1)) // 1 second from now
+
+ subscriptionId := r.URL.Query().Get("id")
+
+ // Basic error checking
+ if r.Header.Get("client-id") == "" {
+ handlerResponseErrorUnauthorized(w, "Client-Id header required")
+ return
+ }
+ if subscriptionId == "" {
+ handlerResponseErrorBadRequest(w, "The id query parameter is required")
+ return
+ }
+
+ server, ok := serverManager.serverList.Get(serverManager.primaryServer)
+ if !ok {
+ handlerResponseErrorInternalServerError(w, "Primary server not found in server list.")
+ return
+ }
+
+ subFound := false
+
+ server.muSubscriptions.Lock()
+
+ for client, clientSubscriptions := range server.Subscriptions {
+ for i, subscription := range clientSubscriptions {
+ if subscription.SubscriptionID == subscriptionId {
+ subFound = true
+ subsPart := make([]Subscription, 0)
+ subsPart = append(subsPart, server.Subscriptions[client][:i]...)
+
+ newSubs := append(subsPart, server.Subscriptions[client][i+1:]...)
+ server.Subscriptions[client] = newSubs
+
+ if serverManager.debugEnabled {
+ log.Printf(
+ "Deleted subscription [%v/%v] of ID [%v] owned by client ID [%v]",
+ subscription.Type,
+ subscription.Version,
+ subscription.SubscriptionID,
+ r.Header.Get("client-id"),
+ )
+ }
+ }
+ }
+ }
+
+ server.muSubscriptions.Unlock()
+
+ if subFound {
+ // Return 204 status code
+ w.WriteHeader(http.StatusNoContent)
+ } else {
+ // Return 404 not found
+ w.WriteHeader(http.StatusNotFound)
+
+ if serverManager.debugEnabled {
+ log.Printf("Failed to delete subscription ID [%v] from client ID [%v]", subscriptionId, r.Header.Get("client-id"))
+ }
+ }
+}
+
+func handlerResponseErrorBadRequest(w http.ResponseWriter, message string) {
+ w.WriteHeader(http.StatusBadRequest)
+ bytes, _ := json.Marshal(&SubscriptionPostErrorResponse{
+ Error: "Bad Request",
+ Message: message,
+ Status: 400,
+ })
+ w.Write(bytes)
+}
+
+func handlerResponseErrorUnauthorized(w http.ResponseWriter, message string) {
+ w.WriteHeader(http.StatusBadRequest)
+ bytes, _ := json.Marshal(&SubscriptionPostErrorResponse{
+ Error: "Unauthorized",
+ Message: message,
+ Status: 401,
+ })
+ w.Write(bytes)
+}
+
+func handlerResponseErrorConflict(w http.ResponseWriter, message string) {
+ w.WriteHeader(http.StatusConflict)
+ bytes, _ := json.Marshal(&SubscriptionPostErrorResponse{
+ Error: "Unauthorized",
+ Message: message,
+ Status: 409,
+ })
+ w.Write(bytes)
+}
+
+func handlerResponseErrorInternalServerError(w http.ResponseWriter, message string) {
+ w.WriteHeader(http.StatusInternalServerError)
+ bytes, _ := json.Marshal(&SubscriptionPostErrorResponse{
+ Error: "Internal Server Error",
+ Message: message,
+ Status: 500,
+ })
+ w.Write(bytes)
+}
diff --git a/internal/events/mock_wss_server/message_types.go b/internal/events/websocket/mock_server/message_types.go
similarity index 74%
rename from internal/events/mock_wss_server/message_types.go
rename to internal/events/websocket/mock_server/message_types.go
index 4b2d6d9f..4f02b701 100644
--- a/internal/events/mock_wss_server/message_types.go
+++ b/internal/events/websocket/mock_server/message_types.go
@@ -1,99 +1,129 @@
-package mock_wss_server
-
-// Generic response message Metadata; Always the same
-
-type MessageMetadata struct {
- MessageID string `json:"message_id"`
- MessageType string `json:"message_type"`
- MessageTimestamp string `json:"message_timestamp"`
-}
-
-/* ** Welcome message **
-{ // <1>
- "metadata": { //
- "message_id": "befa7b53-d79d-478f-86b9-120f112b044e",
- "message_type": "session_welcome",
- "message_timestamp": "2019-11-16T10:11:12.123Z"
- },
- "payload": { // <2>
- "session": { // <3>
- "id": "AQoQexAWVYKSTIu4ec_2VAxyuhAB",
- "status": "connected",
- "keepalive_timeout_seconds": 10,
- "reconnect_url": null,
- "connected_at": "2019-11-16T10:11:12.123Z"
- }
- }
-}
-*/
-
-type WelcomeMessage struct { // <1>
- Metadata MessageMetadata `json:"metadata"`
- Payload WelcomeMessagePayload `json:"payload"`
-}
-
-type WelcomeMessagePayload struct { // <2>
- Session WelcomeMessagePayloadSession `json:"session"`
-}
-
-type WelcomeMessagePayloadSession struct { // <3>
- ID string `json:"id"`
- Status string `json:"status"`
- KeepaliveTimeoutSeconds int `json:"keepalive_timeout_seconds"`
- ReconnectUrl *string `json:"reconnect_url"`
- ConnectedAt string `json:"connected_at"`
-}
-
-/* ** Reconnect message **
-{ // <1>
- "metadata": { //
- "message_id": "84c1e79a-2a4b-4c13-ba0b-4312293e9308",
- "message_type": "session_reconnect",
- "message_timestamp": "2019-11-18T09:10:11.234Z"
- },
- "payload": { // <2>
- "session": { // <3>
- "id": "AQoQexAWVYKSTIu4ec_2VAxyuhAB",
- "status": "reconnecting",
- "keepalive_timeout_seconds": null,
- "reconnect_url": "wss://eventsub-experimental.wss.twitch.tv?...",
- "connected_at": "2019-11-16T10:11:12.123Z"
- }
- }
-}
-*/
-
-type ReconnectMessage struct { // <1>
- Metadata MessageMetadata `json:"metadata"`
- Payload ReconnectMessagePayload `json:"payload"`
-}
-
-type ReconnectMessagePayload struct { // <2>
- Session ReconnectMessagePayloadSession `json:"session"`
-}
-
-type ReconnectMessagePayloadSession struct { // <3>
- ID string `json:"id"`
- Status string `json:"status"`
- KeepaliveTimeoutSeconds *int `json:"keepalive_timeout_seconds"`
- ReconnectUrl string `json:"reconnect_url"`
- ConnectedAt string `json:"connected_at"`
-}
-
-/* ** Keepalive message **
-{ // <1>
- "metadata": { //
- "message_id": "84c1e79a-2a4b-4c13-ba0b-4312293e9308",
- "message_type": "session_keepalive",
- "message_timestamp": "2019-11-16T10:11:12.123Z"
- },
- "payload": {} // struct{}
-}
-*/
-
-type KeepaliveMessage struct { // <1>
- Metadata MessageMetadata `json:"metadata"`
- Payload KeepaliveMessagePayload `json:"payload"`
-}
-
-type KeepaliveMessagePayload struct{}
+package mock_server
+
+import "github.com/twitchdev/twitch-cli/internal/models"
+
+// Generic response message Metadata; Always the same
+
+type MessageMetadata struct {
+ MessageID string `json:"message_id"`
+ MessageType string `json:"message_type"`
+ MessageTimestamp string `json:"message_timestamp"`
+
+ SubscriptionType string `json:"subscription_type,omitempty"`
+ SubscriptionVersion string `json:"subscription_version,omitempty"`
+}
+
+/* ** Welcome message **
+{ // <1>
+ "metadata": { //
+ "message_id": "befa7b53-d79d-478f-86b9-120f112b044e",
+ "message_type": "session_welcome",
+ "message_timestamp": "2019-11-16T10:11:12.123Z"
+ },
+ "payload": { // <2>
+ "session": { // <3>
+ "id": "AQoQexAWVYKSTIu4ec_2VAxyuhAB",
+ "status": "connected",
+ "keepalive_timeout_seconds": 10,
+ "reconnect_url": null,
+ "connected_at": "2019-11-16T10:11:12.123Z"
+ }
+ }
+}
+*/
+
+type WelcomeMessage struct { // <1>
+ Metadata MessageMetadata `json:"metadata"`
+ Payload WelcomeMessagePayload `json:"payload"`
+}
+
+type WelcomeMessagePayload struct { // <2>
+ Session WelcomeMessagePayloadSession `json:"session"`
+}
+
+type WelcomeMessagePayloadSession struct { // <3>
+ ID string `json:"id"`
+ Status string `json:"status"`
+ KeepaliveTimeoutSeconds int `json:"keepalive_timeout_seconds"`
+ ReconnectUrl *string `json:"reconnect_url"`
+ ConnectedAt string `json:"connected_at"`
+}
+
+/* ** Reconnect message **
+{ // <1>
+ "metadata": { //
+ "message_id": "84c1e79a-2a4b-4c13-ba0b-4312293e9308",
+ "message_type": "session_reconnect",
+ "message_timestamp": "2019-11-18T09:10:11.234Z"
+ },
+ "payload": { // <2>
+ "session": { // <3>
+ "id": "AQoQexAWVYKSTIu4ec_2VAxyuhAB",
+ "status": "reconnecting",
+ "keepalive_timeout_seconds": null,
+ "reconnect_url": "wss://eventsub-experimental.wss.twitch.tv?...",
+ "connected_at": "2019-11-16T10:11:12.123Z"
+ }
+ }
+}
+*/
+
+type ReconnectMessage struct { // <1>
+ Metadata MessageMetadata `json:"metadata"`
+ Payload ReconnectMessagePayload `json:"payload"`
+}
+
+type ReconnectMessagePayload struct { // <2>
+ Session ReconnectMessagePayloadSession `json:"session"`
+}
+
+type ReconnectMessagePayloadSession struct { // <3>
+ ID string `json:"id"`
+ Status string `json:"status"`
+ KeepaliveTimeoutSeconds *int `json:"keepalive_timeout_seconds"`
+ ReconnectUrl string `json:"reconnect_url"`
+ ConnectedAt string `json:"connected_at"`
+}
+
+/* ** Keepalive message **
+{ // <1>
+ "metadata": { //
+ "message_id": "84c1e79a-2a4b-4c13-ba0b-4312293e9308",
+ "message_type": "session_keepalive",
+ "message_timestamp": "2019-11-16T10:11:12.123Z"
+ },
+ "payload": {} // struct{}
+}
+*/
+
+type KeepaliveMessage struct { // <1>
+ Metadata MessageMetadata `json:"metadata"`
+ Payload KeepaliveMessagePayload `json:"payload"`
+}
+
+type KeepaliveMessagePayload struct{}
+
+/* ** Notification message **
+{ // <1>
+ "metadata": { //
+ "message_id": "befa7b53-d79d-478f-86b9-120f112b044e",
+ "message_type": "notification",
+ "message_timestamp": "2019-11-16T10:11:12.464757833Z",
+ "subscription_type": "channel.follow",
+ "subscription_version": "1"
+ },
+ "payload": { //
+ "subscription": {
+ ...
+ },
+ "event": {
+ ...
+ }
+ }
+}
+*/
+
+type NotificationMessage struct { // <1>
+ Metadata MessageMetadata `json:"metadata"`
+ Payload models.EventsubResponse `json:"payload"`
+}
diff --git a/internal/events/websocket/mock_server/rpc_handler.go b/internal/events/websocket/mock_server/rpc_handler.go
new file mode 100644
index 00000000..dade9444
--- /dev/null
+++ b/internal/events/websocket/mock_server/rpc_handler.go
@@ -0,0 +1,269 @@
+package mock_server
+
+import (
+ "fmt"
+ "log"
+ "regexp"
+ "strconv"
+
+ "github.com/gorilla/websocket"
+ rpc "github.com/twitchdev/twitch-cli/internal/rpc"
+ "github.com/twitchdev/twitch-cli/internal/util"
+)
+
+var sessionRegex = regexp.MustCompile(`(?P.+)_(?P.+)`)
+
+const (
+ COMMAND_RESPONSE_SUCCESS int = 0
+ COMMAND_RESPONSE_INVALID_CMD int = 1
+ COMMAND_RESPONSE_FAILED_ON_SERVER int = 2
+ COMMAND_RESPONSE_MISSING_FLAG int = 3
+)
+
+// Resolves console commands to their RPC names defined in the server manager
+func ResolveRPCName(cmd string) string {
+ if cmd == "reconnect" {
+ return "EventSubWebSocketReconnect"
+ } else if cmd == "close" {
+ return "EventSubWebSocketCloseClient"
+ } else if cmd == "subscription" {
+ return "EventSubWebSocketSubscription"
+ } else {
+ return ""
+ }
+}
+
+// $ twitch event websocket reconnect
+func RPCReconnectHandler(args rpc.RPCArgs) rpc.RPCResponse {
+ // Initiate reconnect testing
+ log.Printf("Initiating reconnect testing...")
+
+ if serverManager.reconnectTesting {
+ log.Printf("Error on RPC call (EventSubWebSocketReconnect): Cannot execute reconnect testing while its already in progress. Aborting.")
+ return rpc.RPCResponse{
+ ResponseCode: COMMAND_RESPONSE_FAILED_ON_SERVER,
+ }
+ }
+
+ // Find current primary server
+ originalPrimaryServer, ok := serverManager.serverList.Get(serverManager.primaryServer)
+ if !ok {
+ log.Printf("Error on RPC call (EventSubWebSocketReconnect): Primary server not in server list.")
+ return rpc.RPCResponse{
+ ResponseCode: COMMAND_RESPONSE_FAILED_ON_SERVER,
+ }
+ }
+
+ serverManager.reconnectTesting = true
+
+ // Get the list of reconnect clients ready
+ reconnectClients := originalPrimaryServer.GetCurrentSubscriptionsForReconnect()
+
+ // Spin up new server
+ newServer := &WebSocketServer{
+ ServerId: util.RandomGUID()[:8],
+ Status: 2,
+ Clients: &util.List[Client]{
+ Elements: make(map[string]*Client),
+ },
+ Upgrader: websocket.Upgrader{},
+ DebugEnabled: serverManager.debugEnabled,
+ Subscriptions: make(map[string][]Subscription),
+ StrictMode: serverManager.strictMode,
+ ReconnectClients: reconnectClients,
+ }
+ serverManager.serverList.Put(newServer.ServerId, newServer)
+
+ // Switch manager's primary server to new one
+ // Doing this before sending the reconnect messages emulates the Twitch's production load balancer, which will never send to servers shutting down.
+ serverManager.primaryServer = newServer.ServerId
+
+ // Notify primary server to restart (includes not accepting new clients)
+ // This is in a goroutine so it doesn't hang the reconnect command
+ go func() {
+ originalPrimaryServer.InitiateRestart()
+
+ // Remove server from server list
+ serverManager.serverList.Delete(originalPrimaryServer.ServerId)
+
+ if serverManager.debugEnabled {
+ log.Printf(
+ "Removed server [%v] from server list. New server list count: %v",
+ originalPrimaryServer.ServerId,
+ serverManager.serverList.Length(),
+ )
+ }
+
+ serverManager.reconnectTesting = false
+
+ log.Printf("Reconnect testing successful. Primary server is now [%v]\nYou may now execute reconnect testing again.", serverManager.primaryServer)
+ }()
+
+ return rpc.RPCResponse{
+ ResponseCode: COMMAND_RESPONSE_SUCCESS,
+ }
+}
+
+// $ twitch event trigger --transport=websocket
+func RPCFireEventSubHandler(args rpc.RPCArgs) rpc.RPCResponse {
+ server, ok := serverManager.serverList.Get(serverManager.primaryServer)
+ if !ok {
+ log.Printf("Error on RPC call (EventSubWebSocketForwardEvent): Primary server not in server list.")
+ return rpc.RPCResponse{
+ ResponseCode: COMMAND_RESPONSE_FAILED_ON_SERVER,
+ }
+ }
+
+ clientName, exists := args.Variables["ClientName"]
+ if !exists {
+
+ }
+ if sessionRegex.MatchString(clientName) {
+ // Users can include the full session_id given in the response. If they do, subtract it to just the client name
+ clientName = sessionRegex.FindAllStringSubmatch(clientName, -1)[0][2]
+ }
+
+ success, failMsg := server.HandleRPCEventSubForwarding(args.Body, clientName)
+
+ if success {
+ return rpc.RPCResponse{
+ ResponseCode: COMMAND_RESPONSE_SUCCESS,
+ }
+ } else {
+ return rpc.RPCResponse{
+ ResponseCode: COMMAND_RESPONSE_FAILED_ON_SERVER,
+ DetailedInfo: failMsg,
+ }
+ }
+}
+
+// $ twitch event websocket close
+func RPCCloseHandler(args rpc.RPCArgs) rpc.RPCResponse {
+ closeCode, err := strconv.Atoi(args.Variables["CloseReason"])
+
+ if err != nil || args.Variables["ClientName"] == "" || args.Variables["CloseReason"] == "" {
+ return rpc.RPCResponse{
+ ResponseCode: COMMAND_RESPONSE_MISSING_FLAG,
+ DetailedInfo: "Command \"close\" requires flags --session and --reason" +
+ "\nThe flag --reason must be one of the number codes defined here:" +
+ "\nhttps://dev.twitch.tv/docs/eventsub/websocket-reference/#close-message" +
+ "\n\nExample: twitch event websocket close --session=e411cc1e_a2613d4e --reason=4006",
+ }
+ }
+
+ clientName := args.Variables["ClientName"]
+
+ if serverManager.reconnectTesting {
+ log.Printf("Error on RPC call (EventSubWebSocketCloseClient): Could not activate while reconnect testing is active.")
+ return rpc.RPCResponse{
+ ResponseCode: COMMAND_RESPONSE_FAILED_ON_SERVER,
+ DetailedInfo: "Cannot activate this command while reconnect testing is active.",
+ }
+ }
+
+ server, ok := serverManager.serverList.Get(serverManager.primaryServer)
+ if !ok {
+ log.Printf("Error on RPC call (EventSubWebSocketCloseClient): Primary server not in server list.")
+ return rpc.RPCResponse{
+ ResponseCode: COMMAND_RESPONSE_FAILED_ON_SERVER,
+ DetailedInfo: "Primary server not in server list.",
+ }
+ }
+
+ cn := clientName
+ if sessionRegex.MatchString(clientName) {
+ // Client name given was formatted as _. We must extract it
+ sessionRegexExec := sessionRegex.FindAllStringSubmatch(clientName, -1)
+ cn = sessionRegexExec[0][2]
+ }
+
+ server.muClients.Lock()
+
+ client, ok := server.Clients.Get(cn)
+ if !ok {
+ server.muClients.Unlock()
+ return rpc.RPCResponse{
+ ResponseCode: COMMAND_RESPONSE_FAILED_ON_SERVER,
+ DetailedInfo: "Client [" + cn + "] does not exist on WebSocket server.",
+ }
+ }
+
+ closeMessage := GetCloseMessageFromCode(closeCode)
+ if closeMessage == nil {
+ server.muClients.Unlock()
+ return rpc.RPCResponse{
+ ResponseCode: COMMAND_RESPONSE_FAILED_ON_SERVER,
+ DetailedInfo: fmt.Sprintf("Close code [%v] not supported.", closeCode),
+ }
+ }
+
+ server.muClients.Unlock()
+
+ client.CloseWithReason(closeMessage)
+ server.handleClientConnectionClose(client, closeMessage)
+
+ log.Printf("RPC instructed to close client [%v] with code [%v]", clientName, closeCode)
+
+ return rpc.RPCResponse{
+ ResponseCode: COMMAND_RESPONSE_SUCCESS,
+ }
+}
+
+// $ twitch event websocket subscription
+func RPCSubscriptionHandler(args rpc.RPCArgs) rpc.RPCResponse {
+ if args.Variables["SubscriptionID"] == "" || !IsValidSubscriptionStatus(args.Variables["SubscriptionStatus"]) {
+ return rpc.RPCResponse{
+ ResponseCode: COMMAND_RESPONSE_MISSING_FLAG,
+ DetailedInfo: "Command \"subscription\" requires flags --status, --subscription, and --session" +
+ fmt.Sprintf("\nThe flag --subscription must be the ID of the subscription made at http://localhost:%v/eventsub/subscriptions", serverManager.port) +
+ "\nThe flag --status must be one of the non-webhook status options defined here:" +
+ "\nhttps://dev.twitch.tv/docs/api/reference/#get-eventsub-subscriptions" +
+ "\n\nExample: twitch event websocket subscription --status=user_removed --subscription=82a855-fae8-93bff0",
+ }
+ }
+
+ if serverManager.reconnectTesting {
+ return rpc.RPCResponse{
+ ResponseCode: COMMAND_RESPONSE_FAILED_ON_SERVER,
+ DetailedInfo: "Cannot activate this command while reconnect testing is active.",
+ }
+ }
+
+ server, ok := serverManager.serverList.Get(serverManager.primaryServer)
+ if !ok {
+ log.Printf("Error on RPC call (EventSubWebSocketSubscription): Primary server not in server list.")
+ return rpc.RPCResponse{
+ ResponseCode: COMMAND_RESPONSE_FAILED_ON_SERVER,
+ DetailedInfo: "See server console for more details.",
+ }
+ }
+
+ server.muSubscriptions.Lock()
+ found := false
+ for client, clientSubscriptions := range server.Subscriptions {
+ if found {
+ break
+ }
+
+ for i, sub := range clientSubscriptions {
+ if sub.SubscriptionID == args.Variables["SubscriptionID"] {
+ found = true
+
+ server.Subscriptions[client][i].Status = args.Variables["SubscriptionStatus"]
+ break
+ }
+ }
+ }
+ server.muSubscriptions.Unlock()
+
+ if !found {
+ return rpc.RPCResponse{
+ ResponseCode: COMMAND_RESPONSE_FAILED_ON_SERVER,
+ DetailedInfo: fmt.Sprintf("Subscription ID [%v] does not exist", args.Variables["SubscriptionID"]),
+ }
+ }
+
+ return rpc.RPCResponse{
+ ResponseCode: COMMAND_RESPONSE_SUCCESS,
+ }
+}
diff --git a/internal/events/websocket/mock_server/server.go b/internal/events/websocket/mock_server/server.go
new file mode 100644
index 00000000..b9e1a30a
--- /dev/null
+++ b/internal/events/websocket/mock_server/server.go
@@ -0,0 +1,518 @@
+package mock_server
+
+import (
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "log"
+ "net/http"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/gorilla/websocket"
+ "github.com/twitchdev/twitch-cli/internal/models"
+ "github.com/twitchdev/twitch-cli/internal/util"
+)
+
+// Minimum time between messages before the server disconnects a client.
+const KEEPALIVE_TIMEOUT_SECONDS = 10
+
+type WebSocketServer struct {
+ ServerId string // Int representing the ID of the server
+ //ConnectionUrl string // Server's url for people to connect to. Used for messaging in reconnect testing
+ DebugEnabled bool // Display debug messages; --debug
+ StrictMode bool // Force stricter production-like qualities; --strict
+ Upgrader websocket.Upgrader
+
+ Clients *util.List[Client] // All connected clients
+ muClients sync.Mutex // Mutex for WebSocketServer.Clients
+
+ Status int // 0 = shut down; 1 = shutting down; 2 = online
+ muStatus sync.Mutex // Mutex for WebSocketServer.Status
+
+ Subscriptions map[string][]Subscription // Active subscriptions on this server -- Accessed via Subscriptions[clientName]
+ muSubscriptions sync.Mutex // Mutex for WebSocketServer.Subscriptions
+
+ ReconnectClients *util.List[[]Subscription] // Clients that were part of the last server
+ muReconnectClients sync.Mutex // Mutex for WebSocketServer.ReconnectClients
+}
+
+func (ws *WebSocketServer) WsPageHandler(w http.ResponseWriter, r *http.Request) {
+ // This next line is required to disable CORS checking. No sense in caring in a test environment.
+ ws.Upgrader.CheckOrigin = func(r *http.Request) bool { return true }
+
+ conn, err := ws.Upgrader.Upgrade(w, r, nil)
+ if err != nil {
+ log.Print("[[websocket upgrade err]] ", err)
+ return
+ }
+ defer conn.Close()
+
+ // Connection successful. WebSocket is open.
+
+ // Get connected at time and set automatic read timeout
+ connectedAtTimestamp := time.Now().UTC().Format(time.RFC3339Nano)
+ conn.SetReadDeadline(time.Now().Add(time.Second * KEEPALIVE_TIMEOUT_SECONDS))
+
+ client := &Client{
+ clientName: util.RandomGUID()[:8],
+ conn: conn,
+ ConnectedAtTimestamp: connectedAtTimestamp,
+ connectionUrl: fmt.Sprintf("http://%v/ws", r.Host),
+ keepAliveChanOpen: false,
+ pingChanOpen: false,
+ }
+
+ if r.URL.Query().Get("reconnect_id") != "" {
+ reconnectIdBytes, err := base64.StdEncoding.DecodeString(r.URL.Query().Get("reconnect_id") + "=")
+ if err != nil {
+ if ws.DebugEnabled {
+ log.Printf("Could not decode base64 reconnect_id query parameter: '%v'", r.URL.Query().Get("reconnect_id"))
+ }
+ } else {
+ reconnectId := string(reconnectIdBytes)
+
+ ws.muReconnectClients.Lock()
+
+ subscriptions, ok := ws.ReconnectClients.Get(reconnectId)
+ if ok { // User had subscriptions carry over
+ ws.Subscriptions[client.clientName] = *subscriptions
+ }
+
+ ws.ReconnectClients.Delete(reconnectId)
+
+ if ws.DebugEnabled {
+ log.Printf("Reconnected client [%v] was assigned %v subscriptions", client.clientName, len(ws.Subscriptions[client.clientName]))
+ }
+
+ ws.muReconnectClients.Unlock()
+ }
+ }
+
+ // Disconnect the user if the server is in reconnect phase
+ ws.muStatus.Lock()
+ if ws.Status != 2 {
+ // This is the closest we can get to the production environment, as there's no way to route people to a shutting down server
+ log.Printf("New client trying to connect while websocket server in reconnect phase. Disconnecting them.")
+ client.CloseDirty()
+ // No handleClientConnectionClose because client is not in clients list, and chan loop was not set up yet.
+
+ ws.muStatus.Unlock()
+ return
+ }
+
+ // TODO: Check if user is connected to the old server, and if they are then disconnect them from old server with close frame 4004
+
+ ws.muClients.Lock()
+ // Add to the client connections list
+ ws.Clients.Put(client.clientName, client)
+ ws.muClients.Unlock()
+
+ // This is put after ws.Clients.Put to make sure the client gets included in the list before InitiateRestart() kicks everyone out
+ // Avoids any possible rare edge cases. This ain't production but I can still be safe :)
+ ws.muStatus.Unlock()
+
+ log.Printf("Client connected [%v]", client.clientName)
+ ws.printConnections()
+
+ // Send welcome message
+ welcomeMsg, _ := json.Marshal(
+ WelcomeMessage{
+ Metadata: MessageMetadata{
+ MessageID: util.RandomGUID(),
+ MessageType: "session_welcome",
+ MessageTimestamp: time.Now().UTC().Format(time.RFC3339Nano),
+ },
+ Payload: WelcomeMessagePayload{
+ Session: WelcomeMessagePayloadSession{
+ ID: fmt.Sprintf("%v_%v", ws.ServerId, client.clientName),
+ Status: "connected",
+ KeepaliveTimeoutSeconds: KEEPALIVE_TIMEOUT_SECONDS,
+ ReconnectUrl: nil,
+ ConnectedAt: connectedAtTimestamp,
+ },
+ },
+ },
+ )
+ client.SendMessage(websocket.TextMessage, welcomeMsg)
+
+ // Check if any subscriptions are sent after 10 seconds.
+ // Strict mode only
+ client.mustSubscribeTimer = time.NewTimer(10 * time.Second)
+ if ws.StrictMode {
+ go func() {
+ select {
+ case <-client.mustSubscribeTimer.C:
+ if len(ws.Subscriptions[client.clientName]) == 0 {
+ client.CloseWithReason(closeConnectionUnused)
+ ws.handleClientConnectionClose(client, closeConnectionUnused)
+
+ return
+ }
+ }
+ }()
+ }
+
+ // Set up ping/pong and keepalive handling
+ client.keepAliveTimer = time.NewTicker(10 * time.Second)
+ client.pingTimer = time.NewTicker(5 * time.Second)
+ client.keepAliveLoopChan = make(chan struct{})
+ client.pingLoopChan = make(chan struct{})
+ client.keepAliveChanOpen = true
+ client.pingChanOpen = true
+ go func() {
+ // Set pong handler. Resets the read deadline when pong is received.
+ conn.SetPongHandler(func(string) error {
+ conn.SetReadDeadline(time.Now().Add(time.Second * KEEPALIVE_TIMEOUT_SECONDS))
+ return nil
+ })
+
+ for {
+ select {
+ case <-client.keepAliveLoopChan:
+ client.keepAliveTimer.Stop()
+
+ case <-client.keepAliveTimer.C: // Send KeepAlive message
+ keepAliveMsg, _ := json.Marshal(
+ KeepaliveMessage{
+ Metadata: MessageMetadata{
+ MessageID: util.RandomGUID(),
+ MessageType: "session_keepalive",
+ MessageTimestamp: time.Now().UTC().Format(time.RFC3339Nano),
+ },
+ Payload: KeepaliveMessagePayload{},
+ },
+ )
+ err := client.SendMessage(websocket.TextMessage, keepAliveMsg)
+ if err != nil {
+ client.CloseWithReason(closeNetworkError)
+ }
+
+ if ws.DebugEnabled {
+ log.Printf("Sent session_keepalive to client [%s]", client.clientName)
+ }
+
+ case <-client.pingLoopChan:
+ client.pingTimer.Stop()
+
+ case <-client.pingTimer.C: // Send ping
+ err := client.SendMessage(websocket.PingMessage, []byte{})
+ if err != nil {
+ ws.muClients.Lock()
+ client.CloseWithReason(closeClientFailedPingPong)
+ ws.handleClientConnectionClose(client, closeClientFailedPingPong)
+ ws.muClients.Unlock()
+ }
+
+ if ws.DebugEnabled {
+ log.Printf("Sent pong to client [%s]", client.clientName)
+ }
+
+ }
+ }
+ }()
+
+ // Wait for message
+ for {
+ // Reset timeout upon every message, no matter what it is.
+ client.conn.SetReadDeadline(time.Now().Add(time.Second * KEEPALIVE_TIMEOUT_SECONDS))
+
+ mt, message, err := conn.ReadMessage()
+ if err != nil && ws.Status != 0 { // If server is shut down, clients should already be disconnectd.
+ log.Printf("read err [%v]: %v", client.clientName, err)
+
+ ws.muClients.Lock()
+ client.CloseWithReason(closeNetworkError)
+ ws.handleClientConnectionClose(client, closeNetworkError)
+ ws.muClients.Unlock()
+ break
+ }
+
+ if ws.Status == 2 { // Only care about this when the server is running
+ log.Printf("Disconnecting client [%v] due to received inbound traffic.\nMessage[%d]: %s", client.clientName, mt, message)
+
+ ws.muClients.Lock()
+ client.CloseWithReason(closeClientSentInboundTraffic)
+ ws.handleClientConnectionClose(client, closeClientSentInboundTraffic)
+ ws.muClients.Unlock()
+ }
+
+ break
+ }
+}
+
+// Gets client subscriptions to be transfered to another server. Used during reconnect testing.
+func (ws *WebSocketServer) GetCurrentSubscriptionsForReconnect() *util.List[[]Subscription] {
+ reconnectClients := &util.List[[]Subscription]{
+ Elements: make(map[string]*[]Subscription),
+ }
+
+ ws.muSubscriptions.Lock()
+
+ for clientName, clientSubscriptions := range ws.Subscriptions {
+ for _, subscription := range clientSubscriptions {
+ reconnectReference := fmt.Sprintf("%v_%v", ws.ServerId, clientName)
+
+ oldReconnectSubs, ok := reconnectClients.Get(reconnectReference)
+ if !ok {
+ oldReconnectSubs = &[]Subscription{}
+ }
+
+ // Add to oldReconnectSubs
+ *oldReconnectSubs = append(*oldReconnectSubs, subscription)
+
+ // Return to list
+ reconnectClients.Put(reconnectReference, oldReconnectSubs)
+ }
+ }
+
+ ws.muSubscriptions.Unlock()
+
+ return reconnectClients
+}
+
+func (ws *WebSocketServer) InitiateRestart() {
+ // Set status to shutting down; Stop accepting new clients
+ ws.muStatus.Lock()
+ ws.Status = 1
+ ws.muStatus.Unlock()
+
+ ws.muClients.Lock()
+
+ if ws.DebugEnabled {
+ log.Printf("Sending reconnect notices to [%v] clients", ws.Clients.Length())
+ }
+
+ // Send reconnect messages and disable timers on all clients
+ for _, client := range ws.Clients.All() {
+ // Disable keepalive and subscription timers
+ close(client.keepAliveLoopChan)
+ client.keepAliveChanOpen = false
+ client.mustSubscribeTimer.Stop()
+
+ // Send reconnect notice
+ sessionId := fmt.Sprintf("%v_%v", ws.ServerId, client.clientName)
+ reconnectId := base64.StdEncoding.EncodeToString([]byte(sessionId))
+ reconnectId = reconnectId[:len(reconnectId)-1]
+ clientConnectionUrl := strings.Replace(client.connectionUrl, "http://", "ws://", -1)
+ clientConnectionUrl = strings.Replace(clientConnectionUrl, "https://", "wss://", -1)
+ reconnectMsg, _ := json.Marshal(
+ ReconnectMessage{
+ Metadata: MessageMetadata{
+ MessageID: util.RandomGUID(),
+ MessageType: "session_reconnect",
+ MessageTimestamp: time.Now().UTC().Format(time.RFC3339Nano),
+ },
+ Payload: ReconnectMessagePayload{
+ Session: ReconnectMessagePayloadSession{
+ ID: sessionId,
+ Status: "reconnecting",
+ KeepaliveTimeoutSeconds: nil,
+ ReconnectUrl: fmt.Sprintf("%v?reconnect_id=%v", clientConnectionUrl, reconnectId),
+ ConnectedAt: client.ConnectedAtTimestamp,
+ },
+ },
+ },
+ )
+
+ err := client.SendMessage(websocket.TextMessage, reconnectMsg)
+ if err != nil {
+ log.Printf("Error building session_reconnect JSON for client [%v]: %v", client.clientName, err.Error())
+ }
+ }
+
+ log.Printf("Reconnect notices sent for server [%v]", ws.ServerId)
+ log.Printf("Will disconnect all existing clients in 30 seconds...")
+
+ ws.muClients.Unlock()
+
+ // Wait 30 seconds
+ time.Sleep(30 * time.Second)
+
+ // Change server status to 0
+ // This is done before disconnects because the read loop will err out due to the close message, which gets printed unless this is zero.
+ ws.Status = 0
+
+ // Disconnect everyone with reconnect close message
+ for _, client := range ws.Clients.All() {
+ ws.muClients.Lock()
+ client.CloseWithReason(closeReconnectGraceTimeExpired)
+ ws.handleClientConnectionClose(client, closeReconnectGraceTimeExpired)
+ ws.muClients.Unlock()
+ }
+
+ log.Printf("All users disconnected from server [%v]", ws.ServerId)
+}
+
+func (ws *WebSocketServer) HandleRPCEventSubForwarding(eventsubBody string, clientName string) (bool, string) {
+ // If --session is used, make sure the client exists
+ if clientName != "" {
+ _, ok := ws.Clients.Get(strings.ToLower(clientName))
+ if !ok {
+ msg := fmt.Sprintf("Error executing remote triggered EventSub: Client [%v] does not exist on server [%v]", clientName, ws.ServerId)
+ log.Println(msg)
+ return false, msg
+ }
+ }
+
+ if ws.Clients.Length() == 0 {
+ msg := fmt.Sprintf("Warning for remote triggered EventSub: No clients in server [%v]", ws.ServerId)
+ log.Println(msg)
+ return false, msg
+ }
+
+ // Convert to struct for editing
+ eventObj := models.EventsubResponse{}
+ err := json.Unmarshal([]byte(eventsubBody), &eventObj)
+ if err != nil {
+ msg := fmt.Sprintf("Error reading JSON forwarded from EventSub: %v\nRaw: %v", err.Error(), eventsubBody)
+ log.Println(msg)
+ return false, msg
+ }
+
+ didSend := false
+
+ for _, client := range ws.Clients.All() {
+ if clientName != "" && !strings.EqualFold(strings.ToLower(clientName), clientName) {
+ // When --session is used, only send to that client
+ continue
+ }
+
+ // If this is a Revocation message (user.authorization.revoke), set it as revoked
+ if eventObj.Subscription.Type == "user.authorization.revoke" {
+ if serverManager.debugEnabled {
+ log.Printf("Attempting to revoke subscription [%v]", eventObj.Subscription.ID)
+ }
+
+ ws.muSubscriptions.Lock()
+ foundClientId := ""
+ for client, clientSubscriptions := range ws.Subscriptions {
+ if foundClientId != "" {
+ break
+ }
+
+ for i, sub := range clientSubscriptions {
+ if sub.SubscriptionID == eventObj.Subscription.ID {
+ foundClientId = sub.ClientID
+
+ ws.Subscriptions[client][i].Status = STATUS_AUTHORIZATION_REVOKED
+ break
+ }
+ }
+ }
+ ws.muSubscriptions.Unlock()
+
+ if foundClientId != "" {
+ log.Printf("Subscription ID [%v], belonging to Client ID [%v], has been revoked.", eventObj.Subscription.ID, foundClientId)
+ } else {
+ msg := fmt.Sprintf("Failed to revoke Subscription ID [%v]: Subscription by that ID does not exist.", eventObj.Subscription.ID)
+ log.Println(msg)
+ return false, msg
+ }
+ }
+
+ // Check for subscriptions when running with --require-subscription
+ if ws.StrictMode {
+ found := false
+ for _, clientSubscriptions := range ws.Subscriptions {
+ if found {
+ break
+ }
+
+ for _, sub := range clientSubscriptions {
+ if sub.SessionClientName == client.clientName && sub.Type == eventObj.Subscription.Type && sub.Version == eventObj.Subscription.Version {
+ found = true
+ }
+ }
+ }
+
+ if !found {
+ continue
+ }
+ }
+
+ // Change payload's subscription.transport.session_id to contain the correct Session ID
+ eventObj.Subscription.Transport.SessionID = fmt.Sprintf("%v_%v", ws.ServerId, client.clientName)
+
+ // Build notification message
+ notificationMsg, err := json.Marshal(
+ NotificationMessage{
+ Metadata: MessageMetadata{
+ MessageID: util.RandomGUID(),
+ MessageType: "notification",
+ MessageTimestamp: time.Now().UTC().Format(time.RFC3339Nano),
+ SubscriptionType: eventObj.Subscription.Type,
+ SubscriptionVersion: eventObj.Subscription.Version,
+ },
+ Payload: eventObj,
+ },
+ )
+ if err != nil {
+ msg := fmt.Sprintf("Error building JSON for client [%v]: %v", client.clientName, err.Error())
+ log.Println(msg)
+ return false, msg
+ }
+
+ client.SendMessage(websocket.TextMessage, notificationMsg)
+ log.Printf("Sent [%v / %v] to client [%v]", eventObj.Subscription.Type, eventObj.Subscription.Version, client.clientName)
+
+ didSend = true
+ }
+
+ if !didSend {
+ msg := fmt.Sprintf("Error executing remote triggered EventSub: No clients with the subscribed to [%v / %v]", eventObj.Subscription.Type, eventObj.Subscription.Version)
+ log.Println(msg)
+ return false, msg
+ }
+
+ return true, ""
+}
+
+func (ws *WebSocketServer) handleClientConnectionClose(client *Client, closeReason *CloseMessage) {
+ // Prevent further looping
+ client.mustSubscribeTimer.Stop()
+ if client.keepAliveChanOpen {
+ close(client.keepAliveLoopChan)
+ client.keepAliveChanOpen = false
+ }
+ if client.pingChanOpen {
+ close(client.pingLoopChan)
+ client.pingChanOpen = false
+ }
+
+ // Remove from clients list
+ ws.Clients.Delete(client.clientName)
+
+ // Update subscriptions, unless close reason is for reconnect testing.
+ if ws.Status == 2 {
+ ws.muSubscriptions.Lock()
+ subscriptions := ws.Subscriptions[client.clientName]
+ for _, subscription := range subscriptions {
+ if subscription.Status == STATUS_ENABLED {
+ subscription.Status = getStatusFromCloseMessage(closeReason)
+ }
+ }
+ ws.Subscriptions[client.clientName] = subscriptions
+ ws.muSubscriptions.Unlock()
+ }
+
+ log.Printf("Disconnected client [%v] with code [%v]", client.clientName, closeReason.code)
+
+ // Print new clients connections list
+ ws.printConnections()
+}
+
+func (ws *WebSocketServer) printConnections() {
+ currentConnections := ""
+
+ for _, client := range ws.Clients.All() {
+ currentConnections += client.clientName + ", "
+ }
+
+ if currentConnections != "" {
+ currentConnections = string(currentConnections[:len(currentConnections)-2])
+ }
+
+ log.Printf("[%s] Connections: (%d) [ %s ]", ws.ServerId, ws.Clients.Length(), currentConnections)
+}
diff --git a/internal/events/websocket/mock_server/subscription.go b/internal/events/websocket/mock_server/subscription.go
new file mode 100644
index 00000000..39e98f11
--- /dev/null
+++ b/internal/events/websocket/mock_server/subscription.go
@@ -0,0 +1,132 @@
+package mock_server
+
+type Subscription struct {
+ SubscriptionID string // Random GUID for the subscription
+ ClientID string // Client ID included in headers
+ Type string // EventSub topic
+ Version string // EventSub topic version
+ CreatedAt string // Timestamp of when the subscription was created
+ Status string // Status of the subscription
+ SessionClientName string // Client name of the session this is associated with.
+}
+
+// Request - POST /eventsub/subscriptions
+type SubscriptionPostRequest struct {
+ Type string `json:"type"`
+ Version string `json:"version"`
+ Condition interface{} `json:"condition"`
+
+ Transport SubscriptionPostRequestTransport `json:"transport"`
+}
+
+// Request - POST /eventsub/subscriptions
+type SubscriptionPostRequestTransport struct {
+ Method string `json:"method"`
+ SessionID string `json:"session_id"`
+}
+
+// Response (Success) - POST /eventsub/subscriptions
+type SubscriptionPostSuccessResponse struct {
+ Body SubscriptionPostSuccessResponseBody `json:"body"`
+
+ Total int `json:"total"`
+ MaxTotalCost int `json:"max_total_cost"`
+ TotalCost int `json:"total_cost"`
+}
+
+// Response (Success) - POST /eventsub/subscriptions
+// Response (Success) - GET /eventsub/subscriptions
+type SubscriptionPostSuccessResponseBody struct {
+ ID string `json:"id"`
+ Status string `json:"status"`
+ Type string `json:"type"`
+ Version string `json:"version"`
+ CreatedAt string `json:"created_at"`
+ Cost int `json:"cost"`
+
+ Condition EmptyStruct `json:"condition"`
+ Transport SubscriptionTransport `json:"transport"`
+}
+
+// Response (Error) - POST /eventsub/subscriptions
+type SubscriptionPostErrorResponse struct {
+ Error string `json:"error"`
+ Message string `json:"message"`
+ Status int `json:"status"`
+}
+
+// Response (Success) - GET /eventsub/subscriptions
+type SubscriptionGetSuccessResponse struct {
+ Total int `json:"total"`
+ TotalCost int `json:"total_cost"`
+ MaxTotalCost int `json:"max_total_cost"`
+ Pagination EmptyStruct `json:"pagination"`
+
+ Data []SubscriptionPostSuccessResponseBody `json:"data"`
+}
+
+// Cross-usage
+type SubscriptionTransport struct {
+ Method string `json:"method"`
+ SessionID string `json:"session_id"`
+ ConnectedAt string `json:"connected_at"`
+}
+
+// Cross-usage
+type EmptyStruct struct {
+}
+
+// Subscription Statuses
+// Only includes status values that apply to WebSocket connections
+// https://dev.twitch.tv/docs/api/reference/#get-eventsub-subscriptions
+const (
+ STATUS_ENABLED = "enabled"
+ STATUS_AUTHORIZATION_REVOKED = "revoked"
+ STATUS_MODERATOR_REMOVED = "moderator_removed"
+ STATUS_USER_REMOVED = "user_removed"
+ STATUS_VERSION_REMOVED = "version_removed"
+ STATUS_WEBSOCKET_DISCONNECTED = "websocket_disconnected"
+ STATUS_WEBSOCKET_FAILED_PING_PONG = "websocket_failed_ping_pong"
+ STATUS_WEBSOCKET_RECEIVED_INBOUND_TRAFFIC = "websocket_received_inbound_traffic"
+ STATUS_WEBSOCKET_CONNECTION_UNUSED = "websocket_connection_unused"
+ STATUS_INTERNAL_ERROR = "websocket_internal_error"
+ STATUS_NETWORK_TIMEOUT = "network_timeout"
+ STATUS_NETWORK_ERROR = "websocket_network_error"
+)
+
+func IsValidSubscriptionStatus(status string) bool {
+ switch status {
+ case STATUS_ENABLED, STATUS_AUTHORIZATION_REVOKED,
+ STATUS_MODERATOR_REMOVED, STATUS_USER_REMOVED,
+ STATUS_VERSION_REMOVED, STATUS_WEBSOCKET_DISCONNECTED,
+ STATUS_WEBSOCKET_FAILED_PING_PONG, STATUS_WEBSOCKET_RECEIVED_INBOUND_TRAFFIC,
+ STATUS_WEBSOCKET_CONNECTION_UNUSED, STATUS_INTERNAL_ERROR,
+ STATUS_NETWORK_TIMEOUT, STATUS_NETWORK_ERROR:
+ return true
+ default:
+ return false
+ }
+}
+
+func getStatusFromCloseMessage(reason *CloseMessage) string {
+ code := reason.code
+
+ switch code {
+ case 4000:
+ return STATUS_INTERNAL_ERROR
+ case 4001:
+ return STATUS_WEBSOCKET_RECEIVED_INBOUND_TRAFFIC
+ case 4002:
+ return STATUS_WEBSOCKET_FAILED_PING_PONG
+ case 4003:
+ return STATUS_WEBSOCKET_CONNECTION_UNUSED
+ case 4004: // grace time expired. Subscriptions stay open
+ return STATUS_ENABLED
+ case 4005:
+ return STATUS_NETWORK_TIMEOUT
+ case 4006:
+ return STATUS_NETWORK_ERROR
+ default:
+ return STATUS_WEBSOCKET_DISCONNECTED
+ }
+}
diff --git a/internal/events/websocket/websocket_cmd.go b/internal/events/websocket/websocket_cmd.go
new file mode 100644
index 00000000..6d945c83
--- /dev/null
+++ b/internal/events/websocket/websocket_cmd.go
@@ -0,0 +1,64 @@
+package websocket
+
+import (
+ "fmt"
+ "net/rpc"
+
+ "github.com/fatih/color"
+ "github.com/twitchdev/twitch-cli/internal/events/websocket/mock_server"
+ rpc_handler "github.com/twitchdev/twitch-cli/internal/rpc"
+)
+
+type WebsocketCommandParameters struct {
+ Client string
+ Subscription string
+ SubscriptionStatus string
+ CloseReason string
+}
+
+func ForwardWebsocketCommand(cmd string, p WebsocketCommandParameters) {
+ client, err := rpc.DialHTTP("tcp", ":44747")
+ if err != nil {
+ println("Failed to dial RPC handler for WebSocket server. Is it online?")
+ println("Error: " + err.Error())
+ return
+ }
+
+ var reply rpc_handler.RPCResponse
+
+ rpcName := mock_server.ResolveRPCName(cmd)
+ if rpcName == "" {
+ println("Invalid websocket command")
+ return
+ }
+
+ // Command line flags to be passed with the command
+ // Add them all, as it wont hurt anything if they're not relevant
+ variables := make(map[string]string)
+ variables["ClientName"] = p.Client
+ variables["SubscriptionID"] = p.Subscription
+ variables["SubscriptionStatus"] = p.SubscriptionStatus
+ variables["CloseReason"] = p.CloseReason
+
+ args := &rpc_handler.RPCArgs{
+ RPCName: rpcName,
+ Variables: variables,
+ }
+
+ err = client.Call("RPCHandler.ExecuteGenericRPC", args, &reply)
+
+ switch reply.ResponseCode {
+ case mock_server.COMMAND_RESPONSE_SUCCESS:
+ color.New().Add(color.FgGreen).Println(fmt.Sprintf("✔ Forwarded for use in mock EventSub WebSocket server"))
+
+ case mock_server.COMMAND_RESPONSE_FAILED_ON_SERVER:
+ color.New().Add(color.FgRed).Println(fmt.Sprintf("✗ EventSub WebSocket server failed to process command:\n%v", reply.DetailedInfo))
+
+ case mock_server.COMMAND_RESPONSE_MISSING_FLAG:
+ color.New().Add(color.FgRed).Println(fmt.Sprintf("✗ Command rejected for invalid flags:\n%v", reply.DetailedInfo))
+
+ case mock_server.COMMAND_RESPONSE_INVALID_CMD:
+ println("Invalid websocket sub-command: " + cmd)
+
+ }
+}
diff --git a/internal/models/authorization_revoke.go b/internal/models/authorization_revoke.go
index 5a398f2f..a1249854 100644
--- a/internal/models/authorization_revoke.go
+++ b/internal/models/authorization_revoke.go
@@ -10,6 +10,6 @@ type AuthorizationRevokeEvent struct {
}
type AuthorizationRevokeEventSubResponse struct {
- Subscription EventsubSubscription `json:"subscription"`
- Event AuthorizationRevokeEvent `json:"event"`
+ Subscription EventsubSubscription `json:"subscription"`
+ Event *AuthorizationRevokeEvent `json:"event,omitempty"`
}
diff --git a/internal/models/eventsub.go b/internal/models/eventsub.go
index 4c3c3f95..fe535033 100644
--- a/internal/models/eventsub.go
+++ b/internal/models/eventsub.go
@@ -14,8 +14,9 @@ type EventsubSubscription struct {
}
type EventsubTransport struct {
- Method string `json:"method"`
- Callback string `json:"callback"`
+ Method string `json:"method"`
+ Callback string `json:"callback,omitempty"`
+ SessionID string `json:"session_id,omitempty"`
}
type EventsubCondition struct {
@@ -33,7 +34,7 @@ type EventsubCondition struct {
type EventsubResponse struct {
Subscription EventsubSubscription `json:"subscription"`
- Event interface{} `json:"event"`
+ Event interface{} `json:"event,omitempty"`
}
type EventsubSubscriptionVerification struct {
diff --git a/internal/models/transport.go b/internal/models/transport.go
index 29636d72..686df58c 100644
--- a/internal/models/transport.go
+++ b/internal/models/transport.go
@@ -2,5 +2,5 @@
// SPDX-License-Identifier: Apache-2.0
package models
-const TransportEventSub = "eventsub"
-const TransportWebsockets = "websockets"
+const TransportWebhook = "webhook"
+const TransportWebSocket = "websocket"
diff --git a/internal/rpc/rpc_handler.go b/internal/rpc/rpc_handler.go
new file mode 100644
index 00000000..10a95762
--- /dev/null
+++ b/internal/rpc/rpc_handler.go
@@ -0,0 +1,77 @@
+package rpc_handler
+
+import (
+ "net"
+ "net/http"
+ "net/rpc"
+ "strconv"
+)
+
+var globalHandler RPCHandler
+
+type HandlerCallback func(RPCArgs) RPCResponse
+
+type RPCArgs struct {
+ RPCName string
+ Body string
+ Variables map[string]string
+}
+
+type RPCResponse struct {
+ ResponseCode int
+ DetailedInfo string
+}
+
+type RPCHandler struct {
+ Port int
+ Handlers map[string]HandlerCallback
+ listener net.Listener
+}
+
+func (rpch *RPCHandler) RegisterHandler(rpcName string, callback HandlerCallback) bool {
+ _, exists := rpch.Handlers[rpcName]
+ if exists {
+ return false
+ }
+
+ rpch.Handlers[rpcName] = callback
+ return true
+}
+
+func (rpch *RPCHandler) StartBackgroundServer() error {
+ rpc.Register(rpch)
+ rpc.HandleHTTP()
+ l, err := net.Listen("tcp", ":"+strconv.Itoa(rpch.Port))
+ if err != nil {
+ return err
+ }
+ rpch.listener = l
+ go http.Serve(rpch.listener, nil)
+
+ return nil
+}
+
+func (rpch *RPCHandler) ShutdownServer() {
+ rpch.listener.Close()
+}
+
+func (rpch *RPCHandler) ExecuteGenericRPC(args RPCArgs, reply *RPCResponse) error {
+ handler, exists := rpch.Handlers[args.RPCName]
+ if !exists {
+ *reply = RPCResponse{
+ ResponseCode: -1, // -1 is failed here; 0 is success; anything above zero is special failure code
+ DetailedInfo: "Given RPCName is not currently registered",
+ }
+ return nil
+ }
+
+ *reply = handler(args)
+
+ return nil
+}
+
+// Used for testing connection
+func (rphc *RPCHandler) Verify(args RPCArgs, reply *RPCArgs) error {
+ *reply = args
+ return nil
+}
diff --git a/internal/rpc/rpc_test.go b/internal/rpc/rpc_test.go
new file mode 100644
index 00000000..ace962fb
--- /dev/null
+++ b/internal/rpc/rpc_test.go
@@ -0,0 +1,65 @@
+package rpc_handler
+
+import (
+ "encoding/json"
+ "net/rpc"
+ "testing"
+
+ "github.com/twitchdev/twitch-cli/test_setup"
+)
+
+type rpcTestStruct struct {
+ Field1 string `json:"field1"`
+ Field2 int `json:"field2"`
+}
+
+func TestRPCVerify(t *testing.T) {
+ a := test_setup.SetupTestEnv(t)
+
+ field1 := "abcd"
+ field2 := 1234
+ variables := make(map[string]string)
+ variables["Test1"] = "123"
+ variables["Test2"] = "789"
+
+ r := RPCHandler{
+ Port: 44748,
+ Handlers: make(map[string]HandlerCallback),
+ }
+
+ err := r.StartBackgroundServer()
+ a.Nil(err, nil)
+
+ client, err := rpc.DialHTTP("tcp", ":44748")
+ a.Nil(err)
+ defer client.Close()
+
+ rpcBodyFormatted := rpcTestStruct{
+ Field1: field1,
+ Field2: field2,
+ }
+ b, err := json.Marshal(rpcBodyFormatted)
+ a.Nil(err)
+
+ args := RPCArgs{
+ RPCName: "Test",
+ Body: string(b),
+ Variables: variables,
+ }
+
+ var reply RPCArgs
+ err = client.Call("RPCHandler.Verify", args, &reply)
+ a.Nil(err)
+
+ var formattedReply rpcTestStruct
+ err = json.Unmarshal([]byte(reply.Body), &formattedReply)
+ a.Nil(err)
+
+ a.Equal(args.RPCName, reply.RPCName)
+ a.Equal(rpcBodyFormatted.Field1, formattedReply.Field1)
+ a.Equal(rpcBodyFormatted.Field2, formattedReply.Field2)
+ a.Equal(args.Variables["Test1"], reply.Variables["Test1"])
+ a.Equal(args.Variables["Test2"], reply.Variables["Test2"])
+
+ r.listener.Close()
+}
diff --git a/internal/util/generic_list.go b/internal/util/generic_list.go
new file mode 100644
index 00000000..ceef6e2c
--- /dev/null
+++ b/internal/util/generic_list.go
@@ -0,0 +1,31 @@
+package util
+
+type List[T any] struct {
+ Elements map[string]*T
+}
+
+func (c *List[T]) Get(key string) (*T, bool) {
+ element, ok := c.Elements[key]
+ return element, ok
+}
+
+func (c *List[T]) Put(key string, element *T) {
+ c.Elements[key] = element
+}
+
+func (c *List[T]) Delete(key string) {
+ delete(c.Elements, key)
+}
+
+func (c *List[T]) Length() int {
+ n := len(c.Elements)
+ return n
+}
+
+func (c *List[T]) All() []*T {
+ elements := []*T{}
+ for _, el := range c.Elements {
+ elements = append(elements, el)
+ }
+ return elements
+}