@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,61 @@
1
+ import { TwitchEventSub } from "../../eventsub/index.js"
2
+
3
+ export interface Status {
4
+ online: boolean
5
+ type?: string
6
+ startedAt?: Date
7
+ shieldMode: boolean
8
+ title: string
9
+ category: string
10
+ labels: string[]
11
+ }
12
+
13
+ export function onStatus(
14
+ eventsub: TwitchEventSub,
15
+ handleStatus: (status: Status) => void,
16
+ ): void {
17
+ const status: Status = {
18
+ online: false,
19
+ shieldMode: false,
20
+ title: "",
21
+ category: "",
22
+ labels: [],
23
+ }
24
+
25
+ eventsub.subscribe(
26
+ [
27
+ "channel.update",
28
+ "stream.online",
29
+ "stream.offline",
30
+ "channel.shield_mode.begin",
31
+ "channel.shield_mode.end",
32
+ ],
33
+ (payload) => {
34
+ if (payload.type === "stream.online") {
35
+ status.online = true
36
+ status.type = payload.event.type
37
+ status.startedAt = new Date(payload.event.started_at)
38
+ }
39
+
40
+ if (payload.type === "stream.offline") {
41
+ status.online = false
42
+ }
43
+
44
+ if (payload.type === "channel.shield_mode.begin") {
45
+ status.shieldMode = true
46
+ }
47
+
48
+ if (payload.type === "channel.shield_mode.end") {
49
+ status.shieldMode = false
50
+ }
51
+
52
+ if (payload.type === "channel.update") {
53
+ status.title = payload.event.title
54
+ status.category = payload.event.category_name
55
+ status.labels = payload.event.content_classification_labels
56
+ }
57
+
58
+ handleStatus(status)
59
+ },
60
+ )
61
+ }
File without changes
@@ -0,0 +1,230 @@
1
+ import {
2
+ BareAuthentication,
3
+ validateAuthentication,
4
+ } from "../authentication/index.js"
5
+
6
+ declare global {
7
+ interface Window {
8
+ overlaysymphonyTwitchScopes: string[]
9
+ }
10
+ }
11
+
12
+ const localStorageKey = "overlaysymphony:service:twitch"
13
+
14
+ export class TwitchAuthentication extends HTMLElement {
15
+ static get observedAttributes(): string[] {
16
+ return ["client-id", "scopes-key", "popup-url"]
17
+ }
18
+
19
+ root: ShadowRoot
20
+ element?: HTMLElement
21
+
22
+ constructor() {
23
+ super()
24
+
25
+ const stylesheet = new CSSStyleSheet()
26
+
27
+ stylesheet.insertRule(`
28
+ :host {
29
+ display: block;
30
+ }
31
+ `)
32
+
33
+ stylesheet.insertRule(`
34
+ button, .authenticating, .validating, .authenticated {
35
+ display: block;
36
+ box-sizing: border-box;
37
+ width: 100%;
38
+
39
+ color: #ffffff;
40
+ border-radius: 4px;
41
+
42
+ font-size: 20px;
43
+ line-height: 18px;
44
+ padding: 8px 13px;
45
+ text-align: center;
46
+ font-family: sans-serif;
47
+ }
48
+ `)
49
+
50
+ stylesheet.insertRule(`
51
+ button {
52
+ background: #9146FF;
53
+ border: 2px outset buttonBorder;
54
+ }
55
+ `)
56
+
57
+ stylesheet.insertRule(`
58
+ .authenticating, .validating {
59
+ background: #949494;
60
+ border: 2px solid transparent;
61
+ }
62
+ `)
63
+
64
+ stylesheet.insertRule(`
65
+ .authenticated {
66
+ background: #AC75FF;
67
+ border: 2px solid transparent;
68
+ }
69
+ `)
70
+
71
+ this.root = this.attachShadow({ mode: "open" })
72
+ this.root.adoptedStyleSheets.push(stylesheet)
73
+
74
+ this.render()
75
+ }
76
+
77
+ get clientId(): string {
78
+ return this.getAttribute("client-id") ?? ""
79
+ }
80
+
81
+ get scopes(): string[] {
82
+ const key = (this.getAttribute("scopes-key") ??
83
+ "overlaysymphonyTwitchScopes") as "overlaysymphonyTwitchScopes"
84
+
85
+ return window[key] || []
86
+ }
87
+
88
+ get popupUrl(): string {
89
+ const parent = window.location.href
90
+ const end = parent.lastIndexOf("/")
91
+ const baseURL = window.location.href.slice(0, end)
92
+
93
+ return this.getAttribute("popupUrl") ?? `${baseURL}/popup-twitch.html`
94
+ }
95
+
96
+ async authenticate(): Promise<void> {
97
+ this.renderAuthenticating()
98
+
99
+ const url = new URL(
100
+ `${this.popupUrl}?scopes=${this.scopes.join("+")}&clientId=${this.clientId}`,
101
+ )
102
+
103
+ await new Promise<void>((resolve) => {
104
+ const listener = (event: MessageEvent) => {
105
+ if (event.origin !== url.origin) return
106
+
107
+ // const source = (event.source as Window | null)?.name
108
+ // if (source !== "OverlaySymphonyTwitchAuthenticationPopup") return
109
+
110
+ const { type, authentication } = event.data
111
+ if (type !== "authentication") return
112
+
113
+ window.removeEventListener("message", listener)
114
+
115
+ setCached(authentication)
116
+ this.render()
117
+ resolve()
118
+ }
119
+
120
+ window.addEventListener("message", listener)
121
+
122
+ window.open(
123
+ url,
124
+ "OverlaySymphonyTwitchAuthenticationPopup",
125
+ "width=520,height=840",
126
+ )
127
+ })
128
+ }
129
+
130
+ async validate(authentication: BareAuthentication): Promise<void> {
131
+ this.renderValidating()
132
+
133
+ try {
134
+ await validateAuthentication(authentication)
135
+ this.renderAuthenticated()
136
+ } catch (e) {
137
+ clearCached()
138
+ this.render()
139
+ }
140
+ }
141
+
142
+ clear(): void {
143
+ if (this.element) {
144
+ this.element.remove()
145
+ this.element = undefined
146
+ }
147
+ }
148
+
149
+ renderAuthenticating(): void {
150
+ this.clear()
151
+
152
+ this.element = document.createElement("div")
153
+ this.element.classList.add("authenticating")
154
+ this.element.innerText = "Authenticating..."
155
+
156
+ this.root.append(this.element)
157
+ }
158
+
159
+ renderValidating(): void {
160
+ this.clear()
161
+
162
+ this.element = document.createElement("div")
163
+ this.element.classList.add("validating")
164
+ this.element.innerText = "Validating..."
165
+
166
+ this.root.append(this.element)
167
+ }
168
+
169
+ renderAuthenticated(): void {
170
+ this.clear()
171
+
172
+ this.element = document.createElement("div")
173
+ this.element.classList.add("authenticated")
174
+ this.element.innerText = "Authenticated"
175
+
176
+ this.root.append(this.element)
177
+ }
178
+
179
+ renderUnauthenticated(): void {
180
+ this.clear()
181
+
182
+ this.element = document.createElement("button")
183
+ this.element.innerText = "Authenticate with Twitch"
184
+ this.root.append(this.element)
185
+
186
+ this.element.addEventListener("click", () => this.authenticate())
187
+ }
188
+
189
+ render(): void {
190
+ const cached = getCached(this.scopes)
191
+
192
+ if (cached) {
193
+ this.validate(cached)
194
+ } else {
195
+ this.renderUnauthenticated()
196
+ }
197
+ }
198
+ }
199
+
200
+ window.customElements.define(
201
+ "overlaysymfony-twitch-authentication",
202
+ TwitchAuthentication,
203
+ )
204
+
205
+ export function getCached(scopes: string[]): BareAuthentication | undefined {
206
+ const cache = localStorage.getItem(localStorageKey)
207
+ if (cache) {
208
+ const authentication = JSON.parse(cache) as BareAuthentication
209
+ authentication.expires = new Date(authentication.expires)
210
+
211
+ for (const scope of scopes) {
212
+ if (!authentication.scope.includes(scope)) {
213
+ localStorage.removeItem(localStorageKey)
214
+ return undefined
215
+ }
216
+ }
217
+
218
+ return authentication
219
+ }
220
+
221
+ return undefined
222
+ }
223
+
224
+ export function setCached(authentication: BareAuthentication): void {
225
+ localStorage.setItem(localStorageKey, JSON.stringify(authentication))
226
+ }
227
+
228
+ export function clearCached(): void {
229
+ localStorage.removeItem(localStorageKey)
230
+ }
@@ -0,0 +1,115 @@
1
+ import querystring from "@overlaysymphony/core/libs/querystring"
2
+
3
+ import {
4
+ authenticateResult,
5
+ initiateAuthentication,
6
+ } from "../authentication/index.js"
7
+
8
+ const state = getState()
9
+
10
+ if (state.step === "initial") {
11
+ window.location.href = initiateAuthentication(
12
+ state.clientId,
13
+ `${window.location.origin}${window.location.pathname}`,
14
+ state.scopes,
15
+ )
16
+ }
17
+
18
+ if (state.step === "token") {
19
+ const authentication = await authenticateResult(state.clientId, state)
20
+ window.opener.postMessage({ type: "authentication", authentication }, "*")
21
+ window.close()
22
+ }
23
+
24
+ if (state.step === "error") {
25
+ alert(`${state.error}. ${state.description || ""}`)
26
+ }
27
+
28
+ function getState():
29
+ | {
30
+ step: "initial"
31
+ clientId: string
32
+ scopes: string[]
33
+ }
34
+ | {
35
+ step: "token"
36
+ clientId: string
37
+ token_type: "bearer"
38
+ access_token: string
39
+ scope: string
40
+ }
41
+ | {
42
+ step: "error"
43
+ error: string
44
+ description?: string
45
+ } {
46
+ const query = querystring.parse(window.location.search)
47
+ const hash = querystring.parse(window.location.hash)
48
+
49
+ if (hash.token_type === "bearer") {
50
+ const token_type = hash.token_type
51
+ const access_token = validateString(hash.access_token)
52
+ const scope = validateString(hash.scope)
53
+ const clientId = validateString(hash.state)
54
+
55
+ if (!access_token || !scope || !clientId) {
56
+ return {
57
+ step: "error",
58
+ error: "Token Error",
59
+ description: [
60
+ !access_token ? "Missing access_token." : "",
61
+ !scope ? "Missing scope." : "",
62
+ !clientId ? "Missing state." : "",
63
+ ]
64
+ .filter(Boolean)
65
+ .join(" "),
66
+ }
67
+ }
68
+
69
+ return {
70
+ step: "token",
71
+ clientId,
72
+ token_type,
73
+ access_token,
74
+ scope,
75
+ }
76
+ }
77
+
78
+ if ("scopes" in query) {
79
+ const clientId = validateString(query.clientId)
80
+ const scopes = validateString(query.scopes)?.split("+")
81
+
82
+ if (!clientId || !scopes) {
83
+ return {
84
+ step: "error",
85
+ error: "Initialization Error",
86
+ description: [
87
+ !clientId ? "Missing clientId." : "",
88
+ !scopes ? "Missing scopes." : "",
89
+ ]
90
+ .filter(Boolean)
91
+ .join(" "),
92
+ }
93
+ }
94
+
95
+ return {
96
+ step: "initial",
97
+ clientId,
98
+ scopes,
99
+ }
100
+ }
101
+
102
+ return {
103
+ step: "error",
104
+ error: validateString(query.error) || "Unknown Error",
105
+ description: validateString(query.error_description),
106
+ }
107
+ }
108
+
109
+ function validateString(input: string | unknown): string | undefined {
110
+ if (typeof input !== "string" || !input) {
111
+ return undefined
112
+ }
113
+
114
+ return input
115
+ }