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 +20 -0
- package/README.md +252 -0
- package/classes/twitchHelix.js +512 -0
- package/classes/twitchSubscriptions.js +291 -0
- package/index.d.ts +143 -0
- package/index.js +8 -0
- package/package.json +45 -0
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
|
+

|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
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
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
|
+
}
|