@overlaysymphony/twitch 0.3.3 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/package.json +3 -2
  2. package/src/authentication/authentication.ts +2 -2
  3. package/src/chat/chat.ts +6 -2
  4. package/src/eventsub/events/automod.message.hold-2.ts +3 -3
  5. package/src/eventsub/events/automod.message.update-2.ts +3 -3
  6. package/src/eventsub/events/automod.settings.update-1.ts +3 -3
  7. package/src/eventsub/events/automod.terms.update-1.ts +3 -3
  8. package/src/eventsub/events/channel.ad_break.begin-1.ts +2 -2
  9. package/src/eventsub/events/channel.ban-1.ts +2 -2
  10. package/src/eventsub/events/channel.bits.use-1.ts +2 -2
  11. package/src/eventsub/events/channel.channel_points_automatic_reward_redemption.add-2.ts +2 -2
  12. package/src/eventsub/events/channel.channel_points_custom_reward.add-1.ts +2 -2
  13. package/src/eventsub/events/channel.channel_points_custom_reward.remove-1.ts +2 -2
  14. package/src/eventsub/events/channel.channel_points_custom_reward.update-1.ts +2 -2
  15. package/src/eventsub/events/channel.channel_points_custom_reward_redemption.add-1.ts +2 -2
  16. package/src/eventsub/events/channel.channel_points_custom_reward_redemption.update-1.ts +2 -2
  17. package/src/eventsub/events/channel.charity_campaign.donate-1.ts +2 -2
  18. package/src/eventsub/events/channel.charity_campaign.progress-1.ts +2 -2
  19. package/src/eventsub/events/channel.charity_campaign.start-1.ts +2 -2
  20. package/src/eventsub/events/channel.charity_campaign.stop-1.ts +2 -2
  21. package/src/eventsub/events/channel.chat.clear-1.ts +3 -3
  22. package/src/eventsub/events/channel.chat.clear_user_messages-1.ts +3 -3
  23. package/src/eventsub/events/channel.chat.message-1.ts +3 -3
  24. package/src/eventsub/events/channel.chat.message_delete-1.ts +3 -3
  25. package/src/eventsub/events/channel.chat.notification-1.ts +3 -3
  26. package/src/eventsub/events/channel.chat.user_message_hold-1.ts +3 -3
  27. package/src/eventsub/events/channel.chat.user_message_update-1.ts +3 -3
  28. package/src/eventsub/events/channel.chat_settings.update-1.ts +3 -3
  29. package/src/eventsub/events/channel.cheer-1.ts +2 -2
  30. package/src/eventsub/events/channel.follow-2.ts +3 -3
  31. package/src/eventsub/events/channel.goal.begin-1.ts +2 -2
  32. package/src/eventsub/events/channel.goal.end-1.ts +2 -2
  33. package/src/eventsub/events/channel.goal.progress-1.ts +2 -2
  34. package/src/eventsub/events/channel.guest_star_guest.update-beta.ts +3 -3
  35. package/src/eventsub/events/channel.guest_star_session.begin-beta.ts +3 -3
  36. package/src/eventsub/events/channel.guest_star_session.end-beta.ts +3 -3
  37. package/src/eventsub/events/channel.guest_star_settings.update-beta.ts +3 -3
  38. package/src/eventsub/events/channel.hype_train.begin-2.ts +2 -2
  39. package/src/eventsub/events/channel.hype_train.end-2.ts +2 -2
  40. package/src/eventsub/events/channel.hype_train.progress-2.ts +2 -2
  41. package/src/eventsub/events/channel.moderate-2.ts +3 -3
  42. package/src/eventsub/events/channel.moderator.add-1.ts +2 -2
  43. package/src/eventsub/events/channel.moderator.remove-1.ts +2 -2
  44. package/src/eventsub/events/channel.poll.begin-1.ts +2 -2
  45. package/src/eventsub/events/channel.poll.end-1.ts +2 -2
  46. package/src/eventsub/events/channel.poll.progress-1.ts +2 -2
  47. package/src/eventsub/events/channel.prediction.begin-1.ts +2 -2
  48. package/src/eventsub/events/channel.prediction.end-1.ts +2 -2
  49. package/src/eventsub/events/channel.prediction.lock-1.ts +2 -2
  50. package/src/eventsub/events/channel.prediction.progress-1.ts +2 -2
  51. package/src/eventsub/events/channel.raid-1.ts +2 -2
  52. package/src/eventsub/events/channel.shared_chat.begin-1.ts +2 -2
  53. package/src/eventsub/events/channel.shared_chat.end-1.ts +2 -2
  54. package/src/eventsub/events/channel.shared_chat.update-1.ts +2 -2
  55. package/src/eventsub/events/channel.shield_mode.begin-1.ts +3 -3
  56. package/src/eventsub/events/channel.shield_mode.end-1.ts +3 -3
  57. package/src/eventsub/events/channel.shoutout.create-1.ts +3 -3
  58. package/src/eventsub/events/channel.shoutout.receive-1.ts +3 -3
  59. package/src/eventsub/events/channel.subscribe-1.ts +2 -2
  60. package/src/eventsub/events/channel.subscription.end-1.ts +2 -2
  61. package/src/eventsub/events/channel.subscription.gift-1.ts +2 -2
  62. package/src/eventsub/events/channel.subscription.message-1.ts +2 -2
  63. package/src/eventsub/events/channel.suspicious_user.message-1.ts +3 -3
  64. package/src/eventsub/events/channel.suspicious_user.update-1.ts +3 -3
  65. package/src/eventsub/events/channel.unban-1.ts +2 -2
  66. package/src/eventsub/events/channel.unban_request.create-1.ts +3 -3
  67. package/src/eventsub/events/channel.unban_request.resolve-1.ts +3 -3
  68. package/src/eventsub/events/channel.update-2.ts +2 -2
  69. package/src/eventsub/events/channel.vip.add-1.ts +2 -2
  70. package/src/eventsub/events/channel.vip.remove-1.ts +2 -2
  71. package/src/eventsub/events/channel.warning.acknowledge-1.ts +3 -3
  72. package/src/eventsub/events/channel.warning.send-1.ts +3 -3
  73. package/src/eventsub/events/stream.offline-1.ts +2 -2
  74. package/src/eventsub/events/stream.online-1.ts +2 -2
  75. package/src/eventsub/events/user.update-1.ts +2 -2
  76. package/src/eventsub/events/user.whisper.message-1.ts +2 -2
  77. package/src/eventsub/events-helpers.ts +11 -4
  78. package/src/eventsub/eventsub.ts +52 -34
  79. package/src/eventsub/index.ts +1 -0
  80. package/src/helix/chat/emotes.ts +59 -22
  81. package/src/helix/index.ts +1 -0
  82. package/src/helix/streams/index.ts +1 -0
  83. package/src/helix/streams/streams.ts +48 -0
  84. package/src/helix/users/users.ts +51 -0
  85. package/src/helpers/alerts/alerts.ts +3 -3
  86. package/src/helpers/charity/charity.ts +2 -2
  87. package/src/helpers/goal/goal.ts +2 -2
  88. package/src/helpers/hype-train/hype-train.ts +2 -2
  89. package/src/helpers/poll/poll.ts +2 -2
  90. package/src/helpers/prediction/prediction.ts +2 -2
  91. package/src/helpers/redemption/redemption.ts +2 -2
  92. package/src/helpers/status/status.ts +2 -2
@@ -9,42 +9,60 @@ import { createSubscription } from "../helix/subscriptions/index.ts"
9
9
  import { type EventPayload, buildSubscription } from "./events-helpers.ts"
10
10
  import { type TwitchMessage } from "./messages.ts"
11
11
 
12
- export type TwitchEventSub = Events<EventPayload>
12
+ export type TwitchEventSub = Events<EventPayload> & {
13
+ targetUserId: string
14
+ }
13
15
 
16
+ // TODO: connection instead of authentication
14
17
  export default async function createEventSub(
15
18
  authentication: Authentication,
19
+ targetUserId?: string,
16
20
  ): Promise<TwitchEventSub> {
17
- return createEvents<EventPayload, string>(
18
- async (pubsub) => {
19
- const socket = new WebSocket("wss://eventsub.wss.twitch.tv/ws")
20
-
21
- const { promise, resolve } = createDefer<string>()
22
- socket.addEventListener("message", ({ data: rawData }) => {
23
- const data = JSON.parse(rawData as string) as TwitchMessage
24
- data.type = data.metadata.message_type
25
-
26
- if (data.type === "session_welcome") {
27
- resolve(data.payload.session.id)
28
- return
29
- }
30
-
31
- if (data.type === "notification") {
32
- data.payload.id = data.metadata.message_id
33
- data.payload.type = data.payload.subscription.type
34
-
35
- pubsub.dispatch(data.payload)
36
- return
37
- }
38
- })
39
-
40
- return await promise
41
- },
42
- async (pubsub, sessionId, type) => {
43
- await createSubscription(
44
- authentication,
45
- { method: "websocket", session_id: sessionId },
46
- buildSubscription(type, authentication.user.id),
47
- )
48
- },
49
- )
21
+ const currentUserId = authentication.user.id
22
+ targetUserId ??= currentUserId
23
+
24
+ return {
25
+ targetUserId,
26
+ ...(await createEvents<EventPayload, string>(
27
+ async (pubsub) => {
28
+ const socket = new WebSocket("wss://eventsub.wss.twitch.tv/ws")
29
+
30
+ const { promise, resolve } = createDefer<string>()
31
+ socket.addEventListener("message", ({ data: rawData }) => {
32
+ const data = JSON.parse(rawData as string) as TwitchMessage
33
+ data.type = data.metadata.message_type
34
+
35
+ if (data.type === "session_welcome") {
36
+ resolve(data.payload.session.id)
37
+ return
38
+ }
39
+
40
+ if (data.type === "notification") {
41
+ data.payload.id = data.metadata.message_id
42
+ data.payload.type = data.payload.subscription.type
43
+
44
+ pubsub.dispatch(data.payload)
45
+ return
46
+ }
47
+ })
48
+
49
+ return await promise
50
+ },
51
+ async (pubsub, sessionId, type) => {
52
+ await createSubscription(
53
+ authentication,
54
+ { method: "websocket", session_id: sessionId },
55
+ buildSubscription(type, currentUserId, targetUserId),
56
+ )
57
+ },
58
+ (type) => {
59
+ const subscription = buildSubscription(
60
+ type,
61
+ currentUserId,
62
+ targetUserId,
63
+ )
64
+ return `${type}-${JSON.stringify(subscription)}`
65
+ },
66
+ )),
67
+ }
50
68
  }
@@ -1,6 +1,7 @@
1
1
  export {
2
2
  type EventConfig,
3
3
  type EventConfigs,
4
+ type EventPayload,
4
5
  type EventType,
5
6
  registerEvent,
6
7
  } from "./events-helpers.ts"
@@ -1,34 +1,37 @@
1
1
  import { type Authentication } from "../../authentication/index.ts"
2
2
  import { helix } from "../helix.ts"
3
3
 
4
- interface ChannelEmote {
5
- id: string /** An ID that identifies this emote. */
6
- name: string /** The name of the emote. This is the name that viewers type in the chat window to get the emote to appear. */
7
- image: () => string /** A generator for the image URLs for the emote. */
8
- tier: string /** The subscriber tier at which the emote is unlocked. This field contains the tier information only if emote_type is set to subscriptions, otherwise, it's an empty string. */
9
- emote_type: string /** The type of emote. Possible values are:\n* bitstier — A custom Bits tier emote.\n* follower A custom follower emote.\n* subscriptions — A custom subscriber emote. */
10
- emote_set_id: string /** An ID that identifies the emote set that the emote belongs to. */
11
- format: string[] /** The formats that the emote is available in. Possible values are:\n* animated An animated GIF is available for this emote.\n* static A static PNG file is available for this emote. */
12
- scale: string[] /** The sizes that the emote is available in.\n* 1.0 — A small version (28px x 28px) is available.\n* 2.0 — A medium version (56px x 56px) is available.\n* 3.0 — A large version (112px x 112px) is available. */
13
- theme_mode: string[] /** The background themes that the emote is available in. Possible values are:\n* dark\n* light */
4
+ interface Emote {
5
+ /** An ID that identifies this emote. */
6
+ id: string
7
+ /** The name of the emote. This is the name that viewers type in the chat window to get the emote to appear. */
8
+ name: string
9
+ /** A generator for the image URLs for the emote. */
10
+ image: (config?: { format?: string; mode?: string; scale?: string }) => string
11
+ /** The subscriber tier at which the emote is unlocked. This field contains the tier information only if emote_type is set to subscriptions, otherwise, it's an empty string. */
12
+ tier: string
13
+ /** The type of emote. Possible values are:\n* bitstier — A custom Bits tier emote.\n* follower — A custom follower emote.\n* subscriptions — A custom subscriber emote. */
14
+ emote_type: string
15
+ /** An ID that identifies the emote set that the emote belongs to. */
16
+ emote_set_id: string
17
+ /** The ID of the broadcaster who owns the emote. */
18
+ owner_id: string
19
+ /** The formats that the emote is available in. Possible values are:\n* animated — An animated GIF is available for this emote.\n* static — A static PNG file is available for this emote. */
20
+ format: string[]
21
+ /** The sizes that the emote is available in.\n* 1.0 — A small version (28px x 28px) is available.\n* 2.0 — A medium version (56px x 56px) is available.\n* 3.0 — A large version (112px x 112px) is available. */
22
+ scale: string[]
23
+ /** The background themes that the emote is available in. Possible values are:\n* dark\n* light */
24
+ theme_mode: string[]
14
25
  }
15
26
 
16
27
  interface ChannelEmoteResponse {
17
- data: Array<
18
- Omit<ChannelEmote, "image"> & {
19
- images: {
20
- url_1x: string
21
- url_2x: string
22
- url_4x: string
23
- }
24
- }
25
- >
28
+ data: Array<Omit<Emote, "image" | "owner_id">>
26
29
  template: string
27
30
  }
28
31
 
29
32
  export async function getChannelEmotes(
30
33
  authentication: Authentication,
31
- ): Promise<ChannelEmote[]> {
34
+ ): Promise<Emote[]> {
32
35
  const { data: emotes, template } = await helix<
33
36
  ChannelEmoteResponse,
34
37
  {
@@ -42,8 +45,42 @@ export async function getChannelEmotes(
42
45
  },
43
46
  })
44
47
 
45
- return emotes.map(({ images, ...emote }) => ({
48
+ return emotes.map((emote) => ({
49
+ owner_id: authentication.user.id,
50
+ ...emote,
51
+ image: ({ format, mode, scale } = {}) =>
52
+ template
53
+ .replace("{{id}}", emote.id)
54
+ .replace("{{format}}", format ?? emote.format[0])
55
+ .replace("{{theme_mode}}", mode ?? emote.theme_mode[0])
56
+ .replace("{{scale}}", scale ?? emote.scale[emote.scale.length - 1]),
57
+ }))
58
+ }
59
+
60
+ export async function getUserEmotes(
61
+ authentication: Authentication,
62
+ ): Promise<Emote[]> {
63
+ const { data: emotes, template } = await helix<
64
+ ChannelEmoteResponse,
65
+ {
66
+ user_id: string
67
+ }
68
+ >(authentication, {
69
+ method: "GET",
70
+ path: "/chat/emotes/user",
71
+ params: {
72
+ user_id: authentication.user.id,
73
+ },
74
+ })
75
+
76
+ return emotes.map((emote) => ({
77
+ owner_id: authentication.user.id,
46
78
  ...emote,
47
- image: () => template,
79
+ image: ({ format, mode, scale } = {}) =>
80
+ template
81
+ .replace("{{id}}", emote.id)
82
+ .replace("{{format}}", format ?? emote.format[0])
83
+ .replace("{{theme_mode}}", mode ?? emote.theme_mode[0])
84
+ .replace("{{scale}}", scale ?? emote.scale[emote.scale.length - 1]),
48
85
  }))
49
86
  }
@@ -0,0 +1 @@
1
+ export { helix as default } from "./helix.ts"
@@ -0,0 +1 @@
1
+ export * from "./streams.ts"
@@ -0,0 +1,48 @@
1
+ import { type Authentication } from "../../authentication/index.ts"
2
+ import { helix } from "../helix.ts"
3
+
4
+ export interface TwitchStream {
5
+ id: string
6
+ user_id: string
7
+ user_login: string
8
+ user_name: string
9
+ game_id: string
10
+ game_name: string
11
+ type: string
12
+ title: string
13
+ tags: string[]
14
+ viewer_count: number
15
+ started_at: string
16
+ language: string
17
+ thumbnail_url: string
18
+ }
19
+
20
+ interface StreamsResponse {
21
+ data: TwitchStream[]
22
+ }
23
+
24
+ export async function getStream(
25
+ authentication: Authentication,
26
+ login?: string | string[],
27
+ id?: string | string[],
28
+ ): Promise<TwitchStream | undefined> {
29
+ if (!login && !id) return undefined
30
+
31
+ if (typeof login === "string" && login.startsWith("@")) {
32
+ login = login.slice(1)
33
+ }
34
+
35
+ const { data: streams } = await helix<
36
+ StreamsResponse,
37
+ { user_id?: string | string[]; user_login?: string | string[] }
38
+ >(authentication, {
39
+ method: "GET",
40
+ path: "/users",
41
+ params: {
42
+ user_id: id,
43
+ user_login: login,
44
+ },
45
+ })
46
+
47
+ return streams[0]
48
+ }
@@ -26,6 +26,12 @@ export async function getUser(
26
26
  login?: string | string[],
27
27
  id?: string | string[],
28
28
  ): Promise<TwitchUser | undefined> {
29
+ if (!login && !id) return undefined
30
+
31
+ if (typeof login === "string" && login.startsWith("@")) {
32
+ login = login.slice(1)
33
+ }
34
+
29
35
  const { data: users } = await helix<
30
36
  UsersResponse,
31
37
  { id?: string | string[]; login?: string | string[] }
@@ -45,3 +51,48 @@ export async function getUser(
45
51
  created_at: new Date(created_at),
46
52
  }
47
53
  }
54
+
55
+ export async function getUsers(
56
+ authentication: Authentication,
57
+ login?: string | string[],
58
+ id?: string | string[],
59
+ ): Promise<TwitchUser[] | undefined> {
60
+ if (!login && !id) return undefined
61
+
62
+ if (typeof login === "string" && login.startsWith("@")) {
63
+ login = login.slice(1)
64
+ }
65
+
66
+ const { data: users } = await helix<
67
+ UsersResponse,
68
+ { id?: string | string[]; login?: string | string[] }
69
+ >(authentication, {
70
+ method: "GET",
71
+ path: "/users",
72
+ params: {
73
+ id,
74
+ login,
75
+ },
76
+ })
77
+
78
+ return users.map(({ created_at, ...user }) => ({
79
+ ...user,
80
+ created_at: new Date(created_at),
81
+ }))
82
+ }
83
+
84
+ export async function getCurrentUser(
85
+ authentication: Authentication,
86
+ ): Promise<TwitchUser | undefined> {
87
+ const { data: users } = await helix<UsersResponse>(authentication, {
88
+ method: "GET",
89
+ path: "/users",
90
+ })
91
+
92
+ const [{ created_at, ...user }] = users
93
+
94
+ return {
95
+ ...user,
96
+ created_at: new Date(created_at),
97
+ }
98
+ }
@@ -24,8 +24,8 @@ export const mapTypeToPriority = {
24
24
  export function onAlert(
25
25
  eventsub: TwitchEventSub,
26
26
  handleAlert: (alert: Alert) => void,
27
- ): void {
28
- eventsub.on(
27
+ ): () => void {
28
+ return eventsub.on(
29
29
  [
30
30
  "channel.cheer",
31
31
  "channel.follow",
@@ -47,7 +47,7 @@ export function onAlert(
47
47
 
48
48
  export default function createAlertQueue(
49
49
  eventsub: TwitchEventSub,
50
- handleAlert: (alert: Alert) => void,
50
+ handleAlert: (alert: Alert) => unknown,
51
51
  ): Queue<Alert>["dismiss"] {
52
52
  const queue = createQueue<Alert>()
53
53
  queue.listen(handleAlert)
@@ -31,7 +31,7 @@ export function onCharity(
31
31
  trigger: "progress" | "start" | "stop",
32
32
  ) => void,
33
33
  handleDonation: (donation: CharityDonation, charity: Charity) => void,
34
- ): void {
34
+ ): () => void {
35
35
  const charity: Charity = {
36
36
  name: "",
37
37
  description: "",
@@ -42,7 +42,7 @@ export function onCharity(
42
42
  targetAmount: 0,
43
43
  }
44
44
 
45
- eventsub.on(
45
+ return eventsub.on(
46
46
  [
47
47
  "channel.charity_campaign.start",
48
48
  "channel.charity_campaign.progress",
@@ -16,7 +16,7 @@ const mapTypeToTrigger = {
16
16
  export function onGoal(
17
17
  eventsub: TwitchEventSub,
18
18
  handleGoal: (goal: Goal, trigger: "begin" | "progress" | "end") => void,
19
- ): void {
19
+ ): () => void {
20
20
  const goal: Goal = {
21
21
  type: "",
22
22
  description: "",
@@ -24,7 +24,7 @@ export function onGoal(
24
24
  targetAmount: 0,
25
25
  }
26
26
 
27
- eventsub.on(
27
+ return eventsub.on(
28
28
  ["channel.goal.begin", "channel.goal.progress", "channel.goal.end"],
29
29
  (payload) => {
30
30
  goal.type = payload.event.type
@@ -19,7 +19,7 @@ export function onHypeTrain(
19
19
  hypeTrain: HypeTrain,
20
20
  trigger: "begin" | "progress" | "end",
21
21
  ) => void,
22
- ): void {
22
+ ): () => void {
23
23
  const hypeTrain: HypeTrain = {
24
24
  level: 0,
25
25
  total: 0,
@@ -27,7 +27,7 @@ export function onHypeTrain(
27
27
  goal: 0,
28
28
  }
29
29
 
30
- eventsub.on(
30
+ return eventsub.on(
31
31
  [
32
32
  "channel.hype_train.begin",
33
33
  "channel.hype_train.progress",
@@ -23,7 +23,7 @@ const mapTypeToTrigger = {
23
23
  export function onPoll(
24
24
  eventsub: TwitchEventSub,
25
25
  handlePoll: (poll: Poll, trigger: "begin" | "progress" | "end") => void,
26
- ): void {
26
+ ): () => void {
27
27
  const poll: Poll = {
28
28
  id: "",
29
29
  title: "",
@@ -31,7 +31,7 @@ export function onPoll(
31
31
  endsAt: undefined,
32
32
  }
33
33
 
34
- eventsub.on(
34
+ return eventsub.on(
35
35
  ["channel.poll.begin", "channel.poll.progress", "channel.poll.end"],
36
36
  (payload) => {
37
37
  // Twitch sometimes sends duplicate end events.
@@ -25,14 +25,14 @@ export function onPrediction(
25
25
  prediction: Prediction,
26
26
  trigger: "begin" | "progress" | "lock" | "end",
27
27
  ) => void,
28
- ): void {
28
+ ): () => void {
29
29
  const prediction: Prediction = {
30
30
  title: "",
31
31
  outcomes: [],
32
32
  locksAt: new Date(""),
33
33
  }
34
34
 
35
- eventsub.on(
35
+ return eventsub.on(
36
36
  [
37
37
  "channel.prediction.begin",
38
38
  "channel.prediction.progress",
@@ -29,7 +29,7 @@ export function onRedemption(
29
29
  ...config:
30
30
  | [string, (redemption: Redemption) => void]
31
31
  | [(redemption: Redemption) => void]
32
- ): void {
32
+ ): () => void {
33
33
  const id = typeof config[0] === "string" ? config[0] : undefined
34
34
  const handleRedemption =
35
35
  typeof config[0] === "function"
@@ -38,7 +38,7 @@ export function onRedemption(
38
38
  ? config[1]
39
39
  : undefined
40
40
 
41
- eventsub.on(
41
+ return eventsub.on(
42
42
  ["channel.channel_points_custom_reward_redemption.add"],
43
43
  (payload) => {
44
44
  if (typeof id === "undefined" || payload.event.reward.id === id) {
@@ -13,7 +13,7 @@ export interface Status {
13
13
  export function onStatus(
14
14
  eventsub: TwitchEventSub,
15
15
  handleStatus: (status: Status) => void,
16
- ): void {
16
+ ): () => void {
17
17
  const status: Status = {
18
18
  online: false,
19
19
  shieldMode: false,
@@ -22,7 +22,7 @@ export function onStatus(
22
22
  labels: [],
23
23
  }
24
24
 
25
- eventsub.on(
25
+ return eventsub.on(
26
26
  [
27
27
  "channel.update",
28
28
  "stream.online",