RoleManager.js
10.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
'use strict';
const process = require('node:process');
const { Collection } = require('@discordjs/collection');
const CachedManager = require('./CachedManager');
const { TypeError } = require('../errors');
const { Role } = require('../structures/Role');
const DataResolver = require('../util/DataResolver');
const Permissions = require('../util/Permissions');
const { resolveColor, setPosition } = require('../util/Util');
let cacheWarningEmitted = false;
/**
* Manages API methods for roles and stores their cache.
* @extends {CachedManager}
*/
class RoleManager extends CachedManager {
constructor(guild, iterable) {
super(guild.client, Role, iterable);
if (!cacheWarningEmitted && this._cache.constructor.name !== 'Collection') {
cacheWarningEmitted = true;
process.emitWarning(
`Overriding the cache handling for ${this.constructor.name} is unsupported and breaks functionality.`,
'UnsupportedCacheOverwriteWarning',
);
}
/**
* The guild belonging to this manager
* @type {Guild}
*/
this.guild = guild;
}
/**
* The role cache of this manager
* @type {Collection<Snowflake, Role>}
* @name RoleManager#cache
*/
_add(data, cache) {
return super._add(data, cache, { extras: [this.guild] });
}
/**
* Obtains a role from Discord, or the role cache if they're already available.
* @param {Snowflake} [id] The role's id
* @param {BaseFetchOptions} [options] Additional options for this fetch
* @returns {Promise<?Role|Collection<Snowflake, Role>>}
* @example
* // Fetch all roles from the guild
* message.guild.roles.fetch()
* .then(roles => console.log(`There are ${roles.size} roles.`))
* .catch(console.error);
* @example
* // Fetch a single role
* message.guild.roles.fetch('222078108977594368')
* .then(role => console.log(`The role color is: ${role.color}`))
* .catch(console.error);
*/
async fetch(id, { cache = true, force = false } = {}) {
if (id && !force) {
const existing = this.cache.get(id);
if (existing) return existing;
}
// We cannot fetch a single role, as of this commit's date, Discord API throws with 405
const data = await this.client.api.guilds(this.guild.id).roles.get();
const roles = new Collection();
for (const role of data) roles.set(role.id, this._add(role, cache));
return id ? roles.get(id) ?? null : roles;
}
/**
* Data that can be resolved to a Role object. This can be:
* * A Role
* * A Snowflake
* @typedef {Role|Snowflake} RoleResolvable
*/
/**
* Resolves a {@link RoleResolvable} to a {@link Role} object.
* @method resolve
* @memberof RoleManager
* @instance
* @param {RoleResolvable} role The role resolvable to resolve
* @returns {?Role}
*/
/**
* Resolves a {@link RoleResolvable} to a {@link Role} id.
* @method resolveId
* @memberof RoleManager
* @instance
* @param {RoleResolvable} role The role resolvable to resolve
* @returns {?Snowflake}
*/
/**
* Options used to create a new role.
* @typedef {Object} CreateRoleOptions
* @property {string} [name] The name of the new role
* @property {ColorResolvable} [color] The data to create the role with
* @property {boolean} [hoist] Whether or not the new role should be hoisted
* @property {PermissionResolvable} [permissions] The permissions for the new role
* @property {number} [position] The position of the new role
* @property {boolean} [mentionable] Whether or not the new role should be mentionable
* @property {?(BufferResolvable|Base64Resolvable|EmojiResolvable)} [icon] The icon for the role
* <warn>The `EmojiResolvable` should belong to the same guild as the role.
* If not, pass the emoji's URL directly</warn>
* @property {?string} [unicodeEmoji] The unicode emoji for the role
* @property {string} [reason] The reason for creating this role
*/
/**
* Creates a new role in the guild with given information.
* <warn>The position will silently reset to 1 if an invalid one is provided, or none.</warn>
* @param {CreateRoleOptions} [options] Options for creating the new role
* @returns {Promise<Role>}
* @example
* // Create a new role
* guild.roles.create()
* .then(console.log)
* .catch(console.error);
* @example
* // Create a new role with data and a reason
* guild.roles.create({
* name: 'Super Cool Blue People',
* color: 'BLUE',
* reason: 'we needed a role for Super Cool People',
* })
* .then(console.log)
* .catch(console.error);
*/
async create(options = {}) {
let { name, color, hoist, permissions, position, mentionable, reason, icon, unicodeEmoji } = options;
color &&= resolveColor(color);
if (typeof permissions !== 'undefined') permissions = new Permissions(permissions);
if (icon) {
const guildEmojiURL = this.guild.emojis.resolve(icon)?.url;
icon = guildEmojiURL ? await DataResolver.resolveImage(guildEmojiURL) : await DataResolver.resolveImage(icon);
if (typeof icon !== 'string') icon = undefined;
}
const data = await this.client.api.guilds(this.guild.id).roles.post({
data: {
name,
color,
hoist,
permissions,
mentionable,
icon,
unicode_emoji: unicodeEmoji,
},
reason,
});
const { role } = this.client.actions.GuildRoleCreate.handle({
guild_id: this.guild.id,
role: data,
});
if (position) return role.setPosition(position, reason);
return role;
}
/**
* Edits a role of the guild.
* @param {RoleResolvable} role The role to edit
* @param {RoleData} data The new data for the role
* @param {string} [reason] Reason for editing this role
* @returns {Promise<Role>}
* @example
* // Edit a role
* guild.roles.edit('222079219327434752', { name: 'buddies' })
* .then(updated => console.log(`Edited role name to ${updated.name}`))
* .catch(console.error);
*/
async edit(role, data, reason) {
role = this.resolve(role);
if (!role) throw new TypeError('INVALID_TYPE', 'role', 'RoleResolvable');
if (typeof data.position === 'number') {
const updatedRoles = await setPosition(
role,
data.position,
false,
this.guild._sortedRoles(),
this.client.api.guilds(this.guild.id).roles,
reason,
);
this.client.actions.GuildRolesPositionUpdate.handle({
guild_id: this.guild.id,
roles: updatedRoles,
});
}
let icon = data.icon;
if (icon) {
const guildEmojiURL = this.guild.emojis.resolve(icon)?.url;
icon = guildEmojiURL ? await DataResolver.resolveImage(guildEmojiURL) : await DataResolver.resolveImage(icon);
if (typeof icon !== 'string') icon = undefined;
}
const _data = {
name: data.name,
color: typeof data.color === 'undefined' ? undefined : resolveColor(data.color),
hoist: data.hoist,
permissions: typeof data.permissions === 'undefined' ? undefined : new Permissions(data.permissions),
mentionable: data.mentionable,
icon,
unicode_emoji: data.unicodeEmoji,
};
const d = await this.client.api.guilds(this.guild.id).roles(role.id).patch({ data: _data, reason });
const clone = role._clone();
clone._patch(d);
return clone;
}
/**
* Deletes a role.
* @param {RoleResolvable} role The role to delete
* @param {string} [reason] Reason for deleting the role
* @returns {Promise<void>}
* @example
* // Delete a role
* guild.roles.delete('222079219327434752', 'The role needed to go')
* .then(deleted => console.log(`Deleted role ${deleted.name}`))
* .catch(console.error);
*/
async delete(role, reason) {
const id = this.resolveId(role);
await this.client.api.guilds[this.guild.id].roles[id].delete({ reason });
this.client.actions.GuildRoleDelete.handle({ guild_id: this.guild.id, role_id: id });
}
/**
* Batch-updates the guild's role positions
* @param {GuildRolePosition[]} rolePositions Role positions to update
* @returns {Promise<Guild>}
* @example
* guild.roles.setPositions([{ role: roleId, position: updatedRoleIndex }])
* .then(guild => console.log(`Role positions updated for ${guild}`))
* .catch(console.error);
*/
async setPositions(rolePositions) {
// Make sure rolePositions are prepared for API
rolePositions = rolePositions.map(o => ({
id: this.resolveId(o.role),
position: o.position,
}));
// Call the API to update role positions
await this.client.api.guilds(this.guild.id).roles.patch({
data: rolePositions,
});
return this.client.actions.GuildRolesPositionUpdate.handle({
guild_id: this.guild.id,
roles: rolePositions,
}).guild;
}
/**
* Compares the positions of two roles.
* @param {RoleResolvable} role1 First role to compare
* @param {RoleResolvable} role2 Second role to compare
* @returns {number} Negative number if the first role's position is lower (second role's is higher),
* positive number if the first's is higher (second's is lower), 0 if equal
*/
comparePositions(role1, role2) {
const resolvedRole1 = this.resolve(role1);
const resolvedRole2 = this.resolve(role2);
if (!resolvedRole1 || !resolvedRole2) throw new TypeError('INVALID_TYPE', 'role', 'Role nor a Snowflake');
if (resolvedRole1.position === resolvedRole2.position) {
return Number(BigInt(resolvedRole2.id) - BigInt(resolvedRole1.id));
}
return resolvedRole1.position - resolvedRole2.position;
}
/**
* Gets the managed role a user created when joining the guild, if any
* <info>Only ever available for bots</info>
* @param {UserResolvable} user The user to access the bot role for
* @returns {?Role}
*/
botRoleFor(user) {
const userId = this.client.users.resolveId(user);
if (!userId) return null;
return this.cache.find(role => role.tags?.botId === userId) ?? null;
}
/**
* The `@everyone` role of the guild
* @type {Role}
* @readonly
*/
get everyone() {
return this.cache.get(this.guild.id);
}
/**
* The premium subscriber role of the guild, if any
* @type {?Role}
* @readonly
*/
get premiumSubscriberRole() {
return this.cache.find(role => role.tags?.premiumSubscriberRole) ?? null;
}
/**
* The role with the highest position in the cache
* @type {Role}
* @readonly
*/
get highest() {
return this.cache.reduce((prev, role) => (role.comparePositionTo(prev) > 0 ? role : prev), this.cache.first());
}
}
module.exports = RoleManager;