@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.
- package/LICENSE +21 -0
- package/README.md +52 -0
- package/package.json +57 -0
- package/src/client/Client.js +355 -0
- package/src/errors/index.js +59 -0
- package/src/gateway/Gateway.js +172 -0
- package/src/gateway/handleDispatch.js +259 -0
- package/src/index.js +66 -0
- package/src/managers/CachedManager.js +95 -0
- package/src/managers/ChannelManager.js +40 -0
- package/src/managers/DMManager.js +43 -0
- package/src/managers/FriendManager.js +89 -0
- package/src/managers/HouseManager.js +44 -0
- package/src/managers/InviteManager.js +46 -0
- package/src/managers/MemberManager.js +41 -0
- package/src/managers/MessageManager.js +69 -0
- package/src/managers/RoleManager.js +65 -0
- package/src/managers/RoomManager.js +80 -0
- package/src/managers/UserManager.js +41 -0
- package/src/rest/REST.js +250 -0
- package/src/rest/Routes.js +85 -0
- package/src/structures/Base.js +70 -0
- package/src/structures/ClientUser.js +142 -0
- package/src/structures/DMChannel.js +85 -0
- package/src/structures/House.js +171 -0
- package/src/structures/Invite.js +83 -0
- package/src/structures/Member.js +148 -0
- package/src/structures/Message.js +176 -0
- package/src/structures/ReactionGroup.js +42 -0
- package/src/structures/Role.js +107 -0
- package/src/structures/Room.js +264 -0
- package/src/structures/User.js +113 -0
- package/src/structures/VoiceState.js +42 -0
- package/src/util/Collection.js +167 -0
- package/src/util/Constants.js +183 -0
- package/src/util/Permissions.js +148 -0
- package/src/util/Util.js +62 -0
- package/types/index.d.ts +784 -0
package/src/rest/REST.js
ADDED
|
@@ -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;
|