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.
@@ -0,0 +1,273 @@
1
+ "use strict";
2
+ /**
3
+ * taverns.js - Gateway (WebSocket Client)
4
+ *
5
+ * Manages the WebSocket connection to the Tavern gateway.
6
+ * Handles authentication, heartbeats, auto-reconnection with exponential
7
+ * backoff, and event dispatching.
8
+ */
9
+ var __importDefault = (this && this.__importDefault) || function (mod) {
10
+ return (mod && mod.__esModule) ? mod : { "default": mod };
11
+ };
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.Gateway = void 0;
14
+ const events_1 = require("events");
15
+ const ws_1 = __importDefault(require("ws"));
16
+ const constants_1 = require("./constants");
17
+ var ConnectionState;
18
+ (function (ConnectionState) {
19
+ ConnectionState[ConnectionState["DISCONNECTED"] = 0] = "DISCONNECTED";
20
+ ConnectionState[ConnectionState["CONNECTING"] = 1] = "CONNECTING";
21
+ ConnectionState[ConnectionState["CONNECTED"] = 2] = "CONNECTED";
22
+ ConnectionState[ConnectionState["RECONNECTING"] = 3] = "RECONNECTING";
23
+ })(ConnectionState || (ConnectionState = {}));
24
+ class Gateway extends events_1.EventEmitter {
25
+ constructor(options) {
26
+ super();
27
+ this.ws = null;
28
+ this.heartbeatTimer = null;
29
+ this.reconnectTimer = null;
30
+ this.state = ConnectionState.DISCONNECTED;
31
+ this.reconnectAttempts = 0;
32
+ this.destroyed = false;
33
+ this.token = options.token;
34
+ this.url = options.url || constants_1.DEFAULT_WS_URL;
35
+ this.heartbeatInterval = options.heartbeatInterval || 30000;
36
+ this.autoReconnect = options.autoReconnect ?? true;
37
+ this.maxReconnectAttempts = options.maxReconnectAttempts ?? Infinity;
38
+ this.intents = options.intents;
39
+ }
40
+ /** Whether the gateway is currently connected. */
41
+ get connected() {
42
+ return this.state === ConnectionState.CONNECTED;
43
+ }
44
+ /** Update the bot token (used when login() provides a new token). */
45
+ setToken(token) {
46
+ this.token = token;
47
+ }
48
+ /**
49
+ * Connect to the gateway.
50
+ * Resolves once the WebSocket connection is open.
51
+ */
52
+ connect() {
53
+ if (this.destroyed) {
54
+ return Promise.reject(new Error('Gateway has been destroyed'));
55
+ }
56
+ if (this.state === ConnectionState.CONNECTED || this.state === ConnectionState.CONNECTING) {
57
+ return Promise.resolve();
58
+ }
59
+ this.state = ConnectionState.CONNECTING;
60
+ this.emit('debug', `Connecting to gateway: ${this.url}`);
61
+ return new Promise((resolve, reject) => {
62
+ let wsUrl = `${this.url}?token=${encodeURIComponent(this.token)}`;
63
+ if (this.intents !== undefined) {
64
+ wsUrl += `&intents=${this.intents.toString()}`;
65
+ }
66
+ try {
67
+ this.ws = new ws_1.default(wsUrl);
68
+ }
69
+ catch (err) {
70
+ this.state = ConnectionState.DISCONNECTED;
71
+ reject(err);
72
+ return;
73
+ }
74
+ const onOpen = () => {
75
+ this.state = ConnectionState.CONNECTED;
76
+ this.reconnectAttempts = 0;
77
+ this.startHeartbeat();
78
+ this.emit('debug', 'Gateway connection established');
79
+ cleanup();
80
+ resolve();
81
+ };
82
+ const onError = (err) => {
83
+ this.emit('debug', `Gateway connection error: ${err.message}`);
84
+ if (this.state === ConnectionState.CONNECTING) {
85
+ cleanup();
86
+ this.state = ConnectionState.DISCONNECTED;
87
+ reject(err);
88
+ }
89
+ };
90
+ const onClose = (code, reason) => {
91
+ const reasonStr = reason.toString('utf-8');
92
+ this.emit('debug', `Gateway closed during connect: ${code} ${reasonStr}`);
93
+ if (this.state === ConnectionState.CONNECTING) {
94
+ cleanup();
95
+ this.state = ConnectionState.DISCONNECTED;
96
+ reject(new Error(`WebSocket closed: ${code} ${reasonStr}`));
97
+ }
98
+ };
99
+ const cleanup = () => {
100
+ this.ws?.removeListener('open', onOpen);
101
+ this.ws?.removeListener('error', onError);
102
+ this.ws?.removeListener('close', onClose);
103
+ // Re-attach permanent handlers
104
+ if (this.ws) {
105
+ this.ws.on('message', this.onMessage.bind(this));
106
+ this.ws.on('close', this.onClose.bind(this));
107
+ this.ws.on('error', this.onError.bind(this));
108
+ }
109
+ };
110
+ this.ws.once('open', onOpen);
111
+ this.ws.once('error', onError);
112
+ this.ws.once('close', onClose);
113
+ });
114
+ }
115
+ /**
116
+ * Send a JSON message to the gateway.
117
+ */
118
+ send(action, data) {
119
+ if (!this.ws || this.state !== ConnectionState.CONNECTED) {
120
+ this.emit('debug', `Cannot send "${action}": not connected`);
121
+ return;
122
+ }
123
+ const payload = JSON.stringify({ action, data });
124
+ try {
125
+ this.ws.send(payload);
126
+ }
127
+ catch (err) {
128
+ this.emit('debug', `Failed to send "${action}": ${err.message}`);
129
+ }
130
+ }
131
+ /**
132
+ * Gracefully disconnect and clean up. The gateway cannot be reused after this.
133
+ */
134
+ destroy() {
135
+ this.destroyed = true;
136
+ this.autoReconnect = false;
137
+ this.cleanup();
138
+ this.state = ConnectionState.DISCONNECTED;
139
+ this.emit('debug', 'Gateway destroyed');
140
+ this.removeAllListeners();
141
+ }
142
+ /**
143
+ * Disconnect without destroying (allows reconnection).
144
+ */
145
+ disconnect() {
146
+ this.cleanup();
147
+ this.state = ConnectionState.DISCONNECTED;
148
+ this.emit('debug', 'Gateway disconnected');
149
+ }
150
+ // ─── Internal ──────────────────────────────────────────
151
+ onMessage(data) {
152
+ let raw;
153
+ if (data instanceof Buffer) {
154
+ raw = data.toString('utf-8');
155
+ }
156
+ else if (data instanceof ArrayBuffer) {
157
+ raw = Buffer.from(data).toString('utf-8');
158
+ }
159
+ else if (Array.isArray(data)) {
160
+ raw = Buffer.concat(data).toString('utf-8');
161
+ }
162
+ else {
163
+ raw = String(data);
164
+ }
165
+ let payload;
166
+ try {
167
+ payload = JSON.parse(raw);
168
+ }
169
+ catch {
170
+ this.emit('debug', `Received non-JSON message: ${raw.substring(0, 200)}`);
171
+ return;
172
+ }
173
+ const eventName = payload.event || payload.action;
174
+ if (!eventName) {
175
+ this.emit('debug', `Received message without event/action: ${raw.substring(0, 200)}`);
176
+ return;
177
+ }
178
+ this.emit('debug', `Gateway event: ${eventName}`);
179
+ this.emit('event', eventName, payload.data);
180
+ }
181
+ onClose(code, reason) {
182
+ const reasonStr = reason.toString('utf-8');
183
+ this.emit('debug', `Gateway closed: ${code} ${reasonStr}`);
184
+ this.stopHeartbeat();
185
+ this.state = ConnectionState.DISCONNECTED;
186
+ this.emit('close', code, reasonStr);
187
+ // Auth failures should not trigger reconnect
188
+ if (code === 4001 || code === 4003 || code === 4004) {
189
+ this.emit('debug', `Authentication error (${code}), not reconnecting`);
190
+ return;
191
+ }
192
+ if (this.autoReconnect && !this.destroyed) {
193
+ this.scheduleReconnect();
194
+ }
195
+ }
196
+ onError(err) {
197
+ this.emit('debug', `Gateway error: ${err.message}`);
198
+ this.emit('error', err);
199
+ }
200
+ startHeartbeat() {
201
+ this.stopHeartbeat();
202
+ this.heartbeatTimer = setInterval(() => {
203
+ this.send(constants_1.GatewayAction.HEARTBEAT);
204
+ this.emit('debug', 'Sent heartbeat');
205
+ }, this.heartbeatInterval);
206
+ // Prevent the timer from keeping the process alive
207
+ if (this.heartbeatTimer.unref) {
208
+ this.heartbeatTimer.unref();
209
+ }
210
+ }
211
+ stopHeartbeat() {
212
+ if (this.heartbeatTimer) {
213
+ clearInterval(this.heartbeatTimer);
214
+ this.heartbeatTimer = null;
215
+ }
216
+ }
217
+ scheduleReconnect() {
218
+ if (this.reconnectTimer)
219
+ return;
220
+ this.reconnectAttempts++;
221
+ if (this.reconnectAttempts > this.maxReconnectAttempts) {
222
+ this.emit('debug', `Max reconnect attempts (${this.maxReconnectAttempts}) reached, giving up`);
223
+ this.emit('reconnectFailed');
224
+ return;
225
+ }
226
+ // Exponential backoff: 1s, 2s, 4s, 8s, 16s, 30s (max)
227
+ const baseDelay = Math.min(1000 * Math.pow(2, this.reconnectAttempts - 1), 30000);
228
+ // Add jitter (0-25% of base delay)
229
+ const jitter = Math.floor(Math.random() * baseDelay * 0.25);
230
+ const delay = baseDelay + jitter;
231
+ this.state = ConnectionState.RECONNECTING;
232
+ this.emit('debug', `Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
233
+ this.emit('reconnecting', this.reconnectAttempts);
234
+ this.reconnectTimer = setTimeout(async () => {
235
+ this.reconnectTimer = null;
236
+ if (this.destroyed)
237
+ return;
238
+ try {
239
+ await this.connect();
240
+ this.emit('reconnected');
241
+ }
242
+ catch (err) {
243
+ this.emit('debug', `Reconnect attempt ${this.reconnectAttempts} failed: ${err.message}`);
244
+ if (!this.destroyed && this.autoReconnect) {
245
+ this.scheduleReconnect();
246
+ }
247
+ }
248
+ }, delay);
249
+ if (this.reconnectTimer.unref) {
250
+ this.reconnectTimer.unref();
251
+ }
252
+ }
253
+ cleanup() {
254
+ this.stopHeartbeat();
255
+ if (this.reconnectTimer) {
256
+ clearTimeout(this.reconnectTimer);
257
+ this.reconnectTimer = null;
258
+ }
259
+ if (this.ws) {
260
+ this.ws.removeAllListeners();
261
+ if (this.ws.readyState === ws_1.default.OPEN || this.ws.readyState === ws_1.default.CONNECTING) {
262
+ try {
263
+ this.ws.close(1000, 'Client disconnect');
264
+ }
265
+ catch {
266
+ // Ignore close errors
267
+ }
268
+ }
269
+ this.ws = null;
270
+ }
271
+ }
272
+ }
273
+ exports.Gateway = Gateway;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * taverns.js - SDK for building Tavern bots
3
+ *
4
+ * SDK for building bots on the Taverns platform.
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * import { Client, PermissionsBitField } from 'taverns.js';
9
+ *
10
+ * const client = new Client({ token: 'tavbot_...' });
11
+ *
12
+ * client.on('ready', () => {
13
+ * console.log(`Logged in as ${client.user.displayName}`);
14
+ * });
15
+ *
16
+ * client.on('messageCreate', (message) => {
17
+ * if (message.content === '!ping') {
18
+ * message.reply('Pong!');
19
+ * }
20
+ * });
21
+ *
22
+ * client.login();
23
+ * ```
24
+ */
25
+ export { Client } from './client';
26
+ export type { ActionableMessage, ActionableInteraction } from './client';
27
+ export { PermissionsBitField, PermissionFlags } from './permissions';
28
+ export type { PermissionFlagName } from './permissions';
29
+ export { Collection } from './collection';
30
+ export { RESTClient, TavernAPIError } from './rest';
31
+ export { Gateway } from './gateway';
32
+ export type { GatewayOptions } from './gateway';
33
+ export * from './types';
34
+ export * from './constants';
35
+ export { verifyWebhookSignature } from './webhook';
36
+ export { EmbedBuilder } from './embed';
37
+ export type { Embed, EmbedField, EmbedFooter, EmbedImage } from './embed';
package/dist/index.js ADDED
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ /**
3
+ * taverns.js - SDK for building Tavern bots
4
+ *
5
+ * SDK for building bots on the Taverns platform.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import { Client, PermissionsBitField } from 'taverns.js';
10
+ *
11
+ * const client = new Client({ token: 'tavbot_...' });
12
+ *
13
+ * client.on('ready', () => {
14
+ * console.log(`Logged in as ${client.user.displayName}`);
15
+ * });
16
+ *
17
+ * client.on('messageCreate', (message) => {
18
+ * if (message.content === '!ping') {
19
+ * message.reply('Pong!');
20
+ * }
21
+ * });
22
+ *
23
+ * client.login();
24
+ * ```
25
+ */
26
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
27
+ if (k2 === undefined) k2 = k;
28
+ var desc = Object.getOwnPropertyDescriptor(m, k);
29
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
30
+ desc = { enumerable: true, get: function() { return m[k]; } };
31
+ }
32
+ Object.defineProperty(o, k2, desc);
33
+ }) : (function(o, m, k, k2) {
34
+ if (k2 === undefined) k2 = k;
35
+ o[k2] = m[k];
36
+ }));
37
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
38
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
39
+ };
40
+ Object.defineProperty(exports, "__esModule", { value: true });
41
+ exports.EmbedBuilder = exports.verifyWebhookSignature = exports.Gateway = exports.TavernAPIError = exports.RESTClient = exports.Collection = exports.PermissionFlags = exports.PermissionsBitField = exports.Client = void 0;
42
+ var client_1 = require("./client");
43
+ Object.defineProperty(exports, "Client", { enumerable: true, get: function () { return client_1.Client; } });
44
+ var permissions_1 = require("./permissions");
45
+ Object.defineProperty(exports, "PermissionsBitField", { enumerable: true, get: function () { return permissions_1.PermissionsBitField; } });
46
+ Object.defineProperty(exports, "PermissionFlags", { enumerable: true, get: function () { return permissions_1.PermissionFlags; } });
47
+ var collection_1 = require("./collection");
48
+ Object.defineProperty(exports, "Collection", { enumerable: true, get: function () { return collection_1.Collection; } });
49
+ var rest_1 = require("./rest");
50
+ Object.defineProperty(exports, "RESTClient", { enumerable: true, get: function () { return rest_1.RESTClient; } });
51
+ Object.defineProperty(exports, "TavernAPIError", { enumerable: true, get: function () { return rest_1.TavernAPIError; } });
52
+ var gateway_1 = require("./gateway");
53
+ Object.defineProperty(exports, "Gateway", { enumerable: true, get: function () { return gateway_1.Gateway; } });
54
+ __exportStar(require("./types"), exports);
55
+ __exportStar(require("./constants"), exports);
56
+ var webhook_1 = require("./webhook");
57
+ Object.defineProperty(exports, "verifyWebhookSignature", { enumerable: true, get: function () { return webhook_1.verifyWebhookSignature; } });
58
+ var embed_1 = require("./embed");
59
+ Object.defineProperty(exports, "EmbedBuilder", { enumerable: true, get: function () { return embed_1.EmbedBuilder; } });
@@ -0,0 +1,161 @@
1
+ /**
2
+ * taverns.js - PermissionsBitField
3
+ *
4
+ * Permissions utility for working with Tavern permission
5
+ * bitfields. Permissions are stored as BigInt flags, matching the server-side
6
+ * TavernPermission enum.
7
+ */
8
+ /**
9
+ * All Tavern permission flags. Each flag is a bit position (0-35).
10
+ * The actual bitfield value is `1n << BigInt(position)`.
11
+ */
12
+ export declare const PermissionFlags: {
13
+ readonly VIEW_CHANNELS: 0n;
14
+ readonly MANAGE_CHANNELS: 1n;
15
+ readonly MANAGE_ROLES: 2n;
16
+ readonly MANAGE_TAVERN: 3n;
17
+ readonly CREATE_INVITE: 4n;
18
+ readonly KICK_MEMBERS: 5n;
19
+ readonly BAN_MEMBERS: 6n;
20
+ readonly MANAGE_NICKNAMES: 7n;
21
+ readonly MANAGE_PERMISSIONS: 8n;
22
+ readonly PIN_MESSAGES: 9n;
23
+ readonly SEND_MESSAGES: 10n;
24
+ readonly MANAGE_MESSAGES: 11n;
25
+ readonly ATTACH_FILES: 13n;
26
+ readonly ADD_REACTIONS: 14n;
27
+ readonly MENTION_EVERYONE: 15n;
28
+ readonly EMBED_LINKS: 16n;
29
+ readonly READ_MESSAGE_HISTORY: 17n;
30
+ readonly USE_EXTERNAL_EMOJI: 18n;
31
+ readonly USE_EXTERNAL_STICKERS: 19n;
32
+ readonly MANAGE_STICKERS: 12n;
33
+ readonly MUTE_MEMBERS: 20n;
34
+ readonly BYPASS_SLOWMODE: 21n;
35
+ readonly CONNECT_VOICE: 22n;
36
+ readonly SPEAK: 23n;
37
+ readonly MANAGE_VOICE: 24n;
38
+ readonly VIDEO: 25n;
39
+ readonly CREATE_EVENTS: 26n;
40
+ readonly MANAGE_EVENTS: 27n;
41
+ readonly CREATE_POLLS: 28n;
42
+ readonly VOTE_IN_POLLS: 29n;
43
+ readonly ADMINISTRATOR: 30n;
44
+ readonly CREATE_PUBLIC_THREADS: 31n;
45
+ readonly CREATE_PRIVATE_THREADS: 32n;
46
+ readonly MANAGE_THREADS: 33n;
47
+ readonly SEND_MESSAGES_IN_THREADS: 34n;
48
+ readonly MANAGE_BOTS: 35n;
49
+ };
50
+ export type PermissionFlagName = keyof typeof PermissionFlags;
51
+ /**
52
+ * A bitfield that represents a set of permissions.
53
+ *
54
+ * @example
55
+ * ```ts
56
+ * const perms = new PermissionsBitField('2048'); // from API string
57
+ *
58
+ * if (perms.has('SEND_MESSAGES')) {
59
+ * // Bot can send messages
60
+ * }
61
+ *
62
+ * const withAdmin = perms.add('ADMINISTRATOR');
63
+ * console.log(withAdmin.toArray()); // ['SEND_MESSAGES', 'ADMINISTRATOR']
64
+ * ```
65
+ */
66
+ export declare class PermissionsBitField {
67
+ /**
68
+ * Static reference to all permission flags.
69
+ * @example PermissionsBitField.FLAGS.SEND_MESSAGES
70
+ */
71
+ static readonly FLAGS: {
72
+ readonly VIEW_CHANNELS: 0n;
73
+ readonly MANAGE_CHANNELS: 1n;
74
+ readonly MANAGE_ROLES: 2n;
75
+ readonly MANAGE_TAVERN: 3n;
76
+ readonly CREATE_INVITE: 4n;
77
+ readonly KICK_MEMBERS: 5n;
78
+ readonly BAN_MEMBERS: 6n;
79
+ readonly MANAGE_NICKNAMES: 7n;
80
+ readonly MANAGE_PERMISSIONS: 8n;
81
+ readonly PIN_MESSAGES: 9n;
82
+ readonly SEND_MESSAGES: 10n;
83
+ readonly MANAGE_MESSAGES: 11n;
84
+ readonly ATTACH_FILES: 13n;
85
+ readonly ADD_REACTIONS: 14n;
86
+ readonly MENTION_EVERYONE: 15n;
87
+ readonly EMBED_LINKS: 16n;
88
+ readonly READ_MESSAGE_HISTORY: 17n;
89
+ readonly USE_EXTERNAL_EMOJI: 18n;
90
+ readonly USE_EXTERNAL_STICKERS: 19n;
91
+ readonly MANAGE_STICKERS: 12n;
92
+ readonly MUTE_MEMBERS: 20n;
93
+ readonly BYPASS_SLOWMODE: 21n;
94
+ readonly CONNECT_VOICE: 22n;
95
+ readonly SPEAK: 23n;
96
+ readonly MANAGE_VOICE: 24n;
97
+ readonly VIDEO: 25n;
98
+ readonly CREATE_EVENTS: 26n;
99
+ readonly MANAGE_EVENTS: 27n;
100
+ readonly CREATE_POLLS: 28n;
101
+ readonly VOTE_IN_POLLS: 29n;
102
+ readonly ADMINISTRATOR: 30n;
103
+ readonly CREATE_PUBLIC_THREADS: 31n;
104
+ readonly CREATE_PRIVATE_THREADS: 32n;
105
+ readonly MANAGE_THREADS: 33n;
106
+ readonly SEND_MESSAGES_IN_THREADS: 34n;
107
+ readonly MANAGE_BOTS: 35n;
108
+ };
109
+ /** The raw bitfield value. */
110
+ readonly bitfield: bigint;
111
+ /**
112
+ * Create a new PermissionsBitField.
113
+ * @param bits - A BigInt, numeric string, number, or another PermissionsBitField
114
+ */
115
+ constructor(bits?: bigint | string | number | PermissionsBitField);
116
+ /**
117
+ * Check if this bitfield has a specific permission.
118
+ * @param permission - Permission flag name or bit position
119
+ */
120
+ has(permission: PermissionFlagName | bigint): boolean;
121
+ /**
122
+ * Check if this bitfield has any of the specified permissions.
123
+ */
124
+ hasAny(...permissions: (PermissionFlagName | bigint)[]): boolean;
125
+ /**
126
+ * Check if this bitfield has all of the specified permissions.
127
+ */
128
+ hasAll(...permissions: (PermissionFlagName | bigint)[]): boolean;
129
+ /**
130
+ * Return a new PermissionsBitField with the given permission(s) added.
131
+ */
132
+ add(...permissions: (PermissionFlagName | bigint)[]): PermissionsBitField;
133
+ /**
134
+ * Return a new PermissionsBitField with the given permission(s) removed.
135
+ */
136
+ remove(...permissions: (PermissionFlagName | bigint)[]): PermissionsBitField;
137
+ /**
138
+ * Return an array of all permission flag names that are set.
139
+ */
140
+ toArray(): PermissionFlagName[];
141
+ /**
142
+ * Serialize to a decimal string (matches API format).
143
+ */
144
+ toString(): string;
145
+ /**
146
+ * Serialize to JSON as a decimal string.
147
+ */
148
+ toJSON(): string;
149
+ /**
150
+ * Check if this bitfield equals another.
151
+ */
152
+ equals(other: PermissionsBitField | bigint | string): boolean;
153
+ /**
154
+ * Get a bitfield with all permissions set.
155
+ */
156
+ static all(): PermissionsBitField;
157
+ /**
158
+ * Resolve a permission name or bigint to its bitfield flag.
159
+ */
160
+ private resolve;
161
+ }