@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/CHANGELOG.md +76 -0
- package/LICENSE +21 -0
- package/README.md +504 -99
- package/dist/index.cjs +3792 -0
- package/dist/index.d.cts +11315 -0
- package/dist/index.d.ts +11315 -0
- package/dist/index.js +3738 -0
- package/dist/node.cjs +57 -0
- package/dist/node.d.cts +36 -0
- package/dist/node.d.ts +36 -0
- package/dist/node.js +55 -0
- package/dist/tokens-DlOL0kjk.d.cts +97 -0
- package/dist/tokens-DlOL0kjk.d.ts +97 -0
- package/package.json +101 -20
- package/Token.json +0 -6
- package/twitchapi.js +0 -1381
package/README.md
CHANGED
|
@@ -1,99 +1,504 @@
|
|
|
1
|
-
<div align=center>
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
| --- | --- |
|
|
45
|
-
|
|
|
46
|
-
|
|
|
47
|
-
|
|
|
48
|
-
|
|
|
49
|
-
|
|
|
50
|
-
|
|
|
51
|
-
|
|
|
52
|
-
|
|
|
53
|
-
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
client
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
+
[](https://www.npmjs.com/package/@ix-xs/twitch.api)
|
|
10
|
+
[](https://www.npmjs.com/package/@ix-xs/twitch.api)
|
|
11
|
+
[](https://github.com/ix-xs/twitch.api/actions)
|
|
12
|
+
[](https://nodejs.org)
|
|
13
|
+
[](#)
|
|
14
|
+
[](./package.json)
|
|
15
|
+
[](./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)
|