twitch-api-kit 1.0.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.
package/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Code Shadow
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,252 @@
1
+ ```md
2
+ ![npm version](https://img.shields.io/npm/v/twitch-api-kit)
3
+ ![downloads](https://img.shields.io/npm/dw/twitch-api-kit)
4
+ ![node](https://img.shields.io/node/v/twitch-api-kit)
5
+ ![license](https://img.shields.io/github/license/CodeShadow17/twitch-api-kit)
6
+ ![stars](https://img.shields.io/github/stars/CodeShadow17/twitch-api-kit)
7
+ ![issues](https://img.shields.io/github/issues/CodeShadow17/twitch-api-kit)
8
+
9
+ ### Nodejs Classes for Twitch Helix and EventSub
10
+
11
+ A small, focused utility library that exposes two helpers: **Helix** for Twitch Helix API calls and **Subscribe** for creating EventSub websocket subscriptions. The classes handle API requests and subscription creation only. **WebSocket connection management and event handling must be implemented by the consumer**
12
+
13
+ ---
14
+
15
+ ### Installation
16
+
17
+ **Install via npm**
18
+
19
+ ```bash
20
+ npm install twitch-api-kit
21
+ ```
22
+
23
+ ---
24
+
25
+ ### Compatibility
26
+
27
+ - **Node 18+** is recommended. This library relies on native `fetch` and `AbortController`.
28
+ - If you use **Node 16 or older**, install a fetch polyfill:
29
+
30
+ ```bash
31
+ npm install node-fetch@2
32
+ ```
33
+
34
+ ---
35
+
36
+ ### Quick Start
37
+
38
+ **Import and instantiate**
39
+
40
+ ```js
41
+ const { Helix, Subscribe } = require("twitch-api-kit");
42
+
43
+ // Helix
44
+ const helix = new Helix({
45
+ client_token: "YOUR_OAUTH_TOKEN",
46
+ client_id: "YOUR_CLIENT_ID",
47
+ channelName: "your_channel"
48
+ });
49
+ await helix.init();
50
+
51
+ // Subscribe
52
+ const sub = new Subscribe({
53
+ clientId: "YOUR_CLIENT_ID",
54
+ token: "YOUR_OAUTH_TOKEN",
55
+ broadcasterId: "BROADCASTER_USER_ID",
56
+ sessionId: "YOUR_WEBSOCKET_SESSION_ID"
57
+ });
58
+ await sub.toFollows();
59
+ ```
60
+
61
+ **Important**
62
+ The library **does not** open or manage WebSocket connections. Use your own WebSocket client (for example `ws`) to create a session and forward the `session_id` to `Subscribe`. Handle incoming EventSub messages and dispatch them to your app.
63
+
64
+ ---
65
+
66
+ ### API Reference
67
+
68
+ #### Helix class
69
+
70
+ **Constructor options**
71
+ - **client_token** `string` — OAuth token with required scopes
72
+ - **client_id** `string` — Twitch app client id
73
+ - **channelName** `string` — broadcaster login name
74
+
75
+ **Key methods**
76
+ - `init()` → `Promise<void>`
77
+ - `viewerCount()` → `Promise<number>`
78
+ - `getUserID(username)` → `Promise<string | null>`
79
+ - `followCount()` → `Promise<number>`
80
+ - `lastFollow()` → `Promise<string | null>`
81
+ - `createClip(title?, duration?)` → `Promise<object | null>`
82
+ - `createCustomReward(options)` → `Promise<object | null>`
83
+ - `updateRedemptionStatus(options)` → `Promise<object>`
84
+ - `cheerLeaderboard()` → `Promise<Array>`
85
+
86
+ #### Subscribe class
87
+
88
+ **Constructor options**
89
+ - **clientId** `string`
90
+ - **token** `string`
91
+ - **broadcasterId** `string | null`
92
+ - **sessionId** `string | null`
93
+
94
+ **Key methods**
95
+ - `toFollows()` → `Promise<boolean>`
96
+ - `toRedemptions()` → `Promise<boolean>`
97
+ - `toStreamOnline()` → `Promise<boolean>`
98
+ - `toStreamOffline()` → `Promise<boolean>`
99
+ - `toChannelUpdate()` → `Promise<boolean>`
100
+ - `toAll()` → `Promise<object>`
101
+
102
+ ---
103
+
104
+ ### Example: Using the Helix class
105
+
106
+ ```js
107
+ const { Helix } = require("twitch-api-kit");
108
+
109
+ (async () => {
110
+ // Create the Helix client
111
+ const helix = new Helix({
112
+ client_token: process.env.TWITCH_OAUTH_TOKEN,
113
+ client_id: process.env.TWITCH_CLIENT_ID,
114
+ channelName: "your_channel_name"
115
+ });
116
+
117
+ // Initialize (fetches broadcaster_id and sets helix.ready = true)
118
+ await helix.init();
119
+ console.log("Helix is ready. Broadcaster ID:", helix.broadcaster_id);
120
+
121
+ // Get viewer count
122
+ const viewers = await helix.viewerCount();
123
+ console.log("Current viewers:", viewers);
124
+
125
+ // Get total followers
126
+ const followers = await helix.followCount();
127
+ console.log("Total followers:", followers);
128
+
129
+ // Get last follower
130
+ const lastFollower = await helix.lastFollow();
131
+ console.log("Latest follower:", lastFollower);
132
+
133
+ // Get user ID of someone
134
+ const userId = await helix.getUserID("some_username");
135
+ console.log("User ID:", userId);
136
+
137
+ // Create a clip
138
+ const clip = await helix.createClip("Awesome moment!", 20);
139
+ console.log("Clip created:", clip);
140
+
141
+ // Get channel point rewards
142
+ const rewards = await helix.getChannelRewards();
143
+ console.log("Rewards:", rewards);
144
+
145
+ // Example: send a shoutout (requires moderator permissions)
146
+ // Replace moderatorId with your broadcaster ID or a moderator ID
147
+ const shoutoutSuccess = await helix.sendShoutout(userId, helix.broadcaster_id);
148
+ console.log("Shoutout sent:", shoutoutSuccess);
149
+ })();
150
+
151
+ ```
152
+
153
+ ### Example: Using the Subscribe Class
154
+
155
+ ```js
156
+ const WebSocket = require("ws");
157
+ const { Subscribe } = require("twitch-api-kit");
158
+
159
+ const sub = new Subscribe({
160
+ clientId: "CLIENT_ID",
161
+ token: "OAUTH_TOKEN",
162
+ broadcasterId: "BROADCASTER_USER_ID",
163
+ sessionId: null // will be set after welcome message
164
+ });
165
+
166
+ const ws = new WebSocket("wss://eventsub.wss.twitch.tv/ws");
167
+
168
+ ws.on("open", () => {
169
+ console.log("Connected to Twitch EventSub WebSocket");
170
+ });
171
+
172
+ ws.on("message", async (raw) => {
173
+ const data = JSON.parse(raw);
174
+
175
+ if (data.metadata?.message_type === "session_welcome") {
176
+ const sessionId = data.payload.session.id;
177
+ sub.session_id = sessionId;
178
+
179
+ console.log("Session ID:", sessionId);
180
+
181
+ // Example: subscribe to follow events
182
+ await sub.toFollows();
183
+ }
184
+
185
+ // Handle other EventSub messages here...
186
+ });
187
+ ```
188
+
189
+ ---
190
+
191
+ ### TypeScript and Editor Tips
192
+
193
+ **Bundled typings**
194
+ - The package ships `index.d.ts`.
195
+ - `package.json` includes `"types": "index.d.ts"` so TypeScript and VS Code pick up signatures automatically.
196
+
197
+ **When you add methods**
198
+ - Implement the method in JS
199
+ - Add JSDoc above the method
200
+ - Update `index.d.ts`
201
+ - Restart the TypeScript server in VS Code
202
+
203
+ **CommonJS export shape**
204
+ - Keep `module.exports = { Helix, Subscribe }` in sync with `index.d.ts`.
205
+
206
+ ---
207
+
208
+ ### Contributing and License
209
+
210
+ **Contributing**
211
+ - Add new classes under `classes/`
212
+ - Re-export from `index.js`
213
+ - Update `index.d.ts`
214
+ - Add JSDoc for public methods
215
+ - Follow semver
216
+
217
+ ## License
218
+
219
+ The MIT License (MIT)
220
+
221
+ Copyright (c) 2026 Code Shadow
222
+
223
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
224
+ this software and associated documentation files (the "Software"), to deal in
225
+ the Software without restriction, including without limitation the rights to
226
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
227
+ the Software, and to permit persons to whom the Software is furnished to do so,
228
+ subject to the following conditions:
229
+
230
+ The above copyright notice and this permission notice shall be included in all
231
+ copies or substantial portions of the Software.
232
+
233
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
234
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
235
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
236
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
237
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
238
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
239
+
240
+ ---
241
+
242
+ ### Checklist for Adding a Method
243
+
244
+ - Implement JS method
245
+ - Add JSDoc
246
+ - Update `index.d.ts`
247
+ - Ensure `index.js` exports it
248
+ - Update README
249
+ - Bump version
250
+ ```
251
+
252
+ ---
@@ -0,0 +1,512 @@
1
+ const fetch = require('node-fetch');
2
+
3
+ class Helix {
4
+ constructor({client_token, client_id, channelName}){
5
+ this.token = client_token;
6
+ this.channel = channelName;
7
+ this.client_id = client_id; //The client id you have, to get it go to https://dev.twitch.tv/console/apps and obtain it from your app
8
+ this.moderator = null; //the user id of the moderator
9
+ this.broadcaster_id = null; //this can be set later on
10
+ this.ready = false;
11
+ this.shoutoutCooldownGlobal = {last: 0};
12
+ this.shoutoutCooldownPerTarget = new Map();
13
+ this.emoteCache = new Map();
14
+ };
15
+
16
+ _setBroadcasterId(id){
17
+ this.broadcaster_id = id;
18
+ }
19
+
20
+ _error(message, error) {
21
+ return new Error(`${message}: ${error?.message || error}`);
22
+ }
23
+
24
+
25
+ _headers() {
26
+ return {
27
+ "Client-ID": this.client_id,
28
+ "Authorization": `Bearer ${this.token}`,
29
+ "Content-Type": "application/json"
30
+ };
31
+ }
32
+
33
+
34
+ async _withTimeout(promise, ms = 5000){
35
+ let timeout;
36
+ const controller = new AbortController();
37
+
38
+ const timer = new Promise((_, reject) => {
39
+ timeout = setTimeout(() => {
40
+ controller.abort();
41
+ reject(new Error("Helix request timed out"));
42
+ }, ms);
43
+ });
44
+
45
+ try {
46
+ const result = await Promise.race([
47
+ promise(controller.signal),
48
+ timer
49
+ ]);
50
+ return result;
51
+ }catch(error){
52
+ throw this._error(`There was an issue with fetch Timeout:`,error);
53
+ }
54
+ finally {
55
+ clearTimeout(timeout);
56
+ };
57
+ };
58
+
59
+ async _safeFetch(url, options, timeoutMs = 5000){
60
+ try {
61
+ return await this._withTimeout(
62
+ async (signal) => {
63
+ const res = await fetch(url, {...options, signal});
64
+
65
+ if(res.status === 204) return {success: true}
66
+ return await res.json();
67
+ },
68
+ timeoutMs
69
+ );
70
+ } catch (error) {
71
+ console.error("Helix fetch failed:", url, error)
72
+ return null;
73
+ }
74
+ };
75
+
76
+ async init(){
77
+ const id = await this.getUserID(this.channel);
78
+ this.broadcaster_id = id;
79
+ this.ready = true;
80
+ }
81
+
82
+ _checkReady() {
83
+ if (!this.ready) {
84
+ throw new Error("Helix.init() has not been called yet.");
85
+ }
86
+ }
87
+
88
+ async viewerCount(){
89
+ this._checkReady();
90
+ try {
91
+ return await this._withTimeout(async (signal) => {
92
+ const res = await fetch(
93
+ `https://api.twitch.tv/helix/streams?user_login=${this.channel}`,
94
+ {
95
+ method: "GET",
96
+ headers: this._headers(),
97
+ signal
98
+ }
99
+ );
100
+
101
+ const data = await res.json();
102
+ if (data.data.length === 0) return 0;
103
+ return data.data[0].viewer_count;
104
+ }, 3000);
105
+ } catch (error) {
106
+ throw this._error(`There was an error with retrieving Viewer Count`, error );
107
+ }
108
+ };
109
+
110
+
111
+ async getUserID(username){
112
+ //this._checkReady(); //commented out cause without the init starting and this one always waiting for init to have the id, it goes into a logical loop
113
+ try {
114
+ const data = await this._safeFetch(`https://api.twitch.tv/helix/users?login=${username}`, {
115
+ method: "GET",
116
+ headers: this._headers()
117
+ });
118
+
119
+ if (!data || !data.data?.length) {
120
+ console.error("Broadcaster not found:", username, data);
121
+ return null;
122
+ }
123
+
124
+ return data.data[0].id;
125
+ } catch (error) {
126
+ throw this._error(`There was an error obtaining User's twitch Id`, error)
127
+ }
128
+ };
129
+
130
+
131
+ async followCount(){
132
+ this._checkReady();
133
+ try {
134
+ const data = await this._safeFetch(`https://api.twitch.tv/helix/channels/followers?broadcaster_id=${this.broadcaster_id}`, {
135
+ method: "GET",
136
+ headers: this._headers()
137
+ });
138
+
139
+ if(!data || typeof data.total !== "number"){
140
+ console.error("Follower count error:", data);
141
+ return 0;
142
+ }
143
+ return data.total;
144
+
145
+ } catch (error) {
146
+ throw this._error(`There was an error retrieving the follow count`, error);
147
+ }
148
+ };
149
+
150
+ async lastFollow(){
151
+ this._checkReady();
152
+ try {
153
+ const data = await this._safeFetch(`https://api.twitch.tv/helix/channels/followers?broadcaster_id=${this.broadcaster_id}&first=1`, {
154
+ method: "GET",
155
+ headers: this._headers()
156
+ });
157
+
158
+ if (!data?.data?.length) return null;
159
+
160
+ return data.data[0].user_login;
161
+ } catch (error) {
162
+ throw this._error(`There was an error retrieving latest follower`, error)
163
+ }
164
+ }
165
+
166
+ async followAge(userId){
167
+ this._checkReady();
168
+ try {
169
+ const data = await this._safeFetch(
170
+ `https://api.twitch.tv/helix/channels/followers?broadcaster_id=${this.broadcaster_id}&user_id=${userId}`,
171
+ {
172
+ method: "GET",
173
+ headers: this._headers()
174
+ }
175
+ );
176
+
177
+ if (!data || !data.data) return null;
178
+
179
+ return data.data[0] || null;
180
+ } catch (error) {
181
+ throw this._error(`There was an error retrieving the follow age of the user`, error);
182
+ }
183
+ };
184
+
185
+
186
+ async emoteNameById(id){
187
+ if(this.emoteCache.has(id)) return this.emoteCache.get(id);
188
+
189
+ try {
190
+ const data = await this._safeFetch(
191
+ `https://api.twitch.tv/helix/emotes?id=${id}`,
192
+ {
193
+ method: "GET",
194
+ headers: this._headers()
195
+ }
196
+ );
197
+
198
+ const name = data?.data?.[0]?.name || null;
199
+ if (name) this.emoteCache.set(id, name);
200
+
201
+ return name;
202
+ } catch (error) {
203
+ throw this._error(`There was en error retrieving Emote name by Id`, error)
204
+ }
205
+ };
206
+
207
+ _isShoutoutCooldown(targetId){
208
+ const now = Date.now();
209
+
210
+ if(now - this.shoutoutCooldownGlobal.last < 60_000){
211
+ return true
212
+ }
213
+
214
+ const lastTarget = this.shoutoutCooldownPerTarget.get(targetId) || 0;
215
+ if(now - lastTarget < 120_000){
216
+ return true;
217
+ }
218
+ return false
219
+ };
220
+
221
+ _setShoutoutCooldown(targetId){
222
+ const now = Date.now();
223
+ this.shoutoutCooldownGlobal.last = now;
224
+ this.shoutoutCooldownPerTarget.set(targetId, now);
225
+ };
226
+
227
+ async sendShoutout(userId, moderatorId){
228
+ this._checkReady();
229
+ try {
230
+ if(this._isShoutoutCooldown(userId)){
231
+ console.log("Shoutout has been skipped due to cooldown");
232
+ return false;
233
+ }
234
+
235
+ const url = "https://api.twitch.tv/helix/chat/shoutouts";
236
+
237
+ const body = {
238
+ from_broadcaster_id: this.broadcaster_id,
239
+ to_broadcaster_id: userId,
240
+ moderator_id: moderatorId ?? this.broadcaster_id
241
+ };
242
+
243
+ const data = await this._safeFetch(url, {
244
+ method: "POST",
245
+ headers: this._headers(),
246
+ body: JSON.stringify(body)
247
+ });
248
+
249
+ if(data && data.success){
250
+ this._setShoutoutCooldown(userId);
251
+ return true;
252
+ }
253
+ else return false
254
+
255
+ } catch (error) {
256
+ throw this._error(`There was an error using shoutout`, error)
257
+ }
258
+ }
259
+
260
+
261
+ async createClip(title = "", duration = 30){
262
+ try{
263
+ const url = "https://api.twitch.tv/helix/clips";
264
+ duration = Math.min(60, Math.max(5, duration));
265
+
266
+ const extra = title ? `&title=${encodeURIComponent(title)}` : "";
267
+ const time = duration !== 30 ? `&has_delay=false&duration=${duration}` : "";
268
+
269
+ const data = await this._safeFetch(
270
+ `${url}?broadcaster_id=${this.broadcaster_id}${extra}${time}`,
271
+ {
272
+ method: "POST",
273
+ headers: this._headers(),
274
+ }
275
+ );
276
+
277
+ if (!data || data.error) {
278
+ console.error("Clip creation failed:", data);
279
+ return null;
280
+ }
281
+
282
+ return data.data?.[0] || null;
283
+ }
284
+ catch(error){
285
+ throw this._error("There was an error with the create Clip function", error);
286
+ }
287
+ };
288
+
289
+
290
+ async getChannelRewards(){
291
+ const url = `https://api.twitch.tv/helix/channel_points/custom_rewards?broadcaster_id=${this.broadcaster_id}`;
292
+
293
+ try {
294
+ const data = await this._safeFetch(url, {
295
+ method: "GET",
296
+ headers: this._headers(),
297
+ });
298
+
299
+ if(!data || data.error) {
300
+ console.error("Get rewards failed:", data);
301
+ return [];
302
+ }
303
+ return data.data || [];
304
+ } catch (error) {
305
+ throw this._error("There was an error with getting channel rewards", error);
306
+ }
307
+ };
308
+
309
+
310
+ async getChatters(moderatorId){
311
+ const url = `https://api.twitch.tv/helix/chat/chatters`
312
+ const body = {
313
+ broadcaster_id: this.broadcaster_id,
314
+ moderator_id: moderatorId ?? this.broadcaster_id
315
+ };
316
+ try {
317
+ const data = await this._safeFetch(url, {
318
+ method: "GET",
319
+ headers: this._headers(),
320
+ body: JSON.stringify(body)
321
+ });
322
+
323
+ if(!data || data.error){
324
+ console.error("Get chatters failed:", error);
325
+ return null
326
+ }
327
+ return typeof data?.total === "number" ? data.total : null;
328
+ } catch (error) {
329
+ throw this._error("There was an error with Getting chatters", error);
330
+ }
331
+ };
332
+
333
+
334
+ _randomHexColor(){
335
+ const r = Math.floor(100 + Math.random() * 155);
336
+ const g = Math.floor(100 + Math.random() * 155);
337
+ const b = Math.floor(100 + Math.random() * 155);
338
+
339
+ return (
340
+ "#" +
341
+ r.toString(16).padStart(2, "0") +
342
+ g.toString(16).padStart(2, "0") +
343
+ b.toString(16).padStart(2, "0")
344
+ );
345
+ };
346
+
347
+ async createCustomReward({
348
+ title,
349
+ cost,
350
+ prompt = "",
351
+ is_enabled = true,
352
+ background_color,
353
+ is_user_input_required = false,
354
+ is_max_per_stream_enabled = false,
355
+ max_per_stream = null,
356
+ is_max_per_user_per_stream_enabled = false,
357
+ max_per_user_per_stream = null,
358
+ is_global_cooldown_enabled = false,
359
+ global_cooldown_seconds = null
360
+ }){
361
+ if(!title || !cost) return {error: "Missing title or cost for reward"};
362
+
363
+ if(!background_color) background_color = this._randomHexColor();
364
+
365
+ const rewardToCreate = {
366
+ title: title,
367
+ prompt: prompt,
368
+ cost: cost,
369
+ background_color: background_color,
370
+ is_enabled: is_enabled,
371
+ is_user_input_required: is_user_input_required,
372
+ is_max_per_stream_enabled: is_max_per_stream_enabled,
373
+ max_per_stream: max_per_stream,
374
+ is_max_per_user_per_stream_enabled: is_max_per_user_per_stream_enabled,
375
+ max_per_user_per_stream: max_per_user_per_stream,
376
+ is_global_cooldown_enabled: is_global_cooldown_enabled,
377
+ global_cooldown_seconds: global_cooldown_seconds
378
+ }
379
+
380
+
381
+ // Remove invalid fields based on Twitch rules
382
+ if (!is_max_per_stream_enabled) {
383
+ delete rewardToCreate.max_per_stream;
384
+ }
385
+
386
+ if (!is_max_per_user_per_stream_enabled) {
387
+ delete rewardToCreate.max_per_user_per_stream;
388
+ }
389
+
390
+ if (!is_global_cooldown_enabled) {
391
+ delete rewardToCreate.global_cooldown_seconds;
392
+ }
393
+
394
+ // Remove any undefined/null fields Twitch doesn't want
395
+ Object.keys(rewardToCreate).forEach(key => {
396
+ if (rewardToCreate[key] === null || rewardToCreate[key] === undefined) {
397
+ delete rewardToCreate[key];
398
+ }
399
+ });
400
+
401
+ try {
402
+ const data = await this._safeFetch(`https://api.twitch.tv/helix/channel_points/custom_rewards?broadcaster_id=${this.broadcaster_id}`,
403
+ {
404
+ method: "POST",
405
+ headers: this._headers(),
406
+ body: JSON.stringify(rewardToCreate),
407
+ }
408
+ );
409
+
410
+
411
+ if (!data || data.error) {
412
+ console.error("Reward creation failed:", data);
413
+ return null;
414
+ }
415
+ const reward = data.data[0]
416
+
417
+
418
+ let rewardSave = {id: reward.id, title: reward.title, prompt: reward.prompt, cost: reward.cost};
419
+
420
+
421
+
422
+ //console.log(data)
423
+ return rewardSave || null;
424
+
425
+ } catch (error) {
426
+ throw this._error("There was an error with creating the reward", error)
427
+ }
428
+
429
+ };
430
+
431
+
432
+ async deleteCustomRewards(rewardId){
433
+ const url = `https://api.twitch.tv/helix/channel_points/custom_rewards?broadcaster_id=${this.broadcaster_id}&id=${rewardId}`;
434
+
435
+ try {
436
+ const data = await this._safeFetch(url, {
437
+ method: "DELETE",
438
+ headers: this._headers()
439
+ });
440
+
441
+ if(data && data.success){
442
+ return true;
443
+ };
444
+ } catch (error) {
445
+ throw this._error("There was an error deleting Custom Reward", error);
446
+ }
447
+ };
448
+
449
+ async updateRedemptionStatus({redemptionId, rewardId, status = "FULFILLED"}){
450
+ this._checkReady()
451
+ if (!["FULFILLED", "CANCELED"].includes(status)) { status = "FULFILLED"; }
452
+ const url = `https://api.twitch.tv/helix/channel_points/custom_rewards/redemptions?broadcaster_id=${this.broadcaster_id}&reward_id=${rewardId}&id=${redemptionId}`;
453
+
454
+ try {
455
+ const data = await this._safeFetch(url, {
456
+ method: "PATCH",
457
+ headers: this._headers(),
458
+ body: JSON.stringify({status: status})
459
+ });
460
+ if (!data || data.error || !Array.isArray(data.data) || data.data.length === 0) { return {simplified: null, full: null }; }
461
+ const entry = data.data[0];
462
+ const reward = entry.reward;
463
+
464
+ const summary = {
465
+ redeemed_by: entry.user_name,
466
+ user_id: entry.user_id,
467
+ reward_id: reward.id,
468
+ reward_title: reward.title,
469
+ reward_cost: reward.cost,
470
+ reward_status: entry.status,
471
+ reward_redeemed_at: entry.redeemed_at
472
+ };
473
+ return {simplified: summary, full: entry}
474
+ } catch (error) {
475
+ throw this._error("There was an error with adjusting reward status", error)
476
+ }
477
+ }
478
+
479
+
480
+ async cheerLeaderboard(){
481
+ const url = `https://api.twitch.tv/helix/bits/leaderboard`;
482
+ try {
483
+ const data = await this._safeFetch(url, {
484
+ method: "GET",
485
+ headers: this._headers()
486
+ });
487
+
488
+ if (!data || data.error || !Array.isArray(data.data)) { return []; }
489
+ const array = [];
490
+
491
+ for(const profile of data.data){
492
+ const index = {
493
+ userId: profile.user_id,
494
+ username: profile.user_login,
495
+ rank: profile.rank,
496
+ score: profile.score
497
+ };
498
+
499
+ array.push(index);
500
+ }
501
+ return array
502
+
503
+ } catch (error) {
504
+ throw this._error("There was an error getting the cheer Leaderboard", error)
505
+ }
506
+
507
+ }
508
+
509
+ };
510
+
511
+
512
+ module.exports = {Helix}
@@ -0,0 +1,291 @@
1
+ // top of file
2
+ const fs = require("fs");
3
+ const path = require("path");
4
+
5
+ let fetchFn = globalThis.fetch;
6
+ if (!fetchFn) {
7
+ // for environments without global fetch (older Node), require node-fetch v2
8
+ // ensure you install node-fetch@2 if you use this fallback
9
+ fetchFn = require('node-fetch');
10
+ }
11
+
12
+
13
+
14
+
15
+ class Subscribe{
16
+ constructor({clientId, token, broadcasterId = null, sessionId = null}){
17
+ this.client_id = clientId;
18
+ this.token = token;
19
+ this.broadcaster_id = broadcasterId;
20
+ this.session_id = sessionId;
21
+ this._eventsub_url = "https://api.twitch.tv/helix/eventsub/subscriptions";
22
+
23
+
24
+ }
25
+
26
+ _canStart() {
27
+ const required = {
28
+ client_id: this.client_id,
29
+ token: this.token,
30
+ broadcaster_id: this.broadcaster_id,
31
+ session_id: this.session_id
32
+ };
33
+
34
+ for (const [key, value] of Object.entries(required)) {
35
+ if (!value) {
36
+ throw new Error(`${key} was not defined`);
37
+ }
38
+ }
39
+ }
40
+
41
+
42
+ _error(message, error) {
43
+ return new Error(`${message}: ${error?.message || error}`);
44
+ }
45
+
46
+ _headers() {
47
+ return {
48
+ "Client-ID": this.client_id,
49
+ "Authorization": `Bearer ${this.token}`,
50
+ "Content-Type": "application/json"
51
+ }
52
+ }
53
+
54
+
55
+ async _withTimeout(promise, ms = 5000){
56
+ let timeout;
57
+ const controller = new AbortController();
58
+
59
+ const timer = new Promise((_, reject) => {
60
+ timeout = setTimeout(() => {
61
+ controller.abort();
62
+ reject(new Error("Helix request timed out"));
63
+ }, ms);
64
+ });
65
+
66
+ try {
67
+ const result = await Promise.race([
68
+ promise(controller.signal),
69
+ timer
70
+ ]);
71
+ return result;
72
+ }catch(error){
73
+ throw this._error(`There was an issue with fetch Timeout:`,error);
74
+ }
75
+ finally {
76
+ clearTimeout(timeout);
77
+ };
78
+ }
79
+
80
+
81
+ async _safeFetch(url, options, timeoutMs = 5000){
82
+ try {
83
+ const result = await this._withTimeout(
84
+ async (signal) => {
85
+ const res = await fetchFn(url, {...options, signal});
86
+
87
+ if(res.status === 204) return {success: true, data: null}
88
+ const json = await res.json();
89
+ return {success: !json?.error, data: json }
90
+ },
91
+ timeoutMs
92
+ );
93
+ return result
94
+ } catch (error) {
95
+ console.error("Helix fetch failed:", url, error)
96
+ return {success: false, error };
97
+ }
98
+ }
99
+
100
+ async _safeSub(description, fn){
101
+ try {
102
+ const data = await fn();
103
+
104
+ if (!data?.data || data.data.length === 0) {
105
+ console.error(`❌ Subscription FAILED for ${description}:`, data);
106
+ return false;
107
+ }
108
+
109
+ const sub = data.data[0];
110
+ console.log(`✔ Subscription OK: ${sub.type} → ${sub.status}`);
111
+ return true;
112
+
113
+ } catch (err) {
114
+ console.error(`❌ Subscription ERROR for ${description}:`, err);
115
+ return false;
116
+ }
117
+ }
118
+
119
+ async toFollows(){
120
+ this._canStart();
121
+ // try {
122
+ return this._safeSub("Follow Event", async () => {
123
+ const url = this._eventsub_url;
124
+ const body = {
125
+ type: "channel.follow",
126
+ version: "2",
127
+ condition: {
128
+ broadcaster_user_id: this.broadcaster_id,
129
+ moderator_user_id: this.broadcaster_id
130
+ },
131
+ transport: {
132
+ method: "websocket",
133
+ session_id: this.session_id
134
+ }
135
+ };
136
+
137
+ const data = await this._safeFetch(url, {
138
+ method: "POST",
139
+ headers: this._headers(),
140
+ body: JSON.stringify(body)
141
+ });
142
+
143
+ return {success: !data?.error, data: data?.data}
144
+ })
145
+
146
+ // } catch (error) {
147
+ // throw this._error("There was an error with subscribing to twitch follows", error)
148
+ // }
149
+ }
150
+
151
+
152
+ async toRedemptions(){
153
+ this._canStart();
154
+
155
+ return this._safeSub("Redemption Event", async () => {
156
+ const url = this._eventsub_url;
157
+ const body = {
158
+ type: "channel.channel_points_custom_reward_redemption.add",
159
+ version: "1",
160
+ condition: {
161
+ broadcaster_user_id: this.broadcaster_id
162
+ },
163
+ transport: {
164
+ method: "websocket",
165
+ session_id: this.session_id
166
+ }
167
+ };
168
+
169
+ const data = await this._safeFetch(url, {
170
+ method: "POST",
171
+ headers: this._headers(),
172
+ body: JSON.stringify(body)
173
+ });
174
+ return {success: !data?.error, data: data}
175
+ })
176
+ }
177
+
178
+
179
+ async toStreamOnline(){
180
+ this._canStart();
181
+
182
+ return this._safeSub("Stream Online Event", async () => {
183
+ const url = this._eventsub_url;
184
+ const body = {
185
+ type: "stream.online",
186
+ version: "1",
187
+ condition: {
188
+ broadcaster_user_id: this.broadcaster_id
189
+ },
190
+ transport: {
191
+ method: "websocket",
192
+ session_id: this.session_id
193
+ }
194
+ };
195
+
196
+ return await this._safeFetch(url, {
197
+ method: "POST",
198
+ headers: this._headers(),
199
+ body: JSON.stringify(body)
200
+ });
201
+ })
202
+
203
+ }
204
+
205
+
206
+ async toStreamOffline(){
207
+ this._canStart();
208
+
209
+ return this._safeSub("Stream offline Event", async () => {
210
+ const url = this._eventsub_url;
211
+ const body = {
212
+ type: "stream.offline",
213
+ version: "1",
214
+ condition: {
215
+ broadcaster_user_id: this.broadcaster_id,
216
+ },
217
+ transport: {
218
+ method: "websocket",
219
+ session_id: this.session_id
220
+ }
221
+ };
222
+
223
+ return await this._safeFetch(url, {
224
+ method: "POST",
225
+ headers: this._headers(),
226
+ body: JSON.stringify(body)
227
+ });
228
+ })
229
+ }
230
+
231
+ async toChannelUpdate(){
232
+ this._canStart();
233
+ const url = this._eventsub_url;
234
+
235
+ return this._safeSub("Channell Update Event", async () => {
236
+ const body = {
237
+ type: "channel.update",
238
+ version: "2",
239
+ condition: {
240
+ broadcaster_user_id: this.broadcaster_id
241
+ },
242
+ transport: {
243
+ method: "websocket",
244
+ session_id: this.session_id
245
+ }
246
+ };
247
+
248
+ return await this._safeFetch(url, {
249
+ method: "POST",
250
+ headers: this._headers(),
251
+ body: JSON.stringify(body)
252
+ });
253
+ })
254
+
255
+
256
+ }
257
+
258
+
259
+
260
+ async toAll(){
261
+ this._canStart();
262
+ try {
263
+ const [
264
+ updates,
265
+ follows,
266
+ redemptions,
267
+ online,
268
+ offline
269
+ ] = await Promise.all([
270
+ this.toChannelUpdate(),
271
+ this.toFollows(),
272
+ this.toRedemptions(),
273
+ this.toStreamOnline(),
274
+ this.toStreamOffline()
275
+ ]);
276
+
277
+ return {
278
+ channelUpdateData: updates,
279
+ followsData: follows,
280
+ redemptionsData: redemptions,
281
+ isOnlineData: online,
282
+ isOfflineData: offline
283
+ }
284
+ } catch (error) {
285
+ throw this._error("There was an error subscribing to all events", error)
286
+ }
287
+ }
288
+
289
+ }
290
+
291
+ module.exports = {Subscribe}
package/index.d.ts ADDED
@@ -0,0 +1,143 @@
1
+ export interface HelixOptions {
2
+ client_token: string;
3
+ client_id: string;
4
+ channelName: string;
5
+ }
6
+
7
+ export interface CustomRewardOptions {
8
+ title: string;
9
+ cost: number;
10
+ prompt?: string;
11
+ is_enabled?: boolean;
12
+ background_color?: string;
13
+ is_user_input_required?: boolean;
14
+ is_max_per_stream_enabled?: boolean;
15
+ max_per_stream?: number | null;
16
+ is_max_per_user_per_stream_enabled?: boolean;
17
+ max_per_user_per_stream?: number | null;
18
+ is_global_cooldown_enabled?: boolean;
19
+ global_cooldown_seconds?: number | null;
20
+ }
21
+
22
+ export interface RedemptionStatusOptions {
23
+ redemptionId: string;
24
+ rewardId: string;
25
+ status?: "FULFILLED" | "CANCELED";
26
+ }
27
+
28
+ export interface RedemptionSummary {
29
+ redeemed_by: string;
30
+ user_id: string;
31
+ reward_id: string;
32
+ reward_title: string;
33
+ reward_cost: number;
34
+ reward_status: string;
35
+ reward_redeemed_at: string;
36
+ }
37
+
38
+ export interface RedemptionStatusResult {
39
+ simplified: RedemptionSummary | null;
40
+ full: any | null;
41
+ }
42
+
43
+ export interface CheerLeaderboardEntry {
44
+ userId: string;
45
+ username: string;
46
+ rank: number;
47
+ score: number;
48
+ }
49
+
50
+ export class Helix {
51
+ constructor(options: HelixOptions);
52
+
53
+ token: string;
54
+ channel: string;
55
+ client_id: string;
56
+ moderator: string | null;
57
+ broadcaster_id: string | null;
58
+ ready: boolean;
59
+
60
+ init(): Promise<void>;
61
+ viewerCount(): Promise<number>;
62
+ getUserID(username: string): Promise<string | null>;
63
+ followCount(): Promise<number>;
64
+ lastFollow(): Promise<string | null>;
65
+ followAge(userId: string): Promise<any | null>;
66
+ emoteNameById(id: string): Promise<string | null>;
67
+
68
+ sendShoutout(userId: string, moderatorId?: string): Promise<boolean>;
69
+
70
+ createClip(title?: string, duration?: number): Promise<any | null>;
71
+
72
+ getChannelRewards(): Promise<any[]>;
73
+
74
+ getChatters(moderatorId?: string): Promise<number | null>;
75
+
76
+ createCustomReward(options: CustomRewardOptions): Promise<{
77
+ id: string;
78
+ title: string;
79
+ prompt: string;
80
+ cost: number;
81
+ } | null>;
82
+
83
+ deleteCustomRewards(rewardId: string): Promise<boolean>;
84
+
85
+ updateRedemptionStatus(options: RedemptionStatusOptions): Promise<RedemptionStatusResult>;
86
+
87
+ cheerLeaderboard(): Promise<CheerLeaderboardEntry[]>;
88
+ }
89
+
90
+
91
+ export interface SubscribeOptions {
92
+ clientId: string;
93
+ token: string;
94
+ broadcasterId?: string | null;
95
+ sessionId?: string | null;
96
+ }
97
+
98
+ export interface SafeFetchResult {
99
+ success: boolean;
100
+ data?: any | null;
101
+ error?: any;
102
+ }
103
+
104
+ export class Subscribe {
105
+ constructor(options: SubscribeOptions);
106
+
107
+ client_id: string;
108
+ token: string;
109
+ broadcaster_id: string | null;
110
+ session_id: string | null;
111
+ _eventsub_url: string;
112
+
113
+ _canStart(): void;
114
+ _error(message: string, error?: any): Error;
115
+ _headers(): { "Client-ID": string; Authorization: string; "Content-Type": string };
116
+ _withTimeout<T>(promise: (signal: AbortSignal) => Promise<T>, ms?: number): Promise<T>;
117
+ _safeFetch(url: string, options?: any, timeoutMs?: number): Promise<SafeFetchResult>;
118
+ _safeSub(description: string, fn: () => Promise<SafeFetchResult | { success?: boolean; data?: any }>): Promise<boolean>;
119
+
120
+
121
+
122
+ toFollows(): Promise<boolean>;
123
+ toRedemptions(): Promise<boolean>;
124
+ toStreamOnline(): Promise<boolean>;
125
+ toStreamOffline(): Promise<boolean>;
126
+ toChannelUpdate(): Promise<boolean>;
127
+ toAll(): Promise<{
128
+ channelUpdateData: any;
129
+ followData: any;
130
+ redemptionsData: any;
131
+ isOnlineData: any;
132
+ isOfflineData: any;
133
+ }>;
134
+ }
135
+
136
+
137
+
138
+ declare const _default: {
139
+ Helix: typeof Helix;
140
+ Subscribe: typeof Subscribe;
141
+ };
142
+
143
+ export = _default;
package/index.js ADDED
@@ -0,0 +1,8 @@
1
+ const { Helix } = require("./classes/twitchHelix");
2
+ const { Subscribe } = require("./classes/twitchSubscriptions");
3
+
4
+
5
+ module.exports = {
6
+ Helix,
7
+ Subscribe
8
+ }
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "twitch-api-kit",
3
+ "version": "1.0.0",
4
+ "description": "Utility classes for Twitch Helix API and EventSub WebSocket subscriptions",
5
+ "license": "MIT",
6
+ "author": "Code Shadow",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/CodeShadow17/twitch-api-kit.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/CodeShadow17/twitch-api-kit/issues"
13
+ },
14
+ "homepage": "https://github.com/CodeShadow17/twitch-api-kit#readme",
15
+ "type": "commonjs",
16
+ "main": "index.js",
17
+ "types": "index.d.ts",
18
+ "engines": {
19
+ "node": ">=18"
20
+ },
21
+ "keywords": [
22
+ "twitch",
23
+ "twitch-helix",
24
+ "eventsub",
25
+ "twitch-eventsub",
26
+ "twitch-api",
27
+ "twitch-subscribe",
28
+ "twitch-websocket",
29
+ "twitch-api-kit",
30
+ "twitch-helper",
31
+ "nodejs",
32
+ "websocket",
33
+ "streaming"
34
+ ],
35
+ "files": [
36
+ "index.js",
37
+ "index.d.ts",
38
+ "classes/",
39
+ "README.md"
40
+ ],
41
+ "dependencies": {},
42
+ "peerDependencies": {
43
+ "ws": "^8.19.0"
44
+ }
45
+ }