@overlaysymphony/twitch 0.1.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 (95) hide show
  1. package/README.md +9 -0
  2. package/package.json +40 -0
  3. package/src/authentication/authentication.ts +107 -0
  4. package/src/authentication/index.ts +1 -0
  5. package/src/chat/chat.ts +160 -0
  6. package/src/chat/index.ts +6 -0
  7. package/src/chat/interfaces/events.ts +269 -0
  8. package/src/chat/interfaces/index.ts +9 -0
  9. package/src/chat/parser.ts +265 -0
  10. package/src/eventsub/events/channel.ad_break.begin.ts +51 -0
  11. package/src/eventsub/events/channel.ban.ts +59 -0
  12. package/src/eventsub/events/channel.channel_points_custom_reward._.ts +29 -0
  13. package/src/eventsub/events/channel.channel_points_custom_reward.add.ts +75 -0
  14. package/src/eventsub/events/channel.channel_points_custom_reward.remove.ts +77 -0
  15. package/src/eventsub/events/channel.channel_points_custom_reward.update.ts +77 -0
  16. package/src/eventsub/events/channel.channel_points_custom_reward_redemption._.ts +10 -0
  17. package/src/eventsub/events/channel.channel_points_custom_reward_redemption.add.ts +59 -0
  18. package/src/eventsub/events/channel.channel_points_custom_reward_redemption.update.ts +59 -0
  19. package/src/eventsub/events/channel.charity_campaign.donate.ts +66 -0
  20. package/src/eventsub/events/channel.charity_campaign.progress.ts +67 -0
  21. package/src/eventsub/events/channel.charity_campaign.start.ts +69 -0
  22. package/src/eventsub/events/channel.charity_campaign.stop.ts +69 -0
  23. package/src/eventsub/events/channel.chat.clear.ts +42 -0
  24. package/src/eventsub/events/channel.chat.clear_user_messages.ts +48 -0
  25. package/src/eventsub/events/channel.chat.message_delete.ts +50 -0
  26. package/src/eventsub/events/channel.chat.notification.ts +50 -0
  27. package/src/eventsub/events/channel.cheer.ts +51 -0
  28. package/src/eventsub/events/channel.follow.ts +50 -0
  29. package/src/eventsub/events/channel.goal.begin.ts +51 -0
  30. package/src/eventsub/events/channel.goal.end.ts +55 -0
  31. package/src/eventsub/events/channel.goal.progress.ts +51 -0
  32. package/src/eventsub/events/channel.guest_star_guest.update.ts +66 -0
  33. package/src/eventsub/events/channel.guest_star_session.begin.ts +46 -0
  34. package/src/eventsub/events/channel.guest_star_session.end.ts +48 -0
  35. package/src/eventsub/events/channel.guest_star_settings.update.ts +50 -0
  36. package/src/eventsub/events/channel.hype_train._.ts +12 -0
  37. package/src/eventsub/events/channel.hype_train.begin.ts +57 -0
  38. package/src/eventsub/events/channel.hype_train.end.ts +55 -0
  39. package/src/eventsub/events/channel.hype_train.progress.ts +70 -0
  40. package/src/eventsub/events/channel.moderator.add.ts +45 -0
  41. package/src/eventsub/events/channel.moderator.remove.ts +45 -0
  42. package/src/eventsub/events/channel.poll._.ts +26 -0
  43. package/src/eventsub/events/channel.poll.begin.ts +55 -0
  44. package/src/eventsub/events/channel.poll.end.ts +57 -0
  45. package/src/eventsub/events/channel.poll.progress.ts +55 -0
  46. package/src/eventsub/events/channel.prediction._.ts +25 -0
  47. package/src/eventsub/events/channel.prediction.begin.ts +51 -0
  48. package/src/eventsub/events/channel.prediction.end.ts +55 -0
  49. package/src/eventsub/events/channel.prediction.lock.ts +51 -0
  50. package/src/eventsub/events/channel.prediction.progress.ts +51 -0
  51. package/src/eventsub/events/channel.raid.ts +49 -0
  52. package/src/eventsub/events/channel.shield_mode.begin.ts +50 -0
  53. package/src/eventsub/events/channel.shield_mode.end.ts +50 -0
  54. package/src/eventsub/events/channel.shoutout.create.ts +62 -0
  55. package/src/eventsub/events/channel.shoutout.receive.ts +52 -0
  56. package/src/eventsub/events/channel.subscribe.ts +49 -0
  57. package/src/eventsub/events/channel.subscription.end.ts +49 -0
  58. package/src/eventsub/events/channel.subscription.gift.ts +53 -0
  59. package/src/eventsub/events/channel.subscription.message.ts +67 -0
  60. package/src/eventsub/events/channel.unban.ts +51 -0
  61. package/src/eventsub/events/channel.update.ts +49 -0
  62. package/src/eventsub/events/index.ts +284 -0
  63. package/src/eventsub/events/stream.offline.ts +39 -0
  64. package/src/eventsub/events/stream.online.ts +45 -0
  65. package/src/eventsub/events/user.update.ts +45 -0
  66. package/src/eventsub/events-helpers.ts +29 -0
  67. package/src/eventsub/eventsub.ts +88 -0
  68. package/src/eventsub/index.ts +7 -0
  69. package/src/eventsub/messages.ts +34 -0
  70. package/src/helix/channel-points/custom-rewards.ts +63 -0
  71. package/src/helix/channel-points/index.ts +1 -0
  72. package/src/helix/helix.ts +89 -0
  73. package/src/helix/subscriptions/index.ts +1 -0
  74. package/src/helix/subscriptions/subscriptions.ts +122 -0
  75. package/src/helix/users/index.ts +1 -0
  76. package/src/helix/users/users.ts +42 -0
  77. package/src/helpers/alerts/alerts.ts +60 -0
  78. package/src/helpers/alerts/index.ts +2 -0
  79. package/src/helpers/charity/charity.ts +89 -0
  80. package/src/helpers/charity/index.ts +1 -0
  81. package/src/helpers/goal/goal.ts +38 -0
  82. package/src/helpers/goal/index.ts +1 -0
  83. package/src/helpers/hype-train/hype-train.ts +51 -0
  84. package/src/helpers/hype-train/index.ts +1 -0
  85. package/src/helpers/poll/index.ts +1 -0
  86. package/src/helpers/poll/poll.ts +63 -0
  87. package/src/helpers/prediction/index.ts +1 -0
  88. package/src/helpers/prediction/prediction.ts +66 -0
  89. package/src/helpers/redemption/index.ts +1 -0
  90. package/src/helpers/redemption/redemption.ts +42 -0
  91. package/src/helpers/status/index.ts +1 -0
  92. package/src/helpers/status/status.ts +61 -0
  93. package/src/setupTests.ts +0 -0
  94. package/src/ui/authentication.ts +230 -0
  95. package/src/ui/popup.ts +115 -0
@@ -0,0 +1,89 @@
1
+ import querystring from "@overlaysymphony/core/libs/querystring"
2
+
3
+ import { Authentication } from "../authentication/index.js"
4
+
5
+ export async function helix<
6
+ Data = never,
7
+ RawData = never,
8
+ Params = unknown,
9
+ Body = never,
10
+ >(
11
+ authentication: Authentication,
12
+ {
13
+ method,
14
+ path,
15
+ params,
16
+ body,
17
+ }: {
18
+ method: string
19
+ path: string
20
+ params?: Params
21
+ body?: Body
22
+ },
23
+ transform: (data: RawData) => Data = (data) => data as unknown as Data,
24
+ ): Promise<Data[]> {
25
+ const queryString = params ? querystring.stringify(params) : ""
26
+ const { bodyString, contentType } = getBodyString(body)
27
+
28
+ const url = `https://api.twitch.tv/helix${path}?${queryString}`
29
+ const response = await fetch(url, {
30
+ method,
31
+ headers: {
32
+ Authorization: `Bearer ${authentication.accessToken}`,
33
+ "Client-Id": authentication.clientId,
34
+ "Content-Type": contentType,
35
+ },
36
+ body: bodyString,
37
+ })
38
+
39
+ if (!response.ok) {
40
+ console.error({ method, path, params, body })
41
+ throw new Error(await response.text())
42
+ }
43
+
44
+ const text = await response.text()
45
+ if (!text) {
46
+ return undefined as unknown as Data[]
47
+ }
48
+
49
+ const {
50
+ data,
51
+ // pagination: { cursor },
52
+ } = JSON.parse(text) as {
53
+ total: number
54
+ data: RawData[]
55
+ pagination: {
56
+ cursor: string
57
+ }
58
+ }
59
+
60
+ // if (cursor) {
61
+ // return data.concat(await helix<Data>(method, path, params, cursor))
62
+ // }
63
+
64
+ return data.map((data) => transform(data))
65
+ }
66
+
67
+ function getBodyString<Body>(body: Body): {
68
+ bodyString: string
69
+ contentType: string
70
+ } {
71
+ if (typeof body === "string") {
72
+ return {
73
+ bodyString: body,
74
+ contentType: "application/x-www-form-urlencoded",
75
+ }
76
+ }
77
+
78
+ if (typeof body === "object") {
79
+ return {
80
+ bodyString: JSON.stringify(body),
81
+ contentType: "application/json",
82
+ }
83
+ }
84
+
85
+ return {
86
+ bodyString: body as string,
87
+ contentType: undefined as unknown as string,
88
+ }
89
+ }
@@ -0,0 +1 @@
1
+ export * from "./subscriptions.js"
@@ -0,0 +1,122 @@
1
+ import { Authentication } from "../../authentication/index.js"
2
+ import {
3
+ TwitchSubscription,
4
+ TwitchSubscriptionType,
5
+ buildSubscription,
6
+ } from "../../eventsub/events/index.js"
7
+ import { BaseSubscription } from "../../eventsub/events-helpers.js"
8
+ import { helix } from "../helix.js"
9
+
10
+ interface SubscriptionWebhookTransport {
11
+ method: "webhook"
12
+ callback: string
13
+ secret: string
14
+ }
15
+
16
+ interface SubscriptionWebsocketTransport {
17
+ method: "websocket"
18
+ session_id: string
19
+ }
20
+
21
+ type SubscriptionTransport =
22
+ | SubscriptionWebhookTransport
23
+ | SubscriptionWebsocketTransport
24
+
25
+ export type SubscriptionRequest<Subscription extends BaseSubscription> = {
26
+ type: Subscription["type"]
27
+ version: Subscription["version"]
28
+ condition: Subscription["condition"]
29
+ transport: SubscriptionTransport
30
+ }
31
+
32
+ export type ActiveSubscription<Subscription extends BaseSubscription> =
33
+ Subscription & {
34
+ id: string
35
+ status:
36
+ | "enabled"
37
+ | "webhook_callback_verification_pending"
38
+ | "webhook_callback_verification_failed"
39
+ | "notification_failures_exceeded"
40
+ | "authorization_revoked"
41
+ | "moderator_removed"
42
+ | "user_removed"
43
+ | "version_removed"
44
+ | "websocket_disconnected"
45
+ | "websocket_failed_ping_pong"
46
+ | "websocket_received_inbound_traffic"
47
+ | "websocket_connection_unused"
48
+ | "websocket_internal_error"
49
+ | "websocket_network_timeout"
50
+ | "websocket_network_error"
51
+ cost: number
52
+ created_at: Date
53
+ transport: SubscriptionTransport
54
+ }
55
+
56
+ export async function createSubscription<
57
+ Type extends TwitchSubscriptionType,
58
+ Subscription extends TwitchSubscription<Type>,
59
+ >(
60
+ sessionId: string,
61
+ authentication: Authentication,
62
+ type: Type,
63
+ ): Promise<ActiveSubscription<Subscription>> {
64
+ const subscription = buildSubscription(
65
+ type,
66
+ authentication.user.id,
67
+ ) as Subscription
68
+
69
+ const [activeSubscription] = await helix<
70
+ ActiveSubscription<Subscription>,
71
+ never,
72
+ never,
73
+ SubscriptionRequest<Subscription>
74
+ >(authentication, {
75
+ method: "post",
76
+ path: "/eventsub/subscriptions",
77
+ // @ts-ignore
78
+ body: {
79
+ ...subscription,
80
+ transport: {
81
+ method: "websocket" as const,
82
+ session_id: sessionId,
83
+ },
84
+ },
85
+ })
86
+
87
+ return activeSubscription
88
+ }
89
+
90
+ export async function deleteSubscription(
91
+ authentication: Authentication,
92
+ id: string,
93
+ ): Promise<void> {
94
+ await helix<never, never, { id: string }, never>(authentication, {
95
+ method: "delete",
96
+ path: "/eventsub/subscriptions",
97
+ params: {
98
+ id,
99
+ },
100
+ })
101
+ }
102
+
103
+ export async function getSubscriptions(
104
+ authentication: Authentication,
105
+ ): Promise<Array<ActiveSubscription<TwitchSubscription>>> {
106
+ const subscriptions = await helix<
107
+ ActiveSubscription<TwitchSubscription>,
108
+ never,
109
+ {
110
+ status?: ActiveSubscription<TwitchSubscription>["status"]
111
+ type?: TwitchSubscriptionType
112
+ user_id?: string
113
+ after?: string
114
+ },
115
+ never
116
+ >(authentication, {
117
+ method: "get",
118
+ path: "/eventsub/subscriptions",
119
+ })
120
+
121
+ return subscriptions
122
+ }
@@ -0,0 +1 @@
1
+ export * from "./users.js"
@@ -0,0 +1,42 @@
1
+ import { Authentication } from "../../authentication/index.js"
2
+ import { helix } from "../helix.js"
3
+
4
+ export interface TwitchUser {
5
+ id: string
6
+ login: string
7
+ display_name: string
8
+ type: "admin" | "global_mod" | "staff" | ""
9
+ broadcaster_type: "partner" | "affiliate" | ""
10
+ description: string
11
+ profile_image_url: string
12
+ offline_image_url: string
13
+ created_at: Date
14
+ }
15
+
16
+ type RawUser = Omit<TwitchUser, "created_at"> & {
17
+ created_at: string
18
+ }
19
+
20
+ export async function getUsers(
21
+ authentication: Authentication,
22
+ id?: string | string[],
23
+ login?: string | string[],
24
+ ): Promise<TwitchUser[]> {
25
+ const data = await helix<TwitchUser, RawUser>(
26
+ authentication,
27
+ {
28
+ method: "get",
29
+ path: "/users",
30
+ params: {
31
+ id,
32
+ login,
33
+ },
34
+ },
35
+ ({ created_at, ...data }) => ({
36
+ ...data,
37
+ created_at: new Date(created_at),
38
+ }),
39
+ )
40
+
41
+ return data
42
+ }
@@ -0,0 +1,60 @@
1
+ import createQueue, { Queue } from "@overlaysymphony/core/libs/queue"
2
+
3
+ import { TwitchNotificationMessage } from "../../eventsub/events/index.js"
4
+ import { TwitchEventSub } from "../../eventsub/index.js"
5
+
6
+ export type Alert = TwitchNotificationMessage<
7
+ | "channel.cheer"
8
+ | "channel.follow"
9
+ | "channel.raid"
10
+ | "channel.subscribe"
11
+ | "channel.subscription.gift"
12
+ | "channel.subscription.message"
13
+ >["payload"]
14
+
15
+ export const mapTypeToPriority = {
16
+ "channel.follow": 0,
17
+ "channel.cheer": 1,
18
+ "channel.subscribe": 2,
19
+ "channel.subscription.message": 3,
20
+ "channel.subscription.gift": 4,
21
+ "channel.raid": 5,
22
+ }
23
+
24
+ export function onAlert(
25
+ eventsub: TwitchEventSub,
26
+ handleAlert: (alert: Alert) => void,
27
+ ): void {
28
+ eventsub.subscribe(
29
+ [
30
+ "channel.cheer",
31
+ "channel.follow",
32
+ "channel.raid",
33
+ "channel.subscribe",
34
+ "channel.subscription.gift",
35
+ "channel.subscription.message",
36
+ ],
37
+ (payload) => {
38
+ // Don't spam alerts when gifted many subs
39
+ if (payload.type === "channel.subscribe" && payload.event.is_gift) {
40
+ return
41
+ }
42
+
43
+ handleAlert(payload)
44
+ },
45
+ )
46
+ }
47
+
48
+ export default function createAlertQueue(
49
+ eventsub: TwitchEventSub,
50
+ handleAlert: (alert: Alert) => void,
51
+ ): Queue<Alert>["dismiss"] {
52
+ const queue = createQueue<Alert>()
53
+ queue.listen(handleAlert)
54
+
55
+ onAlert(eventsub, (payload) => {
56
+ queue.enqueue(mapTypeToPriority[payload.type], payload)
57
+ })
58
+
59
+ return queue.dismiss
60
+ }
@@ -0,0 +1,2 @@
1
+ export { default } from "./alerts.js"
2
+ export * from "./alerts.js"
@@ -0,0 +1,89 @@
1
+ import { TwitchEventSub } from "../../eventsub/index.js"
2
+
3
+ export interface Charity {
4
+ name: string
5
+ description: string
6
+ logo: string
7
+ website: string
8
+ currency: string
9
+ currentAmount: number
10
+ targetAmount: number
11
+ }
12
+
13
+ export interface CharityDonation {
14
+ userId: string
15
+ userLogin: string
16
+ userName: string
17
+ amount: number
18
+ }
19
+
20
+ const mapTypeToTrigger = {
21
+ "channel.charity_campaign.donate": "donate",
22
+ "channel.charity_campaign.progress": "progress",
23
+ "channel.charity_campaign.start": "start",
24
+ "channel.charity_campaign.stop": "stop",
25
+ } as const
26
+
27
+ export function onCharity(
28
+ eventsub: TwitchEventSub,
29
+ handleCharity: (
30
+ charity: Charity,
31
+ trigger: "progress" | "start" | "stop",
32
+ ) => void,
33
+ handleDonation: (donation: CharityDonation, charity: Charity) => void,
34
+ ): void {
35
+ const charity: Charity = {
36
+ name: "",
37
+ description: "",
38
+ logo: "",
39
+ website: "",
40
+ currency: "",
41
+ currentAmount: 0,
42
+ targetAmount: 0,
43
+ }
44
+
45
+ eventsub.subscribe(
46
+ [
47
+ "channel.charity_campaign.start",
48
+ "channel.charity_campaign.progress",
49
+ "channel.charity_campaign.stop",
50
+ "channel.charity_campaign.donate",
51
+ ],
52
+ (payload) => {
53
+ charity.name = payload.event.charity_name
54
+ charity.description = payload.event.charity_description
55
+ charity.logo = payload.event.charity_logo
56
+ charity.website = payload.event.charity_website
57
+
58
+ if (
59
+ payload.type === "channel.charity_campaign.start" ||
60
+ payload.type === "channel.charity_campaign.progress" ||
61
+ payload.type === "channel.charity_campaign.stop"
62
+ ) {
63
+ charity.currency = payload.event.target_amount.currency
64
+ charity.currentAmount =
65
+ payload.event.current_amount.value /
66
+ 10 ** payload.event.current_amount.decimal_places
67
+ charity.targetAmount =
68
+ payload.event.target_amount.value /
69
+ 10 ** payload.event.target_amount.decimal_places
70
+
71
+ handleCharity(charity, mapTypeToTrigger[payload.type])
72
+ }
73
+
74
+ if (payload.type === "channel.charity_campaign.donate") {
75
+ handleDonation(
76
+ {
77
+ userId: payload.event.user_id,
78
+ userLogin: payload.event.user_login,
79
+ userName: payload.event.user_name,
80
+ amount:
81
+ payload.event.amount.value /
82
+ 10 ** payload.event.amount.decimal_places,
83
+ },
84
+ charity,
85
+ )
86
+ }
87
+ },
88
+ )
89
+ }
@@ -0,0 +1 @@
1
+ export * from "./charity.js"
@@ -0,0 +1,38 @@
1
+ import { TwitchEventSub } from "../../eventsub/index.js"
2
+
3
+ export interface Goal {
4
+ type: string
5
+ description: string
6
+ currentAmount: number
7
+ targetAmount: number
8
+ }
9
+
10
+ const mapTypeToTrigger = {
11
+ "channel.goal.begin": "begin",
12
+ "channel.goal.progress": "progress",
13
+ "channel.goal.end": "end",
14
+ } as const
15
+
16
+ export function onGoal(
17
+ eventsub: TwitchEventSub,
18
+ handleGoal: (goal: Goal, trigger: "begin" | "progress" | "end") => void,
19
+ ): void {
20
+ const goal: Goal = {
21
+ type: "",
22
+ description: "",
23
+ currentAmount: 0,
24
+ targetAmount: 0,
25
+ }
26
+
27
+ eventsub.subscribe(
28
+ ["channel.goal.begin", "channel.goal.progress", "channel.goal.end"],
29
+ (payload) => {
30
+ goal.type = payload.event.type
31
+ goal.description = payload.event.description
32
+ goal.currentAmount = payload.event.current_amount
33
+ goal.targetAmount = payload.event.target_amount
34
+
35
+ handleGoal(goal, mapTypeToTrigger[payload.type])
36
+ },
37
+ )
38
+ }
@@ -0,0 +1 @@
1
+ export * from "./goal.js"
@@ -0,0 +1,51 @@
1
+ import { TwitchEventSub } from "../../eventsub/index.js"
2
+
3
+ export interface HypeTrain {
4
+ level: number
5
+ total: number
6
+ progress: number
7
+ goal: number
8
+ }
9
+
10
+ const mapTypeToTrigger = {
11
+ "channel.hype_train.begin": "begin",
12
+ "channel.hype_train.progress": "progress",
13
+ "channel.hype_train.end": "end",
14
+ } as const
15
+
16
+ export function onHypeTrain(
17
+ eventsub: TwitchEventSub,
18
+ handleHypeTrain: (
19
+ hypeTrain: HypeTrain,
20
+ trigger: "begin" | "progress" | "end",
21
+ ) => void,
22
+ ): void {
23
+ const hypeTrain: HypeTrain = {
24
+ level: 0,
25
+ total: 0,
26
+ progress: 0,
27
+ goal: 0,
28
+ }
29
+
30
+ eventsub.subscribe(
31
+ [
32
+ "channel.hype_train.begin",
33
+ "channel.hype_train.progress",
34
+ "channel.hype_train.end",
35
+ ],
36
+ (payload) => {
37
+ hypeTrain.level = payload.event.level
38
+ hypeTrain.total = payload.event.total
39
+
40
+ if (
41
+ payload.type === "channel.hype_train.begin" ||
42
+ payload.type === "channel.hype_train.progress"
43
+ ) {
44
+ hypeTrain.progress = payload.event.progress
45
+ hypeTrain.goal = payload.event.goal
46
+ }
47
+
48
+ handleHypeTrain(hypeTrain, mapTypeToTrigger[payload.type])
49
+ },
50
+ )
51
+ }
@@ -0,0 +1 @@
1
+ export * from "./hype-train.js"
@@ -0,0 +1 @@
1
+ export * from "./poll.js"
@@ -0,0 +1,63 @@
1
+ import { TwitchEventSub } from "../../eventsub/index.js"
2
+
3
+ export interface Poll {
4
+ title: string
5
+ choices: Array<{
6
+ id: string
7
+ title: string
8
+ votes: number
9
+ votesBits: number
10
+ votesChannelPoints: number
11
+ votesNormal: number
12
+ }>
13
+ endsAt: Date
14
+ }
15
+
16
+ const mapTypeToTrigger = {
17
+ "channel.poll.begin": "begin",
18
+ "channel.poll.progress": "progress",
19
+ "channel.poll.end": "end",
20
+ } as const
21
+
22
+ export function onPoll(
23
+ eventsub: TwitchEventSub,
24
+ handlePoll: (poll: Poll, trigger: "begin" | "progress" | "end") => void,
25
+ ): void {
26
+ const poll: Poll = {
27
+ title: "",
28
+ choices: [],
29
+ endsAt: new Date(""),
30
+ }
31
+
32
+ eventsub.subscribe(
33
+ ["channel.poll.begin", "channel.poll.progress", "channel.poll.end"],
34
+ (payload) => {
35
+ poll.title = payload.event.title
36
+ poll.choices = payload.event.choices.map(
37
+ ({
38
+ id,
39
+ title,
40
+ votes = 0,
41
+ channel_points_votes = 0,
42
+ bits_votes = 0,
43
+ }) => ({
44
+ id,
45
+ title,
46
+ votes: votes,
47
+ votesBits: bits_votes,
48
+ votesChannelPoints: channel_points_votes,
49
+ votesNormal: votes - channel_points_votes - bits_votes,
50
+ }),
51
+ )
52
+
53
+ if (
54
+ payload.type === "channel.poll.begin" ||
55
+ payload.type === "channel.poll.progress"
56
+ ) {
57
+ poll.endsAt = payload.event.ends_at
58
+ }
59
+
60
+ handlePoll(poll, mapTypeToTrigger[payload.type])
61
+ },
62
+ )
63
+ }
@@ -0,0 +1 @@
1
+ export * from "./prediction.js"
@@ -0,0 +1,66 @@
1
+ import { TwitchEventSub } from "../../eventsub/index.js"
2
+
3
+ export interface Prediction {
4
+ title: string
5
+ outcomes: Array<{
6
+ id: string
7
+ title: string
8
+ color: string
9
+ users: number
10
+ points: number
11
+ }>
12
+ locksAt?: Date
13
+ }
14
+
15
+ const mapTypeToTrigger = {
16
+ "channel.prediction.begin": "begin",
17
+ "channel.prediction.progress": "progress",
18
+ "channel.prediction.lock": "lock",
19
+ "channel.prediction.end": "end",
20
+ } as const
21
+
22
+ export function onPrediction(
23
+ eventsub: TwitchEventSub,
24
+ handlePrediction: (
25
+ prediction: Prediction,
26
+ trigger: "begin" | "progress" | "lock" | "end",
27
+ ) => void,
28
+ ): void {
29
+ const prediction: Prediction = {
30
+ title: "",
31
+ outcomes: [],
32
+ locksAt: new Date(""),
33
+ }
34
+
35
+ eventsub.subscribe(
36
+ [
37
+ "channel.prediction.begin",
38
+ "channel.prediction.progress",
39
+ "channel.prediction.lock",
40
+ "channel.prediction.end",
41
+ ],
42
+ (payload) => {
43
+ prediction.title = payload.event.title
44
+ prediction.outcomes = payload.event.outcomes.map(
45
+ ({ id, title, color, users, channel_points }) => ({
46
+ id,
47
+ title,
48
+ color,
49
+ users,
50
+ points: channel_points,
51
+ }),
52
+ )
53
+
54
+ if (
55
+ payload.type === "channel.prediction.begin" ||
56
+ payload.type === "channel.prediction.progress"
57
+ ) {
58
+ prediction.locksAt = payload.event.locks_at
59
+ } else {
60
+ prediction.locksAt = undefined
61
+ }
62
+
63
+ handlePrediction(prediction, mapTypeToTrigger[payload.type])
64
+ },
65
+ )
66
+ }
@@ -0,0 +1 @@
1
+ export * from "./redemption.js"
@@ -0,0 +1,42 @@
1
+ import { TwitchEventSub } from "../../eventsub/index.js"
2
+
3
+ export interface Redemption {
4
+ id: string
5
+ userId: string
6
+ userLogin: string
7
+ userName: string
8
+ userInput: string
9
+ reward: {
10
+ id: string
11
+ title: string
12
+ cost: number
13
+ prompt: string
14
+ }
15
+ }
16
+
17
+ export function onRedemption(
18
+ eventsub: TwitchEventSub,
19
+ id: string,
20
+ handleRedemption: (redemption: Redemption) => void,
21
+ ): void {
22
+ eventsub.subscribe(
23
+ ["channel.channel_points_custom_reward_redemption.add"],
24
+ (payload) => {
25
+ if (payload.event.reward.id === id) {
26
+ handleRedemption({
27
+ id: payload.event.id,
28
+ userId: payload.event.user_id,
29
+ userLogin: payload.event.user_login,
30
+ userName: payload.event.user_name,
31
+ userInput: payload.event.user_input,
32
+ reward: {
33
+ id: payload.event.reward.id,
34
+ title: payload.event.reward.title,
35
+ cost: payload.event.reward.cost,
36
+ prompt: payload.event.reward.prompt,
37
+ },
38
+ })
39
+ }
40
+ },
41
+ )
42
+ }
@@ -0,0 +1 @@
1
+ export * from "./status.js"