@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
@@ -0,0 +1,250 @@
1
+ 'use strict';
2
+
3
+ import { GladeAPIError } from '../errors/index.js';
4
+ import { Routes } from './Routes.js';
5
+ import { makeQuery, trimTrailingSlash } from '../util/Util.js';
6
+ import { REFRESH_COOKIE } from '../util/Constants.js';
7
+
8
+ /** Credential endpoints where a 401 means "bad credentials", not "expired session". */
9
+ const AUTH_ENDPOINTS = /^\/auth\/(login|register|forgot|reset|logout|refresh)/;
10
+
11
+ /**
12
+ * The HTTP layer of glade.js. Talks to the Glade REST API, attaching the bearer
13
+ * access token, persisting the `glade_rt` refresh cookie, and transparently
14
+ * refreshing an expired access token on a 401 (single-flight, retried once).
15
+ */
16
+ export class REST {
17
+ /**
18
+ * @param {object} [options]
19
+ * @param {string} [options.base] REST origin without the version segment.
20
+ * @param {string} [options.version] API version segment, e.g. `v1`.
21
+ * @param {boolean} [options.autoRefresh] Refresh + retry on 401.
22
+ * @param {(token: string) => void} [options.onToken] Called whenever a fresh access token is obtained.
23
+ * @param {(msg: string) => void} [options.debug] Debug sink.
24
+ */
25
+ constructor(options = {}) {
26
+ /** @type {string} The REST origin (without the version segment). */
27
+ this.base = trimTrailingSlash(options.base ?? 'https://api.glade.chat');
28
+ /** @type {string} */
29
+ this.version = options.version ?? 'v1';
30
+ /** @type {boolean} */
31
+ this.autoRefresh = options.autoRefresh ?? true;
32
+ /** @type {((token: string) => void) | null} */
33
+ this.onToken = options.onToken ?? null;
34
+ /** @type {((msg: string) => void) | null} */
35
+ this._debug = options.debug ?? null;
36
+
37
+ /** @type {string | null} The current access token. */
38
+ this.token = null;
39
+ /** @type {string | null} The raw `glade_rt` refresh cookie value. */
40
+ this.refreshCookie = null;
41
+ /** @type {Promise<string | null> | null} In-flight refresh, for single-flight. */
42
+ this._refreshing = null;
43
+ }
44
+
45
+ /** The fully-qualified base, i.e. `<base>/<version>`. */
46
+ get apiBase() {
47
+ return `${this.base}/${this.version}`;
48
+ }
49
+
50
+ /**
51
+ * Sets the access token used for the `Authorization` header.
52
+ * @param {string | null} token
53
+ * @returns {this}
54
+ */
55
+ setToken(token) {
56
+ this.token = token ?? null;
57
+ return this;
58
+ }
59
+
60
+ /**
61
+ * Sets the refresh cookie value directly (e.g. when resuming a saved session).
62
+ * @param {string | null} value
63
+ * @returns {this}
64
+ */
65
+ setRefreshToken(value) {
66
+ this.refreshCookie = value ?? null;
67
+ return this;
68
+ }
69
+
70
+ /**
71
+ * Performs a REST request.
72
+ * @template T
73
+ * @param {object} opts
74
+ * @param {string} opts.method HTTP method.
75
+ * @param {string} opts.path Path relative to the version prefix (use {@link Routes}).
76
+ * @param {any} [opts.body] JSON body.
77
+ * @param {Record<string, any>} [opts.query] Query parameters.
78
+ * @param {Record<string, string>} [opts.headers] Extra headers.
79
+ * @param {boolean} [opts.auth=true] Attach the bearer token.
80
+ * @param {boolean} [opts.sendCookie=false] Attach the refresh cookie.
81
+ * @param {BodyInit} [opts.rawBody] Pre-built body (e.g. FormData); bypasses JSON.
82
+ * @param {boolean} [opts._retry] Internal: marks a post-refresh retry.
83
+ * @returns {Promise<T>}
84
+ */
85
+ async request(opts) {
86
+ const {
87
+ method = 'GET',
88
+ path,
89
+ body,
90
+ query,
91
+ headers = {},
92
+ auth = true,
93
+ sendCookie = false,
94
+ rawBody,
95
+ _retry = false,
96
+ } = opts;
97
+
98
+ const url = `${this.apiBase}${path}${makeQuery(query)}`;
99
+ const finalHeaders = { Accept: 'application/json', ...headers };
100
+
101
+ if (auth && this.token) finalHeaders.Authorization = `Bearer ${this.token}`;
102
+ if (sendCookie && this.refreshCookie) {
103
+ finalHeaders.Cookie = `${REFRESH_COOKIE}=${this.refreshCookie}`;
104
+ }
105
+
106
+ let payload = rawBody;
107
+ if (rawBody === undefined && body !== undefined) {
108
+ finalHeaders['Content-Type'] = 'application/json';
109
+ payload = JSON.stringify(body);
110
+ }
111
+
112
+ this.#debug(`${method} ${path}`);
113
+ const res = await fetch(url, { method, headers: finalHeaders, body: payload });
114
+
115
+ this.#captureCookies(res);
116
+
117
+ // Refresh-and-retry on an expired access token.
118
+ if (
119
+ res.status === 401 &&
120
+ this.autoRefresh &&
121
+ auth &&
122
+ !_retry &&
123
+ !AUTH_ENDPOINTS.test(path)
124
+ ) {
125
+ const fresh = await this.refresh();
126
+ if (fresh) return this.request({ ...opts, _retry: true });
127
+ }
128
+
129
+ if (res.status === 204) return /** @type {T} */ (undefined);
130
+
131
+ let data = null;
132
+ const text = await res.text();
133
+ if (text) {
134
+ try {
135
+ data = JSON.parse(text);
136
+ } catch {
137
+ data = text;
138
+ }
139
+ }
140
+
141
+ if (!res.ok) {
142
+ const message = (data && data.error) || res.statusText || 'Request failed';
143
+ throw new GladeAPIError({
144
+ status: res.status,
145
+ message,
146
+ method,
147
+ path,
148
+ details: data && data.details,
149
+ });
150
+ }
151
+
152
+ return /** @type {T} */ (data);
153
+ }
154
+
155
+ /** Convenience helpers. */
156
+ get(path, opts = {}) {
157
+ return this.request({ ...opts, method: 'GET', path });
158
+ }
159
+ post(path, body, opts = {}) {
160
+ return this.request({ ...opts, method: 'POST', path, body });
161
+ }
162
+ patch(path, body, opts = {}) {
163
+ return this.request({ ...opts, method: 'PATCH', path, body });
164
+ }
165
+ put(path, body, opts = {}) {
166
+ return this.request({ ...opts, method: 'PUT', path, body });
167
+ }
168
+ delete(path, opts = {}) {
169
+ return this.request({ ...opts, method: 'DELETE', path });
170
+ }
171
+
172
+ /**
173
+ * Exchanges the stored refresh cookie for a new access token. Single-flight:
174
+ * concurrent callers share one in-flight request.
175
+ * @returns {Promise<string | null>} The new access token, or null on failure.
176
+ */
177
+ refresh() {
178
+ // Bot/user tokens have no refresh cookie — there is nothing to exchange.
179
+ if (!this.refreshCookie) return Promise.resolve(null);
180
+ if (this._refreshing) return this._refreshing;
181
+ this._refreshing = (async () => {
182
+ try {
183
+ const data = await this.request({
184
+ method: 'POST',
185
+ path: Routes.refresh(),
186
+ auth: false,
187
+ sendCookie: true,
188
+ });
189
+ const token = data?.accessToken ?? null;
190
+ if (token) {
191
+ this.token = token;
192
+ this.onToken?.(token);
193
+ this.#debug('Access token refreshed');
194
+ }
195
+ return token;
196
+ } catch (err) {
197
+ this.#debug(`Refresh failed: ${err.message}`);
198
+ return null;
199
+ } finally {
200
+ this._refreshing = null;
201
+ }
202
+ })();
203
+ return this._refreshing;
204
+ }
205
+
206
+ /**
207
+ * Uploads a file via multipart/form-data to `/uploads`.
208
+ * @param {Buffer | Uint8Array | Blob} file File contents.
209
+ * @param {object} [opts]
210
+ * @param {string} [opts.name] File name.
211
+ * @param {string} [opts.contentType] MIME type.
212
+ * @param {'avatar' | 'attachment'} [opts.kind] Upload bucket; `avatar` for avatars/banners.
213
+ * @returns {Promise<{ url: string, name: string, size: number, contentType: string }>}
214
+ */
215
+ async upload(file, opts = {}) {
216
+ const form = new FormData();
217
+ const blob =
218
+ file instanceof Blob
219
+ ? file
220
+ : new Blob([file], { type: opts.contentType || 'application/octet-stream' });
221
+ form.append('file', blob, opts.name || 'file');
222
+ return this.request({
223
+ method: 'POST',
224
+ path: Routes.uploads(),
225
+ query: opts.kind ? { kind: opts.kind } : undefined,
226
+ rawBody: form,
227
+ });
228
+ }
229
+
230
+ /** Reads `Set-Cookie` headers and stores the refresh cookie value. */
231
+ #captureCookies(res) {
232
+ let cookies = [];
233
+ if (typeof res.headers.getSetCookie === 'function') {
234
+ cookies = res.headers.getSetCookie();
235
+ } else {
236
+ const raw = res.headers.get('set-cookie');
237
+ if (raw) cookies = [raw];
238
+ }
239
+ for (const cookie of cookies) {
240
+ const match = cookie.match(new RegExp(`${REFRESH_COOKIE}=([^;]+)`));
241
+ if (match) this.refreshCookie = match[1];
242
+ }
243
+ }
244
+
245
+ #debug(msg) {
246
+ this._debug?.(`[REST] ${msg}`);
247
+ }
248
+ }
249
+
250
+ export default REST;
@@ -0,0 +1,85 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Builders for every Glade REST endpoint. Each returns a path *relative to the
5
+ * version prefix*, e.g. `Routes.login()` → `/auth/login`, which the
6
+ * {@link REST} client expands to `<base>/<version>/auth/login`.
7
+ */
8
+ export const Routes = {
9
+ // --- Auth ---
10
+ register: () => '/auth/register',
11
+ login: () => '/auth/login',
12
+ loginTwoFactor: () => '/auth/login/2fa',
13
+ twoFactorSetup: () => '/auth/2fa/setup',
14
+ twoFactorEnable: () => '/auth/2fa/enable',
15
+ twoFactorDisable: () => '/auth/2fa/disable',
16
+ refresh: () => '/auth/refresh',
17
+ logout: () => '/auth/logout',
18
+ tokens: () => '/auth/tokens',
19
+ tokensReset: () => '/auth/tokens/reset',
20
+ sessions: () => '/auth/sessions',
21
+ session: (id) => `/auth/sessions/${id}`,
22
+ authMe: () => '/auth/me',
23
+ forgotPassword: () => '/auth/forgot',
24
+ resetPassword: () => '/auth/reset',
25
+
26
+ // --- Users ---
27
+ me: () => '/users/me',
28
+ userSearch: () => '/users',
29
+ user: (id) => `/users/${id}`,
30
+
31
+ // --- Houses ---
32
+ houses: () => '/houses',
33
+ house: (houseId) => `/houses/${houseId}`,
34
+ houseLeave: (houseId) => `/houses/${houseId}/leave`,
35
+ houseMembers: (houseId) => `/houses/${houseId}/members`,
36
+ memberRoles: (houseId, userId) => `/houses/${houseId}/members/${userId}/roles`,
37
+
38
+ // --- Roles ---
39
+ houseRoles: (houseId) => `/houses/${houseId}/roles`,
40
+ houseRolesReorder: (houseId) => `/houses/${houseId}/roles/reorder`,
41
+ role: (roleId) => `/roles/${roleId}`,
42
+
43
+ // --- Rooms (channels) ---
44
+ houseRooms: (houseId) => `/houses/${houseId}/rooms`,
45
+ houseRoomsReorder: (houseId) => `/houses/${houseId}/rooms/reorder`,
46
+ room: (roomId) => `/rooms/${roomId}`,
47
+ roomClone: (roomId) => `/rooms/${roomId}/clone`,
48
+ roomMessages: (roomId) => `/rooms/${roomId}/messages`,
49
+ roomPins: (roomId) => `/rooms/${roomId}/pins`,
50
+ roomPin: (roomId, messageId) => `/rooms/${roomId}/messages/${messageId}/pin`,
51
+ roomPermissions: (roomId) => `/rooms/${roomId}/permissions`,
52
+ roomPermission: (roomId, roleId) => `/rooms/${roomId}/permissions/${roleId}`,
53
+
54
+ // --- E2E house keys ---
55
+ houseKeysSelf: (houseId) => `/houses/${houseId}/keys/self`,
56
+ houseKeysMembers: (houseId) => `/houses/${houseId}/keys/members`,
57
+ houseKeys: (houseId) => `/houses/${houseId}/keys`,
58
+
59
+ // --- DMs ---
60
+ dms: () => '/dms',
61
+ dmMessages: (dmId) => `/dms/${dmId}/messages`,
62
+
63
+ // --- Invites ---
64
+ houseInvites: (houseId) => `/houses/${houseId}/invites`,
65
+ invite: (code) => `/invites/${code}`,
66
+ inviteRedeem: (code) => `/invites/${code}/redeem`,
67
+ inviteRevoke: (id) => `/invites/${id}`,
68
+
69
+ // --- Friends ---
70
+ friends: () => '/friends',
71
+ friendsPending: () => '/friends/pending',
72
+ friendAccept: (id) => `/friends/${id}/accept`,
73
+ friendDecline: (id) => `/friends/${id}/decline`,
74
+ friendRemove: (userId) => `/friends/${userId}`,
75
+
76
+ // --- Uploads ---
77
+ uploads: () => '/uploads',
78
+
79
+ // --- Billing ---
80
+ subscription: () => '/billing/subscription',
81
+ checkout: () => '/billing/checkout',
82
+ portal: () => '/billing/portal',
83
+ };
84
+
85
+ export default Routes;
@@ -0,0 +1,70 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * The base class every cached Glade structure extends. Holds a non-enumerable
5
+ * reference to the owning {@link Client} so structures can perform actions.
6
+ * @abstract
7
+ */
8
+ export class Base {
9
+ /**
10
+ * @param {import('../client/Client.js').Client} client
11
+ */
12
+ constructor(client) {
13
+ /**
14
+ * The client that instantiated this structure.
15
+ * @type {import('../client/Client.js').Client}
16
+ * @readonly
17
+ */
18
+ Object.defineProperty(this, 'client', { value: client });
19
+ }
20
+
21
+ /**
22
+ * Applies raw API data onto this structure. Subclasses override and call super.
23
+ * @param {any} data
24
+ * @returns {any} The same data, for convenience.
25
+ * @protected
26
+ */
27
+ _patch(data) {
28
+ return data;
29
+ }
30
+
31
+ /**
32
+ * Returns a shallow clone of this structure (used for "old" copies in update events).
33
+ * @returns {this}
34
+ * @protected
35
+ */
36
+ _clone() {
37
+ return Object.assign(Object.create(this), this);
38
+ }
39
+
40
+ /**
41
+ * Snapshots this structure (the "old" copy), patches this instance in place with
42
+ * the new data, and returns the old snapshot — handy for `*Update` events.
43
+ * @param {any} data
44
+ * @returns {this} The pre-patch snapshot.
45
+ * @protected
46
+ */
47
+ _update(data) {
48
+ const old = this._clone();
49
+ this._patch(data);
50
+ return old;
51
+ }
52
+
53
+ /**
54
+ * @returns {string | undefined}
55
+ */
56
+ valueOf() {
57
+ return this.id;
58
+ }
59
+
60
+ toJSON() {
61
+ const out = {};
62
+ for (const [key, value] of Object.entries(this)) {
63
+ if (value && typeof value === 'object' && 'cache' in value) continue; // skip managers
64
+ out[key] = value?.toJSON ? value.toJSON() : value;
65
+ }
66
+ return out;
67
+ }
68
+ }
69
+
70
+ export default Base;
@@ -0,0 +1,142 @@
1
+ 'use strict';
2
+
3
+ import { User } from './User.js';
4
+ import { Routes } from '../rest/Routes.js';
5
+ import { SettableStatus } from '../util/Constants.js';
6
+
7
+ /**
8
+ * Represents the currently logged-in account. Adds methods for editing the
9
+ * profile, managing presence, and inspecting connected sessions.
10
+ * @extends {User}
11
+ */
12
+ export class ClientUser extends User {
13
+ /**
14
+ * Updates the live presence broadcast to other users (via the gateway).
15
+ * @param {'online' | 'idle' | 'dnd'} status
16
+ * @returns {this}
17
+ */
18
+ setPresence(status) {
19
+ if (!SettableStatus.includes(status)) {
20
+ throw new RangeError(`Status must be one of ${SettableStatus.join(', ')}`);
21
+ }
22
+ this.client.gateway.send('presence:set', status);
23
+ this.status = status;
24
+ return this;
25
+ }
26
+
27
+ /**
28
+ * Edits the account profile.
29
+ * @param {object} data
30
+ * @param {string} [data.displayName]
31
+ * @param {string | null} [data.avatarUrl]
32
+ * @param {string | null} [data.bannerUrl]
33
+ * @param {string} [data.bio]
34
+ * @param {'online' | 'idle' | 'dnd'} [data.status]
35
+ * @param {string} [data.publicKey]
36
+ * @param {string} [data.currentPassword]
37
+ * @param {string} [data.newPassword]
38
+ * @returns {Promise<this>}
39
+ */
40
+ async edit(data) {
41
+ const { user } = await this.client.rest.patch(Routes.me(), data);
42
+ this._patch(user);
43
+ return this;
44
+ }
45
+
46
+ /**
47
+ * Sets both the persisted preferred status and the live presence.
48
+ * @param {'online' | 'idle' | 'dnd'} status
49
+ * @returns {Promise<this>}
50
+ */
51
+ async setStatus(status) {
52
+ await this.edit({ status });
53
+ if (this.client.gateway.connected) this.setPresence(status);
54
+ return this;
55
+ }
56
+
57
+ /**
58
+ * Sets the display name.
59
+ * @param {string} displayName
60
+ * @returns {Promise<this>}
61
+ */
62
+ setDisplayName(displayName) {
63
+ return this.edit({ displayName });
64
+ }
65
+
66
+ /**
67
+ * Sets the bio.
68
+ * @param {string} bio
69
+ * @returns {Promise<this>}
70
+ */
71
+ setBio(bio) {
72
+ return this.edit({ bio });
73
+ }
74
+
75
+ /**
76
+ * Sets the avatar. Accepts an existing URL string, or raw file data which is
77
+ * uploaded first.
78
+ * @param {string | Buffer | Uint8Array | Blob | null} avatar
79
+ * @param {{ name?: string, contentType?: string }} [fileOptions]
80
+ * @returns {Promise<this>}
81
+ */
82
+ async setAvatar(avatar, fileOptions = {}) {
83
+ const avatarUrl = await this.#resolveImage(avatar, fileOptions);
84
+ return this.edit({ avatarUrl });
85
+ }
86
+
87
+ /**
88
+ * Sets the profile banner. Accepts a URL string or raw file data.
89
+ * @param {string | Buffer | Uint8Array | Blob | null} banner
90
+ * @param {{ name?: string, contentType?: string }} [fileOptions]
91
+ * @returns {Promise<this>}
92
+ */
93
+ async setBanner(banner, fileOptions = {}) {
94
+ const bannerUrl = await this.#resolveImage(banner, fileOptions);
95
+ return this.edit({ bannerUrl });
96
+ }
97
+
98
+ /**
99
+ * Sets the E2E identity public key.
100
+ * @param {string} publicKey
101
+ * @returns {Promise<this>}
102
+ */
103
+ setPublicKey(publicKey) {
104
+ return this.edit({ publicKey });
105
+ }
106
+
107
+ /**
108
+ * Changes the account password.
109
+ * @param {string} currentPassword
110
+ * @param {string} newPassword
111
+ * @returns {Promise<this>}
112
+ */
113
+ setPassword(currentPassword, newPassword) {
114
+ return this.edit({ currentPassword, newPassword });
115
+ }
116
+
117
+ /**
118
+ * Lists the account's connected device sessions.
119
+ * @returns {Promise<Array<{ id: string, userAgent: string | null, location: string | null, createdAt: string, lastSeenAt: string, current: boolean }>>}
120
+ */
121
+ async fetchSessions() {
122
+ const { sessions } = await this.client.rest.get(Routes.sessions());
123
+ return sessions;
124
+ }
125
+
126
+ /**
127
+ * Revokes (signs out) a connected session.
128
+ * @param {string} sessionId
129
+ * @returns {Promise<void>}
130
+ */
131
+ async revokeSession(sessionId) {
132
+ await this.client.rest.delete(Routes.session(sessionId));
133
+ }
134
+
135
+ async #resolveImage(image, fileOptions) {
136
+ if (image === null || typeof image === 'string') return image;
137
+ const { url } = await this.client.rest.upload(image, { ...fileOptions, kind: 'avatar' });
138
+ return url;
139
+ }
140
+ }
141
+
142
+ export default ClientUser;
@@ -0,0 +1,85 @@
1
+ 'use strict';
2
+
3
+ import { Base } from './Base.js';
4
+ import { MessageManager } from '../managers/MessageManager.js';
5
+
6
+ /**
7
+ * Represents a direct-message channel between the client user and one or more
8
+ * other users.
9
+ * @extends {Base}
10
+ */
11
+ export class DMChannel extends Base {
12
+ constructor(client, data) {
13
+ super(client);
14
+ /** @type {string} The DM channel id. */
15
+ this.id = data.id;
16
+ /** @type {MessageManager} Cache + helpers for this channel's messages. */
17
+ this.messages = new MessageManager(client, this);
18
+ /** @type {string[]} Ids of the other participants. */
19
+ this.participantIds = [];
20
+ this._patch(data);
21
+ }
22
+
23
+ _patch(data) {
24
+ if (Array.isArray(data.participants)) {
25
+ this.participantIds = data.participants.map((p) => {
26
+ this.client.users._add(p);
27
+ return p.id;
28
+ });
29
+ }
30
+ return data;
31
+ }
32
+
33
+ /** The other participants in this DM, resolved from the user cache. */
34
+ get participants() {
35
+ return this.participantIds
36
+ .map((id) => this.client.users.cache.get(id))
37
+ .filter((u) => Boolean(u));
38
+ }
39
+
40
+ /** The primary recipient (first other participant). */
41
+ get recipient() {
42
+ return this.participants[0] ?? null;
43
+ }
44
+
45
+ /** This DM channel is always a DM (not a room). */
46
+ isDM() {
47
+ return true;
48
+ }
49
+
50
+ /**
51
+ * Sends a message to this DM channel.
52
+ * @param {string | import('../managers/MessageManager.js').MessagePayload} content
53
+ * @returns {Promise<import('./Message.js').Message>}
54
+ */
55
+ send(content) {
56
+ return this.client._sendMessage({ dmChannelId: this.id }, content);
57
+ }
58
+
59
+ /** Broadcasts a "typing…" indicator to the DM. */
60
+ sendTyping() {
61
+ this.client.gateway.send('typing:start', { dmChannelId: this.id });
62
+ return this;
63
+ }
64
+
65
+ /** Stops the "typing…" indicator. */
66
+ stopTyping() {
67
+ this.client.gateway.send('typing:stop', { dmChannelId: this.id });
68
+ return this;
69
+ }
70
+
71
+ /**
72
+ * Fetches a page of messages.
73
+ * @param {{ cursor?: string, limit?: number }} [options]
74
+ * @returns {Promise<{ messages: import('./Message.js').Message[], nextCursor: string | null }>}
75
+ */
76
+ fetchMessages(options) {
77
+ return this.messages.fetch(options);
78
+ }
79
+
80
+ toString() {
81
+ return this.recipient ? this.recipient.toString() : this.id;
82
+ }
83
+ }
84
+
85
+ export default DMChannel;