@overlaysymphony/twitch 0.1.0 → 0.2.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 (75) hide show
  1. package/README.md +1 -1
  2. package/package.json +21 -22
  3. package/src/authentication/authentication.ts +85 -55
  4. package/src/chat/chat.ts +9 -8
  5. package/src/chat/interfaces/events.ts +29 -29
  6. package/src/chat/interfaces/index.ts +1 -1
  7. package/src/chat/parser.ts +9 -20
  8. package/src/eventsub/events/channel.ad_break.begin.ts +1 -1
  9. package/src/eventsub/events/channel.ban.ts +1 -1
  10. package/src/eventsub/events/channel.channel_points_custom_reward.add.ts +2 -2
  11. package/src/eventsub/events/channel.channel_points_custom_reward.remove.ts +2 -2
  12. package/src/eventsub/events/channel.channel_points_custom_reward.update.ts +2 -2
  13. package/src/eventsub/events/channel.channel_points_custom_reward_redemption.add.ts +2 -2
  14. package/src/eventsub/events/channel.channel_points_custom_reward_redemption.update.ts +2 -2
  15. package/src/eventsub/events/channel.charity_campaign.donate.ts +1 -1
  16. package/src/eventsub/events/channel.charity_campaign.progress.ts +1 -1
  17. package/src/eventsub/events/channel.charity_campaign.start.ts +1 -1
  18. package/src/eventsub/events/channel.charity_campaign.stop.ts +1 -1
  19. package/src/eventsub/events/channel.chat.clear.ts +1 -1
  20. package/src/eventsub/events/channel.chat.clear_user_messages.ts +1 -1
  21. package/src/eventsub/events/channel.chat.message_delete.ts +1 -1
  22. package/src/eventsub/events/channel.chat.notification.ts +1 -1
  23. package/src/eventsub/events/channel.cheer.ts +1 -1
  24. package/src/eventsub/events/channel.follow.ts +1 -1
  25. package/src/eventsub/events/channel.goal.begin.ts +1 -1
  26. package/src/eventsub/events/channel.goal.end.ts +1 -1
  27. package/src/eventsub/events/channel.goal.progress.ts +1 -1
  28. package/src/eventsub/events/channel.guest_star_guest.update.ts +1 -1
  29. package/src/eventsub/events/channel.guest_star_session.begin.ts +1 -1
  30. package/src/eventsub/events/channel.guest_star_session.end.ts +1 -1
  31. package/src/eventsub/events/channel.guest_star_settings.update.ts +1 -1
  32. package/src/eventsub/events/channel.hype_train.begin.ts +2 -2
  33. package/src/eventsub/events/channel.hype_train.end.ts +2 -2
  34. package/src/eventsub/events/channel.hype_train.progress.ts +2 -2
  35. package/src/eventsub/events/channel.moderator.add.ts +1 -1
  36. package/src/eventsub/events/channel.moderator.remove.ts +1 -1
  37. package/src/eventsub/events/channel.poll.begin.ts +2 -2
  38. package/src/eventsub/events/channel.poll.end.ts +2 -2
  39. package/src/eventsub/events/channel.poll.progress.ts +2 -2
  40. package/src/eventsub/events/channel.prediction.begin.ts +2 -2
  41. package/src/eventsub/events/channel.prediction.end.ts +2 -2
  42. package/src/eventsub/events/channel.prediction.lock.ts +2 -2
  43. package/src/eventsub/events/channel.prediction.progress.ts +2 -2
  44. package/src/eventsub/events/channel.raid.ts +1 -1
  45. package/src/eventsub/events/channel.shield_mode.begin.ts +1 -1
  46. package/src/eventsub/events/channel.shield_mode.end.ts +1 -1
  47. package/src/eventsub/events/channel.shoutout.create.ts +1 -1
  48. package/src/eventsub/events/channel.shoutout.receive.ts +1 -1
  49. package/src/eventsub/events/channel.subscribe.ts +1 -1
  50. package/src/eventsub/events/channel.subscription.end.ts +1 -1
  51. package/src/eventsub/events/channel.subscription.gift.ts +1 -1
  52. package/src/eventsub/events/channel.subscription.message.ts +1 -1
  53. package/src/eventsub/events/channel.unban.ts +1 -1
  54. package/src/eventsub/events/channel.update.ts +1 -1
  55. package/src/eventsub/events/index.ts +52 -51
  56. package/src/eventsub/events/stream.offline.ts +1 -1
  57. package/src/eventsub/events/stream.online.ts +1 -1
  58. package/src/eventsub/events/user.update.ts +1 -1
  59. package/src/eventsub/events-helpers.ts +2 -2
  60. package/src/eventsub/eventsub.ts +9 -9
  61. package/src/eventsub/messages.ts +1 -1
  62. package/src/helix/channel-points/custom-rewards.ts +2 -3
  63. package/src/helix/helix.ts +1 -1
  64. package/src/helix/subscriptions/subscriptions.ts +9 -19
  65. package/src/helix/users/users.ts +1 -1
  66. package/src/helpers/alerts/alerts.ts +3 -3
  67. package/src/helpers/charity/charity.ts +1 -1
  68. package/src/helpers/goal/goal.ts +1 -1
  69. package/src/helpers/hype-train/hype-train.ts +1 -1
  70. package/src/helpers/poll/poll.ts +1 -1
  71. package/src/helpers/prediction/prediction.ts +1 -1
  72. package/src/helpers/redemption/redemption.ts +1 -1
  73. package/src/helpers/status/status.ts +1 -1
  74. package/src/ui/authentication.ts +29 -85
  75. package/src/ui/popup.ts +45 -6
@@ -1,10 +1,10 @@
1
- import { Authentication } from "../../authentication/index.js"
1
+ import { type Authentication } from "../../authentication/index.js"
2
2
  import {
3
- TwitchSubscription,
4
- TwitchSubscriptionType,
3
+ type TwitchSubscription,
4
+ type TwitchSubscriptionType,
5
5
  buildSubscription,
6
6
  } from "../../eventsub/events/index.js"
7
- import { BaseSubscription } from "../../eventsub/events-helpers.js"
7
+ import { type BaseSubscription } from "../../eventsub/events-helpers.js"
8
8
  import { helix } from "../helix.js"
9
9
 
10
10
  interface SubscriptionWebhookTransport {
@@ -22,7 +22,7 @@ type SubscriptionTransport =
22
22
  | SubscriptionWebhookTransport
23
23
  | SubscriptionWebsocketTransport
24
24
 
25
- export type SubscriptionRequest<Subscription extends BaseSubscription> = {
25
+ export interface SubscriptionRequest<Subscription extends BaseSubscription> {
26
26
  type: Subscription["type"]
27
27
  version: Subscription["version"]
28
28
  condition: Subscription["condition"]
@@ -61,20 +61,11 @@ export async function createSubscription<
61
61
  authentication: Authentication,
62
62
  type: Type,
63
63
  ): Promise<ActiveSubscription<Subscription>> {
64
- const subscription = buildSubscription(
65
- type,
66
- authentication.user.id,
67
- ) as Subscription
64
+ const subscription = buildSubscription(type, authentication.user.id)
68
65
 
69
- const [activeSubscription] = await helix<
70
- ActiveSubscription<Subscription>,
71
- never,
72
- never,
73
- SubscriptionRequest<Subscription>
74
- >(authentication, {
66
+ const [activeSubscription] = await helix(authentication, {
75
67
  method: "post",
76
68
  path: "/eventsub/subscriptions",
77
- // @ts-ignore
78
69
  body: {
79
70
  ...subscription,
80
71
  transport: {
@@ -91,7 +82,7 @@ export async function deleteSubscription(
91
82
  authentication: Authentication,
92
83
  id: string,
93
84
  ): Promise<void> {
94
- await helix<never, never, { id: string }, never>(authentication, {
85
+ await helix<never, never, { id: string }>(authentication, {
95
86
  method: "delete",
96
87
  path: "/eventsub/subscriptions",
97
88
  params: {
@@ -111,8 +102,7 @@ export async function getSubscriptions(
111
102
  type?: TwitchSubscriptionType
112
103
  user_id?: string
113
104
  after?: string
114
- },
115
- never
105
+ }
116
106
  >(authentication, {
117
107
  method: "get",
118
108
  path: "/eventsub/subscriptions",
@@ -1,4 +1,4 @@
1
- import { Authentication } from "../../authentication/index.js"
1
+ import { type Authentication } from "../../authentication/index.js"
2
2
  import { helix } from "../helix.js"
3
3
 
4
4
  export interface TwitchUser {
@@ -1,7 +1,7 @@
1
- import createQueue, { Queue } from "@overlaysymphony/core/libs/queue"
1
+ import createQueue, { type Queue } from "@overlaysymphony/core/libs/queue"
2
2
 
3
- import { TwitchNotificationMessage } from "../../eventsub/events/index.js"
4
- import { TwitchEventSub } from "../../eventsub/index.js"
3
+ import { type TwitchNotificationMessage } from "../../eventsub/events/index.js"
4
+ import { type TwitchEventSub } from "../../eventsub/index.js"
5
5
 
6
6
  export type Alert = TwitchNotificationMessage<
7
7
  | "channel.cheer"
@@ -1,4 +1,4 @@
1
- import { TwitchEventSub } from "../../eventsub/index.js"
1
+ import { type TwitchEventSub } from "../../eventsub/index.js"
2
2
 
3
3
  export interface Charity {
4
4
  name: string
@@ -1,4 +1,4 @@
1
- import { TwitchEventSub } from "../../eventsub/index.js"
1
+ import { type TwitchEventSub } from "../../eventsub/index.js"
2
2
 
3
3
  export interface Goal {
4
4
  type: string
@@ -1,4 +1,4 @@
1
- import { TwitchEventSub } from "../../eventsub/index.js"
1
+ import { type TwitchEventSub } from "../../eventsub/index.js"
2
2
 
3
3
  export interface HypeTrain {
4
4
  level: number
@@ -1,4 +1,4 @@
1
- import { TwitchEventSub } from "../../eventsub/index.js"
1
+ import { type TwitchEventSub } from "../../eventsub/index.js"
2
2
 
3
3
  export interface Poll {
4
4
  title: string
@@ -1,4 +1,4 @@
1
- import { TwitchEventSub } from "../../eventsub/index.js"
1
+ import { type TwitchEventSub } from "../../eventsub/index.js"
2
2
 
3
3
  export interface Prediction {
4
4
  title: string
@@ -1,4 +1,4 @@
1
- import { TwitchEventSub } from "../../eventsub/index.js"
1
+ import { type TwitchEventSub } from "../../eventsub/index.js"
2
2
 
3
3
  export interface Redemption {
4
4
  id: string
@@ -1,4 +1,4 @@
1
- import { TwitchEventSub } from "../../eventsub/index.js"
1
+ import { type TwitchEventSub } from "../../eventsub/index.js"
2
2
 
3
3
  export interface Status {
4
4
  online: boolean
@@ -1,16 +1,15 @@
1
1
  import {
2
- BareAuthentication,
3
- validateAuthentication,
2
+ clearCachedAuthentication,
3
+ getAuthentication,
4
+ popupAuthentication,
4
5
  } from "../authentication/index.js"
5
6
 
6
7
  declare global {
7
8
  interface Window {
8
- overlaysymphonyTwitchScopes: string[]
9
+ overlaysymphonyTwitchScopes?: string[]
9
10
  }
10
11
  }
11
12
 
12
- const localStorageKey = "overlaysymphony:service:twitch"
13
-
14
13
  export class TwitchAuthentication extends HTMLElement {
15
14
  static get observedAttributes(): string[] {
16
15
  return ["client-id", "scopes-key", "popup-url"]
@@ -31,7 +30,7 @@ export class TwitchAuthentication extends HTMLElement {
31
30
  `)
32
31
 
33
32
  stylesheet.insertRule(`
34
- button, .authenticating, .validating, .authenticated {
33
+ .authenticating, .validating, .authenticated, .unauthenticated {
35
34
  display: block;
36
35
  box-sizing: border-box;
37
36
  width: 100%;
@@ -47,13 +46,6 @@ export class TwitchAuthentication extends HTMLElement {
47
46
  }
48
47
  `)
49
48
 
50
- stylesheet.insertRule(`
51
- button {
52
- background: #9146FF;
53
- border: 2px outset buttonBorder;
54
- }
55
- `)
56
-
57
49
  stylesheet.insertRule(`
58
50
  .authenticating, .validating {
59
51
  background: #949494;
@@ -68,9 +60,17 @@ export class TwitchAuthentication extends HTMLElement {
68
60
  }
69
61
  `)
70
62
 
63
+ stylesheet.insertRule(`
64
+ .unauthenticated {
65
+ background: #9146FF;
66
+ border: 2px outset buttonBorder;
67
+ }
68
+ `)
69
+
71
70
  this.root = this.attachShadow({ mode: "open" })
72
71
  this.root.adoptedStyleSheets.push(stylesheet)
73
72
 
73
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
74
74
  this.render()
75
75
  }
76
76
 
@@ -82,7 +82,7 @@ export class TwitchAuthentication extends HTMLElement {
82
82
  const key = (this.getAttribute("scopes-key") ??
83
83
  "overlaysymphonyTwitchScopes") as "overlaysymphonyTwitchScopes"
84
84
 
85
- return window[key] || []
85
+ return window[key] ?? []
86
86
  }
87
87
 
88
88
  get popupUrl(): string {
@@ -90,53 +90,21 @@ export class TwitchAuthentication extends HTMLElement {
90
90
  const end = parent.lastIndexOf("/")
91
91
  const baseURL = window.location.href.slice(0, end)
92
92
 
93
- return this.getAttribute("popupUrl") ?? `${baseURL}/popup-twitch.html`
93
+ return this.getAttribute("popup-url") ?? `${baseURL}/popup-twitch.html`
94
94
  }
95
95
 
96
96
  async authenticate(): Promise<void> {
97
97
  this.renderAuthenticating()
98
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
99
+ await popupAuthentication(this.clientId, this.scopes, this.popupUrl)
112
100
 
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
- })
101
+ return this.render()
128
102
  }
129
103
 
130
- async validate(authentication: BareAuthentication): Promise<void> {
131
- this.renderValidating()
104
+ async unauthenticate(): Promise<void> {
105
+ clearCachedAuthentication()
132
106
 
133
- try {
134
- await validateAuthentication(authentication)
135
- this.renderAuthenticated()
136
- } catch (e) {
137
- clearCached()
138
- this.render()
139
- }
107
+ return this.render()
140
108
  }
141
109
 
142
110
  clear(): void {
@@ -169,28 +137,31 @@ export class TwitchAuthentication extends HTMLElement {
169
137
  renderAuthenticated(): void {
170
138
  this.clear()
171
139
 
172
- this.element = document.createElement("div")
140
+ this.element = document.createElement("button")
173
141
  this.element.classList.add("authenticated")
174
142
  this.element.innerText = "Authenticated"
175
-
176
143
  this.root.append(this.element)
144
+
145
+ this.element.addEventListener("click", () => this.unauthenticate())
177
146
  }
178
147
 
179
148
  renderUnauthenticated(): void {
180
149
  this.clear()
181
150
 
182
151
  this.element = document.createElement("button")
152
+ this.element.classList.add("unauthenticated")
183
153
  this.element.innerText = "Authenticate with Twitch"
184
154
  this.root.append(this.element)
185
155
 
186
156
  this.element.addEventListener("click", () => this.authenticate())
187
157
  }
188
158
 
189
- render(): void {
190
- const cached = getCached(this.scopes)
159
+ async render(): Promise<void> {
160
+ this.renderValidating()
191
161
 
192
- if (cached) {
193
- this.validate(cached)
162
+ const authentication = await getAuthentication(this.scopes)
163
+ if (authentication) {
164
+ this.renderAuthenticated()
194
165
  } else {
195
166
  this.renderUnauthenticated()
196
167
  }
@@ -201,30 +172,3 @@ window.customElements.define(
201
172
  "overlaysymfony-twitch-authentication",
202
173
  TwitchAuthentication,
203
174
  )
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
- }
package/src/ui/popup.ts CHANGED
@@ -1,14 +1,14 @@
1
1
  import querystring from "@overlaysymphony/core/libs/querystring"
2
2
 
3
3
  import {
4
- authenticateResult,
5
- initiateAuthentication,
4
+ type BareAuthentication,
5
+ validateAuthentication,
6
6
  } from "../authentication/index.js"
7
7
 
8
8
  const state = getState()
9
9
 
10
10
  if (state.step === "initial") {
11
- window.location.href = initiateAuthentication(
11
+ initiateAuthentication(
12
12
  state.clientId,
13
13
  `${window.location.origin}${window.location.pathname}`,
14
14
  state.scopes,
@@ -17,12 +17,13 @@ if (state.step === "initial") {
17
17
 
18
18
  if (state.step === "token") {
19
19
  const authentication = await authenticateResult(state.clientId, state)
20
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
20
21
  window.opener.postMessage({ type: "authentication", authentication }, "*")
21
22
  window.close()
22
23
  }
23
24
 
24
25
  if (state.step === "error") {
25
- alert(`${state.error}. ${state.description || ""}`)
26
+ alert(`${state.error}. ${state.description ?? ""}`)
26
27
  }
27
28
 
28
29
  function getState():
@@ -101,15 +102,53 @@ function getState():
101
102
 
102
103
  return {
103
104
  step: "error",
104
- error: validateString(query.error) || "Unknown Error",
105
+ error: validateString(query.error) ?? "Unknown Error",
105
106
  description: validateString(query.error_description),
106
107
  }
107
108
  }
108
109
 
109
- function validateString(input: string | unknown): string | undefined {
110
+ function validateString(input: unknown): string | undefined {
110
111
  if (typeof input !== "string" || !input) {
111
112
  return undefined
112
113
  }
113
114
 
114
115
  return input
115
116
  }
117
+
118
+ function initiateAuthentication(
119
+ clientId: string,
120
+ redirect: string,
121
+ scope: string[],
122
+ ): void {
123
+ window.location.href = `https://id.twitch.tv/oauth2/authorize?${querystring.stringify(
124
+ {
125
+ response_type: "token",
126
+ client_id: clientId,
127
+ redirect_uri: redirect,
128
+ scope: scope.join("+"),
129
+ state: clientId,
130
+ },
131
+ )}`
132
+ }
133
+
134
+ async function authenticateResult(
135
+ clientId: string,
136
+ result: {
137
+ token_type?: "bearer"
138
+ access_token?: string
139
+ scope?: string
140
+ },
141
+ ): Promise<BareAuthentication> {
142
+ if (!result.token_type || !result.access_token || !result.scope) {
143
+ throw new Error("Invalid result.")
144
+ }
145
+
146
+ const authentication = await validateAuthentication({
147
+ clientId,
148
+ tokenType: result.token_type,
149
+ accessToken: result.access_token,
150
+ scope: result.scope.split("+"),
151
+ })
152
+
153
+ return authentication
154
+ }