@overlaysymphony/twitch 0.1.1 → 0.2.1

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 (78) hide show
  1. package/README.md +1 -1
  2. package/package.json +15 -17
  3. package/src/authentication/authentication.ts +110 -54
  4. package/src/chat/chat.ts +9 -8
  5. package/src/chat/helpers.ts +36 -0
  6. package/src/chat/index.ts +1 -0
  7. package/src/chat/interfaces/events.ts +29 -29
  8. package/src/chat/interfaces/index.ts +1 -1
  9. package/src/chat/parser.ts +9 -20
  10. package/src/eventsub/events/channel.ad_break.begin.ts +1 -1
  11. package/src/eventsub/events/channel.ban.ts +1 -1
  12. package/src/eventsub/events/channel.channel_points_custom_reward.add.ts +2 -2
  13. package/src/eventsub/events/channel.channel_points_custom_reward.remove.ts +2 -2
  14. package/src/eventsub/events/channel.channel_points_custom_reward.update.ts +2 -2
  15. package/src/eventsub/events/channel.channel_points_custom_reward_redemption.add.ts +2 -2
  16. package/src/eventsub/events/channel.channel_points_custom_reward_redemption.update.ts +2 -2
  17. package/src/eventsub/events/channel.charity_campaign.donate.ts +1 -1
  18. package/src/eventsub/events/channel.charity_campaign.progress.ts +1 -1
  19. package/src/eventsub/events/channel.charity_campaign.start.ts +1 -1
  20. package/src/eventsub/events/channel.charity_campaign.stop.ts +1 -1
  21. package/src/eventsub/events/channel.chat.clear.ts +1 -1
  22. package/src/eventsub/events/channel.chat.clear_user_messages.ts +1 -1
  23. package/src/eventsub/events/channel.chat.message_delete.ts +1 -1
  24. package/src/eventsub/events/channel.chat.notification.ts +1 -1
  25. package/src/eventsub/events/channel.cheer.ts +1 -1
  26. package/src/eventsub/events/channel.follow.ts +1 -1
  27. package/src/eventsub/events/channel.goal.begin.ts +1 -1
  28. package/src/eventsub/events/channel.goal.end.ts +1 -1
  29. package/src/eventsub/events/channel.goal.progress.ts +1 -1
  30. package/src/eventsub/events/channel.guest_star_guest.update.ts +1 -1
  31. package/src/eventsub/events/channel.guest_star_session.begin.ts +1 -1
  32. package/src/eventsub/events/channel.guest_star_session.end.ts +1 -1
  33. package/src/eventsub/events/channel.guest_star_settings.update.ts +1 -1
  34. package/src/eventsub/events/channel.hype_train.begin.ts +2 -2
  35. package/src/eventsub/events/channel.hype_train.end.ts +2 -2
  36. package/src/eventsub/events/channel.hype_train.progress.ts +2 -2
  37. package/src/eventsub/events/channel.moderator.add.ts +1 -1
  38. package/src/eventsub/events/channel.moderator.remove.ts +1 -1
  39. package/src/eventsub/events/channel.poll.begin.ts +2 -2
  40. package/src/eventsub/events/channel.poll.end.ts +2 -2
  41. package/src/eventsub/events/channel.poll.progress.ts +2 -2
  42. package/src/eventsub/events/channel.prediction.begin.ts +2 -2
  43. package/src/eventsub/events/channel.prediction.end.ts +2 -2
  44. package/src/eventsub/events/channel.prediction.lock.ts +2 -2
  45. package/src/eventsub/events/channel.prediction.progress.ts +2 -2
  46. package/src/eventsub/events/channel.raid.ts +1 -1
  47. package/src/eventsub/events/channel.shield_mode.begin.ts +1 -1
  48. package/src/eventsub/events/channel.shield_mode.end.ts +1 -1
  49. package/src/eventsub/events/channel.shoutout.create.ts +1 -1
  50. package/src/eventsub/events/channel.shoutout.receive.ts +1 -1
  51. package/src/eventsub/events/channel.subscribe.ts +1 -1
  52. package/src/eventsub/events/channel.subscription.end.ts +1 -1
  53. package/src/eventsub/events/channel.subscription.gift.ts +1 -1
  54. package/src/eventsub/events/channel.subscription.message.ts +1 -1
  55. package/src/eventsub/events/channel.unban.ts +1 -1
  56. package/src/eventsub/events/channel.update.ts +1 -1
  57. package/src/eventsub/events/index.ts +51 -51
  58. package/src/eventsub/events/stream.offline.ts +1 -1
  59. package/src/eventsub/events/stream.online.ts +1 -1
  60. package/src/eventsub/events/user.update.ts +1 -1
  61. package/src/eventsub/events-helpers.ts +2 -2
  62. package/src/eventsub/eventsub.ts +9 -10
  63. package/src/eventsub/messages.ts +1 -1
  64. package/src/helix/channel-points/custom-rewards.ts +2 -3
  65. package/src/helix/helix.ts +1 -1
  66. package/src/helix/subscriptions/subscriptions.ts +9 -19
  67. package/src/helix/users/users.ts +1 -1
  68. package/src/helpers/alerts/alerts.ts +3 -3
  69. package/src/helpers/charity/charity.ts +1 -1
  70. package/src/helpers/goal/goal.ts +1 -1
  71. package/src/helpers/hype-train/hype-train.ts +1 -1
  72. package/src/helpers/poll/poll.ts +17 -3
  73. package/src/helpers/prediction/prediction.ts +1 -1
  74. package/src/helpers/redemption/redemption.ts +21 -2
  75. package/src/helpers/status/status.ts +1 -1
  76. package/src/ui/authentication.ts +29 -86
  77. package/src/ui/popup.ts +46 -7
  78. package/src/ui/vite-env.d.ts +1 -0
@@ -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,10 +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
 
74
- this.render()
73
+ void this.render()
75
74
  }
76
75
 
77
76
  get clientId(): string {
@@ -82,7 +81,7 @@ export class TwitchAuthentication extends HTMLElement {
82
81
  const key = (this.getAttribute("scopes-key") ??
83
82
  "overlaysymphonyTwitchScopes") as "overlaysymphonyTwitchScopes"
84
83
 
85
- return window[key] || []
84
+ return window[key] ?? []
86
85
  }
87
86
 
88
87
  get popupUrl(): string {
@@ -90,53 +89,21 @@ export class TwitchAuthentication extends HTMLElement {
90
89
  const end = parent.lastIndexOf("/")
91
90
  const baseURL = window.location.href.slice(0, end)
92
91
 
93
- return this.getAttribute("popupUrl") ?? `${baseURL}/popup-twitch.html`
92
+ return this.getAttribute("popup-url") ?? `${baseURL}/popup-twitch.html`
94
93
  }
95
94
 
96
95
  async authenticate(): Promise<void> {
97
96
  this.renderAuthenticating()
98
97
 
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
98
+ await popupAuthentication(this.clientId, this.scopes, this.popupUrl)
112
99
 
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
- })
100
+ return this.render()
128
101
  }
129
102
 
130
- async validate(authentication: BareAuthentication): Promise<void> {
131
- this.renderValidating()
103
+ async unauthenticate(): Promise<void> {
104
+ clearCachedAuthentication()
132
105
 
133
- try {
134
- await validateAuthentication(authentication)
135
- this.renderAuthenticated()
136
- } catch (e) {
137
- clearCached()
138
- this.render()
139
- }
106
+ return this.render()
140
107
  }
141
108
 
142
109
  clear(): void {
@@ -169,28 +136,31 @@ export class TwitchAuthentication extends HTMLElement {
169
136
  renderAuthenticated(): void {
170
137
  this.clear()
171
138
 
172
- this.element = document.createElement("div")
139
+ this.element = document.createElement("button")
173
140
  this.element.classList.add("authenticated")
174
141
  this.element.innerText = "Authenticated"
175
-
176
142
  this.root.append(this.element)
143
+
144
+ this.element.addEventListener("click", () => this.unauthenticate())
177
145
  }
178
146
 
179
147
  renderUnauthenticated(): void {
180
148
  this.clear()
181
149
 
182
150
  this.element = document.createElement("button")
151
+ this.element.classList.add("unauthenticated")
183
152
  this.element.innerText = "Authenticate with Twitch"
184
153
  this.root.append(this.element)
185
154
 
186
155
  this.element.addEventListener("click", () => this.authenticate())
187
156
  }
188
157
 
189
- render(): void {
190
- const cached = getCached(this.scopes)
158
+ async render(): Promise<void> {
159
+ this.renderValidating()
191
160
 
192
- if (cached) {
193
- this.validate(cached)
161
+ const authentication = await getAuthentication(this.scopes)
162
+ if (authentication) {
163
+ this.renderAuthenticated()
194
164
  } else {
195
165
  this.renderUnauthenticated()
196
166
  }
@@ -201,30 +171,3 @@ window.customElements.define(
201
171
  "overlaysymfony-twitch-authentication",
202
172
  TwitchAuthentication,
203
173
  )
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
- window.opener.postMessage({ type: "authentication", authentication }, "*")
20
+ const opener = window.opener as Window | undefined
21
+ 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
+ tokenType: result.token_type,
148
+ clientId,
149
+ accessToken: result.access_token,
150
+ scope: result.scope.split("+"),
151
+ })
152
+
153
+ return authentication
154
+ }
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />