@glade-chat/glade.js 0.1.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.
Files changed (38) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +52 -0
  3. package/package.json +57 -0
  4. package/src/client/Client.js +355 -0
  5. package/src/errors/index.js +59 -0
  6. package/src/gateway/Gateway.js +172 -0
  7. package/src/gateway/handleDispatch.js +259 -0
  8. package/src/index.js +66 -0
  9. package/src/managers/CachedManager.js +95 -0
  10. package/src/managers/ChannelManager.js +40 -0
  11. package/src/managers/DMManager.js +43 -0
  12. package/src/managers/FriendManager.js +89 -0
  13. package/src/managers/HouseManager.js +44 -0
  14. package/src/managers/InviteManager.js +46 -0
  15. package/src/managers/MemberManager.js +41 -0
  16. package/src/managers/MessageManager.js +69 -0
  17. package/src/managers/RoleManager.js +65 -0
  18. package/src/managers/RoomManager.js +80 -0
  19. package/src/managers/UserManager.js +41 -0
  20. package/src/rest/REST.js +250 -0
  21. package/src/rest/Routes.js +85 -0
  22. package/src/structures/Base.js +70 -0
  23. package/src/structures/ClientUser.js +142 -0
  24. package/src/structures/DMChannel.js +85 -0
  25. package/src/structures/House.js +171 -0
  26. package/src/structures/Invite.js +83 -0
  27. package/src/structures/Member.js +148 -0
  28. package/src/structures/Message.js +176 -0
  29. package/src/structures/ReactionGroup.js +42 -0
  30. package/src/structures/Role.js +107 -0
  31. package/src/structures/Room.js +264 -0
  32. package/src/structures/User.js +113 -0
  33. package/src/structures/VoiceState.js +42 -0
  34. package/src/util/Collection.js +167 -0
  35. package/src/util/Constants.js +183 -0
  36. package/src/util/Permissions.js +148 -0
  37. package/src/util/Util.js +62 -0
  38. package/types/index.d.ts +784 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 glade.chat
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, 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,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,52 @@
1
+ <div align="center">
2
+
3
+ # glade.js 🌿
4
+
5
+ A powerful, fully-typed Node.js library for the [Glade](https://glade.chat) API and real-time gateway.
6
+
7
+ [![npm version](https://img.shields.io/npm/v/@glade-chat/glade.js.svg?maxAge=3600)](https://www.npmjs.com/package/@glade-chat/glade.js)
8
+ [![npm downloads](https://img.shields.io/npm/dt/@glade-chat/glade.js.svg?maxAge=3600)](https://www.npmjs.com/package/@glade-chat/glade.js)
9
+ [![license](https://img.shields.io/npm/l/@glade-chat/glade.js.svg?maxAge=3600)](./LICENSE)
10
+
11
+ </div>
12
+
13
+ ## About
14
+
15
+ glade.js is an object-oriented library that makes it easy to interact with Glade — Houses,
16
+ Rooms, Messages, Members, Roles, DMs, friends, presence, and voice — over REST and the
17
+ real-time gateway.
18
+
19
+ - Object-oriented
20
+ - Cache-backed and event-driven
21
+ - Handles login and token refresh for you
22
+ - First-class TypeScript types, zero build step (pure ESM)
23
+
24
+ ## Installation
25
+
26
+ **Node.js 18.17 or newer is required.**
27
+
28
+ ```bash
29
+ npm install @glade-chat/glade.js
30
+ ```
31
+
32
+ ## Example usage
33
+
34
+ ```js
35
+ import { Client, Events } from '@glade-chat/glade.js';
36
+
37
+ const client = new Client();
38
+
39
+ client.on(Events.Ready, () => {
40
+ console.log(`Logged in as @${client.user.handle}`);
41
+ });
42
+
43
+ client.on(Events.MessageCreate, (message) => {
44
+ if (message.content === '!ping') message.reply('Pong! 🌿');
45
+ });
46
+
47
+ client.login(`token`);
48
+ ```
49
+
50
+ ## License
51
+
52
+ [MIT](./LICENSE)
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@glade-chat/glade.js",
3
+ "version": "0.1.0",
4
+ "description": "A powerful Node.js library for interacting with the Glade (glade.chat) API and real-time gateway.",
5
+ "type": "module",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "main": "./src/index.js",
10
+ "module": "./src/index.js",
11
+ "types": "./types/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./types/index.d.ts",
15
+ "import": "./src/index.js",
16
+ "default": "./src/index.js"
17
+ },
18
+ "./package.json": "./package.json"
19
+ },
20
+ "files": [
21
+ "src",
22
+ "types",
23
+ "README.md",
24
+ "LICENSE"
25
+ ],
26
+ "engines": {
27
+ "node": ">=18.17.0"
28
+ },
29
+ "scripts": {
30
+ "lint": "node --check src/index.js",
31
+ "example": "node examples/index.js"
32
+ },
33
+ "keywords": [
34
+ "glade",
35
+ "glade.chat",
36
+ "glade.js",
37
+ "chat",
38
+ "bot",
39
+ "api",
40
+ "socket.io",
41
+ "client",
42
+ "library"
43
+ ],
44
+ "author": "",
45
+ "license": "MIT",
46
+ "dependencies": {
47
+ "socket.io-client": "^4.8.1"
48
+ },
49
+ "repository": {
50
+ "type": "git",
51
+ "url": "https://github.com/glade-chat/glade.js.git"
52
+ },
53
+ "homepage": "https://glade.chat",
54
+ "bugs": {
55
+ "url": "https://github.com/glade-chat/glade.js/issues"
56
+ }
57
+ }
@@ -0,0 +1,355 @@
1
+ 'use strict';
2
+
3
+ import { EventEmitter } from 'node:events';
4
+
5
+ import { REST } from '../rest/REST.js';
6
+ import { Gateway } from '../gateway/Gateway.js';
7
+ import { handleDispatch } from '../gateway/handleDispatch.js';
8
+ import { Routes } from '../rest/Routes.js';
9
+
10
+ import { HouseManager } from '../managers/HouseManager.js';
11
+ import { ChannelManager } from '../managers/ChannelManager.js';
12
+ import { UserManager } from '../managers/UserManager.js';
13
+ import { DMManager } from '../managers/DMManager.js';
14
+ import { FriendManager } from '../managers/FriendManager.js';
15
+
16
+ import { ClientUser } from '../structures/ClientUser.js';
17
+ import { Message } from '../structures/Message.js';
18
+ import { Invite } from '../structures/Invite.js';
19
+
20
+ import { GladeError } from '../errors/index.js';
21
+ import { DefaultOptions, Events } from '../util/Constants.js';
22
+ import { decodeJwt, generateNonce } from '../util/Util.js';
23
+
24
+ /**
25
+ * The main hub for interacting with the Glade API. Create one, attach event
26
+ * listeners, then call {@link Client#login}.
27
+ *
28
+ * @example
29
+ * import { Client, Events } from '@glade-chat/glade.js';
30
+ * const client = new Client();
31
+ * client.on(Events.Ready, () => console.log(`Logged in as ${client.user.handle}`));
32
+ * client.on(Events.MessageCreate, (msg) => {
33
+ * if (msg.content === '!ping') msg.reply('Pong!');
34
+ * });
35
+ * client.login({ handle: 'my-bot', password: process.env.GLADE_PASSWORD });
36
+ *
37
+ * @extends {EventEmitter}
38
+ */
39
+ export class Client extends EventEmitter {
40
+ /**
41
+ * @param {Partial<typeof DefaultOptions> & { token?: string }} [options]
42
+ */
43
+ constructor(options = {}) {
44
+ super();
45
+
46
+ /**
47
+ * The resolved client options. URLs default to the public Glade service; when
48
+ * a custom `rest` is given without a `gateway`, the gateway falls back to it.
49
+ * @type {typeof DefaultOptions}
50
+ */
51
+ this.options = {
52
+ ...DefaultOptions,
53
+ ...options,
54
+ rest: options.rest ?? DefaultOptions.rest,
55
+ gateway: options.gateway ?? options.rest ?? DefaultOptions.gateway,
56
+ ws: { ...DefaultOptions.ws, ...(options.ws ?? {}) },
57
+ };
58
+
59
+ const debug = this.options.debug ? (msg) => this.emit(Events.Debug, msg) : null;
60
+
61
+ /**
62
+ * The REST/HTTP layer.
63
+ * @type {REST}
64
+ */
65
+ this.rest = new REST({
66
+ base: this.options.rest,
67
+ version: this.options.version,
68
+ autoRefresh: this.options.autoRefresh,
69
+ onToken: (token) => this.#onToken(token),
70
+ debug,
71
+ });
72
+
73
+ /**
74
+ * The realtime gateway layer.
75
+ * @type {Gateway}
76
+ */
77
+ this.gateway = new Gateway({
78
+ url: this.options.gateway,
79
+ getToken: () => this.rest.token,
80
+ refresh: () => this.rest.refresh(),
81
+ onDispatch: (event, data) => handleDispatch(this, event, data),
82
+ onConnect: () => this.emit(Events.Debug, '[Gateway] socket connected'),
83
+ onDisconnect: (reason) => this.emit(Events.Disconnect, reason),
84
+ onError: (err) => this.#emitError(err),
85
+ ws: this.options.ws,
86
+ debug,
87
+ });
88
+
89
+ /** The logged-in account, available after {@link Client#login}. @type {ClientUser | null} */
90
+ this.user = null;
91
+ /** Whether the gateway has sent its initial `ready`. @type {boolean} */
92
+ this.ready = false;
93
+
94
+ // --- Managers ---
95
+ /** @type {HouseManager} */
96
+ this.houses = new HouseManager(this);
97
+ /** @type {ChannelManager} */
98
+ this.channels = new ChannelManager(this);
99
+ /** @type {UserManager} */
100
+ this.users = new UserManager(this);
101
+ /** @type {DMManager} */
102
+ this.dms = new DMManager(this);
103
+ /** @type {FriendManager} */
104
+ this.friends = new FriendManager(this);
105
+
106
+ /** @type {ReturnType<typeof setTimeout> | null} */
107
+ this._refreshTimer = null;
108
+ }
109
+
110
+ /** The current access token, if any. */
111
+ get token() {
112
+ return this.rest.token;
113
+ }
114
+
115
+ /** Whether the gateway is currently connected. */
116
+ get connected() {
117
+ return this.gateway.connected;
118
+ }
119
+
120
+ /**
121
+ * Authenticates with a bot/user token and opens the gateway connection.
122
+ *
123
+ * Generate a token from your account settings in the Glade app, or
124
+ * programmatically with {@link Client.requestToken}. Tokens are long-lived and
125
+ * are sent both as the REST bearer and the gateway handshake credential.
126
+ *
127
+ * @param {string | { token: string }} token A bot/user token, or `{ token }`.
128
+ * @returns {Promise<string>} The token in use.
129
+ */
130
+ async login(token) {
131
+ const value = typeof token === 'string' ? token : token?.token;
132
+ if (!value) {
133
+ throw new GladeError(
134
+ 'login() requires a bot/user token. Generate one in your Glade account settings or with Client.requestToken().',
135
+ );
136
+ }
137
+ this.rest.setToken(value);
138
+
139
+ const { user } = await this.rest.get(Routes.authMe());
140
+ this.#setUser(user);
141
+
142
+ this.#scheduleRefresh(); // no-op for non-expiring tokens
143
+ await this.#prefetch();
144
+ this.gateway.connect();
145
+ return this.rest.token;
146
+ }
147
+
148
+ /**
149
+ * Mints a long-lived bot/user token from account credentials, without starting a
150
+ * session. Use this once to obtain a token (store it securely), then pass it to
151
+ * {@link Client#login}. A web sign-in (account settings) is the usual way to
152
+ * create a token; this helper is for scripts and self-hosted setups.
153
+ *
154
+ * @param {object} opts
155
+ * @param {string} opts.handle Account handle.
156
+ * @param {string} opts.password Account password.
157
+ * @param {string} [opts.code] Two-factor code, if 2FA is enabled.
158
+ * @param {string} [opts.turnstileToken] Captcha token, if the deployment requires it.
159
+ * @param {string} [opts.rest] Backend REST origin (defaults to the public Glade API).
160
+ * @param {string} [opts.version] API version segment.
161
+ * @returns {Promise<string>} The freshly-minted token.
162
+ */
163
+ static async requestToken({ handle, password, code, turnstileToken, rest, version } = {}) {
164
+ if (!handle || !password) {
165
+ throw new GladeError('requestToken() requires { handle, password }.');
166
+ }
167
+ const api = new REST({ base: rest ?? DefaultOptions.rest, version: version ?? DefaultOptions.version });
168
+ const data = await api.post(Routes.login(), { handle, password, turnstileToken });
169
+ if (data.twoFactorRequired) {
170
+ if (!code) throw new GladeError('This account has two-factor enabled — pass a `code`.');
171
+ const verified = await api.post(Routes.loginTwoFactor(), { pendingToken: data.pendingToken, code });
172
+ api.setToken(verified.accessToken);
173
+ } else {
174
+ api.setToken(data.accessToken);
175
+ }
176
+ const { token } = await api.post(Routes.tokens());
177
+ return token;
178
+ }
179
+
180
+ /**
181
+ * Revokes the account's current token (and all others), returning a fresh one to
182
+ * log in with next time. The active connection keeps working until you reconnect.
183
+ * @returns {Promise<string>} The new token.
184
+ */
185
+ async resetToken() {
186
+ const { token } = await this.rest.post(Routes.tokensReset());
187
+ this.rest.setToken(token);
188
+ return token;
189
+ }
190
+
191
+ /**
192
+ * Disconnects the gateway and clears local state. The token itself remains valid
193
+ * (use {@link Client#resetToken} to revoke it).
194
+ * @returns {Promise<void>}
195
+ */
196
+ async logout() {
197
+ this.destroy();
198
+ }
199
+
200
+ /**
201
+ * Tears down the client: disconnects the gateway and cancels timers. The
202
+ * caches are left intact.
203
+ */
204
+ destroy() {
205
+ if (this._refreshTimer) clearTimeout(this._refreshTimer);
206
+ this._refreshTimer = null;
207
+ this.gateway.disconnect();
208
+ this.ready = false;
209
+ }
210
+
211
+ /**
212
+ * Previews an invite by code (no membership change).
213
+ * @param {string} code
214
+ * @returns {Promise<Invite>}
215
+ */
216
+ async fetchInvite(code) {
217
+ const { invite } = await this.rest.get(Routes.invite(code));
218
+ return new Invite(this, invite);
219
+ }
220
+
221
+ /**
222
+ * Redeems an invite code, joining the House.
223
+ * @param {string} code
224
+ * @returns {Promise<import('../structures/House.js').House>}
225
+ */
226
+ async redeemInvite(code) {
227
+ const { house } = await this.rest.post(Routes.inviteRedeem(code));
228
+ return this.houses._add(house);
229
+ }
230
+
231
+ /**
232
+ * Fetches the client's current subscription status.
233
+ * @returns {Promise<any>}
234
+ */
235
+ fetchSubscription() {
236
+ return this.rest.get(Routes.subscription());
237
+ }
238
+
239
+ // --- Internal helpers ---
240
+
241
+ /**
242
+ * Sends a message via the gateway (preferred) or REST, caches it, and returns
243
+ * the resulting {@link Message}.
244
+ * @param {{ roomId?: string, dmChannelId?: string }} target
245
+ * @param {string | import('../managers/MessageManager.js').MessagePayload} content
246
+ * @returns {Promise<Message>}
247
+ * @protected
248
+ */
249
+ async _sendMessage(target, content) {
250
+ const payload =
251
+ typeof content === 'string' ? { content } : { ...content };
252
+ const body = {
253
+ ...target,
254
+ content: payload.content,
255
+ clientNonce: payload.nonce ?? generateNonce(),
256
+ };
257
+ if (payload.mentions) body.mentions = payload.mentions;
258
+
259
+ let raw;
260
+ if (this.gateway.connected) {
261
+ const ack = await this.gateway.request('message:send', body);
262
+ raw = ack.message;
263
+ } else if (target.roomId) {
264
+ const res = await this.rest.post(Routes.roomMessages(target.roomId), {
265
+ content: body.content,
266
+ clientNonce: body.clientNonce,
267
+ });
268
+ raw = res.message;
269
+ } else {
270
+ throw new GladeError('Cannot send a DM while the gateway is disconnected.');
271
+ }
272
+ return this._cacheMessage(raw);
273
+ }
274
+
275
+ /**
276
+ * Inserts a raw message into the appropriate channel cache and returns a
277
+ * {@link Message}.
278
+ * @param {any} raw
279
+ * @returns {Message}
280
+ * @protected
281
+ */
282
+ _cacheMessage(raw) {
283
+ if (raw.roomId) {
284
+ const room = this.channels.cache.get(raw.roomId);
285
+ if (room) return room.messages._add(raw);
286
+ } else if (raw.dmChannelId) {
287
+ const dm = this.dms.cache.get(raw.dmChannelId);
288
+ if (dm) return dm.messages._add(raw);
289
+ }
290
+ return new Message(this, raw);
291
+ }
292
+
293
+ /**
294
+ * Subscribes the gateway to every cached room's realtime events. Called on each
295
+ * `ready` (initial connect and reconnects) so room messages/typing/reactions are
296
+ * delivered — the server only auto-joins user/house/DM channels.
297
+ * @protected
298
+ */
299
+ _subscribeCachedRooms() {
300
+ if (this.options.autoSubscribeRooms === false) return;
301
+ if (!this.gateway.connected) return;
302
+ for (const room of this.channels.cache.values()) {
303
+ this.gateway.send('room:join', room.id);
304
+ }
305
+ }
306
+
307
+ #setUser(data) {
308
+ this.user = new ClientUser(this, data);
309
+ // Cache the client user as a regular user too, so it resolves by id.
310
+ this.users.cache.set(this.user.id, this.user);
311
+ return this.user;
312
+ }
313
+
314
+ async #prefetch() {
315
+ const { fetchHouses = true, fetchDMs = true } = this.options;
316
+ try {
317
+ if (fetchHouses) {
318
+ await this.houses.fetch();
319
+ await Promise.all(
320
+ this.houses.cache.map((house) =>
321
+ Promise.all([house.rooms.fetch(), house.roles.fetch()]).catch(() => {}),
322
+ ),
323
+ );
324
+ }
325
+ if (fetchDMs) await this.dms.fetch();
326
+ } catch (err) {
327
+ this.emit(Events.Warn, `Prefetch failed: ${err.message}`);
328
+ }
329
+ }
330
+
331
+ #scheduleRefresh() {
332
+ if (this._refreshTimer) clearTimeout(this._refreshTimer);
333
+ const token = this.rest.token;
334
+ if (!token) return;
335
+ const payload = decodeJwt(token);
336
+ if (!payload?.exp) return;
337
+ const ms = payload.exp * 1000 - Date.now() - this.options.refreshSkewMs;
338
+ this._refreshTimer = setTimeout(() => {
339
+ this.rest.refresh().catch(() => {});
340
+ }, Math.max(ms, 1000));
341
+ if (typeof this._refreshTimer.unref === 'function') this._refreshTimer.unref();
342
+ }
343
+
344
+ #onToken(token) {
345
+ this.emit(Events.Debug, '[Client] Access token updated');
346
+ this.#scheduleRefresh();
347
+ }
348
+
349
+ #emitError(err) {
350
+ if (this.listenerCount(Events.Error) > 0) this.emit(Events.Error, err);
351
+ else this.emit(Events.Debug, `[Client] Unhandled error: ${err?.message ?? err}`);
352
+ }
353
+ }
354
+
355
+ export default Client;
@@ -0,0 +1,59 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Base error class for all errors thrown by glade.js.
5
+ */
6
+ export class GladeError extends Error {
7
+ constructor(message) {
8
+ super(message);
9
+ this.name = 'GladeError';
10
+ }
11
+ }
12
+
13
+ /**
14
+ * Thrown when a REST request resolves with a non-2xx response. The backend's
15
+ * error body shape is `{ error: string }` (with optional Zod `details`).
16
+ */
17
+ export class GladeAPIError extends GladeError {
18
+ /**
19
+ * @param {object} opts
20
+ * @param {number} opts.status HTTP status code.
21
+ * @param {string} opts.message Human-readable error message from the server.
22
+ * @param {string} opts.method HTTP method used.
23
+ * @param {string} opts.path Request path (after the version prefix).
24
+ * @param {any} [opts.details] Validation details, if the server returned them.
25
+ */
26
+ constructor({ status, message, method, path, details }) {
27
+ super(`${method} ${path} → ${status} ${message}`);
28
+ this.name = 'GladeAPIError';
29
+ /** @type {number} */
30
+ this.status = status;
31
+ /** @type {string} */
32
+ this.rawMessage = message;
33
+ /** @type {string} */
34
+ this.method = method;
35
+ /** @type {string} */
36
+ this.path = path;
37
+ /** @type {any} */
38
+ this.details = details ?? null;
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Thrown when the realtime gateway reports an error, including failed acks on
44
+ * gateway commands (e.g. a rate-limited `message:send`).
45
+ */
46
+ export class GladeGatewayError extends GladeError {
47
+ /**
48
+ * @param {string} message
49
+ * @param {string} [code] Optional code from the server ack (e.g. `rate_limit`, `forbidden`).
50
+ */
51
+ constructor(message, code) {
52
+ super(message);
53
+ this.name = 'GladeGatewayError';
54
+ /** @type {string | null} */
55
+ this.code = code ?? null;
56
+ }
57
+ }
58
+
59
+ export default { GladeError, GladeAPIError, GladeGatewayError };