@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
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import { Base } from './Base.js';
|
|
4
|
+
import { Routes } from '../rest/Routes.js';
|
|
5
|
+
import { RoomManager } from '../managers/RoomManager.js';
|
|
6
|
+
import { MemberManager } from '../managers/MemberManager.js';
|
|
7
|
+
import { RoleManager } from '../managers/RoleManager.js';
|
|
8
|
+
import { InviteManager } from '../managers/InviteManager.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Represents a Glade House — a community, analogous to a server/guild. Holds the
|
|
12
|
+
* House's rooms, members, roles, and invites.
|
|
13
|
+
* @extends {Base}
|
|
14
|
+
*/
|
|
15
|
+
export class House extends Base {
|
|
16
|
+
constructor(client, data) {
|
|
17
|
+
super(client);
|
|
18
|
+
/** @type {string} The House id. */
|
|
19
|
+
this.id = data.id;
|
|
20
|
+
|
|
21
|
+
/** @type {RoomManager} Manager for the House's rooms (channels). */
|
|
22
|
+
this.rooms = new RoomManager(client, this);
|
|
23
|
+
/** @type {MemberManager} Manager for the House's members. */
|
|
24
|
+
this.members = new MemberManager(client, this);
|
|
25
|
+
/** @type {RoleManager} Manager for the House's roles. */
|
|
26
|
+
this.roles = new RoleManager(client, this);
|
|
27
|
+
/** @type {InviteManager} Manager for the House's invites. */
|
|
28
|
+
this.invites = new InviteManager(client, this);
|
|
29
|
+
|
|
30
|
+
this._patch(data);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
_patch(data) {
|
|
34
|
+
if ('name' in data) {
|
|
35
|
+
/** @type {string} The House name. */
|
|
36
|
+
this.name = data.name;
|
|
37
|
+
}
|
|
38
|
+
if ('iconUrl' in data) {
|
|
39
|
+
/** @type {string | null} URL of the House icon. */
|
|
40
|
+
this.iconUrl = data.iconUrl ?? null;
|
|
41
|
+
}
|
|
42
|
+
if ('accent' in data) {
|
|
43
|
+
/** @type {string | null} The House accent color. */
|
|
44
|
+
this.accent = data.accent ?? null;
|
|
45
|
+
}
|
|
46
|
+
if ('ownerId' in data) {
|
|
47
|
+
/** @type {string} Id of the House owner. */
|
|
48
|
+
this.ownerId = data.ownerId;
|
|
49
|
+
}
|
|
50
|
+
if ('createdAt' in data) {
|
|
51
|
+
/** @type {string | null} ISO creation timestamp. */
|
|
52
|
+
this.createdAt = data.createdAt ?? null;
|
|
53
|
+
}
|
|
54
|
+
if (Array.isArray(data.rooms)) {
|
|
55
|
+
for (const room of data.rooms) this.rooms._add(room);
|
|
56
|
+
}
|
|
57
|
+
return data;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The owner of this House, if cached. */
|
|
61
|
+
get owner() {
|
|
62
|
+
return this.client.users.cache.get(this.ownerId) ?? null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Whether the logged-in account owns this House. */
|
|
66
|
+
get isOwner() {
|
|
67
|
+
return this.client.user?.id === this.ownerId;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Edits this House.
|
|
72
|
+
* @param {{ name?: string, iconUrl?: string | null }} data
|
|
73
|
+
* @returns {Promise<House>}
|
|
74
|
+
*/
|
|
75
|
+
async edit(data) {
|
|
76
|
+
const { house } = await this.client.rest.patch(Routes.house(this.id), data);
|
|
77
|
+
this._patch(house);
|
|
78
|
+
return this;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Sets the House name. */
|
|
82
|
+
setName(name) {
|
|
83
|
+
return this.edit({ name });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Sets the House icon URL (or null to clear). */
|
|
87
|
+
setIcon(iconUrl) {
|
|
88
|
+
return this.edit({ iconUrl });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Permanently deletes this House. Owner only.
|
|
93
|
+
* @returns {Promise<void>}
|
|
94
|
+
*/
|
|
95
|
+
async delete() {
|
|
96
|
+
await this.client.rest.delete(Routes.house(this.id));
|
|
97
|
+
this.client.houses.cache.delete(this.id);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Leaves this House.
|
|
102
|
+
* @returns {Promise<void>}
|
|
103
|
+
*/
|
|
104
|
+
async leave() {
|
|
105
|
+
await this.client.rest.post(Routes.houseLeave(this.id));
|
|
106
|
+
this.client.houses.cache.delete(this.id);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Creates a room (channel) in this House.
|
|
111
|
+
* @param {string} name
|
|
112
|
+
* @param {{ type?: 'text' | 'voice' | 'portal' }} [options]
|
|
113
|
+
* @returns {Promise<import('./Room.js').Room>}
|
|
114
|
+
*/
|
|
115
|
+
createRoom(name, options = {}) {
|
|
116
|
+
return this.rooms.create(name, options);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Creates a role in this House.
|
|
121
|
+
* @param {object} data
|
|
122
|
+
* @returns {Promise<import('./Role.js').Role>}
|
|
123
|
+
*/
|
|
124
|
+
createRole(data) {
|
|
125
|
+
return this.roles.create(data);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Creates an invite to this House.
|
|
130
|
+
* @param {{ expiresInMinutes?: number | null, maxUses?: number | null }} [options]
|
|
131
|
+
* @returns {Promise<import('./Invite.js').Invite>}
|
|
132
|
+
*/
|
|
133
|
+
createInvite(options = {}) {
|
|
134
|
+
return this.invites.create(options);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Fetches and caches all members. */
|
|
138
|
+
fetchMembers() {
|
|
139
|
+
return this.members.fetch();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Fetches and caches all rooms. */
|
|
143
|
+
fetchRooms() {
|
|
144
|
+
return this.rooms.fetch();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Fetches and caches all roles. */
|
|
148
|
+
fetchRoles() {
|
|
149
|
+
return this.roles.fetch();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Fetches all active invites for this House. */
|
|
153
|
+
fetchInvites() {
|
|
154
|
+
return this.invites.fetch();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Fetches current voice-room occupancy for this House over the gateway.
|
|
159
|
+
* @returns {Promise<Array<{ roomId: string, users: Array<{ userId: string, muted: boolean, deafened: boolean }> }>>}
|
|
160
|
+
*/
|
|
161
|
+
async fetchVoiceStates() {
|
|
162
|
+
const ack = await this.client.gateway.request('voice:sync', this.id);
|
|
163
|
+
return ack?.states ?? [];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
toString() {
|
|
167
|
+
return this.name;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export default House;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import { Base } from './Base.js';
|
|
4
|
+
import { Routes } from '../rest/Routes.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Represents an invite to a House. Depending on where it came from, some fields
|
|
8
|
+
* may be absent (e.g. a previewed invite has no `id`, so it cannot be revoked).
|
|
9
|
+
* @extends {Base}
|
|
10
|
+
*/
|
|
11
|
+
export class Invite extends Base {
|
|
12
|
+
constructor(client, data) {
|
|
13
|
+
super(client);
|
|
14
|
+
this._patch(data);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
_patch(data) {
|
|
18
|
+
if ('id' in data) {
|
|
19
|
+
/** @type {string | undefined} The invite id (present when listed/created). */
|
|
20
|
+
this.id = data.id;
|
|
21
|
+
}
|
|
22
|
+
if ('code' in data) {
|
|
23
|
+
/** @type {string} The invite code. */
|
|
24
|
+
this.code = data.code;
|
|
25
|
+
}
|
|
26
|
+
if ('uses' in data) {
|
|
27
|
+
/** @type {number | undefined} How many times the invite has been used. */
|
|
28
|
+
this.uses = data.uses;
|
|
29
|
+
}
|
|
30
|
+
if ('maxUses' in data) {
|
|
31
|
+
/** @type {number | null | undefined} Maximum uses, or null for unlimited. */
|
|
32
|
+
this.maxUses = data.maxUses ?? null;
|
|
33
|
+
}
|
|
34
|
+
if ('expiresAt' in data) {
|
|
35
|
+
/** @type {string | null} ISO expiry timestamp, or null if it never expires. */
|
|
36
|
+
this.expiresAt = data.expiresAt ?? null;
|
|
37
|
+
}
|
|
38
|
+
if ('createdAt' in data) {
|
|
39
|
+
/** @type {string | undefined} ISO creation timestamp. */
|
|
40
|
+
this.createdAt = data.createdAt;
|
|
41
|
+
}
|
|
42
|
+
if (data.house) {
|
|
43
|
+
/** @type {{ id: string, name: string, iconUrl: string | null, accent: string | null } | undefined} Partial House info from a preview. */
|
|
44
|
+
this.house = data.house;
|
|
45
|
+
this.houseId = data.house.id;
|
|
46
|
+
}
|
|
47
|
+
// From listing: `creator { handle, displayName }`. From preview: `inviter { handle, displayName }`.
|
|
48
|
+
if (data.creator || data.inviter) {
|
|
49
|
+
/** @type {{ handle: string, displayName: string }} The user who created the invite. */
|
|
50
|
+
this.inviter = data.creator ?? data.inviter;
|
|
51
|
+
}
|
|
52
|
+
return data;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Whether the invite has expired (based on `expiresAt`). */
|
|
56
|
+
get expired() {
|
|
57
|
+
return this.expiresAt ? Date.parse(this.expiresAt) < Date.now() : false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Redeems this invite for the logged-in account, joining the House.
|
|
62
|
+
* @returns {Promise<import('./House.js').House>}
|
|
63
|
+
*/
|
|
64
|
+
redeem() {
|
|
65
|
+
return this.client.redeemInvite(this.code);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Revokes (deletes) this invite. Requires the invite `id` (present on listed or
|
|
70
|
+
* freshly-created invites).
|
|
71
|
+
* @returns {Promise<void>}
|
|
72
|
+
*/
|
|
73
|
+
async delete() {
|
|
74
|
+
if (!this.id) throw new Error('This invite has no id and cannot be revoked');
|
|
75
|
+
await this.client.rest.delete(Routes.inviteRevoke(this.id));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
toString() {
|
|
79
|
+
return this.code;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export default Invite;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import { Base } from './Base.js';
|
|
4
|
+
import { Routes } from '../rest/Routes.js';
|
|
5
|
+
import { Collection } from '../util/Collection.js';
|
|
6
|
+
import { PermissionsBitField, ALL_PERMISSIONS, PermissionFlags } from '../util/Permissions.js';
|
|
7
|
+
import { resolveId } from '../util/Util.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Represents a member of a House — a user plus their House-specific data (roles,
|
|
11
|
+
* nickname, presence). The member id equals the underlying user id.
|
|
12
|
+
* @extends {Base}
|
|
13
|
+
*/
|
|
14
|
+
export class Member extends Base {
|
|
15
|
+
/**
|
|
16
|
+
* @param {import('../client/Client.js').Client} client
|
|
17
|
+
* @param {import('./House.js').House} house
|
|
18
|
+
* @param {any} data
|
|
19
|
+
*/
|
|
20
|
+
constructor(client, house, data) {
|
|
21
|
+
super(client);
|
|
22
|
+
/** @type {string} The member (and user) id. */
|
|
23
|
+
this.id = data.id;
|
|
24
|
+
/** @type {string} Id of the House this membership belongs to. */
|
|
25
|
+
this.houseId = house.id;
|
|
26
|
+
this._patch(data);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
_patch(data) {
|
|
30
|
+
// Keep the underlying user in the global cache fresh.
|
|
31
|
+
if ('handle' in data || 'displayName' in data) this.client.users._add(data);
|
|
32
|
+
if ('nickname' in data) {
|
|
33
|
+
/**
|
|
34
|
+
* The member's House nickname, if any.
|
|
35
|
+
* @type {string | null}
|
|
36
|
+
* @remarks The current backend members endpoint does not serialize nicknames,
|
|
37
|
+
* so this is typically `null`; {@link Member#displayName} falls back to the user's.
|
|
38
|
+
*/
|
|
39
|
+
this.nickname = data.nickname ?? null;
|
|
40
|
+
} else if (this.nickname === undefined) {
|
|
41
|
+
this.nickname = null;
|
|
42
|
+
}
|
|
43
|
+
if ('roleIds' in data) {
|
|
44
|
+
/** @type {string[]} Ids of the roles assigned to this member. */
|
|
45
|
+
this.roleIds = data.roleIds ?? [];
|
|
46
|
+
} else if (!this.roleIds) {
|
|
47
|
+
this.roleIds = [];
|
|
48
|
+
}
|
|
49
|
+
if ('status' in data) {
|
|
50
|
+
/** @type {string} The member's current presence status. */
|
|
51
|
+
this.status = data.status ?? 'offline';
|
|
52
|
+
}
|
|
53
|
+
return data;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The underlying {@link User}, from the user cache. */
|
|
57
|
+
get user() {
|
|
58
|
+
return this.client.users.cache.get(this.id) ?? null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The House this member belongs to, if cached. */
|
|
62
|
+
get house() {
|
|
63
|
+
return this.client.houses.cache.get(this.houseId) ?? null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The member's effective display name (nickname falls back to the user's). */
|
|
67
|
+
get displayName() {
|
|
68
|
+
return this.nickname ?? this.user?.displayName ?? null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Whether this member owns the House. */
|
|
72
|
+
get isOwner() {
|
|
73
|
+
return this.house?.ownerId === this.id;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The roles assigned to this member (including `@everyone`), resolved from the
|
|
78
|
+
* House role cache.
|
|
79
|
+
* @returns {Collection<string, import('./Role.js').Role>}
|
|
80
|
+
*/
|
|
81
|
+
get roles() {
|
|
82
|
+
const collection = new Collection();
|
|
83
|
+
const house = this.house;
|
|
84
|
+
if (!house) return collection;
|
|
85
|
+
const everyone = house.roles.cache.find((r) => r.isDefault);
|
|
86
|
+
if (everyone) collection.set(everyone.id, everyone);
|
|
87
|
+
for (const id of this.roleIds) {
|
|
88
|
+
const role = house.roles.cache.get(id);
|
|
89
|
+
if (role) collection.set(role.id, role);
|
|
90
|
+
}
|
|
91
|
+
return collection;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The member's computed House-wide permissions. Requires the House's roles to be
|
|
96
|
+
* cached for non-owners; owners and Administrators resolve to all permissions.
|
|
97
|
+
* @returns {PermissionsBitField}
|
|
98
|
+
*/
|
|
99
|
+
get permissions() {
|
|
100
|
+
if (this.isOwner) return new PermissionsBitField(ALL_PERMISSIONS);
|
|
101
|
+
let bits = 0;
|
|
102
|
+
for (const role of this.roles.values()) bits |= role.permissions.bitfield;
|
|
103
|
+
if (bits & PermissionFlags.Administrator) bits = ALL_PERMISSIONS;
|
|
104
|
+
return new PermissionsBitField(bits);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Replaces this member's (non-default) roles. Requires `ManageRoles`.
|
|
109
|
+
* @param {Array<string | import('./Role.js').Role>} roles
|
|
110
|
+
* @returns {Promise<Member>}
|
|
111
|
+
*/
|
|
112
|
+
async setRoles(roles) {
|
|
113
|
+
const roleIds = roles.map(resolveId);
|
|
114
|
+
const result = await this.client.rest.put(
|
|
115
|
+
Routes.memberRoles(this.houseId, this.id),
|
|
116
|
+
{ roleIds },
|
|
117
|
+
);
|
|
118
|
+
this.roleIds = result.roleIds ?? roleIds;
|
|
119
|
+
return this;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Adds one or more roles to this member.
|
|
124
|
+
* @param {...(string | import('./Role.js').Role)} roles
|
|
125
|
+
* @returns {Promise<Member>}
|
|
126
|
+
*/
|
|
127
|
+
addRole(...roles) {
|
|
128
|
+
const next = new Set(this.roleIds);
|
|
129
|
+
for (const role of roles) next.add(resolveId(role));
|
|
130
|
+
return this.setRoles([...next]);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Removes one or more roles from this member.
|
|
135
|
+
* @param {...(string | import('./Role.js').Role)} roles
|
|
136
|
+
* @returns {Promise<Member>}
|
|
137
|
+
*/
|
|
138
|
+
removeRole(...roles) {
|
|
139
|
+
const remove = new Set(roles.map(resolveId));
|
|
140
|
+
return this.setRoles(this.roleIds.filter((id) => !remove.has(id)));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
toString() {
|
|
144
|
+
return this.displayName ?? this.id;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export default Member;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import { Base } from './Base.js';
|
|
4
|
+
import { Routes } from '../rest/Routes.js';
|
|
5
|
+
import { ReactionGroup } from './ReactionGroup.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Represents a message in a room or DM channel.
|
|
9
|
+
* @extends {Base}
|
|
10
|
+
*/
|
|
11
|
+
export class Message extends Base {
|
|
12
|
+
constructor(client, data) {
|
|
13
|
+
super(client);
|
|
14
|
+
/** @type {string} The message id. */
|
|
15
|
+
this.id = data.id;
|
|
16
|
+
this._patch(data);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
_patch(data) {
|
|
20
|
+
if ('roomId' in data) {
|
|
21
|
+
/** @type {string | null} Id of the room this message was sent in, if any. */
|
|
22
|
+
this.roomId = data.roomId ?? null;
|
|
23
|
+
}
|
|
24
|
+
if ('dmChannelId' in data) {
|
|
25
|
+
/** @type {string | null} Id of the DM channel this message was sent in, if any. */
|
|
26
|
+
this.dmChannelId = data.dmChannelId ?? null;
|
|
27
|
+
}
|
|
28
|
+
if ('houseId' in data) {
|
|
29
|
+
/** @type {string | null} Id of the House the room belongs to (null for DMs). */
|
|
30
|
+
this.houseId = data.houseId ?? null;
|
|
31
|
+
}
|
|
32
|
+
if ('content' in data) {
|
|
33
|
+
/** @type {string} The message content (may be E2E ciphertext). */
|
|
34
|
+
this.content = data.content;
|
|
35
|
+
}
|
|
36
|
+
if ('clientNonce' in data) {
|
|
37
|
+
/** @type {string | null} The client nonce echoed back for optimistic dedupe. */
|
|
38
|
+
this.clientNonce = data.clientNonce ?? null;
|
|
39
|
+
}
|
|
40
|
+
if ('pinned' in data) {
|
|
41
|
+
/** @type {boolean} Whether the message is pinned. */
|
|
42
|
+
this.pinned = Boolean(data.pinned);
|
|
43
|
+
}
|
|
44
|
+
if ('createdAt' in data) {
|
|
45
|
+
/** @type {string} ISO timestamp of when the message was sent. */
|
|
46
|
+
this.createdAt = data.createdAt;
|
|
47
|
+
}
|
|
48
|
+
if ('editedAt' in data) {
|
|
49
|
+
/** @type {string | null} ISO timestamp of the last edit, if edited. */
|
|
50
|
+
this.editedAt = data.editedAt ?? null;
|
|
51
|
+
}
|
|
52
|
+
if (data.author) {
|
|
53
|
+
/** @type {string} Id of the message author. */
|
|
54
|
+
this.authorId = data.author.id;
|
|
55
|
+
// Cache/merge the author as a User.
|
|
56
|
+
this.client.users._add(data.author);
|
|
57
|
+
}
|
|
58
|
+
if (Array.isArray(data.reactions)) {
|
|
59
|
+
/** @type {ReactionGroup[]} Aggregated reactions on this message. */
|
|
60
|
+
this.reactions = data.reactions.map((r) => new ReactionGroup(this.client, this, r));
|
|
61
|
+
} else if (!this.reactions) {
|
|
62
|
+
this.reactions = [];
|
|
63
|
+
}
|
|
64
|
+
return data;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The author of this message, resolved from the user cache. */
|
|
68
|
+
get author() {
|
|
69
|
+
return this.client.users.cache.get(this.authorId) ?? null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The channel (room or DM) this message belongs to, if cached. */
|
|
73
|
+
get channel() {
|
|
74
|
+
if (this.roomId) return this.client.channels.cache.get(this.roomId) ?? null;
|
|
75
|
+
if (this.dmChannelId) return this.client.dms.cache.get(this.dmChannelId) ?? null;
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The room this message belongs to, if any and cached. */
|
|
80
|
+
get room() {
|
|
81
|
+
return this.roomId ? (this.client.channels.cache.get(this.roomId) ?? null) : null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Millisecond timestamp of when the message was created. */
|
|
85
|
+
get createdTimestamp() {
|
|
86
|
+
return this.createdAt ? Date.parse(this.createdAt) : null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Whether the message was edited. */
|
|
90
|
+
get edited() {
|
|
91
|
+
return this.editedAt != null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Sends a message to the same channel this message is in.
|
|
96
|
+
* @param {string | import('../managers/MessageManager.js').MessagePayload} content
|
|
97
|
+
* @returns {Promise<Message>}
|
|
98
|
+
*/
|
|
99
|
+
reply(content) {
|
|
100
|
+
const target = this.roomId ? { roomId: this.roomId } : { dmChannelId: this.dmChannelId };
|
|
101
|
+
return this.client._sendMessage(target, content);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Edits this message (author only). Emits over the gateway; the updated content
|
|
106
|
+
* is also delivered via `messageUpdate`.
|
|
107
|
+
* @param {string} content
|
|
108
|
+
* @returns {Promise<Message>}
|
|
109
|
+
*/
|
|
110
|
+
async edit(content) {
|
|
111
|
+
await this.client.gateway.request('message:edit', { messageId: this.id, content });
|
|
112
|
+
this.content = content;
|
|
113
|
+
this.editedAt = new Date().toISOString();
|
|
114
|
+
return this;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Deletes this message.
|
|
119
|
+
* @returns {Promise<Message>}
|
|
120
|
+
*/
|
|
121
|
+
async delete() {
|
|
122
|
+
await this.client.gateway.request('message:delete', { messageId: this.id });
|
|
123
|
+
return this;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Pins this message (room messages only). Requires `ManageMessages`.
|
|
128
|
+
* @returns {Promise<Message>}
|
|
129
|
+
*/
|
|
130
|
+
async pin() {
|
|
131
|
+
return this.#setPinned(true);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Unpins this message.
|
|
136
|
+
* @returns {Promise<Message>}
|
|
137
|
+
*/
|
|
138
|
+
async unpin() {
|
|
139
|
+
return this.#setPinned(false);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async #setPinned(pinned) {
|
|
143
|
+
if (!this.roomId) throw new Error('Only room messages can be pinned');
|
|
144
|
+
const { message } = await this.client.rest.post(Routes.roomPin(this.roomId, this.id), {
|
|
145
|
+
pinned,
|
|
146
|
+
});
|
|
147
|
+
this._patch(message);
|
|
148
|
+
return this;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Adds a reaction from the client user.
|
|
153
|
+
* @param {string} emoji
|
|
154
|
+
* @returns {Promise<Message>}
|
|
155
|
+
*/
|
|
156
|
+
async react(emoji) {
|
|
157
|
+
await this.client.gateway.request('reaction:toggle', { messageId: this.id, emoji, add: true });
|
|
158
|
+
return this;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Removes the client user's reaction.
|
|
163
|
+
* @param {string} emoji
|
|
164
|
+
* @returns {Promise<Message>}
|
|
165
|
+
*/
|
|
166
|
+
async unreact(emoji) {
|
|
167
|
+
await this.client.gateway.request('reaction:toggle', { messageId: this.id, emoji, add: false });
|
|
168
|
+
return this;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
toString() {
|
|
172
|
+
return this.content;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export default Message;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* An aggregated reaction on a {@link Message}: one emoji and the users who reacted
|
|
5
|
+
* with it. Mirrors the backend's `{ emoji, count, userIds }` shape.
|
|
6
|
+
*/
|
|
7
|
+
export class ReactionGroup {
|
|
8
|
+
/**
|
|
9
|
+
* @param {import('../client/Client.js').Client} client
|
|
10
|
+
* @param {import('./Message.js').Message} message
|
|
11
|
+
* @param {{ emoji: string, count: number, userIds: string[] }} data
|
|
12
|
+
*/
|
|
13
|
+
constructor(client, message, data) {
|
|
14
|
+
Object.defineProperty(this, 'client', { value: client });
|
|
15
|
+
Object.defineProperty(this, 'message', { value: message });
|
|
16
|
+
/** @type {string} The reaction emoji. */
|
|
17
|
+
this.emoji = data.emoji;
|
|
18
|
+
/** @type {number} How many users reacted with this emoji. */
|
|
19
|
+
this.count = data.count;
|
|
20
|
+
/** @type {string[]} Ids of the users who reacted. */
|
|
21
|
+
this.userIds = data.userIds ?? [];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Whether the logged-in account reacted with this emoji. */
|
|
25
|
+
get me() {
|
|
26
|
+
const id = this.client.user?.id;
|
|
27
|
+
return id ? this.userIds.includes(id) : false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The reacting users that are present in the user cache. */
|
|
31
|
+
get users() {
|
|
32
|
+
return this.userIds
|
|
33
|
+
.map((id) => this.client.users.cache.get(id))
|
|
34
|
+
.filter((u) => Boolean(u));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
toJSON() {
|
|
38
|
+
return { emoji: this.emoji, count: this.count, userIds: this.userIds };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default ReactionGroup;
|