taverns.js 0.2.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/dist/rest.js ADDED
@@ -0,0 +1,425 @@
1
+ "use strict";
2
+ /**
3
+ * taverns.js - REST Client
4
+ *
5
+ * HTTP client for the Taverns API.
6
+ * Handles bot token authentication, rate limiting, and retries.
7
+ */
8
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
+ if (k2 === undefined) k2 = k;
10
+ var desc = Object.getOwnPropertyDescriptor(m, k);
11
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12
+ desc = { enumerable: true, get: function() { return m[k]; } };
13
+ }
14
+ Object.defineProperty(o, k2, desc);
15
+ }) : (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ o[k2] = m[k];
18
+ }));
19
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
20
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
21
+ }) : function(o, v) {
22
+ o["default"] = v;
23
+ });
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.TavernAPIError = exports.RESTClient = void 0;
43
+ const events_1 = require("events");
44
+ const https = __importStar(require("https"));
45
+ const http = __importStar(require("http"));
46
+ const url_1 = require("url");
47
+ const constants_1 = require("./constants");
48
+ class RESTClient extends events_1.EventEmitter {
49
+ constructor(token, baseUrl = constants_1.DEFAULT_API_URL) {
50
+ super();
51
+ this.buckets = new Map();
52
+ this.globalRateLimit = null;
53
+ this.token = token;
54
+ this.baseUrl = baseUrl;
55
+ }
56
+ /** Update the bot token (used when login() provides a new token). */
57
+ setToken(token) {
58
+ this.token = token;
59
+ }
60
+ // ─── Core HTTP Methods ──────────────────────────────────
61
+ async get(path, query) {
62
+ return this.request({ method: 'GET', path, query });
63
+ }
64
+ async post(path, body) {
65
+ return this.request({ method: 'POST', path, body });
66
+ }
67
+ async patch(path, body) {
68
+ return this.request({ method: 'PATCH', path, body });
69
+ }
70
+ async put(path, body) {
71
+ return this.request({ method: 'PUT', path, body });
72
+ }
73
+ async delete(path, body) {
74
+ return this.request({ method: 'DELETE', path, body });
75
+ }
76
+ // ─── Bot Identity ──────────────────────────────────────
77
+ /** Get the bot's own profile and installed taverns. */
78
+ async getSelf() {
79
+ return this.get('/bots/@me');
80
+ }
81
+ /** Get the list of taverns the bot is installed in. */
82
+ async getMyTaverns() {
83
+ return this.get('/bots/@me/taverns');
84
+ }
85
+ // ─── Taverns ───────────────────────────────────────────
86
+ /** Get a tavern by ID. */
87
+ async getTavern(tavernId) {
88
+ return this.get(`/taverns/${tavernId}`);
89
+ }
90
+ // ─── Channels ──────────────────────────────────────────
91
+ /** List all channels in a tavern. */
92
+ async getChannels(tavernId) {
93
+ return this.get(`/taverns/${tavernId}/channels`);
94
+ }
95
+ /** Create a new channel in a tavern. */
96
+ async createChannel(tavernId, options) {
97
+ return this.post(`/taverns/${tavernId}/channels`, options);
98
+ }
99
+ /** Update a channel. */
100
+ async updateChannel(tavernId, channelId, options) {
101
+ return this.patch(`/taverns/${tavernId}/channels/${channelId}`, options);
102
+ }
103
+ /** Delete a channel. */
104
+ async deleteChannel(tavernId, channelId) {
105
+ await this.delete(`/taverns/${tavernId}/channels/${channelId}`);
106
+ }
107
+ // ─── Messages ──────────────────────────────────────────
108
+ /** Send a message to a channel. */
109
+ async sendMessage(tavernId, channelId, options) {
110
+ return this.post(`/taverns/${tavernId}/channels/${channelId}/messages`, options);
111
+ }
112
+ /** Get messages from a channel. */
113
+ async getMessages(tavernId, channelId, options) {
114
+ const query = {};
115
+ if (options?.limit)
116
+ query.limit = options.limit;
117
+ if (options?.before)
118
+ query.before = options.before;
119
+ if (options?.after)
120
+ query.after = options.after;
121
+ return this.get(`/taverns/${tavernId}/channels/${channelId}/messages`, query);
122
+ }
123
+ /** Edit a message. */
124
+ async editMessage(tavernId, messageId, options) {
125
+ return this.patch(`/taverns/${tavernId}/messages/${messageId}`, options);
126
+ }
127
+ /** Delete a message. */
128
+ async deleteMessage(tavernId, messageId) {
129
+ await this.delete(`/taverns/${tavernId}/messages/${messageId}`);
130
+ }
131
+ /** Pin a message. */
132
+ async pinMessage(tavernId, messageId) {
133
+ await this.post(`/taverns/${tavernId}/messages/${messageId}/pin`);
134
+ }
135
+ /** Unpin a message. */
136
+ async unpinMessage(tavernId, messageId) {
137
+ await this.delete(`/taverns/${tavernId}/messages/${messageId}/pin`);
138
+ }
139
+ /** Get pinned messages in a channel. */
140
+ async getPinnedMessages(tavernId, channelId) {
141
+ return this.get(`/taverns/${tavernId}/channels/${channelId}/pins`);
142
+ }
143
+ /** Search messages in a tavern or specific channel. */
144
+ async searchMessages(tavernId, options) {
145
+ const query = {
146
+ query: options.query,
147
+ };
148
+ if (options.senderId)
149
+ query.senderId = options.senderId;
150
+ if (options.limit)
151
+ query.limit = options.limit;
152
+ if (options.offset)
153
+ query.offset = options.offset;
154
+ if (options.channelId) {
155
+ return this.get(`/taverns/${tavernId}/channels/${options.channelId}/messages/search`, query);
156
+ }
157
+ return this.get(`/taverns/${tavernId}/messages/search`, query);
158
+ }
159
+ /** Add a reaction to a message. */
160
+ async addReaction(tavernId, messageId, emoji) {
161
+ await this.post(`/taverns/${tavernId}/messages/${messageId}/reactions`, { emoji });
162
+ }
163
+ /** Remove a reaction from a message. */
164
+ async removeReaction(tavernId, messageId, emoji) {
165
+ await this.delete(`/taverns/${tavernId}/messages/${messageId}/reactions/${encodeURIComponent(emoji)}`);
166
+ }
167
+ /** Bulk delete messages in a channel. */
168
+ async bulkDeleteMessages(tavernId, channelId, messageIds) {
169
+ await this.post(`/taverns/${tavernId}/channels/${channelId}/messages/bulk-delete`, { messageIds });
170
+ }
171
+ // ─── Members ───────────────────────────────────────────
172
+ /** List members of a tavern. */
173
+ async getMembers(tavernId, options) {
174
+ const query = {};
175
+ if (options?.limit)
176
+ query.limit = options.limit;
177
+ if (options?.after)
178
+ query.after = options.after;
179
+ return this.get(`/taverns/${tavernId}/members`, query);
180
+ }
181
+ /** Get a specific member. */
182
+ async getMember(tavernId, userId) {
183
+ return this.get(`/taverns/${tavernId}/members/${userId}`);
184
+ }
185
+ /** Kick a member from a tavern. */
186
+ async kickMember(tavernId, userId, reason) {
187
+ await this.delete(`/taverns/${tavernId}/members/${userId}`, reason ? { reason } : undefined);
188
+ }
189
+ /** Ban a member from a tavern. */
190
+ async banMember(tavernId, userId, reason) {
191
+ await this.post(`/taverns/${tavernId}/bans/${userId}`, reason ? { reason } : undefined);
192
+ }
193
+ /** Unban a member from a tavern. */
194
+ async unbanMember(tavernId, userId) {
195
+ await this.delete(`/taverns/${tavernId}/bans/${userId}`);
196
+ }
197
+ // ─── Roles ─────────────────────────────────────────────
198
+ /** List roles in a tavern. */
199
+ async getRoles(tavernId) {
200
+ return this.get(`/taverns/${tavernId}/roles`);
201
+ }
202
+ /** Create a new role in a tavern. */
203
+ async createRole(tavernId, options) {
204
+ return this.post(`/taverns/${tavernId}/roles`, options);
205
+ }
206
+ /** Update a role. */
207
+ async updateRole(tavernId, roleId, options) {
208
+ return this.patch(`/taverns/${tavernId}/roles/${roleId}`, options);
209
+ }
210
+ /** Delete a role. */
211
+ async deleteRole(tavernId, roleId) {
212
+ await this.delete(`/taverns/${tavernId}/roles/${roleId}`);
213
+ }
214
+ /** Add a role to a member. */
215
+ async addMemberRole(tavernId, userId, roleId) {
216
+ await this.post(`/taverns/${tavernId}/members/${userId}/roles/${roleId}`);
217
+ }
218
+ /** Remove a role from a member. */
219
+ async removeMemberRole(tavernId, userId, roleId) {
220
+ await this.delete(`/taverns/${tavernId}/members/${userId}/roles/${roleId}`);
221
+ }
222
+ // ─── Threads ───────────────────────────────────────────
223
+ /** Create a thread in a channel. */
224
+ async createThread(tavernId, channelId, options) {
225
+ return this.post(`/taverns/${tavernId}/channels/${channelId}/threads`, options);
226
+ }
227
+ /** List threads in a channel. */
228
+ async getThreads(tavernId, channelId) {
229
+ return this.get(`/taverns/${tavernId}/channels/${channelId}/threads`);
230
+ }
231
+ /** Get active threads across the tavern. */
232
+ async getActiveThreads(tavernId) {
233
+ return this.get(`/taverns/${tavernId}/active-threads`);
234
+ }
235
+ // ─── Bot Commands (Slash Commands) ─────────────────────
236
+ /** Register or overwrite all slash commands for this bot. */
237
+ async registerCommands(commands) {
238
+ return this.put('/bots/@me/commands', commands);
239
+ }
240
+ /** Get all registered commands for this bot. */
241
+ async getCommands() {
242
+ return this.get('/bots/@me/commands');
243
+ }
244
+ /** Delete a registered command by ID. */
245
+ async deleteCommand(commandId) {
246
+ await this.delete(`/bots/@me/commands/${commandId}`);
247
+ }
248
+ // ─── Interactions ─────────────────────────────────────
249
+ /** Reply to an interaction. */
250
+ async replyToInteraction(interactionId, data) {
251
+ await this.post(`/interactions/${interactionId}/callback`, data);
252
+ }
253
+ /** Acknowledge an interaction with a deferred response (shows loading state). */
254
+ async deferInteraction(interactionId) {
255
+ await this.post(`/interactions/${interactionId}/defer`);
256
+ }
257
+ /** Send a follow-up message after replying or deferring an interaction. */
258
+ async followUpInteraction(interactionId, data) {
259
+ await this.post(`/interactions/${interactionId}/followup`, data);
260
+ }
261
+ // ─── Internal Request Logic ────────────────────────────
262
+ async request(options, retryCount = 0) {
263
+ // Check global rate limit
264
+ if (this.globalRateLimit && Date.now() < this.globalRateLimit) {
265
+ const waitMs = this.globalRateLimit - Date.now();
266
+ this.emit('debug', `Global rate limit hit, waiting ${waitMs}ms`);
267
+ await this.sleep(waitMs);
268
+ }
269
+ // Check bucket rate limit
270
+ const bucketKey = `${options.method}:${options.path.split('/').slice(0, 4).join('/')}`;
271
+ const bucket = this.buckets.get(bucketKey);
272
+ if (bucket && bucket.remaining <= 0 && Date.now() < bucket.reset) {
273
+ const waitMs = bucket.reset - Date.now();
274
+ this.emit('debug', `Bucket rate limit hit for ${bucketKey}, waiting ${waitMs}ms`);
275
+ await this.sleep(waitMs);
276
+ }
277
+ // Build URL
278
+ let url = `${this.baseUrl}${options.path}`;
279
+ if (options.query) {
280
+ const params = new URLSearchParams();
281
+ for (const [key, value] of Object.entries(options.query)) {
282
+ if (value !== undefined)
283
+ params.append(key, String(value));
284
+ }
285
+ const queryString = params.toString();
286
+ if (queryString)
287
+ url += `?${queryString}`;
288
+ }
289
+ const parsedUrl = new url_1.URL(url);
290
+ const isHttps = parsedUrl.protocol === 'https:';
291
+ const transport = isHttps ? https : http;
292
+ const bodyStr = options.body ? JSON.stringify(options.body) : undefined;
293
+ const requestOptions = {
294
+ hostname: parsedUrl.hostname,
295
+ port: parsedUrl.port || (isHttps ? 443 : 80),
296
+ path: parsedUrl.pathname + parsedUrl.search,
297
+ method: options.method,
298
+ headers: {
299
+ 'Authorization': `Bot ${this.token}`,
300
+ 'Content-Type': 'application/json',
301
+ 'Accept': 'application/json',
302
+ 'User-Agent': 'taverns.js/0.1.0',
303
+ },
304
+ };
305
+ if (bodyStr) {
306
+ requestOptions.headers['Content-Length'] = Buffer.byteLength(bodyStr).toString();
307
+ }
308
+ return new Promise((resolve, reject) => {
309
+ const req = transport.request(requestOptions, (res) => {
310
+ const chunks = [];
311
+ res.on('data', (chunk) => chunks.push(chunk));
312
+ res.on('end', () => {
313
+ const raw = Buffer.concat(chunks).toString('utf-8');
314
+ // Update rate limit tracking
315
+ this.updateRateLimits(bucketKey, res.headers);
316
+ const status = res.statusCode || 0;
317
+ // Handle 429 (rate limited)
318
+ if (status === 429) {
319
+ const retryAfter = this.parseRetryAfter(res.headers);
320
+ const isGlobal = res.headers['x-ratelimit-global'] === 'true';
321
+ if (isGlobal) {
322
+ this.globalRateLimit = Date.now() + retryAfter;
323
+ }
324
+ if (retryCount < 5) {
325
+ this.emit('debug', `Rate limited (attempt ${retryCount + 1}), retrying in ${retryAfter}ms`);
326
+ this.sleep(retryAfter).then(() => {
327
+ this.request(options, retryCount + 1).then(resolve, reject);
328
+ });
329
+ return;
330
+ }
331
+ reject(new TavernAPIError('Rate limited', status, raw));
332
+ return;
333
+ }
334
+ // Handle 204 (no content)
335
+ if (status === 204 || !raw) {
336
+ resolve(undefined);
337
+ return;
338
+ }
339
+ // Parse JSON response
340
+ let data;
341
+ try {
342
+ data = JSON.parse(raw);
343
+ }
344
+ catch {
345
+ if (status >= 200 && status < 300) {
346
+ resolve(undefined);
347
+ return;
348
+ }
349
+ reject(new TavernAPIError(`HTTP ${status}: ${raw}`, status, raw));
350
+ return;
351
+ }
352
+ // Success
353
+ if (status >= 200 && status < 300) {
354
+ resolve(data);
355
+ return;
356
+ }
357
+ // Server errors - retry with backoff
358
+ if (status >= 500 && retryCount < 3) {
359
+ const backoff = Math.min(1000 * Math.pow(2, retryCount), 10000);
360
+ this.emit('debug', `Server error ${status}, retrying in ${backoff}ms (attempt ${retryCount + 1})`);
361
+ this.sleep(backoff).then(() => {
362
+ this.request(options, retryCount + 1).then(resolve, reject);
363
+ });
364
+ return;
365
+ }
366
+ // Client/server error
367
+ const errorMessage = typeof data === 'object' && data !== null && 'message' in data
368
+ ? String(data.message)
369
+ : `HTTP ${status}`;
370
+ reject(new TavernAPIError(errorMessage, status, raw));
371
+ });
372
+ });
373
+ req.on('error', (error) => {
374
+ // Network errors - retry with backoff
375
+ if (retryCount < 3) {
376
+ const backoff = Math.min(1000 * Math.pow(2, retryCount), 10000);
377
+ this.emit('debug', `Network error: ${error.message}, retrying in ${backoff}ms`);
378
+ this.sleep(backoff).then(() => {
379
+ this.request(options, retryCount + 1).then(resolve, reject);
380
+ });
381
+ return;
382
+ }
383
+ reject(error);
384
+ });
385
+ if (bodyStr) {
386
+ req.write(bodyStr);
387
+ }
388
+ req.end();
389
+ });
390
+ }
391
+ updateRateLimits(bucketKey, headers) {
392
+ const limit = headers['x-ratelimit-limit'];
393
+ const remaining = headers['x-ratelimit-remaining'];
394
+ const reset = headers['x-ratelimit-reset'];
395
+ if (limit !== undefined && remaining !== undefined) {
396
+ this.buckets.set(bucketKey, {
397
+ limit: Number(limit),
398
+ remaining: Number(remaining),
399
+ reset: reset ? Number(reset) * 1000 : Date.now() + 1000,
400
+ });
401
+ }
402
+ }
403
+ parseRetryAfter(headers) {
404
+ const retryAfter = headers['retry-after'];
405
+ if (retryAfter) {
406
+ const seconds = Number(retryAfter);
407
+ return isNaN(seconds) ? 5000 : seconds * 1000;
408
+ }
409
+ return 5000;
410
+ }
411
+ sleep(ms) {
412
+ return new Promise((resolve) => setTimeout(resolve, ms));
413
+ }
414
+ }
415
+ exports.RESTClient = RESTClient;
416
+ // ─── API Error ──────────────────────────────────────────────
417
+ class TavernAPIError extends Error {
418
+ constructor(message, status, body) {
419
+ super(message);
420
+ this.name = 'TavernAPIError';
421
+ this.status = status;
422
+ this.body = body;
423
+ }
424
+ }
425
+ exports.TavernAPIError = TavernAPIError;