@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,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;