@ix-xs/twitch.api 1.0.5 → 2.0.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.
package/README.md CHANGED
@@ -1,99 +1,504 @@
1
- <div align=center>
2
- <span><b><code>@ix-xs/twitch.api</code></b></span><br><br>
3
- </div>
4
-
5
- <b>@ix-xs/twitch.api</b> est une bibliothèque JavaScript simplifiant les interactions avec l'API de Twitch. Conçue pour automatiser la gestion des tokens d'authentification, elle permet aux développeurs d'accéder facilement aux données de Twitch sans se soucier des tâches fastidieuses liées à la validation ou au renouvellement des tokens.
6
-
7
- <h2>✨ Fonctionnalités principales</h2>
8
- <ul>
9
- <li><b>Gestion automatique des tokens</b> : Génération, validation et renouvellement des tokens se font en arrière-plan.</li>
10
- <li><b>Intégration fluide</b> : Initialisez simplement le client avec votre clientId et clientSecret, et laissez la bibliothèque gérer le reste.</li>
11
- <li><b>API complète</b> : Accédez à des fonctionnalités essentielles de l'API Twitch comme les streams, utilisateurs, clips, émotes et bien plus encore.</li>
12
- </ul>
13
-
14
- <h2>✨Installation</h2>
15
- <code>npm install @ix-xs/twitch.api</code>
16
-
17
- <h2>👀Utilisation</h2>
18
- <ul>
19
- 1. Créez un nouveau client en fournissant votre <code>client_id</code> et <code>client_secret</code> :
20
-
21
- ```js
22
- const twitchAPI = require("@ix-xs/twitch.api");
23
-
24
- const client = new twitchAPI({ clientId:"votre_id_client", clientSecret:"votre_secret_client" });
25
- ```
26
-
27
- 2. Initialisez le client avec la méthode initialize() pour démarrer la gestion automatique des tokens :
28
-
29
- ```js
30
- await client.initialize();
31
- ```
32
-
33
- <b>Note</b> : Cette fonction permet de mettre en place un système de validation périodique du token, effectué automatiquement une fois par heure. En raison de cette vérification régulière, l'utilisation de la console de développement devient indisponible pendant le processus. Pensez à utiliser un gestionnaire de processus comme PM2.
34
- </ul>
35
-
36
- <h2>📚 Documentation détaillée</h2>
37
- Consultez la documentation ci-dessous pour en savoir plus sur les méthodes disponibles et leur utilisation, ou contactez-moi sur Discord (ix_xs) si vous avez des questions ou des difficultés.
38
-
39
- <br>
40
- <h3>Liste des appels</h3>
41
- <br>
42
-
43
- Méthode | options | Description |
44
- | --- | --- | --- |
45
- | `initialize()` | | 👀 **Démarre le client**<br>💡 **Note** : Cette fonction est requise afin de démarrer la surveillance du token qui doit maintenant être valider toutes les heures |
46
- | `generateToken()` | | 👀 **Génère un nouveau token**<br>💡 **Note** : Le token se gère de façon automatique, cette fonction sert uniquement a son auto-gestion|
47
- | `isExpiredToken()` | | 👀 **Renvoi la validité du token actuel** (true ou false)<br>💡 **Note** : Le token se gère de façon automatique, cette fonction sert uniquement a son auto-gestion |
48
- | `refreshToken()` | | 👀 **Génère un nouveau token**<br>💡 **Note** : Le token se gère de façon automatique, cette fonction sert uniquement a son auto-gestion |
49
- | `validateToken()` | | 👀 **Valide le token**<br>💡 **Note** : Le token se gère de façon automatique, cette fonction sert uniquement a son auto-gestion |
50
- | `updateTokenFile()` | | 👀 **Met à jour le token**<br>💡 **Note** : Le token se gère de façon automatique, cette fonction sert uniquement a son auto-gestion |
51
- | `startPeriodicValidation()` | | 👀 **Démarre la surveillance pour la validation periodique du token (toutes les heures)**<br>💡 **Note** : Le token se gère de façon automatique, cette fonction sert uniquement a son auto-gestion |
52
- | `getStreams()` | {<br>`users_names?`:Array\<string\>;<br>`games_names?`:Array\<string\>;<br>`type?`:"all"\|"live";<br>`languages?`:Array\<string\>;<br>`first?`:number;<br>`before?`:string;<br>`after?`:string;<br>} | 👀 **Renvoi une liste des streams**<br>💡 **Note** : La liste est classée par ordre décroissant du nombre de téléspectateurs regardant le flux. Étant donné que les spectateurs vont et viennent pendant un flux, il est possible de trouver des flux en double ou manquants dans la liste au fur et à mesure que vous parcourez les résultats. |
53
- | `getUsers()` | `usernames`:Array\<string\>; | 👀 **Renvoi les informations d'un ou plusieurs utilisateur(s)** |
54
- | `getCheermotes()` | | 👀 **Renvoi une liste de Cheermotes que les utilisateurs peuvent utiliser pour encourager Bits dans la salle de discussion de n'importe quelle chaîne compatible Bits.**<br>💡 **Note** : Les cheermotes sont des émoticônes animées auxquelles les spectateurs peuvent attribuer des Bits. |
55
- | `getChatEmotes()` | `username?`:string; | 👀 **Obtient la liste des [émotes globales](https://www.twitch.tv/creatorcamp/fr-fr/paths/getting-started-on-twitch/emotes/) ou la liste des émotes personnalisées du diffuseur.**<br>💡 **Note** : Les émotes globales sont des émoticônes créées par Twitch que les utilisateurs peuvent utiliser dans n'importe quel chat Twitch. Les diffuseurs créent ces émotes personnalisées pour les utilisateurs qui s'abonnent ou suivent la chaîne ou encouragent Bits dans la fenêtre de discussion de la chaîne. [En savoir plus](https://dev.twitch.tv/docs/irc/emotes/).<br>💡 **Note** : À l'exception des émoticônes personnalisées, les utilisateurs peuvent utiliser des émoticônes personnalisées dans n'importe quel chat Twitch. |
56
- | `getChatBadges()` | `username?`:string; | 👀 **Obtient la liste des badges de discussion de Twitch, que les utilisateurs peuvent utiliser dans la salle de discussion de n'importe quelle chaîne, ou la liste des badges de discussion personnalisés du diffuseur.**<br>💡 **Note** : Pour plus d'informations sur les badges de chat, consultez le [Guide des badges de chat Twitch](https://help.twitch.tv/s/article/twitch-chat-badges-guide?langue=en_US).<br>💡 **Note** : La liste est vide si le diffuseur n'a pas créé de badges de discussion personnalisés. Pour plus d'informations sur les badges personnalisés, consultez [badges d'abonné](https://help.twitch.tv/s/article/subscriber-badge-guide?langue=en_US) et [badges Bits](https://help.twitch.tv/s/article/custom-bit-badges-guide?langue=en_US). |
57
- | `getChatSettings()` | `username`:string; | 👀 **Renvoi les paramètres de discussion du diffuseur** |
58
- | `getUsersChatColor()` | `usernames`:Array\<string>; | 👀 **Renvoi la couleur utilisée pour le nom de l'utilisateur dans le chat.** |
59
- | `getClips()` | `username`:string; | 👀 **Obtient un ou plusieurs clips vidéo capturés à partir de flux**<br>💡 **Note** : Pour plus d'informations sur les clips, consultez [Comment utiliser les clips](https://help.twitch.tv/s/article/how-to-use-clips?langage=en_US). |
60
- | `getTopGames()` | `params?`:<br>{<br>`first?`:number;<br>`after?`:string;<br>`before?`:string;<br>} | 👀 **Renvoi des informations sur toutes les diffusions sur Twitch** |
61
- | `getGames()` | `gameNames`:Array\<string\>; | 👀 **Renvoi des informations sur les catégories ou les jeux spécifiés.**<br>💡 **Note** : Vous pouvez obtenir jusqu'à 100 catégories ou jeux en spécifiant leur identifiant ou leur nom. Vous pouvez spécifier tous les identifiants, tous les noms ou une combinaison d'identifiants et de noms. Si vous spécifiez une combinaison d'ID et de noms, le nombre total d'ID et de noms ne doit pas dépasser 100. |
62
- | `getVideos()` | `username`:string; | 👀 **Obtient des informations sur une ou plusieurs vidéos publiées. Vous pouvez obtenir des vidéos par identifiant, par utilisateur ou par jeu/catégorie.**<br>💡 **Note** : Vous pouvez appliquer plusieurs filtres pour obtenir un sous-ensemble de vidéos. Les filtres sont appliqués sous forme d’opération ET à chaque vidéo. Par exemple, si la langue est définie sur « de » et que game_id est défini sur 21779, la réponse inclut uniquement les vidéos montrant des utilisateurs jouant à League of Legends en allemand. Les filtres s'appliquent uniquement si vous obtenez des vidéos par ID utilisateur ou ID de jeu. |
63
- ___
64
-
65
- <h2>💡 Exemples</h2>
66
-
67
- ```js
68
- const twitchAPI = require("@ix-xs/twitch.api");
69
- const client = new twitchAPI({ client_id:"votre_client_id", client_secret:"votre_client_secret" });
70
-
71
- client.getStreams().then(console.log); // Renvoi les 20 flux les plus actifs
72
-
73
- client.getStreams({
74
- users_names:["twitchdev"],
75
- }); // Renvoi les flux pour les connexions spécifiées. Si l’utilisateur n’est pas en direct, la réponse ne l’inclut pas.
76
-
77
- client.getUsers(["twitchdev"]).then(console.log); // Renvoi les informations sur l'utilisateur
78
-
79
- client.getCheermotes(); // Renvoi une liste des cheermotes
80
-
81
- client.getChatEmotes().then(console.log); // Renvoi toutes les émoticônes globales
82
-
83
- client.getChatEmotes("twitchdev").then(console.log); // Renvoi les émoticônes personnalisées créées par la chaîne utilisateur
84
-
85
- client.getChatBadges().then(console.log); // Renvoi la liste des badges de discussion globaux
86
- client.getChatBadges("twitchdev").then(console.log); // Renvoi la liste des badges de discussion personnalisés créés par la chaîne utilisateur
87
-
88
- client.getChatSettings("twitchdev").then(console.log); // Renvoi les paramètre de discussion du diffuseur
89
-
90
- client.getUsersChatColor(["twitchdev"]).then(console.log); // Renvoi la couleur utilisée pour le nom de l'utilisateur dans le chat
91
-
92
- client.getClips("twitchdev").then(console.log) // Renvoi les clips de l'utilisateur
93
-
94
- client.getTopGames().then(console.log); // Renvoi des informations sur toutes les diffusions sur Twitch
95
-
96
- client.getGames(["Fortnite"]).then(console.log); // Renvoi des informations sur les catégories ou les jeux spécifiés
97
-
98
- client.getVideos("twitchdev").then(console.log); // Renvoi des informations sur une ou plusieurs vidéos publiées.
99
- ```
1
+ <div align="center">
2
+
3
+ # @ix-xs/twitch.api
4
+
5
+ **The complete Twitch API client for JavaScript and TypeScript.**
6
+
7
+ _Every Helix endpoint. Every EventSub event. Fully typed. Zero dependencies._
8
+
9
+ [![npm version](https://img.shields.io/npm/v/@ix-xs/twitch.api.svg?color=cb3837&logo=npm)](https://www.npmjs.com/package/@ix-xs/twitch.api)
10
+ [![npm downloads](https://img.shields.io/npm/dm/@ix-xs/twitch.api.svg?color=cb3837&logo=npm)](https://www.npmjs.com/package/@ix-xs/twitch.api)
11
+ [![CI](https://img.shields.io/github/actions/workflow/status/ix-xs/twitch.api/ci.yml?branch=main&label=CI&logo=github)](https://github.com/ix-xs/twitch.api/actions)
12
+ [![node](https://img.shields.io/badge/node-%3E%3D22-brightgreen?logo=node.js&logoColor=white)](https://nodejs.org)
13
+ [![types](https://img.shields.io/badge/types-included-3178c6?logo=typescript&logoColor=white)](#)
14
+ [![dependencies](https://img.shields.io/badge/dependencies-0-brightgreen)](./package.json)
15
+ [![license](https://img.shields.io/badge/license-MIT-blue)](./LICENSE)
16
+
17
+ </div>
18
+
19
+ ```bash
20
+ npm install @ix-xs/twitch.api
21
+ ```
22
+
23
+ Requires **Node 22+**. Also runs on Bun, Deno, Cloudflare Workers and in the browser.
24
+
25
+ ---
26
+
27
+ ```ts
28
+ import { Twitch } from "@ix-xs/twitch.api";
29
+
30
+ const twitch = new Twitch({ clientId, clientSecret });
31
+
32
+ const user = await twitch.users.getByLogin("twitchdev");
33
+
34
+ for await (const stream of twitch.streams.get({ language: "fr" })) {
35
+ console.log(stream.user_name, stream.viewer_count);
36
+ }
37
+ ```
38
+
39
+ No token to fetch, no header to set, no cursor to carry around, no `429` to catch. You call a method, you get typed data.
40
+
41
+ ## What you get
42
+
43
+ | | |
44
+ | --- | --- |
45
+ | **The whole API** | All 149 Helix endpoints in 30 groups (`twitch.chat`, `twitch.moderation`, `twitch.polls`...) and all 83 EventSub subscription types. |
46
+ | **Types everywhere** | Parameters, responses, events, scopes and subscription names autocomplete. Events with variants narrow on their discriminator. |
47
+ | **Tokens handled** | App token on demand, user tokens refreshed before they expire and after a `401`, validated hourly as Twitch requires, persisted in the store you choose. |
48
+ | **The right token, picked for you** | Each endpoint knows which token it needs. Register your users once: the client uses the broadcaster's token for broadcaster calls, the moderator's for moderation calls. |
49
+ | **Scopes checked up front** | A missing scope fails before the request leaves, with the name of the scope to ask for. |
50
+ | **Pagination that disappears** | `await` for one page, `for await` for every item, `.all()`, `.take(n)`, `.total()`. |
51
+ | **Rate limits respected** | The client follows Twitch's rate limit headers and waits instead of failing. Lists of more than 100 IDs are split and fetched in parallel. |
52
+ | **EventSub, three ways** | WebSocket, webhooks and conduits behind the same typed `on()`. Reconnects, resubscribes, signature checks and duplicate filtering are built in. |
53
+ | **Small and portable** | No dependency. About 23 kB gzipped. ESM and CommonJS. Only web standard APIs in the main entry. |
54
+
55
+ ## Contents
56
+
57
+ - [Getting credentials](#getting-credentials)
58
+ - [Calling the API](#calling-the-api)
59
+ - [Acting as a user](#acting-as-a-user)
60
+ - [Getting user tokens](#getting-user-tokens)
61
+ - [Keeping tokens between restarts](#keeping-tokens-between-restarts)
62
+ - [Pagination](#pagination)
63
+ - [Events (EventSub)](#events-eventsub)
64
+ - [Errors](#errors)
65
+ - [Rate limits, retries and timeouts](#rate-limits-retries-and-timeouts)
66
+ - [Observability](#observability)
67
+ - [Endpoints newer than the library](#endpoints-newer-than-the-library)
68
+ - [API groups](#api-groups)
69
+ - [Coming from version 1](#coming-from-version-1)
70
+
71
+ ## Getting credentials
72
+
73
+ Create an application in the [Twitch developer console](https://dev.twitch.tv/console/apps). You get a **client ID** and can generate a **client secret**.
74
+
75
+ - With both, the client can call every public endpoint right away (users, streams, games, clips, search...).
76
+ - Endpoints that act for someone (send a chat message, ban a user, create a poll) need a **user token**, granted by that user to your application. See [Getting user tokens](#getting-user-tokens).
77
+
78
+ Keep the client secret on a server. In a browser, a desktop app or a CLI, leave it out and use the [device code flow](#device-code-flow-clis-desktop-apps-no-server).
79
+
80
+ ## Calling the API
81
+
82
+ Every endpoint is a method on a group. Parameters are one object, with the names Twitch uses.
83
+
84
+ ```ts
85
+ const [channel] = await twitch.channels.get({ broadcaster_id: user.id });
86
+ const games = await twitch.games.get({ name: ["Fortnite", "Minecraft"] });
87
+ const clips = await twitch.clips.get({ broadcaster_id: user.id, first: 5 });
88
+ const results = await twitch.search.channels({ query: "speedrun", live_only: true }).take(20);
89
+ const followers = await twitch.channels.followers({ broadcaster_id: user.id }).total();
90
+ ```
91
+
92
+ Lookups by ID take any number of IDs. Twitch caps most of them at 100 per request: the client splits the list, sends the requests in parallel and merges the answers.
93
+
94
+ ```ts
95
+ const users = await twitch.users.get({ id: fiveHundredIds }); // 5 requests, one array
96
+ ```
97
+
98
+ Single lookups made at the same moment are merged the other way around:
99
+
100
+ ```ts
101
+ // One HTTP request, not three.
102
+ const [a, b, c] = await Promise.all([
103
+ twitch.users.getById("141981764"),
104
+ twitch.users.getById("12826"),
105
+ twitch.users.getById("57781936"),
106
+ ]);
107
+ ```
108
+
109
+ ## Acting as a user
110
+
111
+ Register a user token once. From then on, endpoints that need it find it.
112
+
113
+ ```ts
114
+ const bot = await twitch.auth.addUser({ accessToken, refreshToken });
115
+
116
+ await twitch.chat.send({ broadcaster_id: channelId, message: "Hello chat" });
117
+ await twitch.moderation.ban({ broadcaster_id: channelId, user_id: spammerId, duration: 600, reason: "Spam" });
118
+ await twitch.polls.create({ title: "Next game?", choices: ["Hades", "Celeste"], duration: 120 });
119
+ ```
120
+
121
+ Notice what is missing: `sender_id`, `moderator_id` and `broadcaster_id` default to the user making the call. You can always pass them explicitly.
122
+
123
+ With several users registered, the client reads the request to decide whose token to use: a call with `broadcaster_id: "123"` on a broadcaster endpoint uses the token of user `123`, a moderation call uses the token of its `moderator_id`. When that is not enough, say who is acting:
124
+
125
+ ```ts
126
+ const asBot = twitch.as(bot.userId);
127
+ await asBot.chat.send({ broadcaster_id: channelId, message: "Hello" });
128
+
129
+ // or for a single call
130
+ await twitch.chat.announce({ broadcaster_id: channelId, message: "Stream starts in 5" }, { as: bot.userId });
131
+ ```
132
+
133
+ On an endpoint that accepts both kinds of token, `{ as: "app" }` asks for the app token even when the acting user is registered.
134
+
135
+ If the token lacks a scope, the call fails before any request is sent:
136
+
137
+ ```
138
+ TwitchScopeError: The token of user 123 is missing the "moderator:manage:banned_users" scope required by POST /moderation/bans.
139
+ ```
140
+
141
+ ## Getting user tokens
142
+
143
+ ### Authorization code flow (web servers)
144
+
145
+ ```ts
146
+ // 1. Send the user to Twitch. Scope names autocomplete.
147
+ const url = twitch.auth.authorizeUrl({
148
+ redirectUri: "https://example.com/auth/callback",
149
+ scopes: ["user:read:chat", "user:write:chat", "moderator:manage:banned_users"],
150
+ state, // a random value you check in step 2
151
+ });
152
+
153
+ // 2. Twitch redirects back with ?code=...&state=...
154
+ const user = await twitch.auth.exchangeCode(code, "https://example.com/auth/callback");
155
+ console.log(user.userId, user.login, user.scopes);
156
+ ```
157
+
158
+ The user is registered and their token is refreshed for as long as the refresh token lives.
159
+
160
+ ### Device code flow (CLIs, desktop apps, no server)
161
+
162
+ Works without a client secret.
163
+
164
+ ```ts
165
+ const twitch = new Twitch({ clientId });
166
+
167
+ const flow = await twitch.auth.deviceFlow({ scopes: ["user:read:chat", "user:write:chat"] });
168
+ console.log(`Open ${flow.verificationUri} and confirm the code ${flow.userCode}`);
169
+ const user = await flow.wait();
170
+ ```
171
+
172
+ ### Other helpers
173
+
174
+ ```ts
175
+ await twitch.auth.validate(accessToken); // who owns it, which scopes, when it expires
176
+ await twitch.auth.refresh(userId); // renew now
177
+ await twitch.auth.revoke(accessToken); // invalidate
178
+ await twitch.auth.removeUser(userId, { revoke: true });
179
+ ```
180
+
181
+ ## Keeping tokens between restarts
182
+
183
+ By default tokens live in memory. Give the client a store and it restores users on first use and saves every refreshed token.
184
+
185
+ ```ts
186
+ import { Twitch } from "@ix-xs/twitch.api";
187
+ import { FileTokenStore } from "@ix-xs/twitch.api/node";
188
+
189
+ const twitch = new Twitch({
190
+ clientId,
191
+ clientSecret,
192
+ tokens: new FileTokenStore("./tokens.json"), // add this file to .gitignore
193
+ });
194
+ ```
195
+
196
+ A store is four methods, so a database or a KV namespace takes a few lines:
197
+
198
+ ```ts
199
+ import type { TokenStore } from "@ix-xs/twitch.api";
200
+
201
+ const store: TokenStore = {
202
+ get: (userId) => db.tokens.find(userId),
203
+ set: (userId, token) => db.tokens.upsert(userId, token),
204
+ delete: (userId) => db.tokens.remove(userId),
205
+ list: () => db.tokens.ids(),
206
+ };
207
+ ```
208
+
209
+ Prefer to handle it yourself? Listen instead:
210
+
211
+ ```ts
212
+ twitch.on("tokenRefresh", (token) => save(token));
213
+ twitch.on("tokenInvalid", (userId) => askToLogInAgain(userId));
214
+ ```
215
+
216
+ ## Pagination
217
+
218
+ Every list endpoint returns a paginator. Nothing is fetched until you use it.
219
+
220
+ ```ts
221
+ // One page
222
+ const page = await twitch.streams.get({ game_id: "509658", first: 20 });
223
+ page.data; // the items
224
+ page.cursor; // cursor of the next page
225
+ await page.next();
226
+
227
+ // Every item, fetched page after page at the largest page size
228
+ for await (const follower of twitch.channels.followers({ broadcaster_id })) {
229
+ console.log(follower.user_name);
230
+ }
231
+
232
+ // Into an array
233
+ const all = await twitch.moderation.bans().all();
234
+ const top = await twitch.streams.get().take(250);
235
+ const first = await twitch.clips.get({ broadcaster_id }).first();
236
+
237
+ // Totals, with the cheapest possible request
238
+ const subscribers = await twitch.subscriptions.get().total();
239
+ ```
240
+
241
+ ## Events (EventSub)
242
+
243
+ The three transports share one interface. The subscription name autocompletes, and the condition and the event are typed from it.
244
+
245
+ ### WebSocket
246
+
247
+ Nothing to host. Uses a user token.
248
+
249
+ ```ts
250
+ const events = twitch.eventsub.websocket();
251
+
252
+ events.on("channel.chat.message", { broadcaster_user_id: channelId, user_id: bot.userId }, async (event) => {
253
+ if (event.message.text === "!ping") {
254
+ await twitch.chat.send({
255
+ broadcaster_id: event.broadcaster_user_id,
256
+ message: "pong",
257
+ reply_parent_message_id: event.message_id,
258
+ });
259
+ }
260
+ });
261
+
262
+ events.on("channel.follow", { broadcaster_user_id: channelId, moderator_user_id: bot.userId }, (event) => {
263
+ console.log(`${event.user_name} followed`);
264
+ });
265
+
266
+ events.onError(console.error);
267
+ ```
268
+
269
+ The listener connects when you add the first handler. It answers Twitch's reconnect requests without dropping an event, detects dead connections from the keepalive, reconnects with backoff and subscribes again. Past 300 subscriptions it opens another connection, up to the 3 Twitch allows.
270
+
271
+ ### Webhooks
272
+
273
+ For servers reachable from the internet. Uses the app token, so no user has to be online, and subscriptions survive restarts.
274
+
275
+ ```ts
276
+ const hooks = twitch.eventsub.webhook({
277
+ callback: "https://example.com/twitch/eventsub",
278
+ secret: process.env.EVENTSUB_SECRET,
279
+ });
280
+
281
+ hooks.on("stream.online", { broadcaster_user_id: "141981764" }, (event) => {
282
+ console.log(`${event.broadcaster_user_name} is live`);
283
+ });
284
+
285
+ // Bun, Deno, Hono, Next.js route handlers
286
+ export default { fetch: (request: Request) => hooks.handle(request) };
287
+
288
+ // Node.js
289
+ import { createServer } from "node:http";
290
+ createServer(hooks.nodeHandler()).listen(3000);
291
+
292
+ // Express: mount it before express.json()
293
+ app.post("/twitch/eventsub", hooks.nodeHandler());
294
+ ```
295
+
296
+ Every delivery is checked: HMAC signature (constant time, Web Crypto), age of the message, and duplicates. The verification challenge is answered for you.
297
+
298
+ On Cloudflare Workers and other platforms that stop work once the response is sent, two things change. Create the subscriptions from a deploy script with `twitch.eventsub.subscribe()` and build the listener with `subscribe: false`, because a Worker cannot make requests while its module loads. And hand over `waitUntil` so that asynchronous handlers are allowed to finish:
299
+
300
+ ```ts
301
+ const hooks = twitch.eventsub.webhook({ callback, secret, subscribe: false });
302
+ hooks.on("stream.online", { broadcaster_user_id: "141981764" }, handler);
303
+
304
+ export default {
305
+ fetch: (request, env, ctx) => hooks.handle(request, { waitUntil: (work) => ctx.waitUntil(work) }),
306
+ };
307
+ ```
308
+
309
+ ### Conduits
310
+
311
+ One WebSocket per process, subscriptions created with the app token: follow thousands of channels without a connection per user. Run one process per shard to scale out.
312
+
313
+ ```ts
314
+ const events = twitch.eventsub.conduit(); // finds your conduit or creates one
315
+
316
+ events.on("channel.chat.message", { broadcaster_user_id: channelId, user_id: botId }, (event) => {
317
+ console.log(event.chatter_user_name, event.message.text);
318
+ });
319
+ ```
320
+
321
+ ### Typed variants
322
+
323
+ Events that come in several shapes narrow on their discriminator:
324
+
325
+ ```ts
326
+ events.on("channel.chat.notification", { broadcaster_user_id: channelId, user_id: bot.userId }, (event) => {
327
+ if (event.notice_type === "resub") {
328
+ console.log(event.resub.cumulative_months); // typed, not null
329
+ } else if (event.notice_type === "raid") {
330
+ console.log(event.raid.viewer_count);
331
+ }
332
+ });
333
+
334
+ events.on("channel.moderate", { broadcaster_user_id: channelId, moderator_user_id: bot.userId }, (event) => {
335
+ if (event.action === "timeout") console.log(event.timeout.user_name, event.timeout.expires_at);
336
+ });
337
+ ```
338
+
339
+ A type that exists in several versions is available as `"channel.moderate"` (newest) and `"channel.moderate@1"`. A type Twitch released after your version of the library works the same way: `events.on("some.new.type@1", condition, handler)`.
340
+
341
+ ### Managing subscriptions
342
+
343
+ ```ts
344
+ const handle = events.on("stream.online", { broadcaster_user_id: channelId }, handler);
345
+ await handle.ready; // Twitch accepted the subscription
346
+ await handle.stop(); // remove it
347
+
348
+ events.onRevocation((subscription) => console.log(subscription.type, subscription.status));
349
+
350
+ const page = await twitch.eventsub.subscriptions({ status: "enabled" });
351
+ console.log(page.raw.total_cost, "/", page.raw.max_total_cost);
352
+ ```
353
+
354
+ ## Errors
355
+
356
+ Everything the library throws extends `TwitchError`. A call you abort rejects with the reason of your signal.
357
+
358
+ | Class | When |
359
+ | --- | --- |
360
+ | `TwitchApiError` | Twitch answered with an error. Carries `status`, `error`, `message`, `method`, `url`, `body`, `headers`. |
361
+ | `TwitchRateLimitError` | A `429` that was not retried. Carries `resetAt` and `retryAfter`. |
362
+ | `TwitchAuthError` | No usable token: no user registered, refresh token rejected, missing client secret. |
363
+ | `TwitchScopeError` | The token lacks a scope. Carries `required` and `granted`. Thrown before the request. |
364
+ | `TwitchTimeoutError` | The request took longer than `timeout`. |
365
+ | `TwitchNetworkError` | The request never reached Twitch, or the connection broke. |
366
+ | `TwitchEventSubError` | A subscription was refused or a connection could not be established. |
367
+
368
+ ```ts
369
+ import { TwitchApiError, TwitchScopeError } from "@ix-xs/twitch.api";
370
+
371
+ try {
372
+ await twitch.moderation.ban({ broadcaster_id, user_id });
373
+ } catch (error) {
374
+ if (error instanceof TwitchScopeError) console.log("Ask the user for", error.required);
375
+ else if (error instanceof TwitchApiError && error.status === 400) console.log(error.message);
376
+ else throw error;
377
+ }
378
+ ```
379
+
380
+ Lookups that can legitimately find nothing return `null` instead of throwing: `users.getById`, `subscriptions.check`, `teams.get`, `charity.campaign`...
381
+
382
+ ## Rate limits, retries and timeouts
383
+
384
+ ```ts
385
+ const twitch = new Twitch({
386
+ clientId,
387
+ clientSecret,
388
+ timeout: 15_000, // per attempt, default 15 s
389
+ retries: 3, // default 3
390
+ rateLimit: "wait", // or "throw"
391
+ });
392
+ ```
393
+
394
+ - The client tracks each bucket (the app token and every user have their own) from the `Ratelimit-*` headers and holds requests back when a bucket is empty.
395
+ - A `429` is retried when the bucket refills. Network failures and `5xx` are retried with backoff for `GET`, `PUT` and `DELETE`. `POST` and `PATCH` are never replayed after a failure Twitch may have processed.
396
+ - A `401` is checked first: if the token really is dead it is renewed and the call retried once, and if the token is fine (Twitch also answers `401` when the token is not allowed to do something) the error is raised as is.
397
+ - Every method takes request options as its last argument: `{ as, token, signal, timeout, retries }`.
398
+
399
+ ```ts
400
+ const controller = new AbortController();
401
+ const streams = await twitch.streams.get({}, { signal: controller.signal, timeout: 5000 });
402
+ ```
403
+
404
+ ## Observability
405
+
406
+ ```ts
407
+ twitch.on("request", ({ method, url, attempt, as }) => {});
408
+ twitch.on("response", ({ method, url, status, duration, remaining }) => {});
409
+ twitch.on("rateLimit", ({ bucket, waitMs }) => {});
410
+
411
+ twitch.rateLimitOf(); // { limit, remaining, resetAt } of the app token
412
+ twitch.rateLimitOf(userId); // of a user
413
+ ```
414
+
415
+ ## Endpoints newer than the library
416
+
417
+ Twitch ships endpoints regularly. Until the library catches up, `request` and `paginate` give you the same pipeline (token choice, rate limit, retries, typed errors) for any path:
418
+
419
+ ```ts
420
+ const body = await twitch.request<{ data: Thing[] }>({
421
+ method: "POST",
422
+ path: "some/new/endpoint",
423
+ query: { broadcaster_id },
424
+ body: { enabled: true },
425
+ auth: "user",
426
+ scopes: ["channel:manage:broadcast"],
427
+ context: "broadcaster_id",
428
+ });
429
+
430
+ const things = await twitch.paginate<Thing>({ path: "some/new/list" }).all();
431
+ ```
432
+
433
+ Custom `fetch`, base URLs and WebSocket class are options too, which makes the [Twitch CLI](https://dev.twitch.tv/docs/cli/) mock server a drop-in backend for tests:
434
+
435
+ ```ts
436
+ const twitch = new Twitch({ clientId, clientSecret, baseUrl: "http://localhost:8080/mock", authUrl: "http://localhost:8080/auth" });
437
+ ```
438
+
439
+ ## API groups
440
+
441
+ | Group | Covers |
442
+ | --- | --- |
443
+ | `twitch.ads` | Commercials, ad schedule, snooze |
444
+ | `twitch.analytics` | Extension and game reports |
445
+ | `twitch.bits` | Leaderboard, Cheermotes, custom Power-ups, extension transactions |
446
+ | `twitch.channels` | Channel information, editors, follows, followers |
447
+ | `twitch.channelPoints` | Custom rewards and redemptions |
448
+ | `twitch.charity` | Campaign and donations |
449
+ | `twitch.chat` | Messages, announcements, shoutouts, chatters, emotes, badges, settings, colors, pins, shared chat |
450
+ | `twitch.clips` | Create, look up, download |
451
+ | `twitch.conduits` | Conduits and shards |
452
+ | `twitch.contentLabels` | Content classification labels |
453
+ | `twitch.entitlements` | Drops entitlements |
454
+ | `twitch.eventsub` | Subscriptions, and the WebSocket, webhook and conduit listeners |
455
+ | `twitch.extensions` | Configuration, secrets, PubSub, chat, Bits products |
456
+ | `twitch.games` | Games and top games |
457
+ | `twitch.goals` | Creator goals |
458
+ | `twitch.guestStar` | Guest Star sessions, invites and slots (beta) |
459
+ | `twitch.hypeTrain` | Hype Train status |
460
+ | `twitch.moderation` | Bans, timeouts, warnings, AutoMod, blocked terms, moderators, VIPs, Shield Mode, unban requests, suspicious users |
461
+ | `twitch.polls` | Polls |
462
+ | `twitch.predictions` | Predictions |
463
+ | `twitch.raids` | Start and cancel raids |
464
+ | `twitch.schedule` | Stream schedule and iCalendar |
465
+ | `twitch.search` | Categories and channels |
466
+ | `twitch.streams` | Live streams, followed streams, stream key, markers |
467
+ | `twitch.subscriptions` | Subscribers and subscription checks |
468
+ | `twitch.teams` | Teams |
469
+ | `twitch.users` | Users, block list, extensions, authorizations |
470
+ | `twitch.videos` | Videos |
471
+ | `twitch.whispers` | Whispers |
472
+ | `twitch.auth` | Tokens and OAuth flows |
473
+
474
+ Every method has an example and a link to the official reference in its documentation: hover it in your editor.
475
+
476
+ ## Coming from version 1
477
+
478
+ Version 2 is a rewrite. The main differences:
479
+
480
+ - The client is a named export and takes `clientId` / `clientSecret`: `new Twitch({ clientId, clientSecret })`.
481
+ - No `initialize()`. Tokens are fetched when needed, and nothing is written to `node_modules` anymore.
482
+ - Methods throw typed errors instead of returning `{ ok, result }`.
483
+ - Methods take IDs like the Twitch API does. Resolve a login with `twitch.users.getByLogin(login)`.
484
+
485
+ | Version 1 | Version 2 |
486
+ | --- | --- |
487
+ | `client.getUsers(["twitchdev"])` | `twitch.users.get({ login: ["twitchdev"] })` |
488
+ | `client.getStreams({ users_names })` | `twitch.streams.get({ user_login })` |
489
+ | `client.getStreams({ games_names })` | `twitch.games.get({ name })` then `twitch.streams.get({ game_id })` |
490
+ | `client.getCheermotes()` | `twitch.bits.cheermotes()` |
491
+ | `client.getChatEmotes()` | `twitch.chat.globalEmotes()` |
492
+ | `client.getChatEmotes(login)` | `twitch.chat.channelEmotes({ broadcaster_id })` |
493
+ | `client.getChatBadges()` | `twitch.chat.globalBadges()` |
494
+ | `client.getChatBadges(login)` | `twitch.chat.channelBadges({ broadcaster_id })` |
495
+ | `client.getChatSettings(login)` | `twitch.chat.settings({ broadcaster_id })` |
496
+ | `client.getUsersChatColor(logins)` | `twitch.chat.colors({ user_id })` |
497
+ | `client.getClips(login)` | `twitch.clips.get({ broadcaster_id })` |
498
+ | `client.getTopGames()` | `twitch.games.top()` |
499
+ | `client.getGames(names)` | `twitch.games.get({ name })` |
500
+ | `client.getVideos(login)` | `twitch.videos.get({ user_id })` |
501
+
502
+ ## License
503
+
504
+ [MIT](./LICENSE)