Initial commit

This commit is contained in:
March 7th
2022-03-19 17:37:45 +07:00
commit ac49705f3e
282 changed files with 39756 additions and 0 deletions

View File

@@ -0,0 +1,217 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const { Routes } = require('discord-api-types/v9');
const ApplicationCommandPermissionsManager = require('./ApplicationCommandPermissionsManager');
const CachedManager = require('./CachedManager');
const { TypeError } = require('../errors');
const ApplicationCommand = require('../structures/ApplicationCommand');
/**
* Manages API methods for application commands and stores their cache.
* @extends {CachedManager}
*/
class ApplicationCommandManager extends CachedManager {
constructor(client, iterable) {
super(client, ApplicationCommand, iterable);
/**
* The manager for permissions of arbitrary commands on arbitrary guilds
* @type {ApplicationCommandPermissionsManager}
*/
this.permissions = new ApplicationCommandPermissionsManager(this);
}
/**
* The cache of this manager
* @type {Collection<Snowflake, ApplicationCommand>}
* @name ApplicationCommandManager#cache
*/
_add(data, cache, guildId) {
return super._add(data, cache, { extras: [this.guild, guildId] });
}
/**
* The APIRouter path to the commands
* @param {Snowflake} [options.id] The application command's id
* @param {Snowflake} [options.guildId] The guild's id to use in the path,
* ignored when using a {@link GuildApplicationCommandManager}
* @returns {string}
* @private
*/
commandPath({ id, guildId } = {}) {
let path = this.client.api.applications(this.client.application.id);
if(this.guild ?? guildId) path = path.guilds(this.guild?.id ?? guildId);
return id ? path.commands(id) : path.commands;
}
/**
* Data that resolves to give an ApplicationCommand object. This can be:
* * An ApplicationCommand object
* * A Snowflake
* @typedef {ApplicationCommand|Snowflake} ApplicationCommandResolvable
*/
/**
* Options used to fetch data from Discord
* @typedef {Object} BaseFetchOptions
* @property {boolean} [cache=true] Whether to cache the fetched data if it wasn't already
* @property {boolean} [force=false] Whether to skip the cache check and request the API
*/
/**
* Options used to fetch Application Commands from Discord
* @typedef {BaseFetchOptions} FetchApplicationCommandOptions
* @property {Snowflake} [guildId] The guild's id to fetch commands for, for when the guild is not cached
*/
/**
* Obtains one or multiple application commands from Discord, or the cache if it's already available.
* @param {Snowflake} [id] The application command's id
* @param {FetchApplicationCommandOptions} [options] Additional options for this fetch
* @returns {Promise<ApplicationCommand|Collection<Snowflake, ApplicationCommand>>}
* @example
* // Fetch a single command
* client.application.commands.fetch('123456789012345678')
* .then(command => console.log(`Fetched command ${command.name}`))
* .catch(console.error);
* @example
* // Fetch all commands
* guild.commands.fetch()
* .then(commands => console.log(`Fetched ${commands.size} commands`))
* .catch(console.error);
*/
async fetch(id, { guildId, cache = true, force = false } = {}) {
if (typeof id === 'object') {
({ guildId, cache = true } = id);
} else if (id) {
if (!force) {
const existing = this.cache.get(id);
if (existing) return existing;
}
const command = await this.commandPath({ id, guildId }).get();
return this._add(command, cache);
}
const data = await this.commandPath({ guildId }).get();
return data.reduce((coll, command) => coll.set(command.id, this._add(command, cache, guildId)), new Collection());
}
/**
* Creates an application command.
* @param {ApplicationCommandData|APIApplicationCommand} command The command
* @param {Snowflake} [guildId] The guild's id to create this command in,
* ignored when using a {@link GuildApplicationCommandManager}
* @returns {Promise<ApplicationCommand>}
* @example
* // Create a new command
* client.application.commands.create({
* name: 'test',
* description: 'A test command',
* })
* .then(console.log)
* .catch(console.error);
*/
async create(command, guildId) {
const data = await this.commandPath({ guildId }).post({
data: this.constructor.transformCommand(command),
});
return this._add(data, true, guildId);
}
/**
* Sets all the commands for this application or guild.
* @param {ApplicationCommandData[]|APIApplicationCommand[]} commands The commands
* @param {Snowflake} [guildId] The guild's id to create the commands in,
* ignored when using a {@link GuildApplicationCommandManager}
* @returns {Promise<Collection<Snowflake, ApplicationCommand>>}
* @example
* // Set all commands to just this one
* client.application.commands.set([
* {
* name: 'test',
* description: 'A test command',
* },
* ])
* .then(console.log)
* .catch(console.error);
* @example
* // Remove all commands
* guild.commands.set([])
* .then(console.log)
* .catch(console.error);
*/
async set(commands, guildId) {
const data = await this.commandPath({ guildId }).put({
data: commands.map(c => this.constructor.transformCommand(c)),
});
return data.reduce((coll, command) => coll.set(command.id, this._add(command, true, guildId)), new Collection());
}
/**
* Edits an application command.
* @param {ApplicationCommandResolvable} command The command to edit
* @param {ApplicationCommandData|APIApplicationCommand} data The data to update the command with
* @param {Snowflake} [guildId] The guild's id where the command registered,
* ignored when using a {@link GuildApplicationCommandManager}
* @returns {Promise<ApplicationCommand>}
* @example
* // Edit an existing command
* client.application.commands.edit('123456789012345678', {
* description: 'New description',
* })
* .then(console.log)
* .catch(console.error);
*/
async edit(command, data, guildId) {
const id = this.resolveId(command);
if (!id) throw new TypeError('INVALID_TYPE', 'command', 'ApplicationCommandResolvable');
const patched = await this.commandPath({ id, guildId }).patch({
body: this.constructor.transformCommand(data),
});
return this._add(patched, true, guildId);
}
/**
* Deletes an application command.
* @param {ApplicationCommandResolvable} command The command to delete
* @param {Snowflake} [guildId] The guild's id where the command is registered,
* ignored when using a {@link GuildApplicationCommandManager}
* @returns {Promise<?ApplicationCommand>}
* @example
* // Delete a command
* guild.commands.delete('123456789012345678')
* .then(console.log)
* .catch(console.error);
*/
async delete(command, guildId) {
const id = this.resolveId(command);
if (!id) throw new TypeError('INVALID_TYPE', 'command', 'ApplicationCommandResolvable');
await this.commandPath({ id, guildId }).delete();
const cached = this.cache.get(id);
this.cache.delete(id);
return cached ?? null;
}
/**
* Transforms an {@link ApplicationCommandData} object into something that can be used with the API.
* @param {ApplicationCommandData|APIApplicationCommand} command The command to transform
* @returns {APIApplicationCommand}
* @private
*/
static transformCommand(command) {
return {
name: command.name,
description: command.description,
type: command.type,
options: command.options?.map(o => ApplicationCommand.transformOption(o)),
default_permission: command.defaultPermission ?? command.default_permission,
};
}
}
module.exports = ApplicationCommandManager;

View File

@@ -0,0 +1,399 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const { RESTJSONErrorCodes, Routes } = require('discord-api-types/v9');
const BaseManager = require('./BaseManager');
const { Error, TypeError } = require('../errors');
/**
* Manages API methods for permissions of Application Commands.
* @extends {BaseManager}
*/
class ApplicationCommandPermissionsManager extends BaseManager {
constructor(manager) {
super(manager.client);
/**
* The manager or command that this manager belongs to
* @type {ApplicationCommandManager|ApplicationCommand}
* @private
*/
this.manager = manager;
/**
* The guild that this manager acts on
* @type {?Guild}
*/
this.guild = manager.guild ?? null;
/**
* The id of the guild that this manager acts on
* @type {?Snowflake}
*/
this.guildId = manager.guildId ?? manager.guild?.id ?? null;
/**
* The id of the command this manager acts on
* @type {?Snowflake}
*/
this.commandId = manager.id ?? null;
}
/**
* The APIRouter path to the commands
* @param {Snowflake} guildId The guild's id to use in the path,
* @param {Snowflake} [commandId] The application command's id
* @returns {string}
* @private
*/
permissionsPath(guildId, commandId) {
return this.client.api.applications(this.client.application.id).guilds(guildId).commands(commandId).permissions;
}
/**
* Data for setting the permissions of an application command.
* @typedef {Object} ApplicationCommandPermissionData
* @property {Snowflake} id The role or user's id
* @property {ApplicationCommandPermissionType|number} type Whether this permission is for a role or a user
* @property {boolean} permission Whether the role or user has the permission to use this command
*/
/**
* The object returned when fetching permissions for an application command.
* @typedef {Object} ApplicationCommandPermissions
* @property {Snowflake} id The role or user's id
* @property {ApplicationCommandPermissionType} type Whether this permission is for a role or a user
* @property {boolean} permission Whether the role or user has the permission to use this command
*/
/**
* Options for managing permissions for one or more Application Commands
* <warn>When passing these options to a manager where `guildId` is `null`,
* `guild` is a required parameter</warn>
* @typedef {Object} BaseApplicationCommandPermissionsOptions
* @property {GuildResolvable} [guild] The guild to modify / check permissions for
* <warn>Ignored when the manager has a non-null `guildId` property</warn>
* @property {ApplicationCommandResolvable} [command] The command to modify / check permissions for
* <warn>Ignored when the manager has a non-null `commandId` property</warn>
*/
/**
* Fetches the permissions for one or multiple commands.
* @param {BaseApplicationCommandPermissionsOptions} [options] Options used to fetch permissions
* @returns {Promise<ApplicationCommandPermissions[]|Collection<Snowflake, ApplicationCommandPermissions[]>>}
* @example
* // Fetch permissions for one command
* guild.commands.permissions.fetch({ command: '123456789012345678' })
* .then(perms => console.log(`Fetched permissions for ${perms.length} users`))
* .catch(console.error);
* @example
* // Fetch permissions for all commands in a guild
* client.application.commands.permissions.fetch({ guild: '123456789012345678' })
* .then(perms => console.log(`Fetched permissions for ${perms.size} commands`))
* .catch(console.error);
*/
async fetch({ guild, command } = {}) {
const { guildId, commandId } = this._validateOptions(guild, command);
if (commandId) {
const data = await this.permissionsPath(guildId, commandId).get();
return data.permissions.map(perm => this.constructor.transformPermissions(perm, true));
}
const data = await this.permissionsPath(guildId).get();
return data.reduce(
(coll, perm) =>
coll.set(
perm.id,
perm.permissions.map(p => this.constructor.transformPermissions(p, true)),
),
new Collection(),
);
}
/**
* Data used for overwriting the permissions for all application commands in a guild.
* @typedef {Object} GuildApplicationCommandPermissionData
* @property {Snowflake} id The command's id
* @property {ApplicationCommandPermissionData[]} permissions The permissions for this command
*/
/**
* Options used to set permissions for one or more Application Commands in a guild
* <warn>One of `command` AND `permissions`, OR `fullPermissions` is required.
* `fullPermissions` is not a valid option when passing to a manager where `commandId` is non-null</warn>
* @typedef {BaseApplicationCommandPermissionsOptions} SetApplicationCommandPermissionsOptions
* @property {ApplicationCommandPermissionData[]} [permissions] The new permissions for the command
* @property {GuildApplicationCommandPermissionData[]} [fullPermissions] The new permissions for all commands
* in a guild <warn>When this parameter is set, `permissions` and `command` are ignored</warn>
*/
/**
* Sets the permissions for one or more commands.
* @param {SetApplicationCommandPermissionsOptions} options Options used to set permissions
* @returns {Promise<ApplicationCommandPermissions[]|Collection<Snowflake, ApplicationCommandPermissions[]>>}
* @example
* // Set the permissions for one command
* client.application.commands.permissions.set({ guild: '892455839386304532', command: '123456789012345678',
* permissions: [
* {
* id: '876543210987654321',
* type: ApplicationCommandOptionType.User,
* permission: false,
* },
* ]})
* .then(console.log)
* .catch(console.error);
* @example
* // Set the permissions for all commands
* guild.commands.permissions.set({ fullPermissions: [
* {
* id: '123456789012345678',
* permissions: [{
* id: '876543210987654321',
* type: ApplicationCommandOptionType.User,
* permission: false,
* }],
* },
* ]})
* .then(console.log)
* .catch(console.error);
*/
async set({ guild, command, permissions, fullPermissions } = {}) {
const { guildId, commandId } = this._validateOptions(guild, command);
if (commandId) {
if (!Array.isArray(permissions)) {
throw new TypeError('INVALID_TYPE', 'permissions', 'Array of ApplicationCommandPermissionData', true);
}
const data = await this.permissionsPath(guildId, commandId).put({
data: { permissions: permissions.map(perm => this.constructor.transformPermissions(perm)) },
});
return data.permissions.map(perm => this.constructor.transformPermissions(perm, true));
}
if (!Array.isArray(fullPermissions)) {
throw new TypeError('INVALID_TYPE', 'fullPermissions', 'Array of GuildApplicationCommandPermissionData', true);
}
const APIPermissions = [];
for (const perm of fullPermissions) {
if (!Array.isArray(perm.permissions)) throw new TypeError('INVALID_ELEMENT', 'Array', 'fullPermissions', perm);
APIPermissions.push({
id: perm.id,
permissions: perm.permissions.map(p => this.constructor.transformPermissions(p)),
});
}
const data = await this.permissionsPath(guildId).put({
data: APIPermissions,
});
return data.reduce(
(coll, perm) =>
coll.set(
perm.id,
perm.permissions.map(p => this.constructor.transformPermissions(p, true)),
),
new Collection(),
);
}
/**
* Options used to add permissions to a command
* <warn>The `command` parameter is not optional when the managers `commandId` is `null`</warn>
* @typedef {BaseApplicationCommandPermissionsOptions} AddApplicationCommandPermissionsOptions
* @property {ApplicationCommandPermissionData[]} permissions The permissions to add to the command
*/
/**
* Add permissions to a command.
* @param {AddApplicationCommandPermissionsOptions} options Options used to add permissions
* @returns {Promise<ApplicationCommandPermissions[]>}
* @example
* // Block a role from the command permissions
* guild.commands.permissions.add({ command: '123456789012345678', permissions: [
* {
* id: '876543211234567890',
* type: ApplicationCommandPermissionType.Role,
* permission: false
* },
* ]})
* .then(console.log)
* .catch(console.error);
*/
async add({ guild, command, permissions }) {
const { guildId, commandId } = this._validateOptions(guild, command);
if (!commandId) throw new TypeError('INVALID_TYPE', 'command', 'ApplicationCommandResolvable');
if (!Array.isArray(permissions)) {
throw new TypeError('INVALID_TYPE', 'permissions', 'Array of ApplicationCommandPermissionData', true);
}
let existing = [];
try {
existing = await this.fetch({ guild: guildId, command: commandId });
} catch (error) {
if (error.code !== RESTJSONErrorCodes.UnknownApplicationCommandPermissions) throw error;
}
const newPermissions = permissions.slice();
for (const perm of existing) {
if (!newPermissions.some(x => x.id === perm.id)) {
newPermissions.push(perm);
}
}
return this.set({ guild: guildId, command: commandId, permissions: newPermissions });
}
/**
* Options used to remove permissions from a command
* <warn>The `command` parameter is not optional when the managers `commandId` is `null`</warn>
* @typedef {BaseApplicationCommandPermissionsOptions} RemoveApplicationCommandPermissionsOptions
* @property {UserResolvable|UserResolvable[]} [users] The user(s) to remove from the command permissions
* <warn>One of `users` or `roles` is required</warn>
* @property {RoleResolvable|RoleResolvable[]} [roles] The role(s) to remove from the command permissions
* <warn>One of `users` or `roles` is required</warn>
*/
/**
* Remove permissions from a command.
* @param {RemoveApplicationCommandPermissionsOptions} options Options used to remove permissions
* @returns {Promise<ApplicationCommandPermissions[]>}
* @example
* // Remove a user permission from this command
* guild.commands.permissions.remove({ command: '123456789012345678', users: '876543210123456789' })
* .then(console.log)
* .catch(console.error);
* @example
* // Remove multiple roles from this command
* guild.commands.permissions.remove({
* command: '123456789012345678', roles: ['876543210123456789', '765432101234567890']
* })
* .then(console.log)
* .catch(console.error);
*/
async remove({ guild, command, users, roles }) {
const { guildId, commandId } = this._validateOptions(guild, command);
if (!commandId) throw new TypeError('INVALID_TYPE', 'command', 'ApplicationCommandResolvable');
if (!users && !roles) throw new TypeError('INVALID_TYPE', 'users OR roles', 'Array or Resolvable', true);
let resolvedIds = [];
if (Array.isArray(users)) {
users.forEach(user => {
const userId = this.client.users.resolveId(user);
if (!userId) throw new TypeError('INVALID_ELEMENT', 'Array', 'users', user);
resolvedIds.push(userId);
});
} else if (users) {
const userId = this.client.users.resolveId(users);
if (!userId) {
throw new TypeError('INVALID_TYPE', 'users', 'Array or UserResolvable');
}
resolvedIds.push(userId);
}
if (Array.isArray(roles)) {
roles.forEach(role => {
if (typeof role === 'string') {
resolvedIds.push(role);
return;
}
if (!this.guild) throw new Error('GUILD_UNCACHED_ROLE_RESOLVE');
const roleId = this.guild.roles.resolveId(role);
if (!roleId) throw new TypeError('INVALID_ELEMENT', 'Array', 'users', role);
resolvedIds.push(roleId);
});
} else if (roles) {
if (typeof roles === 'string') {
resolvedIds.push(roles);
} else {
if (!this.guild) throw new Error('GUILD_UNCACHED_ROLE_RESOLVE');
const roleId = this.guild.roles.resolveId(roles);
if (!roleId) {
throw new TypeError('INVALID_TYPE', 'users', 'Array or RoleResolvable');
}
resolvedIds.push(roleId);
}
}
let existing = [];
try {
existing = await this.fetch({ guild: guildId, command: commandId });
} catch (error) {
if (error.code !== RESTJSONErrorCodes.UnknownApplicationCommandPermissions) throw error;
}
const permissions = existing.filter(perm => !resolvedIds.includes(perm.id));
return this.set({ guild: guildId, command: commandId, permissions });
}
/**
* Options used to check the existence of permissions on a command
* <warn>The `command` parameter is not optional when the managers `commandId` is `null`</warn>
* @typedef {BaseApplicationCommandPermissionsOptions} HasApplicationCommandPermissionsOptions
* @property {UserResolvable|RoleResolvable} permissionId The user or role to check if a permission exists for
* on this command.
*/
/**
* Check whether a permission exists for a user or role
* @param {AddApplicationCommandPermissionsOptions} options Options used to check permissions
* @returns {Promise<boolean>}
* @example
* // Check whether a user has permission to use a command
* guild.commands.permissions.has({ command: '123456789012345678', permissionId: '876543210123456789' })
* .then(console.log)
* .catch(console.error);
*/
async has({ guild, command, permissionId }) {
const { guildId, commandId } = this._validateOptions(guild, command);
if (!commandId) throw new TypeError('INVALID_TYPE', 'command', 'ApplicationCommandResolvable');
if (!permissionId) throw new TypeError('INVALID_TYPE', 'permissionId', 'UserResolvable or RoleResolvable');
let resolvedId = permissionId;
if (typeof permissionId !== 'string') {
resolvedId = this.client.users.resolveId(permissionId);
if (!resolvedId) {
if (!this.guild) throw new Error('GUILD_UNCACHED_ROLE_RESOLVE');
resolvedId = this.guild.roles.resolveId(permissionId);
}
if (!resolvedId) {
throw new TypeError('INVALID_TYPE', 'permissionId', 'UserResolvable or RoleResolvable');
}
}
let existing = [];
try {
existing = await this.fetch({ guild: guildId, command: commandId });
} catch (error) {
if (error.code !== RESTJSONErrorCodes.UnknownApplicationCommandPermissions) throw error;
}
return existing.some(perm => perm.id === resolvedId);
}
_validateOptions(guild, command) {
const guildId = this.guildId ?? this.client.guilds.resolveId(guild);
if (!guildId) throw new Error('GLOBAL_COMMAND_PERMISSIONS');
let commandId = this.commandId;
if (command && !commandId) {
commandId = this.manager.resolveId?.(command);
if (!commandId && this.guild) {
commandId = this.guild.commands.resolveId(command);
}
commandId ??= this.client.application?.commands.resolveId(command);
if (!commandId) {
throw new TypeError('INVALID_TYPE', 'command', 'ApplicationCommandResolvable', true);
}
}
return { guildId, commandId };
}
}
module.exports = ApplicationCommandPermissionsManager;
/* eslint-disable max-len */
/**
* @external APIApplicationCommandPermissions
* @see {@link https://discord.com/developers/docs/interactions/application-commands#application-command-permissions-object-application-command-permissions-structure}
*/

View File

@@ -0,0 +1,80 @@
'use strict';
const CachedManager = require('./CachedManager');
const GuildEmoji = require('../structures/GuildEmoji');
const ReactionEmoji = require('../structures/ReactionEmoji');
const { parseEmoji } = require('../util/Util');
/**
* Holds methods to resolve GuildEmojis and stores their cache.
* @extends {CachedManager}
*/
class BaseGuildEmojiManager extends CachedManager {
constructor(client, iterable) {
super(client, GuildEmoji, iterable);
}
/**
* The cache of GuildEmojis
* @type {Collection<Snowflake, GuildEmoji>}
* @name BaseGuildEmojiManager#cache
*/
/**
* Data that can be resolved into a GuildEmoji object. This can be:
* * A Snowflake
* * A GuildEmoji object
* * A ReactionEmoji object
* @typedef {Snowflake|GuildEmoji|ReactionEmoji} EmojiResolvable
*/
/**
* Resolves an EmojiResolvable to an Emoji object.
* @param {EmojiResolvable} emoji The Emoji resolvable to identify
* @returns {?GuildEmoji}
*/
resolve(emoji) {
if (emoji instanceof ReactionEmoji) return super.resolve(emoji.id);
return super.resolve(emoji);
}
/**
* Resolves an EmojiResolvable to an Emoji id string.
* @param {EmojiResolvable} emoji The Emoji resolvable to identify
* @returns {?Snowflake}
*/
resolveId(emoji) {
if (emoji instanceof ReactionEmoji) return emoji.id;
return super.resolveId(emoji);
}
/**
* Data that can be resolved to give an emoji identifier. This can be:
* * The unicode representation of an emoji
* * The `<a:name:id>`, `<:name:id>`, `a:name:id` or `name:id` emoji identifier string of an emoji
* * An EmojiResolvable
* @typedef {string|EmojiResolvable} EmojiIdentifierResolvable
*/
/**
* Resolves an EmojiResolvable to an emoji identifier.
* @param {EmojiIdentifierResolvable} emoji The emoji resolvable to resolve
* @returns {?string}
*/
resolveIdentifier(emoji) {
const emojiResolvable = this.resolve(emoji);
if (emojiResolvable) return emojiResolvable.identifier;
if (emoji instanceof ReactionEmoji) return emoji.identifier;
if (typeof emoji === 'string') {
const res = parseEmoji(emoji);
if (res?.name.length) {
emoji = `${res.animated ? 'a:' : ''}${res.name}${res.id ? `:${res.id}` : ''}`;
}
if (!emoji.includes('%')) return encodeURIComponent(emoji);
return emoji;
}
return null;
}
}
module.exports = BaseGuildEmojiManager;

View File

@@ -0,0 +1,19 @@
'use strict';
/**
* Manages the API methods of a data model.
* @abstract
*/
class BaseManager {
constructor(client) {
/**
* The client that instantiated this Manager
* @name BaseManager#client
* @type {Client}
* @readonly
*/
Object.defineProperty(this, 'client', { value: client });
}
}
module.exports = BaseManager;

View File

@@ -0,0 +1,75 @@
'use strict';
const CachedManager = require('./CachedManager');
const GuildMember = require('../structures/GuildMember');
const Message = require('../structures/Message');
const ThreadMember = require('../structures/ThreadMember');
const User = require('../structures/User');
/**
* Manages API methods for users and stores their cache.
* @extends {CachedManager}
*/
class BlockedManager extends CachedManager {
constructor(client, iterable) {
super(client, User, iterable);
}
/**
* The cache of this manager
* @type {Collection<Snowflake, User>}
* @name BlockedManager#cache
*/
/**
* Data that resolves to give a User object. This can be:
* * A User object
* * A Snowflake
* * A Message object (resolves to the message author)
* * A GuildMember object
* * A ThreadMember object
* @typedef {User|Snowflake|Message|GuildMember|ThreadMember} UserResolvable
*/
/**
* Resolves a {@link UserResolvable} to a {@link User} object.
* @param {UserResolvable} user The UserResolvable to identify
* @returns {?User}
*/
resolve(user) {
if (user instanceof GuildMember || user instanceof ThreadMember) return user.user;
if (user instanceof Message) return user.author;
return super.resolve(user);
}
/**
* Resolves a {@link UserResolvable} to a {@link User} id.
* @param {UserResolvable} user The UserResolvable to identify
* @returns {?Snowflake}
*/
resolveId(user) {
if (user instanceof ThreadMember) return user.id;
if (user instanceof GuildMember) return user.user.id;
if (user instanceof Message) return user.author.id;
return super.resolveId(user);
}
/**
* Obtains a user from Discord, or the user cache if it's already available.
* @param {UserResolvable} user The user to fetch
* @param {BaseFetchOptions} [options] Additional options for this fetch
* @returns {Promise<User>}
*/
async fetch(user, { cache = true, force = false } = {}) {
const id = this.resolveId(user);
if (!force) {
const existing = this.cache.get(id);
if (existing && !existing.partial) return existing;
}
const data = await this.client.api.users(id).get();
return this._add(data, cache);
}
}
module.exports = BlockedManager;

View File

@@ -0,0 +1,50 @@
'use strict';
const DataManager = require('./DataManager');
/**
* Manages the API methods of a data model with a mutable cache of instances.
* @extends {DataManager}
* @abstract
*/
class CachedManager extends DataManager {
constructor(client, holds, iterable) {
super(client, holds);
Object.defineProperty(this, '_cache', { value: this.client.options.makeCache(this.constructor, this.holds) });
if (iterable) {
for (const item of iterable) {
this._add(item);
}
}
}
/**
* The cache of items for this manager.
* @type {Collection}
* @abstract
*/
get cache() {
return this._cache;
}
_add(data, cache = true, { id, extras = [] } = {}) {
const existing = this.cache.get(id ?? data.id);
if (existing) {
if (cache) {
existing._patch(data);
return existing;
}
const clone = existing._clone();
clone._patch(data);
return clone;
}
const entry = this.holds ? new this.holds(this.client, data, ...extras) : data;
if (cache) this.cache.set(id ?? entry.id, entry);
return entry;
}
}
module.exports = CachedManager;

View File

@@ -0,0 +1,69 @@
'use strict';
const DataManager = require('./DataManager');
const GuildChannel = require('../structures/GuildChannel');
/**
* Manages API methods for CategoryChannels' children.
* @extends {DataManager}
*/
class CategoryChannelChildManager extends DataManager {
constructor(channel) {
super(channel.client, GuildChannel);
/**
* The category channel this manager belongs to
* @type {CategoryChannel}
*/
this.channel = channel;
}
/**
* The channels that are a part of this category
* @type {Collection<Snowflake, GuildChannel>}
* @readonly
*/
get cache() {
return this.guild.channels.cache.filter(c => c.parentId === this.channel.id);
}
/**
* The guild this manager belongs to
* @type {Guild}
* @readonly
*/
get guild() {
return this.channel.guild;
}
/**
* Options for creating a channel using {@link CategoryChannel#createChannel}.
* @typedef {Object} CategoryCreateChannelOptions
* @property {ChannelType} [type=ChannelType.GuildText] The type of the new channel.
* @property {string} [topic] The topic for the new channel
* @property {boolean} [nsfw] Whether the new channel is NSFW
* @property {number} [bitrate] Bitrate of the new channel in bits (only voice)
* @property {number} [userLimit] Maximum amount of users allowed in the new channel (only voice)
* @property {OverwriteResolvable[]|Collection<Snowflake, OverwriteResolvable>} [permissionOverwrites]
* Permission overwrites of the new channel
* @property {number} [position] Position of the new channel
* @property {number} [rateLimitPerUser] The rate limit per user (slowmode) for the new channel in seconds
* @property {string} [rtcRegion] The specific region of the new channel.
* @property {string} [reason] Reason for creating the new channel
*/
/**
* Creates a new channel within this category.
* <info>You cannot create a channel of type {@link ChannelType.GuildCategory} inside a CategoryChannel.</info>
* @param {string} name The name of the new channel
* @param {CategoryCreateChannelOptions} options Options for creating the new channel
* @returns {Promise<GuildChannel>}
*/
create(name, options) {
return this.guild.channels.create(name, {
...options,
parent: this.channel.id,
});
}
}
module.exports = CategoryChannelChildManager;

View File

@@ -0,0 +1,122 @@
'use strict';
const process = require('node:process');
const { Routes } = require('discord-api-types/v9');
const CachedManager = require('./CachedManager');
const { Channel } = require('../structures/Channel');
const { ThreadChannelTypes } = require('../util/Constants');
const Events = require('../util/Events');
let cacheWarningEmitted = false;
/**
* A manager of channels belonging to a client
* @extends {CachedManager}
*/
class ChannelManager extends CachedManager {
constructor(client, iterable) {
super(client, Channel, iterable);
const defaultCaching =
this._cache.constructor.name === 'Collection' ||
this._cache.maxSize === undefined ||
this._cache.maxSize === Infinity;
if (!cacheWarningEmitted && !defaultCaching) {
cacheWarningEmitted = true;
process.emitWarning(
`Overriding the cache handling for ${this.constructor.name} is unsupported and breaks functionality.`,
'UnsupportedCacheOverwriteWarning',
);
}
}
/**
* The cache of Channels
* @type {Collection<Snowflake, Channel>}
* @name ChannelManager#cache
*/
_add(data, guild, { cache = true, allowUnknownGuild = false, fromInteraction = false } = {}) {
const existing = this.cache.get(data.id);
if (existing) {
if (cache) existing._patch(data, fromInteraction);
guild?.channels?._add(existing);
if (ThreadChannelTypes.includes(existing.type)) {
existing.parent?.threads?._add(existing);
}
return existing;
}
const channel = Channel.create(this.client, data, guild, { allowUnknownGuild, fromInteraction });
if (!channel) {
this.client.emit(Events.Debug, `Failed to find guild, or unknown type for channel ${data.id} ${data.type}`);
return null;
}
if (cache && !allowUnknownGuild) this.cache.set(channel.id, channel);
return channel;
}
_remove(id) {
const channel = this.cache.get(id);
channel?.guild?.channels.cache.delete(id);
channel?.parent?.threads?.cache.delete(id);
this.cache.delete(id);
}
/**
* Data that can be resolved to give a Channel object. This can be:
* * A Channel object
* * A Snowflake
* @typedef {Channel|Snowflake} ChannelResolvable
*/
/**
* Resolves a ChannelResolvable to a Channel object.
* @method resolve
* @memberof ChannelManager
* @instance
* @param {ChannelResolvable} channel The channel resolvable to resolve
* @returns {?Channel}
*/
/**
* Resolves a ChannelResolvable to a channel id string.
* @method resolveId
* @memberof ChannelManager
* @instance
* @param {ChannelResolvable} channel The channel resolvable to resolve
* @returns {?Snowflake}
*/
/**
* Options for fetching a channel from Discord
* @typedef {BaseFetchOptions} FetchChannelOptions
* @property {boolean} [allowUnknownGuild=false] Allows the channel to be returned even if the guild is not in cache,
* it will not be cached. <warn>Many of the properties and methods on the returned channel will throw errors</warn>
*/
/**
* Obtains a channel from Discord, or the channel cache if it's already available.
* @param {Snowflake} id The channel's id
* @param {FetchChannelOptions} [options] Additional options for this fetch
* @returns {Promise<?Channel>}
* @example
* // Fetch a channel by its id
* client.channels.fetch('222109930545610754')
* .then(channel => console.log(channel.name))
* .catch(console.error);
*/
async fetch(id, { allowUnknownGuild = false, cache = true, force = false } = {}) {
if (!force) {
const existing = this.cache.get(id);
if (existing && !existing.partial) return existing;
}
const data = await this.client.api.channels(id).get();
return this._add(data, null, { cache, allowUnknownGuild });
}
}
module.exports = ChannelManager;

View File

@@ -0,0 +1,228 @@
'use strict';
const CachedManager = require('./CachedManager');
const { default: Collection } = require('@discordjs/collection');
const { Error } = require('../errors/DJSError');
/**
* Manages API methods for users and stores their cache.
* @extends {CachedManager}
*/
const localeObject = {
DANISH: 'da',
GERMAN: 'de',
ENGLISH_UK: 'en-GB',
ENGLISH_US: 'en-US',
SPANISH: 'es-ES',
FRENCH: 'fr',
CROATIAN: 'hr',
ITALIAN: 'it',
LITHUANIAN: 'lt',
HUNGARIAN: 'hu',
DUTCH: 'nl',
NORWEGIAN: 'no',
POLISH: 'pl',
BRAZILIAN_PORTUGUESE: 'pt-BR',
ROMANIA_ROMANIAN: 'ro',
FINNISH: 'fi',
SWEDISH: 'sv-SE',
VIETNAMESE: 'vi',
TURKISH: 'tr',
CZECH: 'cs',
GREEK: 'el',
BULGARIAN: 'bg',
RUSSIAN: 'ru',
UKRAINIAN: 'uk',
HINDI: 'hi',
THAI: 'th',
CHINA_CHINESE: 'zh-CN',
JAPANESE: 'ja',
TAIWAN_CHINESE: 'zh-TW',
KOREAN: 'ko',
};
class ClientUserSettingManager extends CachedManager {
constructor(client, iterable) {
super(client);
// Raw data
this.rawSetting = {};
// Language
this.locale = null;
// Setting => ACTIVITY SETTINGS => Activity Status => Display current activity as a status message
this.showCurrentGame = null;
// Setting => APP SETTINGS => Accessibility => Automatically play GIFs when Discord is focused.
this.autoplayGIF = null;
// Setting => APP SETTINGS => Appearance => Message Display => Compact Mode [OK]
this.compactMode = null;
// Setting => APP SETTINGS => Text & Images => Emoji => Convert Emoticons
this.convertEmoticons = null;
// Setting => APP SETTINGS => Accessibility => Text-to-speech => Allow playback
this.allowTTS = null;
// Setting => APP SETTINGS => Appearance => Theme [OK]
this.theme = '';
// Setting => APP SETTINGS => Accessibility => Play Animated Emojis
this.animatedEmojis = null;
// Setting => APP SETTINGS => Text & Images => Emoji => Show emoji reactions
this.showEmojiReactions = null;
// Custom Stauts [It's not working now]
this.customStatus = null;
// Guild folder and position
this.guildMetadata = new Collection();
}
_patch(data) {
this.rawSetting = data;
if ('locale' in data) {
this.locale = data.locale;
}
if ('show_current_game' in data) {
this.showCurrentGame = data.show_current_game;
}
if ('gif_auto_play' in data) {
this.autoplayGIF = data.gif_auto_play;
}
if ('message_display_compact' in data) {
this.compactMode = data.message_display_compact;
}
if ('convert_emoticons' in data) {
this.convertEmoticons = data.convert_emoticons;
}
if ('enable_tts_command' in data) {
this.allowTTS = data.enable_tts_command;
}
if ('theme' in data) {
this.theme = data.theme;
}
if ('animate_emoji' in data) {
this.animatedEmojis = data.animate_emoji;
}
if ('render_reactions' in data) {
this.showEmojiReactions = data.render_reactions;
}
if ('custom_status' in data) {
this.customStatus = data.custom_status;
this.customStatus.status = data.status;
}
if ('guild_folders' in data) {
const data_ = data.guild_positions.map((guildId, i) => {
// Find folder
const folderIndex = data.guild_folders.findIndex((obj) =>
obj.guild_ids.includes(guildId),
);
const metadata = {
guildId: guildId,
guildIndex: i,
folderId: data.guild_folders[folderIndex]?.id,
folderIndex,
folderName: data.guild_folders[folderIndex]?.name,
folderColor: data.guild_folders[folderIndex]?.color,
folderGuilds: data.guild_folders[folderIndex]?.guild_ids,
};
return [guildId, metadata];
});
this.guildMetadata = new Collection(data_);
}
}
async fetch() {
if (this.client.bot) throw new Error('INVALID_BOT_METHOD');
try {
const data = await this.client.api.users('@me').settings.get();
this._patch(data);
return this;
} catch (e) {
throw e;
}
}
/**
* Edit data
* @param {Object} data Data to edit
* @private
*/
async edit(data) {
if (this.client.bot) throw new Error('INVALID_BOT_METHOD');
try {
const res = await this.client.api.users('@me').settings.patch({ data });
this._patch(res);
return this;
} catch (e) {
throw e;
}
}
/**
* Set compact mode
* @param {Boolean | null} value Compact mode enable or disable
* @returns {Boolean}
*/
async setDisplayCompactMode(value) {
if (this.client.bot) throw new Error('INVALID_BOT_METHOD');
if (!value) value = !this.compactMode;
if (value !== this.compactMode) {
await this.edit({ message_display_compact: value });
}
return this.compactMode;
}
/**
* Discord Theme
* @param {null |dark |light} value Theme to set
* @returns {theme}
*/
async setTheme(value) {
if (this.client.bot) throw new Error('INVALID_BOT_METHOD');
const validValues = ['dark', 'light'];
if (!validValues.includes(value)) {
value == validValues[0]
? (value = validValues[1])
: (value = validValues[0]);
}
if (value !== this.theme) {
await this.edit({ theme: value });
}
return this.theme;
}
/**
* Locale Setting, must be one of:
* * `DANISH`
* * `GERMAN`
* * `ENGLISH_UK`
* * `ENGLISH_US`
* * `SPANISH`
* * `FRENCH`
* * `CROATIAN`
* * `ITALIAN`
* * `LITHUANIAN`
* * `HUNGARIAN`
* * `DUTCH`
* * `NORWEGIAN`
* * `POLISH`
* * `BRAZILIAN_PORTUGUESE`
* * `ROMANIA_ROMANIAN`
* * `FINNISH`
* * `SWEDISH`
* * `VIETNAMESE`
* * `TURKISH`
* * `CZECH`
* * `GREEK`
* * `BULGARIAN`
* * `RUSSIAN`
* * `UKRAINIAN`
* * `HINDI`
* * `THAI`
* * `CHINA_CHINESE`
* * `JAPANESE`
* * `TAIWAN_CHINESE`
* * `KOREAN`
* @typedef {string} LocaleStrings
*/
/**
*
* @param {LocaleStrings} value
* @returns {locale}
*/
async setLocale(value) {
if (this.client.bot) throw new Error('INVALID_BOT_METHOD');
if (!localeObject[value]) throw new Error('INVALID_LOCALE');
if (localeObject[value] !== this.locale) {
await this.edit({ locale: localeObject[value] });
}
return this.locale;
}
}
module.exports = ClientUserSettingManager;

View File

@@ -0,0 +1,61 @@
'use strict';
const BaseManager = require('./BaseManager');
const { Error } = require('../errors');
/**
* Manages the API methods of a data model along with a collection of instances.
* @extends {BaseManager}
* @abstract
*/
class DataManager extends BaseManager {
constructor(client, holds) {
super(client);
/**
* The data structure belonging to this manager.
* @name DataManager#holds
* @type {Function}
* @private
* @readonly
*/
Object.defineProperty(this, 'holds', { value: holds });
}
/**
* The cache of items for this manager.
* @type {Collection}
* @abstract
*/
get cache() {
throw new Error('NOT_IMPLEMENTED', 'get cache', this.constructor.name);
}
/**
* Resolves a data entry to a data Object.
* @param {string|Object} idOrInstance The id or instance of something in this Manager
* @returns {?Object} An instance from this Manager
*/
resolve(idOrInstance) {
if (idOrInstance instanceof this.holds) return idOrInstance;
if (typeof idOrInstance === 'string') return this.cache.get(idOrInstance) ?? null;
return null;
}
/**
* Resolves a data entry to an instance id.
* @param {string|Object} idOrInstance The id or instance of something in this Manager
* @returns {?Snowflake}
*/
resolveId(idOrInstance) {
if (idOrInstance instanceof this.holds) return idOrInstance.id;
if (typeof idOrInstance === 'string') return idOrInstance;
return null;
}
valueOf() {
return this.cache;
}
}
module.exports = DataManager;

View File

@@ -0,0 +1,75 @@
'use strict';
const CachedManager = require('./CachedManager');
const GuildMember = require('../structures/GuildMember');
const Message = require('../structures/Message');
const ThreadMember = require('../structures/ThreadMember');
const User = require('../structures/User');
/**
* Manages API methods for users and stores their cache.
* @extends {CachedManager}
*/
class FriendsManager extends CachedManager {
constructor(client, iterable) {
super(client, User, iterable);
}
/**
* The cache of this manager
* @type {Collection<Snowflake, User>}
* @name FriendsManager#cache
*/
/**
* Data that resolves to give a User object. This can be:
* * A User object
* * A Snowflake
* * A Message object (resolves to the message author)
* * A GuildMember object
* * A ThreadMember object
* @typedef {User|Snowflake|Message|GuildMember|ThreadMember} UserResolvable
*/
/**
* Resolves a {@link UserResolvable} to a {@link User} object.
* @param {UserResolvable} user The UserResolvable to identify
* @returns {?User}
*/
resolve(user) {
if (user instanceof GuildMember || user instanceof ThreadMember) return user.user;
if (user instanceof Message) return user.author;
return super.resolve(user);
}
/**
* Resolves a {@link UserResolvable} to a {@link User} id.
* @param {UserResolvable} user The UserResolvable to identify
* @returns {?Snowflake}
*/
resolveId(user) {
if (user instanceof ThreadMember) return user.id;
if (user instanceof GuildMember) return user.user.id;
if (user instanceof Message) return user.author.id;
return super.resolveId(user);
}
/**
* Obtains a user from Discord, or the user cache if it's already available.
* @param {UserResolvable} user The user to fetch
* @param {BaseFetchOptions} [options] Additional options for this fetch
* @returns {Promise<User>}
*/
async fetch(user, { cache = true, force = false } = {}) {
const id = this.resolveId(user);
if (!force) {
const existing = this.cache.get(id);
if (existing && !existing.partial) return existing;
}
const data = await this.client.api.users(id).get();
return this._add(data, cache);
}
}
module.exports = FriendsManager;

View File

@@ -0,0 +1,28 @@
'use strict';
const ApplicationCommandManager = require('./ApplicationCommandManager');
const ApplicationCommandPermissionsManager = require('./ApplicationCommandPermissionsManager');
/**
* An extension for guild-specific application commands.
* @extends {ApplicationCommandManager}
*/
class GuildApplicationCommandManager extends ApplicationCommandManager {
constructor(guild, iterable) {
super(guild.client, iterable);
/**
* The guild that this manager belongs to
* @type {Guild}
*/
this.guild = guild;
/**
* The manager for permissions of arbitrary commands on this guild
* @type {ApplicationCommandPermissionsManager}
*/
this.permissions = new ApplicationCommandPermissionsManager(this);
}
}
module.exports = GuildApplicationCommandManager;

View File

@@ -0,0 +1,175 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const { Routes } = require('discord-api-types/v9');
const CachedManager = require('./CachedManager');
const { TypeError, Error } = require('../errors');
const GuildBan = require('../structures/GuildBan');
const { GuildMember } = require('../structures/GuildMember');
/**
* Manages API methods for GuildBans and stores their cache.
* @extends {CachedManager}
*/
class GuildBanManager extends CachedManager {
constructor(guild, iterable) {
super(guild.client, GuildBan, iterable);
/**
* The guild this Manager belongs to
* @type {Guild}
*/
this.guild = guild;
}
/**
* The cache of this Manager
* @type {Collection<Snowflake, GuildBan>}
* @name GuildBanManager#cache
*/
_add(data, cache) {
return super._add(data, cache, { id: data.user.id, extras: [this.guild] });
}
/**
* Data that resolves to give a GuildBan object. This can be:
* * A GuildBan object
* * A User resolvable
* @typedef {GuildBan|UserResolvable} GuildBanResolvable
*/
/**
* Resolves a GuildBanResolvable to a GuildBan object.
* @param {GuildBanResolvable} ban The ban that is in the guild
* @returns {?GuildBan}
*/
resolve(ban) {
return super.resolve(ban) ?? super.resolve(this.client.users.resolveId(ban));
}
/**
* Options used to fetch a single ban from a guild.
* @typedef {BaseFetchOptions} FetchBanOptions
* @property {UserResolvable} user The ban to fetch
*/
/**
* Options used to fetch all bans from a guild.
* @typedef {Object} FetchBansOptions
* @property {boolean} cache Whether or not to cache the fetched bans
*/
/**
* Fetches ban(s) from Discord.
* @param {UserResolvable|FetchBanOptions|FetchBansOptions} [options] Options for fetching guild ban(s)
* @returns {Promise<GuildBan|Collection<Snowflake, GuildBan>>}
* @example
* // Fetch all bans from a guild
* guild.bans.fetch()
* .then(console.log)
* .catch(console.error);
* @example
* // Fetch all bans from a guild without caching
* guild.bans.fetch({ cache: false })
* .then(console.log)
* .catch(console.error);
* @example
* // Fetch a single ban
* guild.bans.fetch('351871113346809860')
* .then(console.log)
* .catch(console.error);
* @example
* // Fetch a single ban without checking cache
* guild.bans.fetch({ user, force: true })
* .then(console.log)
* .catch(console.error)
* @example
* // Fetch a single ban without caching
* guild.bans.fetch({ user, cache: false })
* .then(console.log)
* .catch(console.error);
*/
fetch(options) {
if (!options) return this._fetchMany();
const user = this.client.users.resolveId(options);
if (user) return this._fetchSingle({ user, cache: true });
options.user &&= this.client.users.resolveId(options.user);
if (!options.user) {
if ('cache' in options) return this._fetchMany(options.cache);
return Promise.reject(new Error('FETCH_BAN_RESOLVE_ID'));
}
return this._fetchSingle(options);
}
async _fetchSingle({ user, cache, force = false }) {
if (!force) {
const existing = this.cache.get(user);
if (existing && !existing.partial) return existing;
}
const data = await this.client.api.guilds(this.guild.id).bans(user).get();
return this._add(data, cache);
}
async _fetchMany(cache) {
const data = await this.client.api.guilds(this.guild.id).bans.get();
return data.reduce((col, ban) => col.set(ban.user.id, this._add(ban, cache)), new Collection());
}
/**
* Options used to ban a user from a guild.
* @typedef {Object} BanOptions
* @property {number} [deleteMessageDays] Number of days of messages to delete, must be between 0 and 7, inclusive
* @property {string} [reason] The reason for the ban
*/
/**
* Bans a user from the guild.
* @param {UserResolvable} user The user to ban
* @param {BanOptions} [options] Options for the ban
* @returns {Promise<GuildMember|User|Snowflake>} Result object will be resolved as specifically as possible.
* If the GuildMember cannot be resolved, the User will instead be attempted to be resolved. If that also cannot
* be resolved, the user id will be the result.
* @example
* // Ban a user by id (or with a user/guild member object)
* guild.bans.create('84484653687267328')
* .then(banInfo => console.log(`Banned ${banInfo.user?.tag ?? banInfo.tag ?? banInfo}`))
* .catch(console.error);
*/
async create(user, options = {}) {
if (typeof options !== 'object') throw new TypeError('INVALID_TYPE', 'options', 'object', true);
const id = this.client.users.resolveId(user);
if (!id) throw new Error('BAN_RESOLVE_ID', true);
await this.client.api.guilds(this.guild.id).bans(id).put({
body: { delete_message_days: options.deleteMessageDays },
reason: options.reason,
});
if (user instanceof GuildMember) return user;
const _user = this.client.users.resolve(id);
if (_user) {
return this.guild.members.resolve(_user) ?? _user;
}
return id;
}
/**
* Unbans a user from the guild.
* @param {UserResolvable} user The user to unban
* @param {string} [reason] Reason for unbanning user
* @returns {Promise<?User>}
* @example
* // Unban a user by id (or with a user/guild member object)
* guild.bans.remove('84484653687267328')
* .then(user => console.log(`Unbanned ${user.username} from ${guild.name}`))
* .catch(console.error);
*/
async remove(user, reason) {
const id = this.client.users.resolveId(user);
if (!id) throw new Error('BAN_RESOLVE_ID');
await this.client.api.guilds(this.guild.id).bans(id).delete({ reason });
return this.client.users.resolve(user);
}
}
module.exports = GuildBanManager;

View File

@@ -0,0 +1,443 @@
'use strict';
const process = require('node:process');
const { Collection } = require('@discordjs/collection');
const { ChannelType, Routes } = require('discord-api-types/v9');
const CachedManager = require('./CachedManager');
const ThreadManager = require('./ThreadManager');
const { Error, TypeError } = require('../errors');
const GuildChannel = require('../structures/GuildChannel');
const PermissionOverwrites = require('../structures/PermissionOverwrites');
const ThreadChannel = require('../structures/ThreadChannel');
const Webhook = require('../structures/Webhook');
const { ThreadChannelTypes } = require('../util/Constants');
const DataResolver = require('../util/DataResolver');
const Util = require('../util/Util');
let cacheWarningEmitted = false;
let storeChannelDeprecationEmitted = false;
/**
* Manages API methods for GuildChannels and stores their cache.
* @extends {CachedManager}
*/
class GuildChannelManager extends CachedManager {
constructor(guild, iterable) {
super(guild.client, GuildChannel, iterable);
const defaultCaching =
this._cache.constructor.name === 'Collection' ||
this._cache.maxSize === undefined ||
this._cache.maxSize === Infinity;
if (!cacheWarningEmitted && !defaultCaching) {
cacheWarningEmitted = true;
process.emitWarning(
`Overriding the cache handling for ${this.constructor.name} is unsupported and breaks functionality.`,
'UnsupportedCacheOverwriteWarning',
);
}
/**
* The guild this Manager belongs to
* @type {Guild}
*/
this.guild = guild;
}
/**
* The number of channels in this managers cache excluding thread channels
* that do not count towards a guild's maximum channels restriction.
* @type {number}
* @readonly
*/
get channelCountWithoutThreads() {
return this.cache.reduce((acc, channel) => {
if (ThreadChannelTypes.includes(channel.type)) return acc;
return ++acc;
}, 0);
}
/**
* The cache of this Manager
* @type {Collection<Snowflake, GuildChannel|ThreadChannel>}
* @name GuildChannelManager#cache
*/
_add(channel) {
const existing = this.cache.get(channel.id);
if (existing) return existing;
this.cache.set(channel.id, channel);
return channel;
}
/**
* Data that can be resolved to give a Guild Channel object. This can be:
* * A GuildChannel object
* * A ThreadChannel object
* * A Snowflake
* @typedef {GuildChannel|ThreadChannel|Snowflake} GuildChannelResolvable
*/
/**
* Resolves a GuildChannelResolvable to a Channel object.
* @param {GuildChannelResolvable} channel The GuildChannel resolvable to resolve
* @returns {?(GuildChannel|ThreadChannel)}
*/
resolve(channel) {
if (channel instanceof ThreadChannel) return super.resolve(channel.id);
return super.resolve(channel);
}
/**
* Resolves a GuildChannelResolvable to a channel id.
* @param {GuildChannelResolvable} channel The GuildChannel resolvable to resolve
* @returns {?Snowflake}
*/
resolveId(channel) {
if (channel instanceof ThreadChannel) return super.resolveId(channel.id);
return super.resolveId(channel);
}
/**
* Options used to create a new channel in a guild.
* @typedef {CategoryCreateChannelOptions} GuildChannelCreateOptions
* @property {CategoryChannelResolvable} [parent] Parent of the new channel
*/
/**
* Creates a new channel in the guild.
* @param {string} name The name of the new channel
* @param {GuildChannelCreateOptions} [options={}] Options for creating the new channel
* @returns {Promise<GuildChannel>}
* @example
* // Create a new text channel
* guild.channels.create('new-general', { reason: 'Needed a cool new channel' })
* .then(console.log)
* .catch(console.error);
* @example
* // Create a new channel with permission overwrites
* guild.channels.create('new-voice', {
* type: ChannelType.GuildVoice,
* permissionOverwrites: [
* {
* id: message.author.id,
* deny: [PermissionFlagsBits.ViewChannel],
* },
* ],
* })
*/
async create(
name,
{
type,
topic,
nsfw,
bitrate,
userLimit,
parent,
permissionOverwrites,
position,
rateLimitPerUser,
rtcRegion,
reason,
} = {},
) {
parent &&= this.client.channels.resolveId(parent);
permissionOverwrites &&= permissionOverwrites.map(o => PermissionOverwrites.resolve(o, this.guild));
if (type === ChannelType.GuildStore && !storeChannelDeprecationEmitted) {
storeChannelDeprecationEmitted = true;
process.emitWarning(
// eslint-disable-next-line max-len
'Creating store channels is deprecated by Discord and will stop working in March 2022. Check the docs for more info.',
'DeprecationWarning',
);
}
const data = await this.client.api.guilds(this.guild.id).channels.post({
body: {
name,
topic,
type,
nsfw,
bitrate,
user_limit: userLimit,
parent_id: parent,
position,
permission_overwrites: permissionOverwrites,
rate_limit_per_user: rateLimitPerUser,
rtc_region: rtcRegion,
},
reason,
});
return this.client.actions.ChannelCreate.handle(data).channel;
}
/**
* Creates a webhook for the channel.
* @param {GuildChannelResolvable} channel The channel to create the webhook for
* @param {string} name The name of the webhook
* @param {ChannelWebhookCreateOptions} [options] Options for creating the webhook
* @returns {Promise<Webhook>} Returns the created Webhook
* @example
* // Create a webhook for the current channel
* guild.channels.createWebhook('222197033908436994', 'Snek', {
* avatar: 'https://i.imgur.com/mI8XcpG.jpg',
* reason: 'Needed a cool new Webhook'
* })
* .then(console.log)
* .catch(console.error)
*/
async createWebhook(channel, name, { avatar, reason } = {}) {
const id = this.resolveId(channel);
if (!id) throw new TypeError('INVALID_TYPE', 'channel', 'GuildChannelResolvable');
if (typeof avatar === 'string' && !avatar.startsWith('data:')) {
avatar = await DataResolver.resolveImage(avatar);
}
const data = await this.client.api.channels(id).webhooks.post({
body: {
name,
avatar,
},
reason,
});
return new Webhook(this.client, data);
}
/**
* The data for a guild channel.
* @typedef {Object} ChannelData
* @property {string} [name] The name of the channel
* @property {ChannelType} [type] The type of the channel (only conversion between text and news is supported)
* @property {number} [position] The position of the channel
* @property {string} [topic] The topic of the text channel
* @property {boolean} [nsfw] Whether the channel is NSFW
* @property {number} [bitrate] The bitrate of the voice channel
* @property {number} [userLimit] The user limit of the voice channel
* @property {?CategoryChannelResolvable} [parent] The parent of the channel
* @property {boolean} [lockPermissions]
* Lock the permissions of the channel to what the parent's permissions are
* @property {OverwriteResolvable[]|Collection<Snowflake, OverwriteResolvable>} [permissionOverwrites]
* Permission overwrites for the channel
* @property {number} [rateLimitPerUser] The rate limit per user (slowmode) for the channel in seconds
* @property {ThreadAutoArchiveDuration} [defaultAutoArchiveDuration]
* The default auto archive duration for all new threads in this channel
* @property {?string} [rtcRegion] The RTC region of the channel
*/
/**
* Edits the channel.
* @param {GuildChannelResolvable} channel The channel to edit
* @param {ChannelData} data The new data for the channel
* @param {string} [reason] Reason for editing this channel
* @returns {Promise<GuildChannel>}
* @example
* // Edit a channel
* guild.channels.edit('222197033908436994', { name: 'new-channel' })
* .then(console.log)
* .catch(console.error);
*/
async edit(channel, data, reason) {
channel = this.resolve(channel);
if (!channel) throw new TypeError('INVALID_TYPE', 'channel', 'GuildChannelResolvable');
const parent = data.parent && this.client.channels.resolveId(data.parent);
if (typeof data.position !== 'undefined') await this.setPosition(channel, data.position, { reason });
let permission_overwrites = data.permissionOverwrites?.map(o => PermissionOverwrites.resolve(o, this.guild));
if (data.lockPermissions) {
if (parent) {
const newParent = this.guild.channels.resolve(parent);
if (newParent?.type === ChannelType.GuildCategory) {
permission_overwrites = newParent.permissionOverwrites.cache.map(o =>
PermissionOverwrites.resolve(o, this.guild),
);
}
} else if (channel.parent) {
permission_overwrites = this.parent.permissionOverwrites.cache.map(o =>
PermissionOverwrites.resolve(o, this.guild),
);
}
}
const newData = await this.client.api.channels(channel.id).patch({
body: {
name: (data.name ?? channel.name).trim(),
type: data.type,
topic: data.topic,
nsfw: data.nsfw,
bitrate: data.bitrate ?? channel.bitrate,
user_limit: data.userLimit ?? channel.userLimit,
rtc_region: data.rtcRegion ?? channel.rtcRegion,
parent_id: parent,
lock_permissions: data.lockPermissions,
rate_limit_per_user: data.rateLimitPerUser,
default_auto_archive_duration: data.defaultAutoArchiveDuration,
permission_overwrites,
},
reason,
})
return this.client.actions.ChannelUpdate.handle(newData).updated;
}
/**
* Sets a new position for the guild channel.
* @param {GuildChannelResolvable} channel The channel to set the position for
* @param {number} position The new position for the guild channel
* @param {SetChannelPositionOptions} [options] Options for setting position
* @returns {Promise<GuildChannel>}
* @example
* // Set a new channel position
* guild.channels.setPosition('222078374472843266', 2)
* .then(newChannel => console.log(`Channel's new position is ${newChannel.position}`))
* .catch(console.error);
*/
async setPosition(channel, position, { relative, reason } = {}) {
channel = this.resolve(channel);
if (!channel) throw new TypeError('INVALID_TYPE', 'channel', 'GuildChannelResolvable');
const updatedChannels = await Util.setPosition(
channel,
position,
relative,
this.guild._sortedChannels(channel),
this.client,
Routes.guildChannels(this.guild.id),
reason,
);
this.client.actions.GuildChannelsPositionUpdate.handle({
guild_id: this.guild.id,
channels: updatedChannels,
});
return channel;
}
/**
* Obtains one or more guild channels from Discord, or the channel cache if they're already available.
* @param {Snowflake} [id] The channel's id
* @param {BaseFetchOptions} [options] Additional options for this fetch
* @returns {Promise<?GuildChannel|Collection<Snowflake, GuildChannel>>}
* @example
* // Fetch all channels from the guild (excluding threads)
* message.guild.channels.fetch()
* .then(channels => console.log(`There are ${channels.size} channels.`))
* .catch(console.error);
* @example
* // Fetch a single channel
* message.guild.channels.fetch('222197033908436994')
* .then(channel => console.log(`The channel name is: ${channel.name}`))
* .catch(console.error);
*/
async fetch(id, { cache = true, force = false } = {}) {
if (id && !force) {
const existing = this.cache.get(id);
if (existing) return existing;
}
if (id) {
const data = await this.client.api.channels(id).get();
// Since this is the guild manager, throw if on a different guild
if (this.guild.id !== data.guild_id) throw new Error('GUILD_CHANNEL_UNOWNED');
return this.client.channels._add(data, this.guild, { cache });
}
const data = await this.client.api.guilds(this.guild.id).channels.get();
const channels = new Collection();
for (const channel of data) channels.set(channel.id, this.client.channels._add(channel, this.guild, { cache }));
return channels;
}
/**
* Fetches all webhooks for the channel.
* @param {GuildChannelResolvable} channel The channel to fetch webhooks for
* @returns {Promise<Collection<Snowflake, Webhook>>}
* @example
* // Fetch webhooks
* guild.channels.fetchWebhooks('769862166131245066')
* .then(hooks => console.log(`This channel has ${hooks.size} hooks`))
* .catch(console.error);
*/
async fetchWebhooks(channel) {
const id = this.resolveId(channel);
if (!id) throw new TypeError('INVALID_TYPE', 'channel', 'GuildChannelResolvable');
const data = await this.client.api.channels(id).webhooks.get();
return data.reduce((hooks, hook) => hooks.set(hook.id, new Webhook(this.client, hook)), new Collection());
}
/**
* Data that can be resolved to give a Category Channel object. This can be:
* * A CategoryChannel object
* * A Snowflake
* @typedef {CategoryChannel|Snowflake} CategoryChannelResolvable
*/
/**
* The data needed for updating a channel's position.
* @typedef {Object} ChannelPosition
* @property {GuildChannel|Snowflake} channel Channel to update
* @property {number} [position] New position for the channel
* @property {CategoryChannelResolvable} [parent] Parent channel for this channel
* @property {boolean} [lockPermissions] If the overwrites should be locked to the parents overwrites
*/
/**
* Batch-updates the guild's channels' positions.
* <info>Only one channel's parent can be changed at a time</info>
* @param {ChannelPosition[]} channelPositions Channel positions to update
* @returns {Promise<Guild>}
* @example
* guild.channels.setPositions([{ channel: channelId, position: newChannelIndex }])
* .then(guild => console.log(`Updated channel positions for ${guild}`))
* .catch(console.error);
*/
async setPositions(channelPositions) {
channelPositions = channelPositions.map(r => ({
id: this.client.channels.resolveId(r.channel),
position: r.position,
lock_permissions: r.lockPermissions,
parent_id: typeof r.parent !== 'undefined' ? this.channels.resolveId(r.parent) : undefined,
}));
await this.client.api.guilds(this.guild.id).channels.post({ body: channelPositions });
return this.client.actions.GuildChannelsPositionUpdate.handle({
guild_id: this.guild.id,
channels: channelPositions,
}).guild;
}
/**
* Obtains all active thread channels in the guild from Discord
* @param {boolean} [cache=true] Whether to cache the fetched data
* @returns {Promise<FetchedThreads>}
* @example
* // Fetch all threads from the guild
* message.guild.channels.fetchActiveThreads()
* .then(fetched => console.log(`There are ${fetched.threads.size} threads.`))
* .catch(console.error);
*/
async fetchActiveThreads(cache = true) {
const raw = await this.client.api.guilds(this.guild.id).threads.active.get();
return ThreadManager._mapThreads(raw, this.client, { guild: this.guild, cache });
}
/**
* Deletes the channel.
* @param {GuildChannelResolvable} channel The channel to delete
* @param {string} [reason] Reason for deleting this channel
* @returns {Promise<void>}
* @example
* // Delete the channel
* guild.channels.delete('858850993013260338', 'making room for new channels')
* .then(console.log)
* .catch(console.error);
*/
async delete(channel, reason) {
const id = this.resolveId(channel);
if (!id) throw new TypeError('INVALID_TYPE', 'channel', 'GuildChannelResolvable');
await this.client.api.channels(id).delete({ reason });
}
}
module.exports = GuildChannelManager;

View File

@@ -0,0 +1,168 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const { Routes, PermissionFlagsBits } = require('discord-api-types/v9');
const BaseGuildEmojiManager = require('./BaseGuildEmojiManager');
const { Error, TypeError } = require('../errors');
const DataResolver = require('../util/DataResolver');
/**
* Manages API methods for GuildEmojis and stores their cache.
* @extends {BaseGuildEmojiManager}
*/
class GuildEmojiManager extends BaseGuildEmojiManager {
constructor(guild, iterable) {
super(guild.client, iterable);
/**
* The guild this manager belongs to
* @type {Guild}
*/
this.guild = guild;
}
_add(data, cache) {
return super._add(data, cache, { extras: [this.guild] });
}
/**
* Options used for creating an emoji in a guild.
* @typedef {Object} GuildEmojiCreateOptions
* @property {Collection<Snowflake, Role>|RoleResolvable[]} [roles] The roles to limit the emoji to
* @property {string} [reason] The reason for creating the emoji
*/
/**
* Creates a new custom emoji in the guild.
* @param {BufferResolvable|Base64Resolvable} attachment The image for the emoji
* @param {string} name The name for the emoji
* @param {GuildEmojiCreateOptions} [options] Options for creating the emoji
* @returns {Promise<Emoji>} The created emoji
* @example
* // Create a new emoji from a URL
* guild.emojis.create('https://i.imgur.com/w3duR07.png', 'rip')
* .then(emoji => console.log(`Created new emoji with name ${emoji.name}!`))
* .catch(console.error);
* @example
* // Create a new emoji from a file on your computer
* guild.emojis.create('./memes/banana.png', 'banana')
* .then(emoji => console.log(`Created new emoji with name ${emoji.name}!`))
* .catch(console.error);
*/
async create(attachment, name, { roles, reason } = {}) {
attachment = await DataResolver.resolveImage(attachment);
if (!attachment) throw new TypeError('REQ_RESOURCE_TYPE');
const body = { image: attachment, name };
if (roles) {
if (!Array.isArray(roles) && !(roles instanceof Collection)) {
throw new TypeError('INVALID_TYPE', 'options.roles', 'Array or Collection of Roles or Snowflakes', true);
}
body.roles = [];
for (const role of roles.values()) {
const resolvedRole = this.guild.roles.resolveId(role);
if (!resolvedRole) throw new TypeError('INVALID_ELEMENT', 'Array or Collection', 'options.roles', role);
body.roles.push(resolvedRole);
}
}
const emoji = await this.client.api.guilds(this.guild.id).emojis.post({ body, reason });
return this.client.actions.GuildEmojiCreate.handle(this.guild, emoji).emoji;
}
/**
* Obtains one or more emojis from Discord, or the emoji cache if they're already available.
* @param {Snowflake} [id] The emoji's id
* @param {BaseFetchOptions} [options] Additional options for this fetch
* @returns {Promise<GuildEmoji|Collection<Snowflake, GuildEmoji>>}
* @example
* // Fetch all emojis from the guild
* message.guild.emojis.fetch()
* .then(emojis => console.log(`There are ${emojis.size} emojis.`))
* .catch(console.error);
* @example
* // Fetch a single emoji
* message.guild.emojis.fetch('222078108977594368')
* .then(emoji => console.log(`The emoji name is: ${emoji.name}`))
* .catch(console.error);
*/
async fetch(id, { cache = true, force = false } = {}) {
if (id) {
if (!force) {
const existing = this.cache.get(id);
if (existing) return existing;
}
const emoji = await this.client.api.guilds(this.guild.id).emojis(id).get();
return this._add(emoji, cache);
}
const data = await this.client.api.guilds(this.guild.id).emojis.get();
const emojis = new Collection();
for (const emoji of data) emojis.set(emoji.id, this._add(emoji, cache));
return emojis;
}
/**
* Deletes an emoji.
* @param {EmojiResolvable} emoji The Emoji resolvable to delete
* @param {string} [reason] Reason for deleting the emoji
* @returns {Promise<void>}
*/
async delete(emoji, reason) {
const id = this.resolveId(emoji);
if (!id) throw new TypeError('INVALID_TYPE', 'emoji', 'EmojiResolvable', true);
await this.client.api.guilds(this.guild.id).emojis(id).delete({ reason });
}
/**
* Edits an emoji.
* @param {EmojiResolvable} emoji The Emoji resolvable to edit
* @param {GuildEmojiEditData} data The new data for the emoji
* @param {string} [reason] Reason for editing this emoji
* @returns {Promise<GuildEmoji>}
*/
async edit(emoji, data, reason) {
const id = this.resolveId(emoji);
if (!id) throw new TypeError('INVALID_TYPE', 'emoji', 'EmojiResolvable', true);
const roles = data.roles?.map(r => this.guild.roles.resolveId(r));
const newData = await this.client.api.guilds(this.guild.id).emojis(id).patch({
body: {
name: data.name,
roles,
},
reason,
})
const existing = this.cache.get(id);
if (existing) {
const clone = existing._clone();
clone._patch(newData);
return clone;
}
return this._add(newData);
}
/**
* Fetches the author for this emoji
* @param {EmojiResolvable} emoji The emoji to fetch the author of
* @returns {Promise<User>}
*/
async fetchAuthor(emoji) {
emoji = this.resolve(emoji);
if (!emoji) throw new TypeError('INVALID_TYPE', 'emoji', 'EmojiResolvable', true);
if (emoji.managed) {
throw new Error('EMOJI_MANAGED');
}
const { me } = this.guild;
if (!me) throw new Error('GUILD_UNCACHED_ME');
if (!me.permissions.has(PermissionFlagsBits.ManageEmojisAndStickers)) {
throw new Error('MISSING_MANAGE_EMOJIS_AND_STICKERS_PERMISSION', this.guild);
}
const data = await this.client.api.guilds(this.guild.id).emojis(emoji.id).get();
emoji._patch(data);
return emoji.author;
}
}
module.exports = GuildEmojiManager;

View File

@@ -0,0 +1,118 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const DataManager = require('./DataManager');
const { TypeError } = require('../errors');
const { Role } = require('../structures/Role');
/**
* Manages API methods for roles belonging to emojis and stores their cache.
* @extends {DataManager}
*/
class GuildEmojiRoleManager extends DataManager {
constructor(emoji) {
super(emoji.client, Role);
/**
* The emoji belonging to this manager
* @type {GuildEmoji}
*/
this.emoji = emoji;
/**
* The guild belonging to this manager
* @type {Guild}
*/
this.guild = emoji.guild;
}
/**
* The cache of roles belonging to this emoji
* @type {Collection<Snowflake, Role>}
* @readonly
*/
get cache() {
return this.guild.roles.cache.filter(role => this.emoji._roles.includes(role.id));
}
/**
* Adds a role (or multiple roles) to the list of roles that can use this emoji.
* @param {RoleResolvable|RoleResolvable[]|Collection<Snowflake, Role>} roleOrRoles The role or roles to add
* @returns {Promise<GuildEmoji>}
*/
add(roleOrRoles) {
if (!Array.isArray(roleOrRoles) && !(roleOrRoles instanceof Collection)) roleOrRoles = [roleOrRoles];
const resolvedRoles = [];
for (const role of roleOrRoles.values()) {
const resolvedRole = this.guild.roles.resolveId(role);
if (!resolvedRole) {
return Promise.reject(new TypeError('INVALID_ELEMENT', 'Array or Collection', 'roles', role));
}
resolvedRoles.push(resolvedRole);
}
const newRoles = [...new Set(resolvedRoles.concat(...this.cache.keys()))];
return this.set(newRoles);
}
/**
* Removes a role (or multiple roles) from the list of roles that can use this emoji.
* @param {RoleResolvable|RoleResolvable[]|Collection<Snowflake, Role>} roleOrRoles The role or roles to remove
* @returns {Promise<GuildEmoji>}
*/
remove(roleOrRoles) {
if (!Array.isArray(roleOrRoles) && !(roleOrRoles instanceof Collection)) roleOrRoles = [roleOrRoles];
const resolvedRoleIds = [];
for (const role of roleOrRoles.values()) {
const roleId = this.guild.roles.resolveId(role);
if (!roleId) {
return Promise.reject(new TypeError('INVALID_ELEMENT', 'Array or Collection', 'roles', role));
}
resolvedRoleIds.push(roleId);
}
const newRoles = [...this.cache.keys()].filter(id => !resolvedRoleIds.includes(id));
return this.set(newRoles);
}
/**
* Sets the role(s) that can use this emoji.
* @param {Collection<Snowflake, Role>|RoleResolvable[]} roles The roles or role ids to apply
* @returns {Promise<GuildEmoji>}
* @example
* // Set the emoji's roles to a single role
* guildEmoji.roles.set(['391156570408615936'])
* .then(console.log)
* .catch(console.error);
* @example
* // Remove all roles from an emoji
* guildEmoji.roles.set([])
* .then(console.log)
* .catch(console.error);
*/
set(roles) {
return this.emoji.edit({ roles });
}
clone() {
const clone = new this.constructor(this.emoji);
clone._patch([...this.cache.keys()]);
return clone;
}
/**
* Patches the roles for this manager's cache
* @param {Snowflake[]} roles The new roles
* @private
*/
_patch(roles) {
this.emoji._roles = roles;
}
valueOf() {
return this.cache;
}
}
module.exports = GuildEmojiRoleManager;

View File

@@ -0,0 +1,214 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const { Routes } = require('discord-api-types/v9');
const CachedManager = require('./CachedManager');
const { Error } = require('../errors');
const Invite = require('../structures/Invite');
const DataResolver = require('../util/DataResolver');
/**
* Manages API methods for GuildInvites and stores their cache.
* @extends {CachedManager}
*/
class GuildInviteManager extends CachedManager {
constructor(guild, iterable) {
super(guild.client, Invite, iterable);
/**
* The guild this Manager belongs to
* @type {Guild}
*/
this.guild = guild;
}
/**
* The cache of this Manager
* @type {Collection<string, Invite>}
* @name GuildInviteManager#cache
*/
_add(data, cache) {
return super._add(data, cache, { id: data.code, extras: [this.guild] });
}
/**
* Data that resolves to give an Invite object. This can be:
* * An invite code
* * An invite URL
* @typedef {string} InviteResolvable
*/
/**
* Data that can be resolved to a channel that an invite can be created on. This can be:
* * TextChannel
* * VoiceChannel
* * NewsChannel
* * StoreChannel
* * StageChannel
* * Snowflake
* @typedef {TextChannel|VoiceChannel|NewsChannel|StoreChannel|StageChannel|Snowflake}
* GuildInvitableChannelResolvable
*/
/**
* Resolves an InviteResolvable to an Invite object.
* @method resolve
* @memberof GuildInviteManager
* @instance
* @param {InviteResolvable} invite The invite resolvable to resolve
* @returns {?Invite}
*/
/**
* Resolves an InviteResolvable to an invite code string.
* @method resolveId
* @memberof GuildInviteManager
* @instance
* @param {InviteResolvable} invite The invite resolvable to resolve
* @returns {?string}
*/
/**
* Options used to fetch a single invite from a guild.
* @typedef {Object} FetchInviteOptions
* @property {InviteResolvable} code The invite to fetch
* @property {boolean} [cache=true] Whether or not to cache the fetched invite
* @property {boolean} [force=false] Whether to skip the cache check and request the API
*/
/**
* Options used to fetch all invites from a guild.
* @typedef {Object} FetchInvitesOptions
* @property {GuildInvitableChannelResolvable} [channelId]
* The channel to fetch all invites from
* @property {boolean} [cache=true] Whether or not to cache the fetched invites
*/
/**
* Fetches invite(s) from Discord.
* @param {InviteResolvable|FetchInviteOptions|FetchInvitesOptions} [options] Options for fetching guild invite(s)
* @returns {Promise<Invite|Collection<string, Invite>>}
* @example
* // Fetch all invites from a guild
* guild.invites.fetch()
* .then(console.log)
* .catch(console.error);
* @example
* // Fetch all invites from a guild without caching
* guild.invites.fetch({ cache: false })
* .then(console.log)
* .catch(console.error);
* @example
* // Fetch all invites from a channel
* guild.invites.fetch({ channelId: '222197033908436994' })
* .then(console.log)
* .catch(console.error);
* @example
* // Fetch a single invite
* guild.invites.fetch('bRCvFy9')
* .then(console.log)
* .catch(console.error);
* @example
* // Fetch a single invite without checking cache
* guild.invites.fetch({ code: 'bRCvFy9', force: true })
* .then(console.log)
* .catch(console.error)
* @example
* // Fetch a single invite without caching
* guild.invites.fetch({ code: 'bRCvFy9', cache: false })
* .then(console.log)
* .catch(console.error);
*/
fetch(options) {
if (!options) return this._fetchMany();
if (typeof options === 'string') {
const code = DataResolver.resolveInviteCode(options);
if (!code) return Promise.reject(new Error('INVITE_RESOLVE_CODE'));
return this._fetchSingle({ code, cache: true });
}
if (!options.code) {
if (options.channelId) {
const id = this.guild.channels.resolveId(options.channelId);
if (!id) return Promise.reject(new Error('GUILD_CHANNEL_RESOLVE'));
return this._fetchChannelMany(id, options.cache);
}
if ('cache' in options) return this._fetchMany(options.cache);
return Promise.reject(new Error('INVITE_RESOLVE_CODE'));
}
return this._fetchSingle({
...options,
code: DataResolver.resolveInviteCode(options.code),
});
}
async _fetchSingle({ code, cache, force = false }) {
if (!force) {
const existing = this.cache.get(code);
if (existing) return existing;
}
const invites = await this._fetchMany(cache);
const invite = invites.get(code);
if (!invite) throw new Error('INVITE_NOT_FOUND');
return invite;
}
async _fetchMany(cache) {
const data = await this.client.api.guilds(this.guild.id).invites.get();
return data.reduce((col, invite) => col.set(invite.code, this._add(invite, cache)), new Collection());
}
async _fetchChannelMany(channelId, cache) {
const data = await this.client.api.channels(channelId).invites.get();
return data.reduce((col, invite) => col.set(invite.code, this._add(invite, cache)), new Collection());
}
/**
* Create an invite to the guild from the provided channel.
* @param {GuildInvitableChannelResolvable} channel The options for creating the invite from a channel.
* @param {CreateInviteOptions} [options={}] The options for creating the invite from a channel.
* @returns {Promise<Invite>}
* @example
* // Create an invite to a selected channel
* guild.invites.create('599942732013764608')
* .then(console.log)
* .catch(console.error);
*/
async create(
channel,
{ temporary, maxAge, maxUses, unique, targetUser, targetApplication, targetType, reason } = {},
) {
const id = this.guild.channels.resolveId(channel);
if (!id) throw new Error('GUILD_CHANNEL_RESOLVE');
const invite = await this.client.api.channels(id).invites.post({
body: {
temporary,
max_age: maxAge,
max_uses: maxUses,
unique,
target_user_id: this.client.users.resolveId(targetUser),
target_application_id: targetApplication?.id ?? targetApplication?.applicationId ?? targetApplication,
target_type: targetType,
},
reason,
});
return new Invite(this.client, invite);
}
/**
* Deletes an invite.
* @param {InviteResolvable} invite The invite to delete
* @param {string} [reason] Reason for deleting the invite
* @returns {Promise<void>}
*/
async delete(invite, reason) {
const code = DataResolver.resolveInviteCode(invite);
await this.client.api.invites(code).delete({ reason });
}
}
module.exports = GuildInviteManager;

View File

@@ -0,0 +1,281 @@
'use strict';
const process = require('node:process');
const { setTimeout, clearTimeout } = require('node:timers');
const { Collection } = require('@discordjs/collection');
const { Routes } = require('discord-api-types/v9');
const CachedManager = require('./CachedManager');
const { Guild } = require('../structures/Guild');
const GuildChannel = require('../structures/GuildChannel');
const GuildEmoji = require('../structures/GuildEmoji');
const { GuildMember } = require('../structures/GuildMember');
const Invite = require('../structures/Invite');
const OAuth2Guild = require('../structures/OAuth2Guild');
const { Role } = require('../structures/Role');
const DataResolver = require('../util/DataResolver');
const Events = require('../util/Events');
const PermissionsBitField = require('../util/PermissionsBitField');
const SystemChannelFlagsBitField = require('../util/SystemChannelFlagsBitField');
const { resolveColor } = require('../util/Util');
let cacheWarningEmitted = false;
/**
* Manages API methods for Guilds and stores their cache.
* @extends {CachedManager}
*/
class GuildManager extends CachedManager {
constructor(client, iterable) {
super(client, Guild, 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 cache of this Manager
* @type {Collection<Snowflake, Guild>}
* @name GuildManager#cache
*/
/**
* Data that resolves to give a Guild object. This can be:
* * A Guild object
* * A GuildChannel object
* * A GuildEmoji object
* * A Role object
* * A Snowflake
* * An Invite object
* @typedef {Guild|GuildChannel|GuildMember|GuildEmoji|Role|Snowflake|Invite} GuildResolvable
*/
/**
* Partial data for a Role.
* @typedef {Object} PartialRoleData
* @property {Snowflake|number} [id] The role's id, used to set channel overrides,
* this is a placeholder and will be replaced by the API after consumption
* @property {string} [name] The name of the role
* @property {ColorResolvable} [color] The color of the role, either a hex string or a base 10 number
* @property {boolean} [hoist] Whether or not the role should be hoisted
* @property {number} [position] The position of the role
* @property {PermissionResolvable} [permissions] The permissions of the role
* @property {boolean} [mentionable] Whether or not the role should be mentionable
*/
/**
* Partial overwrite data.
* @typedef {Object} PartialOverwriteData
* @property {Snowflake|number} id The id of the {@link Role} or {@link User} this overwrite belongs to
* @property {OverwriteType} [type] The type of this overwrite
* @property {PermissionResolvable} [allow] The permissions to allow
* @property {PermissionResolvable} [deny] The permissions to deny
*/
/**
* Partial data for a Channel.
* @typedef {Object} PartialChannelData
* @property {Snowflake|number} [id] The channel's id, used to set its parent,
* this is a placeholder and will be replaced by the API after consumption
* @property {Snowflake|number} [parentId] The parent id for this channel
* @property {ChannelType|number} [type] The type of the channel
* @property {string} name The name of the channel
* @property {string} [topic] The topic of the text channel
* @property {boolean} [nsfw] Whether the channel is NSFW
* @property {number} [bitrate] The bitrate of the voice channel
* @property {number} [userLimit] The user limit of the channel
* @property {?string} [rtcRegion] The RTC region of the channel
* @property {PartialOverwriteData[]} [permissionOverwrites]
* Overwrites of the channel
* @property {number} [rateLimitPerUser] The rate limit per user (slowmode) of the channel in seconds
*/
/**
* Resolves a GuildResolvable to a Guild object.
* @method resolve
* @memberof GuildManager
* @instance
* @param {GuildResolvable} guild The guild resolvable to identify
* @returns {?Guild}
*/
resolve(guild) {
if (
guild instanceof GuildChannel ||
guild instanceof GuildMember ||
guild instanceof GuildEmoji ||
guild instanceof Role ||
(guild instanceof Invite && guild.guild)
) {
return super.resolve(guild.guild);
}
return super.resolve(guild);
}
/**
* Resolves a {@link GuildResolvable} to a {@link Guild} id string.
* @method resolveId
* @memberof GuildManager
* @instance
* @param {GuildResolvable} guild The guild resolvable to identify
* @returns {?Snowflake}
*/
resolveId(guild) {
if (
guild instanceof GuildChannel ||
guild instanceof GuildMember ||
guild instanceof GuildEmoji ||
guild instanceof Role ||
(guild instanceof Invite && guild.guild)
) {
return super.resolveId(guild.guild.id);
}
return super.resolveId(guild);
}
/**
* Options used to create a guild.
* @typedef {Object} GuildCreateOptions
* @property {Snowflake|number} [afkChannelId] The AFK channel's id
* @property {number} [afkTimeout] The AFK timeout in seconds
* @property {PartialChannelData[]} [channels=[]] The channels for this guild
* @property {DefaultMessageNotificationLevel|number} [defaultMessageNotifications] The default message notifications
* for the guild
* @property {ExplicitContentFilterLevel} [explicitContentFilter] The explicit content filter level for the guild
* @property {?(BufferResolvable|Base64Resolvable)} [icon=null] The icon for the guild
* @property {PartialRoleData[]} [roles=[]] The roles for this guild,
* the first element of this array is used to change properties of the guild's everyone role.
* @property {Snowflake|number} [systemChannelId] The system channel's id
* @property {SystemChannelFlagsResolvable} [systemChannelFlags] The flags of the system channel
* @property {VerificationLevel} [verificationLevel] The verification level for the guild
*/
/**
* Creates a guild.
* <warn>This is only available to bots in fewer than 10 guilds.</warn>
* @param {string} name The name of the guild
* @param {GuildCreateOptions} [options] Options for creating the guild
* @returns {Promise<Guild>} The guild that was created
*/
async create(
name,
{
afkChannelId,
afkTimeout,
channels = [],
defaultMessageNotifications,
explicitContentFilter,
icon = null,
roles = [],
systemChannelId,
systemChannelFlags,
verificationLevel,
} = {},
) {
icon = await DataResolver.resolveImage(icon);
for (const channel of channels) {
channel.parent_id = channel.parentId;
delete channel.parentId;
channel.user_limit = channel.userLimit;
delete channel.userLimit;
channel.rate_limit_per_user = channel.rateLimitPerUser;
delete channel.rateLimitPerUser;
channel.rtc_region = channel.rtcRegion;
delete channel.rtcRegion;
if (!channel.permissionOverwrites) continue;
for (const overwrite of channel.permissionOverwrites) {
overwrite.allow &&= PermissionsBitField.resolve(overwrite.allow).toString();
overwrite.deny &&= PermissionsBitField.resolve(overwrite.deny).toString();
}
channel.permission_overwrites = channel.permissionOverwrites;
delete channel.permissionOverwrites;
}
for (const role of roles) {
role.color &&= resolveColor(role.color);
role.permissions &&= PermissionsBitField.resolve(role.permissions).toString();
}
systemChannelFlags &&= SystemChannelFlagsBitField.resolve(systemChannelFlags);
const data = await this.client.api.guilds.post({
body: {
name,
icon,
verification_level: verificationLevel,
default_message_notifications: defaultMessageNotifications,
explicit_content_filter: explicitContentFilter,
roles,
channels,
afk_channel_id: afkChannelId,
afk_timeout: afkTimeout,
system_channel_id: systemChannelId,
system_channel_flags: systemChannelFlags,
},
});
if (this.client.guilds.cache.has(data.id)) return this.client.guilds.cache.get(data.id);
return new Promise(resolve => {
const handleGuild = guild => {
if (guild.id === data.id) {
clearTimeout(timeout);
this.client.removeListener(Events.GuildCreate, handleGuild);
this.client.decrementMaxListeners();
resolve(guild);
}
};
this.client.incrementMaxListeners();
this.client.on(Events.GuildCreate, handleGuild);
const timeout = setTimeout(() => {
this.client.removeListener(Events.GuildCreate, handleGuild);
this.client.decrementMaxListeners();
resolve(this.client.guilds._add(data));
}, 10_000).unref();
});
}
/**
* Options used to fetch a single guild.
* @typedef {BaseFetchOptions} FetchGuildOptions
* @property {GuildResolvable} guild The guild to fetch
* @property {boolean} [withCounts=true] Whether the approximate member and presence counts should be returned
*/
/**
* Options used to fetch multiple guilds.
* @typedef {Object} FetchGuildsOptions
* @property {Snowflake} [before] Get guilds before this guild id
* @property {Snowflake} [after] Get guilds after this guild id
* @property {number} [limit] Maximum number of guilds to request (1-200)
*/
/**
* Obtains one or multiple guilds from Discord, or the guild cache if it's already available.
* @param {GuildResolvable|FetchGuildOptions|FetchGuildsOptions} [options] The guild's id or options
* @returns {Promise<Guild|Collection<Snowflake, OAuth2Guild>>}
*/
async fetch(options = {}) {
const id = this.resolveId(options) ?? this.resolveId(options.guild);
if (id) {
if (!options.force) {
const existing = this.cache.get(id);
if (existing) return existing;
}
const data = await this.client.api.guilds(id).get({
query: new URLSearchParams({ with_counts: options.withCounts ?? true }),
})
return this._add(data, options.cache);
}
const data = await this.client.api.users('@me').guilds.get({ query: new URLSearchParams(options) });
return data.reduce((coll, guild) => coll.set(guild.id, new OAuth2Guild(this.client, guild)), new Collection());
}
}
module.exports = GuildManager;

View File

@@ -0,0 +1,472 @@
'use strict';
const { Buffer } = require('node:buffer');
const { setTimeout, clearTimeout } = require('node:timers');
const { Collection } = require('@discordjs/collection');
const { DiscordSnowflake } = require('@sapphire/snowflake');
const { Routes, GatewayOpcodes } = require('discord-api-types/v9');
const CachedManager = require('./CachedManager');
const { Error, TypeError, RangeError } = require('../errors');
const BaseGuildVoiceChannel = require('../structures/BaseGuildVoiceChannel');
const { GuildMember } = require('../structures/GuildMember');
const { Role } = require('../structures/Role');
const Events = require('../util/Events');
/**
* Manages API methods for GuildMembers and stores their cache.
* @extends {CachedManager}
*/
class GuildMemberManager extends CachedManager {
constructor(guild, iterable) {
super(guild.client, GuildMember, iterable);
/**
* The guild this manager belongs to
* @type {Guild}
*/
this.guild = guild;
}
/**
* The cache of this Manager
* @type {Collection<Snowflake, GuildMember>}
* @name GuildMemberManager#cache
*/
_add(data, cache = true) {
return super._add(data, cache, { id: data.user.id, extras: [this.guild] });
}
/**
* Data that resolves to give a GuildMember object. This can be:
* * A GuildMember object
* * A User resolvable
* @typedef {GuildMember|UserResolvable} GuildMemberResolvable
*/
/**
* Resolves a {@link GuildMemberResolvable} to a {@link GuildMember} object.
* @param {GuildMemberResolvable} member The user that is part of the guild
* @returns {?GuildMember}
*/
resolve(member) {
const memberResolvable = super.resolve(member);
if (memberResolvable) return memberResolvable;
const userResolvable = this.client.users.resolveId(member);
if (userResolvable) return super.resolve(userResolvable);
return null;
}
/**
* Resolves a {@link GuildMemberResolvable} to a member id.
* @param {GuildMemberResolvable} member The user that is part of the guild
* @returns {?Snowflake}
*/
resolveId(member) {
const memberResolvable = super.resolveId(member);
if (memberResolvable) return memberResolvable;
const userResolvable = this.client.users.resolveId(member);
return this.cache.has(userResolvable) ? userResolvable : null;
}
/**
* Options used to add a user to a guild using OAuth2.
* @typedef {Object} AddGuildMemberOptions
* @property {string} accessToken An OAuth2 access token for the user with the `guilds.join` scope granted to the
* bot's application
* @property {string} [nick] The nickname to give to the member (requires `MANAGE_NICKNAMES`)
* @property {Collection<Snowflake, Role>|RoleResolvable[]} [roles] The roles to add to the member
* (requires `MANAGE_ROLES`)
* @property {boolean} [mute] Whether the member should be muted (requires `MUTE_MEMBERS`)
* @property {boolean} [deaf] Whether the member should be deafened (requires `DEAFEN_MEMBERS`)
* @property {boolean} [force] Whether to skip the cache check and call the API directly
* @property {boolean} [fetchWhenExisting=true] Whether to fetch the user if not cached and already a member
*/
/**
* Adds a user to the guild using OAuth2. Requires the `CREATE_INSTANT_INVITE` permission.
* @param {UserResolvable} user The user to add to the guild
* @param {AddGuildMemberOptions} options Options for adding the user to the guild
* @returns {Promise<GuildMember|null>}
*/
async add(user, options) {
const userId = this.client.users.resolveId(user);
if (!userId) throw new TypeError('INVALID_TYPE', 'user', 'UserResolvable');
if (!options.force) {
const cachedUser = this.cache.get(userId);
if (cachedUser) return cachedUser;
}
const resolvedOptions = {
access_token: options.accessToken,
nick: options.nick,
mute: options.mute,
deaf: options.deaf,
};
if (options.roles) {
if (!Array.isArray(options.roles) && !(options.roles instanceof Collection)) {
throw new TypeError('INVALID_TYPE', 'options.roles', 'Array or Collection of Roles or Snowflakes', true);
}
const resolvedRoles = [];
for (const role of options.roles.values()) {
const resolvedRole = this.guild.roles.resolveId(role);
if (!resolvedRole) throw new TypeError('INVALID_ELEMENT', 'Array or Collection', 'options.roles', role);
resolvedRoles.push(resolvedRole);
}
resolvedOptions.roles = resolvedRoles;
}
const data = await this.client.rest.put(Routes.guildMember(this.guild.id, userId), { body: resolvedOptions });
// Data is an empty buffer if the member is already part of the guild.
return data instanceof Buffer ? (options.fetchWhenExisting === false ? null : this.fetch(userId)) : this._add(data);
}
/**
* Options used to fetch a single member from a guild.
* @typedef {BaseFetchOptions} FetchMemberOptions
* @property {UserResolvable} user The user to fetch
*/
/**
* Options used to fetch multiple members from a guild.
* @typedef {Object} FetchMembersOptions
* @property {UserResolvable|UserResolvable[]} user The user(s) to fetch
* @property {?string} query Limit fetch to members with similar usernames
* @property {number} [limit=0] Maximum number of members to request
* @property {boolean} [withPresences=false] Whether or not to include the presences
* @property {number} [time=120e3] Timeout for receipt of members
* @property {?string} nonce Nonce for this request (32 characters max - default to base 16 now timestamp)
* @property {boolean} [force=false] Whether to skip the cache check and request the API
*/
/**
* Fetches member(s) from Discord, even if they're offline.
* @param {UserResolvable|FetchMemberOptions|FetchMembersOptions} [options] If a UserResolvable, the user to fetch.
* If undefined, fetches all members.
* If a query, it limits the results to users with similar usernames.
* @returns {Promise<GuildMember|Collection<Snowflake, GuildMember>>}
* @example
* // Fetch all members from a guild
* guild.members.fetch()
* .then(console.log)
* .catch(console.error);
* @example
* // Fetch a single member
* guild.members.fetch('66564597481480192')
* .then(console.log)
* .catch(console.error);
* @example
* // Fetch a single member without checking cache
* guild.members.fetch({ user, force: true })
* .then(console.log)
* .catch(console.error)
* @example
* // Fetch a single member without caching
* guild.members.fetch({ user, cache: false })
* .then(console.log)
* .catch(console.error);
* @example
* // Fetch by an array of users including their presences
* guild.members.fetch({ user: ['66564597481480192', '191615925336670208'], withPresences: true })
* .then(console.log)
* .catch(console.error);
* @example
* // Fetch by query
* guild.members.fetch({ query: 'hydra', limit: 1 })
* .then(console.log)
* .catch(console.error);
*/
fetch(options) {
if (!options) return this._fetchMany();
const user = this.client.users.resolveId(options);
if (user) return this._fetchSingle({ user, cache: true });
if (options.user) {
if (Array.isArray(options.user)) {
options.user = options.user.map(u => this.client.users.resolveId(u));
return this._fetchMany(options);
} else {
options.user = this.client.users.resolveId(options.user);
}
if (!options.limit && !options.withPresences) return this._fetchSingle(options);
}
return this._fetchMany(options);
}
/**
* Options used for searching guild members.
* @typedef {Object} GuildSearchMembersOptions
* @property {string} query Filter members whose username or nickname start with this query
* @property {number} [limit] Maximum number of members to search
* @property {boolean} [cache=true] Whether or not to cache the fetched member(s)
*/
/**
* Searches for members in the guild based on a query.
* @param {GuildSearchMembersOptions} options Options for searching members
* @returns {Promise<Collection<Snowflake, GuildMember>>}
*/
async search({ query, limit, cache = true } = {}) {
const data = await this.client.api.guilds(this.guild.id).members.search.get({
query: new URLSearchParams({ query, limit }),
})
return data.reduce((col, member) => col.set(member.user.id, this._add(member, cache)), new Collection());
}
/**
* Options used for listing guild members.
* @typedef {Object} GuildListMembersOptions
* @property {Snowflake} [after] Limit fetching members to those with an id greater than the supplied id
* @property {number} [limit] Maximum number of members to list
* @property {boolean} [cache=true] Whether or not to cache the fetched member(s)
*/
/**
* Lists up to 1000 members of the guild.
* @param {GuildListMembersOptions} [options] Options for listing members
* @returns {Promise<Collection<Snowflake, GuildMember>>}
*/
async list({ after, limit, cache = true } = {}) {
const query = new URLSearchParams({ limit });
if (after) {
query.set('after', after);
}
const data = await this.client.api.guilds(this.guild.id).members.get({ query });
return data.reduce((col, member) => col.set(member.user.id, this._add(member, cache)), new Collection());
}
/**
* The data for editing a guild member.
* @typedef {Object} GuildMemberEditData
* @property {?string} [nick] The nickname to set for the member
* @property {Collection<Snowflake, Role>|RoleResolvable[]} [roles] The roles or role ids to apply
* @property {boolean} [mute] Whether or not the member should be muted
* @property {boolean} [deaf] Whether or not the member should be deafened
* @property {GuildVoiceChannelResolvable|null} [channel] Channel to move the member to
* (if they are connected to voice), or `null` if you want to disconnect them from voice
* @property {DateResolvable|null} [communicationDisabledUntil] The date or timestamp
* for the member's communication to be disabled until. Provide `null` to enable communication again.
*/
/**
* Edits a member of the guild.
* <info>The user must be a member of the guild</info>
* @param {UserResolvable} user The member to edit
* @param {GuildMemberEditData} data The data to edit the member with
* @param {string} [reason] Reason for editing this user
* @returns {Promise<GuildMember>}
*/
async edit(user, data, reason) {
const id = this.client.users.resolveId(user);
if (!id) throw new TypeError('INVALID_TYPE', 'user', 'UserResolvable');
// Clone the data object for immutability
const _data = { ...data };
if (_data.channel) {
_data.channel = this.guild.channels.resolve(_data.channel);
if (!(_data.channel instanceof BaseGuildVoiceChannel)) {
throw new Error('GUILD_VOICE_CHANNEL_RESOLVE');
}
_data.channel_id = _data.channel.id;
_data.channel = undefined;
} else if (_data.channel === null) {
_data.channel_id = null;
_data.channel = undefined;
}
_data.roles &&= _data.roles.map(role => (role instanceof Role ? role.id : role));
_data.communication_disabled_until =
// eslint-disable-next-line eqeqeq
_data.communicationDisabledUntil != null
? new Date(_data.communicationDisabledUntil).toISOString()
: _data.communicationDisabledUntil;
let endpoint = this.client.api.guilds(this.guild.id);
if (id === this.client.user.id) {
const keys = Object.keys(data);
if (keys.length === 1 && keys[0] === 'nick') endpoint = endpoint.members('@me');
else endpoint = endpoint.members(id);
} else {
endpoint = endpoint.members(id);
}
const d = await endpoint.patch({ data: _data, reason });
const clone = this.cache.get(id)?._clone();
clone?._patch(d);
return clone ?? this._add(d, false);
}
/**
* Options used for pruning guild members.
* <info>It's recommended to set {@link GuildPruneMembersOptions#count options.count}
* to `false` for large guilds.</info>
* @typedef {Object} GuildPruneMembersOptions
* @property {number} [days] Number of days of inactivity required to kick
* @property {boolean} [dry=false] Get the number of users that will be kicked, without actually kicking them
* @property {boolean} [count] Whether or not to return the number of users that have been kicked.
* @property {RoleResolvable[]} [roles] Array of roles to bypass the "...and no roles" constraint when pruning
* @property {string} [reason] Reason for this prune
*/
/**
* Prunes members from the guild based on how long they have been inactive.
* @param {GuildPruneMembersOptions} [options] Options for pruning
* @returns {Promise<number|null>} The number of members that were/will be kicked
* @example
* // See how many members will be pruned
* guild.members.prune({ dry: true })
* .then(pruned => console.log(`This will prune ${pruned} people!`))
* .catch(console.error);
* @example
* // Actually prune the members
* guild.members.prune({ days: 1, reason: 'too many people!' })
* .then(pruned => console.log(`I just pruned ${pruned} people!`))
* .catch(console.error);
* @example
* // Include members with a specified role
* guild.members.prune({ days: 7, roles: ['657259391652855808'] })
* .then(pruned => console.log(`I just pruned ${pruned} people!`))
* .catch(console.error);
*/
async prune({ days, dry = false, count: compute_prune_count, roles = [], reason } = {}) {
if (typeof days !== 'number') throw new TypeError('PRUNE_DAYS_TYPE');
const query = { days };
const resolvedRoles = [];
for (const role of roles) {
const resolvedRole = this.guild.roles.resolveId(role);
if (!resolvedRole) {
throw new TypeError('INVALID_ELEMENT', 'Array', 'options.roles', role);
}
resolvedRoles.push(resolvedRole);
}
if (resolvedRoles.length) {
query.include_roles = dry ? resolvedRoles.join(',') : resolvedRoles;
}
const endpoint = this.client.api.guilds(this.guild.id).prune;
const { pruned } = await (dry
? endpoint.get({ query: new URLSearchParams(query), reason })
: endpoint.post({ body: { ...query, compute_prune_count }, reason }));
return pruned;
}
/**
* Kicks a user from the guild.
* <info>The user must be a member of the guild</info>
* @param {UserResolvable} user The member to kick
* @param {string} [reason] Reason for kicking
* @returns {Promise<GuildMember|User|Snowflake>} Result object will be resolved as specifically as possible.
* If the GuildMember cannot be resolved, the User will instead be attempted to be resolved. If that also cannot
* be resolved, the user's id will be the result.
* @example
* // Kick a user by id (or with a user/guild member object)
* guild.members.kick('84484653687267328')
* .then(kickInfo => console.log(`Kicked ${kickInfo.user?.tag ?? kickInfo.tag ?? kickInfo}`))
* .catch(console.error);
*/
async kick(user, reason) {
const id = this.client.users.resolveId(user);
if (!id) return Promise.reject(new TypeError('INVALID_TYPE', 'user', 'UserResolvable'));
await this.clinet.api.guilds(this.guild.id).members(id).delete({ reason });
return this.resolve(user) ?? this.client.users.resolve(user) ?? id;
}
/**
* Bans a user from the guild.
* @param {UserResolvable} user The user to ban
* @param {BanOptions} [options] Options for the ban
* @returns {Promise<GuildMember|User|Snowflake>} Result object will be resolved as specifically as possible.
* If the GuildMember cannot be resolved, the User will instead be attempted to be resolved. If that also cannot
* be resolved, the user id will be the result.
* Internally calls the GuildBanManager#create method.
* @example
* // Ban a user by id (or with a user/guild member object)
* guild.members.ban('84484653687267328')
* .then(banInfo => console.log(`Banned ${banInfo.user?.tag ?? banInfo.tag ?? banInfo}`))
* .catch(console.error);
*/
ban(user, options) {
return this.guild.bans.create(user, options);
}
/**
* Unbans a user from the guild. Internally calls the {@link GuildBanManager#remove} method.
* @param {UserResolvable} user The user to unban
* @param {string} [reason] Reason for unbanning user
* @returns {Promise<User>} The user that was unbanned
* @example
* // Unban a user by id (or with a user/guild member object)
* guild.members.unban('84484653687267328')
* .then(user => console.log(`Unbanned ${user.username} from ${guild.name}`))
* .catch(console.error);
*/
unban(user, reason) {
return this.guild.bans.remove(user, reason);
}
async _fetchSingle({ user, cache, force = false }) {
if (!force) {
const existing = this.cache.get(user);
if (existing && !existing.partial) return existing;
}
const data = await this.client.api.guilds(this.guild.id).members(user).get();
return this._add(data, cache);
}
_fetchMany({
limit = 0,
withPresences: presences = false,
user: user_ids,
query,
time = 120e3,
nonce = DiscordSnowflake.generate().toString(),
} = {}) {
return new Promise((resolve, reject) => {
if (!query && !user_ids) query = '';
if (nonce.length > 32) throw new RangeError('MEMBER_FETCH_NONCE_LENGTH');
this.guild.shard.send({
op: GatewayOpcodes.RequestGuildMembers,
d: {
guild_id: this.guild.id,
presences,
user_ids,
query,
nonce,
limit,
},
});
const fetchedMembers = new Collection();
let i = 0;
const handler = (members, _, chunk) => {
timeout.refresh();
if (chunk.nonce !== nonce) return;
i++;
for (const member of members.values()) {
fetchedMembers.set(member.id, member);
}
if (members.size < 1_000 || (limit && fetchedMembers.size >= limit) || i === chunk.count) {
clearTimeout(timeout);
this.client.removeListener(Events.GuildMembersChunk, handler);
this.client.decrementMaxListeners();
let fetched = fetchedMembers;
if (user_ids && !Array.isArray(user_ids) && fetched.size) fetched = fetched.first();
resolve(fetched);
}
};
const timeout = setTimeout(() => {
this.client.removeListener(Events.GuildMembersChunk, handler);
this.client.decrementMaxListeners();
reject(new Error('GUILD_MEMBERS_TIMEOUT'));
}, time).unref();
this.client.incrementMaxListeners();
this.client.on(Events.GuildMembersChunk, handler);
});
}
}
module.exports = GuildMemberManager;

View File

@@ -0,0 +1,192 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const { Routes } = require('discord-api-types/v9');
const DataManager = require('./DataManager');
const { TypeError } = require('../errors');
const { Role } = require('../structures/Role');
/**
* Manages API methods for roles of a GuildMember and stores their cache.
* @extends {DataManager}
*/
class GuildMemberRoleManager extends DataManager {
constructor(member) {
super(member.client, Role);
/**
* The GuildMember this manager belongs to
* @type {GuildMember}
*/
this.member = member;
/**
* The Guild this manager belongs to
* @type {Guild}
*/
this.guild = member.guild;
}
/**
* The roles of this member
* @type {Collection<Snowflake, Role>}
* @readonly
*/
get cache() {
const everyone = this.guild.roles.everyone;
return this.guild.roles.cache.filter(role => this.member._roles.includes(role.id)).set(everyone.id, everyone);
}
/**
* The role of the member used to hoist them in a separate category in the users list
* @type {?Role}
* @readonly
*/
get hoist() {
const hoistedRoles = this.cache.filter(role => role.hoist);
if (!hoistedRoles.size) return null;
return hoistedRoles.reduce((prev, role) => (role.comparePositionTo(prev) > 0 ? role : prev));
}
/**
* The role of the member used to set their role icon
* @type {?Role}
* @readonly
*/
get icon() {
const iconRoles = this.cache.filter(role => role.icon || role.unicodeEmoji);
if (!iconRoles.size) return null;
return iconRoles.reduce((prev, role) => (role.comparePositionTo(prev) > 0 ? role : prev));
}
/**
* The role of the member used to set their color
* @type {?Role}
* @readonly
*/
get color() {
const coloredRoles = this.cache.filter(role => role.color);
if (!coloredRoles.size) return null;
return coloredRoles.reduce((prev, role) => (role.comparePositionTo(prev) > 0 ? role : prev));
}
/**
* The role of the member with the highest position
* @type {Role}
* @readonly
*/
get highest() {
return this.cache.reduce((prev, role) => (role.comparePositionTo(prev) > 0 ? role : prev), this.cache.first());
}
/**
* The premium subscriber role of the guild, if present on the member
* @type {?Role}
* @readonly
*/
get premiumSubscriberRole() {
return this.cache.find(role => role.tags?.premiumSubscriberRole) ?? null;
}
/**
* The managed role this member created when joining the guild, if any
* <info>Only ever available on bots</info>
* @type {?Role}
* @readonly
*/
get botRole() {
if (!this.member.user.bot) return null;
return this.cache.find(role => role.tags?.botId === this.member.user.id) ?? null;
}
/**
* Adds a role (or multiple roles) to the member.
* @param {RoleResolvable|RoleResolvable[]|Collection<Snowflake, Role>} roleOrRoles The role or roles to add
* @param {string} [reason] Reason for adding the role(s)
* @returns {Promise<GuildMember>}
*/
async add(roleOrRoles, reason) {
if (roleOrRoles instanceof Collection || Array.isArray(roleOrRoles)) {
const resolvedRoles = [];
for (const role of roleOrRoles.values()) {
const resolvedRole = this.guild.roles.resolveId(role);
if (!resolvedRole) throw new TypeError('INVALID_ELEMENT', 'Array or Collection', 'roles', role);
resolvedRoles.push(resolvedRole);
}
const newRoles = [...new Set(resolvedRoles.concat(...this.cache.keys()))];
return this.set(newRoles, reason);
} else {
roleOrRoles = this.guild.roles.resolveId(roleOrRoles);
if (roleOrRoles === null) {
throw new TypeError('INVALID_TYPE', 'roles', 'Role, Snowflake or Array or Collection of Roles or Snowflakes');
}
await this.client.api.guilds[this.guild.id].members[this.member.id].roles[roleOrRoles].put({ reason });
const clone = this.member._clone();
clone._roles = [...this.cache.keys(), roleOrRoles];
return clone;
}
}
/**
* Removes a role (or multiple roles) from the member.
* @param {RoleResolvable|RoleResolvable[]|Collection<Snowflake, Role>} roleOrRoles The role or roles to remove
* @param {string} [reason] Reason for removing the role(s)
* @returns {Promise<GuildMember>}
*/
async remove(roleOrRoles, reason) {
if (roleOrRoles instanceof Collection || Array.isArray(roleOrRoles)) {
const resolvedRoles = [];
for (const role of roleOrRoles.values()) {
const resolvedRole = this.guild.roles.resolveId(role);
if (!resolvedRole) throw new TypeError('INVALID_ELEMENT', 'Array or Collection', 'roles', role);
resolvedRoles.push(resolvedRole);
}
const newRoles = this.cache.filter(role => !resolvedRoles.includes(role.id));
return this.set(newRoles, reason);
} else {
roleOrRoles = this.guild.roles.resolveId(roleOrRoles);
if (roleOrRoles === null) {
throw new TypeError('INVALID_TYPE', 'roles', 'Role, Snowflake or Array or Collection of Roles or Snowflakes');
}
await this.client.api.guilds[this.guild.id].members[this.member.id].roles[roleOrRoles].delete({ reason });
const clone = this.member._clone();
const newRoles = this.cache.filter(role => role.id !== roleOrRoles);
clone._roles = [...newRoles.keys()];
return clone;
}
}
/**
* Sets the roles applied to the member.
* @param {Collection<Snowflake, Role>|RoleResolvable[]} roles The roles or role ids to apply
* @param {string} [reason] Reason for applying the roles
* @returns {Promise<GuildMember>}
* @example
* // Set the member's roles to a single role
* guildMember.roles.set(['391156570408615936'])
* .then(console.log)
* .catch(console.error);
* @example
* // Remove all the roles from a member
* guildMember.roles.set([])
* .then(member => console.log(`Member roles is now of ${member.roles.cache.size} size`))
* .catch(console.error);
*/
set(roles, reason) {
return this.member.edit({ roles }, reason);
}
clone() {
const clone = new this.constructor(this.member);
clone.member._roles = [...this.cache.keys()];
return clone;
}
}
module.exports = GuildMemberRoleManager;

View File

@@ -0,0 +1,309 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const { GuildScheduledEventEntityType, Routes } = require('discord-api-types/v9');
const CachedManager = require('./CachedManager');
const { TypeError, Error } = require('../errors');
const { GuildScheduledEvent } = require('../structures/GuildScheduledEvent');
const DataResolver = require('../util/DataResolver');
/**
* Manages API methods for GuildScheduledEvents and stores their cache.
* @extends {CachedManager}
*/
class GuildScheduledEventManager extends CachedManager {
constructor(guild, iterable) {
super(guild.client, GuildScheduledEvent, iterable);
/**
* The guild this manager belongs to
* @type {Guild}
*/
this.guild = guild;
}
/**
* The cache of this manager
* @type {Collection<Snowflake, GuildScheduledEvent>}
* @name GuildScheduledEventManager#cache
*/
/**
* Data that resolves to give a GuildScheduledEvent object. This can be:
* * A Snowflake
* * A GuildScheduledEvent object
* @typedef {Snowflake|GuildScheduledEvent} GuildScheduledEventResolvable
*/
/**
* Options used to create a guild scheduled event.
* @typedef {Object} GuildScheduledEventCreateOptions
* @property {string} name The name of the guild scheduled event
* @property {DateResolvable} scheduledStartTime The time to schedule the event at
* @property {DateResolvable} [scheduledEndTime] The time to end the event at
* <warn>This is required if `entityType` is {@link GuildScheduledEventEntityType.External}</warn>
* @property {PrivacyLevel|number} privacyLevel The privacy level of the guild scheduled event
* @property {GuildScheduledEventEntityType|number} entityType The scheduled entity type of the event
* @property {string} [description] The description of the guild scheduled event
* @property {GuildVoiceChannelResolvable} [channel] The channel of the guild scheduled event
* <warn>This is required if `entityType` is {@link GuildScheduledEventEntityType.StageInstance} or
* {@link GuildScheduledEventEntityType.Voice}</warn>
* @property {GuildScheduledEventEntityMetadataOptions} [entityMetadata] The entity metadata of the
* guild scheduled event
* <warn>This is required if `entityType` is {@link GuildScheduledEventEntityType.External}</warn>
* @property {?(BufferResolvable|Base64Resolvable)} [image] The cover image of the guild scheduled event
* @property {string} [reason] The reason for creating the guild scheduled event
*/
/**
* Options used to set entity metadata of a guild scheduled event.
* @typedef {Object} GuildScheduledEventEntityMetadataOptions
* @property {string} [location] The location of the guild scheduled event
* <warn>This is required if `entityType` is {@link GuildScheduledEventEntityType.External}</warn>
*/
/**
* Creates a new guild scheduled event.
* @param {GuildScheduledEventCreateOptions} options Options for creating the guild scheduled event
* @returns {Promise<GuildScheduledEvent>}
*/
async create(options) {
if (typeof options !== 'object') throw new TypeError('INVALID_TYPE', 'options', 'object', true);
let {
privacyLevel,
entityType,
channel,
name,
scheduledStartTime,
description,
scheduledEndTime,
entityMetadata,
reason,
image,
} = options;
let entity_metadata, channel_id;
if (entityType === GuildScheduledEventEntityType.External) {
channel_id = typeof channel === 'undefined' ? channel : null;
entity_metadata = { location: entityMetadata?.location };
} else {
channel_id = this.guild.channels.resolveId(channel);
if (!channel_id) throw new Error('GUILD_VOICE_CHANNEL_RESOLVE');
entity_metadata = typeof entityMetadata === 'undefined' ? entityMetadata : null;
}
const data = await this.client.api.guilds(this.guild.id, 'scheduled-events').post({
body: {
channel_id,
name,
privacy_level: privacyLevel,
scheduled_start_time: new Date(scheduledStartTime).toISOString(),
scheduled_end_time: scheduledEndTime ? new Date(scheduledEndTime).toISOString() : scheduledEndTime,
description,
entity_type: entityType,
entity_metadata,
image: image && (await DataResolver.resolveImage(image)),
},
reason,
})
return this._add(data);
}
/**
* Options used to fetch a single guild scheduled event from a guild.
* @typedef {BaseFetchOptions} FetchGuildScheduledEventOptions
* @property {GuildScheduledEventResolvable} guildScheduledEvent The guild scheduled event to fetch
* @property {boolean} [withUserCount=true] Whether to fetch the number of users subscribed to the scheduled event
*/
/**
* Options used to fetch multiple guild scheduled events from a guild.
* @typedef {Object} FetchGuildScheduledEventsOptions
* @property {boolean} [cache] Whether or not to cache the fetched guild scheduled events
* @property {boolean} [withUserCount=true] Whether to fetch the number of users subscribed to each scheduled event
* should be returned
*/
/**
* Obtains one or more guild scheduled events from Discord, or the guild cache if it's already available.
* @param {GuildScheduledEventResolvable|FetchGuildScheduledEventOptions|FetchGuildScheduledEventsOptions} [options]
* The id of the guild scheduled event or options
* @returns {Promise<GuildScheduledEvent|Collection<Snowflake, GuildScheduledEvent>>}
*/
async fetch(options = {}) {
const id = this.resolveId(options.guildScheduledEvent ?? options);
if (id) {
if (!options.force) {
const existing = this.cache.get(id);
if (existing) return existing;
}
const data = await this.client.api.guilds(this.guild.id, 'scheduled-events', id).get({
query: new URLSearchParams({ with_user_count: options.withUserCount ?? true }),
})
return this._add(data, options.cache);
}
const data = await this.client.api.guilds(this.guild.id, 'scheduled-events').get({
query: new URLSearchParams({ with_user_count: options.withUserCount ?? true }),
})
return data.reduce(
(coll, rawGuildScheduledEventData) =>
coll.set(
rawGuildScheduledEventData.id,
this.guild.scheduledEvents._add(rawGuildScheduledEventData, options.cache),
),
new Collection(),
);
}
/**
* Options used to edit a guild scheduled event.
* @typedef {Object} GuildScheduledEventEditOptions
* @property {string} [name] The name of the guild scheduled event
* @property {DateResolvable} [scheduledStartTime] The time to schedule the event at
* @property {DateResolvable} [scheduledEndTime] The time to end the event at
* @property {PrivacyLevel|number} [privacyLevel] The privacy level of the guild scheduled event
* @property {GuildScheduledEventEntityType|number} [entityType] The scheduled entity type of the event
* @property {string} [description] The description of the guild scheduled event
* @property {?GuildVoiceChannelResolvable} [channel] The channel of the guild scheduled event
* @property {GuildScheduledEventStatus|number} [status] The status of the guild scheduled event
* @property {GuildScheduledEventEntityMetadataOptions} [entityMetadata] The entity metadata of the
* guild scheduled event
* <warn>This can be modified only if `entityType` of the `GuildScheduledEvent` to be edited is
* {@link GuildScheduledEventEntityType.External}</warn>
* @property {?(BufferResolvable|Base64Resolvable)} [image] The cover image of the guild scheduled event
* @property {string} [reason] The reason for editing the guild scheduled event
*/
/**
* Edits a guild scheduled event.
* @param {GuildScheduledEventResolvable} guildScheduledEvent The guild scheduled event to edit
* @param {GuildScheduledEventEditOptions} options Options to edit the guild scheduled event
* @returns {Promise<GuildScheduledEvent>}
*/
async edit(guildScheduledEvent, options) {
const guildScheduledEventId = this.resolveId(guildScheduledEvent);
if (!guildScheduledEventId) throw new Error('GUILD_SCHEDULED_EVENT_RESOLVE');
if (typeof options !== 'object') throw new TypeError('INVALID_TYPE', 'options', 'object', true);
let {
privacyLevel,
entityType,
channel,
status,
name,
scheduledStartTime,
description,
scheduledEndTime,
entityMetadata,
reason,
image,
} = options;
let entity_metadata;
if (entityMetadata) {
entity_metadata = {
location: entityMetadata.location,
};
}
const data = await this.client.api.guilds(this.guild.id, 'scheduled-events', guildScheduledEventId).patch({
body: {
channel_id: typeof channel === 'undefined' ? channel : this.guild.channels.resolveId(channel),
name,
privacy_level: privacyLevel,
scheduled_start_time: scheduledStartTime ? new Date(scheduledStartTime).toISOString() : undefined,
scheduled_end_time: scheduledEndTime ? new Date(scheduledEndTime).toISOString() : scheduledEndTime,
description,
entity_type: entityType,
status,
image: image && (await DataResolver.resolveImage(image)),
entity_metadata,
},
reason,
})
return this._add(data);
}
/**
* Deletes a guild scheduled event.
* @param {GuildScheduledEventResolvable} guildScheduledEvent The guild scheduled event to delete
* @returns {Promise<void>}
*/
async delete(guildScheduledEvent) {
const guildScheduledEventId = this.resolveId(guildScheduledEvent);
if (!guildScheduledEventId) throw new Error('GUILD_SCHEDULED_EVENT_RESOLVE');
await this.client.api.guilds(this.guild.id, 'scheduled-events', guildScheduledEventId).delete();
}
/**
* Options used to fetch subscribers of a guild scheduled event
* @typedef {Object} FetchGuildScheduledEventSubscribersOptions
* @property {number} [limit] The maximum numbers of users to fetch
* @property {boolean} [withMember] Whether to fetch guild member data of the users
* @property {Snowflake} [before] Consider only users before this user id
* @property {Snowflake} [after] Consider only users after this user id
* <warn>If both `before` and `after` are provided, only `before` is respected</warn>
*/
/**
* Represents a subscriber of a {@link GuildScheduledEvent}
* @typedef {Object} GuildScheduledEventUser
* @property {Snowflake} guildScheduledEventId The id of the guild scheduled event which the user subscribed to
* @property {User} user The user that subscribed to the guild scheduled event
* @property {?GuildMember} member The guild member associated with the user, if any
*/
/**
* Fetches subscribers of a guild scheduled event.
* @param {GuildScheduledEventResolvable} guildScheduledEvent The guild scheduled event to fetch subscribers of
* @param {FetchGuildScheduledEventSubscribersOptions} [options={}] Options for fetching the subscribers
* @returns {Promise<Collection<Snowflake, GuildScheduledEventUser>>}
*/
async fetchSubscribers(guildScheduledEvent, options = {}) {
const guildScheduledEventId = this.resolveId(guildScheduledEvent);
if (!guildScheduledEventId) throw new Error('GUILD_SCHEDULED_EVENT_RESOLVE');
let { limit, withMember, before, after } = options;
const query = new URLSearchParams();
if (limit) {
query.set('limit', limit);
}
if (typeof withMember !== 'undefined') {
query.set('with_member', withMember);
}
if (before) {
query.set('before', before);
}
if (after) {
query.set('after', after);
}
const data = await this.client.api.guilds(this.guild.id, 'scheduled-events', guildScheduledEventId).users.get({
query,
});
return data.reduce(
(coll, rawData) =>
coll.set(rawData.user.id, {
guildScheduledEventId: rawData.guild_scheduled_event_id,
user: this.client.users._add(rawData.user),
member: rawData.member ? this.guild.members._add({ ...rawData.member, user: rawData.user }) : null,
}),
new Collection(),
);
}
}
module.exports = GuildScheduledEventManager;

View File

@@ -0,0 +1,183 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const { Routes } = require('discord-api-types/v9');
const CachedManager = require('./CachedManager');
const { TypeError } = require('../errors');
const MessagePayload = require('../structures/MessagePayload');
const { Sticker } = require('../structures/Sticker');
/**
* Manages API methods for Guild Stickers and stores their cache.
* @extends {CachedManager}
*/
class GuildStickerManager extends CachedManager {
constructor(guild, iterable) {
super(guild.client, Sticker, iterable);
/**
* The guild this manager belongs to
* @type {Guild}
*/
this.guild = guild;
}
/**
* The cache of Guild Stickers
* @type {Collection<Snowflake, Sticker>}
* @name GuildStickerManager#cache
*/
_add(data, cache) {
return super._add(data, cache, { extras: [this.guild] });
}
/**
* Options for creating a guild sticker.
* @typedef {Object} GuildStickerCreateOptions
* @property {?string} [description] The description for the sticker
* @property {string} [reason] Reason for creating the sticker
*/
/**
* Creates a new custom sticker in the guild.
* @param {BufferResolvable|Stream|FileOptions|MessageAttachment} file The file for the sticker
* @param {string} name The name for the sticker
* @param {string} tags The Discord name of a unicode emoji representing the sticker's expression
* @param {GuildStickerCreateOptions} [options] Options
* @returns {Promise<Sticker>} The created sticker
* @example
* // Create a new sticker from a URL
* guild.stickers.create('https://i.imgur.com/w3duR07.png', 'rip', 'headstone')
* .then(sticker => console.log(`Created new sticker with name ${sticker.name}!`))
* .catch(console.error);
* @example
* // Create a new sticker from a file on your computer
* guild.stickers.create('./memes/banana.png', 'banana', 'banana')
* .then(sticker => console.log(`Created new sticker with name ${sticker.name}!`))
* .catch(console.error);
*/
async create(file, name, tags, { description, reason } = {}) {
const resolvedFile = await MessagePayload.resolveFile(file);
if (!resolvedFile) throw new TypeError('REQ_RESOURCE_TYPE');
file = { ...resolvedFile, key: 'file' };
const body = { name, tags, description: description ?? '' };
const sticker = await this.client.api.guilds(this.guild.id).stickers.post({
appendToFormData: true,
body,
files: [file],
reason,
});
return this.client.actions.GuildStickerCreate.handle(this.guild, sticker).sticker;
}
/**
* Data that resolves to give a Sticker object. This can be:
* * A Sticker object
* * A Snowflake
* @typedef {Sticker|Snowflake} StickerResolvable
*/
/**
* Resolves a StickerResolvable to a Sticker object.
* @method resolve
* @memberof GuildStickerManager
* @instance
* @param {StickerResolvable} sticker The Sticker resolvable to identify
* @returns {?Sticker}
*/
/**
* Resolves a StickerResolvable to a Sticker id string.
* @method resolveId
* @memberof GuildStickerManager
* @instance
* @param {StickerResolvable} sticker The Sticker resolvable to identify
* @returns {?Snowflake}
*/
/**
* Edits a sticker.
* @param {StickerResolvable} sticker The sticker to edit
* @param {GuildStickerEditData} [data] The new data for the sticker
* @param {string} [reason] Reason for editing this sticker
* @returns {Promise<Sticker>}
*/
async edit(sticker, data, reason) {
const stickerId = this.resolveId(sticker);
if (!stickerId) throw new TypeError('INVALID_TYPE', 'sticker', 'StickerResolvable');
const d = await this.client.api.guilds(this.guild.id).stickers(stickerId).patch({
body: data,
reason,
});
const existing = this.cache.get(stickerId);
if (existing) {
const clone = existing._clone();
clone._patch(d);
return clone;
}
return this._add(d);
}
/**
* Deletes a sticker.
* @param {StickerResolvable} sticker The sticker to delete
* @param {string} [reason] Reason for deleting this sticker
* @returns {Promise<void>}
*/
async delete(sticker, reason) {
sticker = this.resolveId(sticker);
if (!sticker) throw new TypeError('INVALID_TYPE', 'sticker', 'StickerResolvable');
await this.client.api.guilds(this.guild.id).stickers(sticker).delete({ reason });
}
/**
* Obtains one or more stickers from Discord, or the sticker cache if they're already available.
* @param {Snowflake} [id] The Sticker's id
* @param {BaseFetchOptions} [options] Additional options for this fetch
* @returns {Promise<Sticker|Collection<Snowflake, Sticker>>}
* @example
* // Fetch all stickers from the guild
* message.guild.stickers.fetch()
* .then(stickers => console.log(`There are ${stickers.size} stickers.`))
* .catch(console.error);
* @example
* // Fetch a single sticker
* message.guild.stickers.fetch('222078108977594368')
* .then(sticker => console.log(`The sticker name is: ${sticker.name}`))
* .catch(console.error);
*/
async fetch(id, { cache = true, force = false } = {}) {
if (id) {
if (!force) {
const existing = this.cache.get(id);
if (existing) return existing;
}
const sticker = await this.client.api.guilds(this.guild.id).stickers(id).get();
return this._add(sticker, cache);
}
const data = await this.client.api.guilds(this.guild.id).stickers.get();
return new Collection(data.map(sticker => [sticker.id, this._add(sticker, cache)]));
}
/**
* Fetches the user who uploaded this sticker, if this is a guild sticker.
* @param {StickerResolvable} sticker The sticker to fetch the user for
* @returns {Promise<?User>}
*/
async fetchUser(sticker) {
sticker = this.resolve(sticker);
if (!sticker) throw new TypeError('INVALID_TYPE', 'sticker', 'StickerResolvable');
const data = await this.client.api.guilds(this.guild.id).stickers(sticker.id).get();
sticker._patch(data);
return sticker.user;
}
}
module.exports = GuildStickerManager;

View File

@@ -0,0 +1,235 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const { Routes } = require('discord-api-types/v9');
const CachedManager = require('./CachedManager');
const { TypeError } = require('../errors');
const { Message } = require('../structures/Message');
const MessagePayload = require('../structures/MessagePayload');
const Util = require('../util/Util');
/**
* Manages API methods for Messages and holds their cache.
* @extends {CachedManager}
*/
class MessageManager extends CachedManager {
constructor(channel, iterable) {
super(channel.client, Message, iterable);
/**
* The channel that the messages belong to
* @type {TextBasedChannels}
*/
this.channel = channel;
}
/**
* The cache of Messages
* @type {Collection<Snowflake, Message>}
* @name MessageManager#cache
*/
_add(data, cache) {
return super._add(data, cache);
}
/**
* The parameters to pass in when requesting previous messages from a channel. `around`, `before` and
* `after` are mutually exclusive. All the parameters are optional.
* @typedef {Object} ChannelLogsQueryOptions
* @property {number} [limit] Number of messages to acquire
* @property {Snowflake} [before] The message's id to get the messages that were posted before it
* @property {Snowflake} [after] The message's id to get the messages that were posted after it
* @property {Snowflake} [around] The message's id to get the messages that were posted around it
*/
/**
* Gets a message, or messages, from this channel.
* <info>The returned Collection does not contain reaction users of the messages if they were not cached.
* Those need to be fetched separately in such a case.</info>
* @param {Snowflake|ChannelLogsQueryOptions} [message] The id of the message to fetch, or query parameters.
* @param {BaseFetchOptions} [options] Additional options for this fetch
* @returns {Promise<Message|Collection<Snowflake, Message>>}
* @example
* // Get message
* channel.messages.fetch('99539446449315840')
* .then(message => console.log(message.content))
* .catch(console.error);
* @example
* // Get messages
* channel.messages.fetch({ limit: 10 })
* .then(messages => console.log(`Received ${messages.size} messages`))
* .catch(console.error);
* @example
* // Get messages and filter by user id
* channel.messages.fetch()
* .then(messages => console.log(`${messages.filter(m => m.author.id === '84484653687267328').size} messages`))
* .catch(console.error);
*/
fetch(message, { cache = true, force = false } = {}) {
return typeof message === 'string' ? this._fetchId(message, cache, force) : this._fetchMany(message, cache);
}
/**
* Fetches the pinned messages of this channel and returns a collection of them.
* <info>The returned Collection does not contain any reaction data of the messages.
* Those need to be fetched separately.</info>
* @param {boolean} [cache=true] Whether to cache the message(s)
* @returns {Promise<Collection<Snowflake, Message>>}
* @example
* // Get pinned messages
* channel.messages.fetchPinned()
* .then(messages => console.log(`Received ${messages.size} messages`))
* .catch(console.error);
*/
async fetchPinned(cache = true) {
const data = await this.client.api.channels(this.channel.id).pins.get();
const messages = new Collection();
for (const message of data) messages.set(message.id, this._add(message, cache));
return messages;
}
/**
* Data that can be resolved to a Message object. This can be:
* * A Message
* * A Snowflake
* @typedef {Message|Snowflake} MessageResolvable
*/
/**
* Resolves a {@link MessageResolvable} to a {@link Message} object.
* @method resolve
* @memberof MessageManager
* @instance
* @param {MessageResolvable} message The message resolvable to resolve
* @returns {?Message}
*/
/**
* Resolves a {@link MessageResolvable} to a {@link Message} id.
* @method resolveId
* @memberof MessageManager
* @instance
* @param {MessageResolvable} message The message resolvable to resolve
* @returns {?Snowflake}
*/
/**
* Edits a message, even if it's not cached.
* @param {MessageResolvable} message The message to edit
* @param {string|MessageEditOptions|MessagePayload} options The options to edit the message
* @returns {Promise<Message>}
*/
async edit(message, options) {
const messageId = this.resolveId(message);
if (!messageId) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable');
const { body, files } = await (options instanceof MessagePayload
? options
: MessagePayload.create(message instanceof Message ? message : this, options)
)
.resolveBody()
.resolveFiles();
const d = await this.client.api.channels(this.channel.id).messages(messageId).patch({ body, files });
const existing = this.cache.get(messageId);
if (existing) {
const clone = existing._clone();
clone._patch(d);
return clone;
}
return this._add(d);
}
/**
* Publishes a message in an announcement channel to all channels following it, even if it's not cached.
* @param {MessageResolvable} message The message to publish
* @returns {Promise<Message>}
*/
async crosspost(message) {
message = this.resolveId(message);
if (!message) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable');
const data = await this.client.api.channels(this.channel.id).messages(message).crosspost.post();
return this.cache.get(data.id) ?? this._add(data);
}
/**
* Pins a message to the channel's pinned messages, even if it's not cached.
* @param {MessageResolvable} message The message to pin
* @param {string} [reason] Reason for pinning
* @returns {Promise<void>}
*/
async pin(message, reason) {
message = this.resolveId(message);
if (!message) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable');
await this.client.api.channels(this.channel.id).pins(message).put({ reason });
}
/**
* Unpins a message from the channel's pinned messages, even if it's not cached.
* @param {MessageResolvable} message The message to unpin
* @param {string} [reason] Reason for unpinning
* @returns {Promise<void>}
*/
async unpin(message, reason) {
message = this.resolveId(message);
if (!message) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable');
await this.client.api.channels(this.channel.id).pins(message).delete({ reason });
}
/**
* Adds a reaction to a message, even if it's not cached.
* @param {MessageResolvable} message The message to react to
* @param {EmojiIdentifierResolvable} emoji The emoji to react with
* @returns {Promise<void>}
*/
async react(message, emoji) {
message = this.resolveId(message);
if (!message) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable');
emoji = Util.resolvePartialEmoji(emoji);
if (!emoji) throw new TypeError('EMOJI_TYPE', 'emoji', 'EmojiIdentifierResolvable');
const emojiId = emoji.id
? `${emoji.animated ? 'a:' : ''}${emoji.name}:${emoji.id}`
: encodeURIComponent(emoji.name);
await this.client.api.channels(this.channel.id).messages(message).reactions(emojiId, '@me').put();
}
/**
* Deletes a message, even if it's not cached.
* @param {MessageResolvable} message The message to delete
* @returns {Promise<void>}
*/
async delete(message) {
message = this.resolveId(message);
if (!message) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable');
await this.client.api.channels(this.channel.id).messages(message).delete();
}
async _fetchId(messageId, cache, force) {
if (!force) {
const existing = this.cache.get(messageId);
if (existing && !existing.partial) return existing;
}
const data = await this.client.api.channels(this.channel.id).messages(messageId).get();
return this._add(data, cache);
}
async _fetchMany(options = {}, cache) {
const data = await this.client.api.channels(this.channel.id).messages.get({
query: new URLSearchParams(options),
});
const messages = new Collection();
for (const message of data) messages.set(message.id, this._add(message, cache));
return messages;
}
}
module.exports = MessageManager;

View File

@@ -0,0 +1,162 @@
'use strict';
const process = require('node:process');
const { Collection } = require('@discordjs/collection');
const { OverwriteType, Routes } = require('discord-api-types/v9');
const CachedManager = require('./CachedManager');
const { TypeError } = require('../errors');
const PermissionOverwrites = require('../structures/PermissionOverwrites');
const { Role } = require('../structures/Role');
let cacheWarningEmitted = false;
/**
* Manages API methods for guild channel permission overwrites and stores their cache.
* @extends {CachedManager}
*/
class PermissionOverwriteManager extends CachedManager {
constructor(channel, iterable) {
super(channel.client, PermissionOverwrites);
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 channel of the permission overwrite this manager belongs to
* @type {GuildChannel}
*/
this.channel = channel;
if (iterable) {
for (const item of iterable) {
this._add(item);
}
}
}
/**
* The cache of this Manager
* @type {Collection<Snowflake, PermissionOverwrites>}
* @name PermissionOverwriteManager#cache
*/
_add(data, cache) {
return super._add(data, cache, { extras: [this.channel] });
}
/**
* Replaces the permission overwrites in this channel.
* @param {OverwriteResolvable[]|Collection<Snowflake, OverwriteResolvable>} overwrites
* Permission overwrites the channel gets updated with
* @param {string} [reason] Reason for updating the channel overwrites
* @returns {Promise<GuildChannel>}
* @example
* message.channel.permissionOverwrites.set([
* {
* id: message.author.id,
* deny: [PermissionsFlagsBit.ViewChannel],
* },
* ], 'Needed to change permissions');
*/
set(overwrites, reason) {
if (!Array.isArray(overwrites) && !(overwrites instanceof Collection)) {
return Promise.reject(
new TypeError('INVALID_TYPE', 'overwrites', 'Array or Collection of Permission Overwrites', true),
);
}
return this.channel.edit({ permissionOverwrites: overwrites, reason });
}
/**
* Extra information about the overwrite
* @typedef {Object} GuildChannelOverwriteOptions
* @property {string} [reason] Reason for creating/editing this overwrite
* @property {number} [type] The type of overwrite, either `0` for a role or `1` for a member. Use this to bypass
* automatic resolution of type that results in an error for uncached structure
*/
/**
* Creates or edits permission overwrites for a user or role in this channel.
* @param {RoleResolvable|UserResolvable} userOrRole The user or role to update
* @param {PermissionOverwriteOptions} options The options for the update
* @param {GuildChannelOverwriteOptions} [overwriteOptions] The extra information for the update
* @param {PermissionOverwrites} [existing] The existing overwrites to merge with this update
* @returns {Promise<GuildChannel>}
* @private
*/
async upsert(userOrRole, options, overwriteOptions = {}, existing) {
let userOrRoleId = this.channel.guild.roles.resolveId(userOrRole) ?? this.client.users.resolveId(userOrRole);
let { type, reason } = overwriteOptions;
if (typeof type !== 'number') {
userOrRole = this.channel.guild.roles.resolve(userOrRole) ?? this.client.users.resolve(userOrRole);
if (!userOrRole) throw new TypeError('INVALID_TYPE', 'parameter', 'User nor a Role');
type = userOrRole instanceof Role ? OverwriteType.Role : OverwriteType.Member;
}
const { allow, deny } = PermissionOverwrites.resolveOverwriteOptions(options, existing);
await this.client.api.channels(this.channel.id).permissions(userOrRoleId).put({
body: { id: userOrRoleId, type, allow, deny },
reason,
});
return this.channel;
}
/**
* Creates permission overwrites for a user or role in this channel, or replaces them if already present.
* @param {RoleResolvable|UserResolvable} userOrRole The user or role to update
* @param {PermissionOverwriteOptions} options The options for the update
* @param {GuildChannelOverwriteOptions} [overwriteOptions] The extra information for the update
* @returns {Promise<GuildChannel>}
* @example
* // Create or Replace permission overwrites for a message author
* message.channel.permissionOverwrites.create(message.author, {
* SEND_MESSAGES: false
* })
* .then(channel => console.log(channel.permissionOverwrites.cache.get(message.author.id)))
* .catch(console.error);
*/
create(userOrRole, options, overwriteOptions) {
return this.upsert(userOrRole, options, overwriteOptions);
}
/**
* Edits permission overwrites for a user or role in this channel, or creates an entry if not already present.
* @param {RoleResolvable|UserResolvable} userOrRole The user or role to update
* @param {PermissionOverwriteOptions} options The options for the update
* @param {GuildChannelOverwriteOptions} [overwriteOptions] The extra information for the update
* @returns {Promise<GuildChannel>}
* @example
* // Edit or Create permission overwrites for a message author
* message.channel.permissionOverwrites.edit(message.author, {
* SEND_MESSAGES: false
* })
* .then(channel => console.log(channel.permissionOverwrites.cache.get(message.author.id)))
* .catch(console.error);
*/
edit(userOrRole, options, overwriteOptions) {
userOrRole = this.channel.guild.roles.resolveId(userOrRole) ?? this.client.users.resolveId(userOrRole);
const existing = this.cache.get(userOrRole);
return this.upsert(userOrRole, options, overwriteOptions, existing);
}
/**
* Deletes permission overwrites for a user or role in this channel.
* @param {UserResolvable|RoleResolvable} userOrRole The user or role to delete
* @param {string} [reason] The reason for deleting the overwrite
* @returns {Promise<GuildChannel>}
*/
async delete(userOrRole, reason) {
const userOrRoleId = this.channel.guild.roles.resolveId(userOrRole) ?? this.client.users.resolveId(userOrRole);
if (!userOrRoleId) throw new TypeError('INVALID_TYPE', 'parameter', 'User nor a Role');
await this.client.api.channels(this.channel.id).permissions(userOrRoleId).delete({ reason });
return this.channel;
}
}
module.exports = PermissionOverwriteManager;

View File

@@ -0,0 +1,58 @@
'use strict';
const CachedManager = require('./CachedManager');
const { Presence } = require('../structures/Presence');
/**
* Manages API methods for Presences and holds their cache.
* @extends {CachedManager}
*/
class PresenceManager extends CachedManager {
constructor(client, iterable) {
super(client, Presence, iterable);
}
/**
* The cache of Presences
* @type {Collection<Snowflake, Presence>}
* @name PresenceManager#cache
*/
_add(data, cache) {
return super._add(data, cache, { id: data.user.id });
}
/**
* Data that can be resolved to a Presence object. This can be:
* * A Presence
* * A UserResolvable
* * A Snowflake
* @typedef {Presence|UserResolvable|Snowflake} PresenceResolvable
*/
/**
* Resolves a {@link PresenceResolvable} to a {@link Presence} object.
* @param {PresenceResolvable} presence The presence resolvable to resolve
* @returns {?Presence}
*/
resolve(presence) {
const presenceResolvable = super.resolve(presence);
if (presenceResolvable) return presenceResolvable;
const UserResolvable = this.client.users.resolveId(presence);
return super.resolve(UserResolvable);
}
/**
* Resolves a {@link PresenceResolvable} to a {@link Presence} id.
* @param {PresenceResolvable} presence The presence resolvable to resolve
* @returns {?Snowflake}
*/
resolveId(presence) {
const presenceResolvable = super.resolveId(presence);
if (presenceResolvable) return presenceResolvable;
const userResolvable = this.client.users.resolveId(presence);
return this.cache.has(userResolvable) ? userResolvable : null;
}
}
module.exports = PresenceManager;

View File

@@ -0,0 +1,67 @@
'use strict';
const { Routes } = require('discord-api-types/v9');
const CachedManager = require('./CachedManager');
const MessageReaction = require('../structures/MessageReaction');
/**
* Manages API methods for reactions and holds their cache.
* @extends {CachedManager}
*/
class ReactionManager extends CachedManager {
constructor(message, iterable) {
super(message.client, MessageReaction, iterable);
/**
* The message that this manager belongs to
* @type {Message}
*/
this.message = message;
}
_add(data, cache) {
return super._add(data, cache, { id: data.emoji.id ?? data.emoji.name, extras: [this.message] });
}
/**
* The reaction cache of this manager
* @type {Collection<string|Snowflake, MessageReaction>}
* @name ReactionManager#cache
*/
/**
* Data that can be resolved to a MessageReaction object. This can be:
* * A MessageReaction
* * A Snowflake
* @typedef {MessageReaction|Snowflake} MessageReactionResolvable
*/
/**
* Resolves a {@link MessageReactionResolvable} to a {@link MessageReaction} object.
* @method resolve
* @memberof ReactionManager
* @instance
* @param {MessageReactionResolvable} reaction The MessageReaction to resolve
* @returns {?MessageReaction}
*/
/**
* Resolves a {@link MessageReactionResolvable} to a {@link MessageReaction} id.
* @method resolveId
* @memberof ReactionManager
* @instance
* @param {MessageReactionResolvable} reaction The MessageReaction to resolve
* @returns {?Snowflake}
*/
/**
* Removes all reactions from a message.
* @returns {Promise<Message>}
*/
async removeAll() {
await this.client.api.channels(this.message.channelId).messages(this.message.id).reactions.delete();
return this.message;
}
}
module.exports = ReactionManager;

View File

@@ -0,0 +1,75 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const { Routes } = require('discord-api-types/v9');
const CachedManager = require('./CachedManager');
const { Error } = require('../errors');
const User = require('../structures/User');
/**
* Manages API methods for users who reacted to a reaction and stores their cache.
* @extends {CachedManager}
*/
class ReactionUserManager extends CachedManager {
constructor(reaction, iterable) {
super(reaction.client, User, iterable);
/**
* The reaction that this manager belongs to
* @type {MessageReaction}
*/
this.reaction = reaction;
}
/**
* The cache of this manager
* @type {Collection<Snowflake, User>}
* @name ReactionUserManager#cache
*/
/**
* Options used to fetch users who gave a reaction.
* @typedef {Object} FetchReactionUsersOptions
* @property {number} [limit=100] The maximum amount of users to fetch, defaults to `100`
* @property {Snowflake} [after] Limit fetching users to those with an id greater than the supplied id
*/
/**
* Fetches all the users that gave this reaction. Resolves with a collection of users, mapped by their ids.
* @param {FetchReactionUsersOptions} [options] Options for fetching the users
* @returns {Promise<Collection<Snowflake, User>>}
*/
async fetch({ limit = 100, after } = {}) {
const message = this.reaction.message;
const query = new URLSearchParams({ limit });
if (after) {
query.set('after', after);
}
const data = await this.client.api.channels(message.channelId).messages(message.id).reactions(this.reaction.emoji.identifier).get({ query });
const users = new Collection();
for (const rawUser of data) {
const user = this.client.users._add(rawUser);
this.cache.set(user.id, user);
users.set(user.id, user);
}
return users;
}
/**
* Removes a user from this reaction.
* @param {UserResolvable} [user=this.client.user] The user to remove the reaction of
* @returns {Promise<MessageReaction>}
*/
async remove(user = this.client.user) {
const userId = this.client.users.resolveId(user);
if (!userId) throw new Error('REACTION_RESOLVE_USER');
const message = this.reaction.message;
await this.client.api.channels[message.channelId]
.messages[message.id]
.reactions[this.reaction.emoji.identifier][userId === this.client.user.id ? '@me' : userId]
.delete();
return this.reaction;
}
}
module.exports = ReactionUserManager;

351
src/managers/RoleManager.js Normal file
View File

@@ -0,0 +1,351 @@
'use strict';
const process = require('node:process');
const { Collection } = require('@discordjs/collection');
const { Routes } = require('discord-api-types/v9');
const CachedManager = require('./CachedManager');
const { TypeError } = require('../errors');
const { Role } = require('../structures/Role');
const DataResolver = require('../util/DataResolver');
const PermissionsBitField = require('../util/PermissionsBitField');
const { resolveColor } = require('../util/Util');
const Util = 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: Colors.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 PermissionsBitField(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({
body: {
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 this.setPosition(role, 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') {
await this.setPosition(role, data.position, { reason });
}
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 body = {
name: data.name,
color: typeof data.color === 'undefined' ? undefined : resolveColor(data.color),
hoist: data.hoist,
permissions: typeof data.permissions === 'undefined' ? undefined : new PermissionsBitField(data.permissions),
mentionable: data.mentionable,
icon,
unicode_emoji: data.unicodeEmoji,
};
const d = await this.client.api.guilds(this.guild.id).roles(role.id).patch({ body, 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(() => console.log('Deleted the role'))
* .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 });
}
/**
* Sets the new position of the role.
* @param {RoleResolvable} role The role to change the position of
* @param {number} position The new position for the role
* @param {SetRolePositionOptions} [options] Options for setting the position
* @returns {Promise<Role>}
* @example
* // Set the position of the role
* guild.roles.setPosition('222197033908436994', 1)
* .then(updated => console.log(`Role position: ${updated.position}`))
* .catch(console.error);
*/
async setPosition(role, position, { relative, reason } = {}) {
role = this.resolve(role);
if (!role) throw new TypeError('INVALID_TYPE', 'role', 'RoleResolvable');
const updatedRoles = await Util.setPosition(
role,
position,
relative,
this.guild._sortedRoles(),
this.client,
Routes.guildRoles(this.guild.id),
reason,
);
this.client.actions.GuildRolesPositionUpdate.handle({
guild_id: this.guild.id,
roles: updatedRoles,
});
return role;
}
/**
* The data needed for updating a guild role's position
* @typedef {Object} GuildRolePosition
* @property {RoleResolvable} role The role's id
* @property {number} position The position to update
*/
/**
* 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({ body: 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;

View File

@@ -0,0 +1,152 @@
'use strict';
const { Routes } = require('discord-api-types/v9');
const CachedManager = require('./CachedManager');
const { TypeError, Error } = require('../errors');
const { StageInstance } = require('../structures/StageInstance');
/**
* Manages API methods for {@link StageInstance} objects and holds their cache.
* @extends {CachedManager}
*/
class StageInstanceManager extends CachedManager {
constructor(guild, iterable) {
super(guild.client, StageInstance, iterable);
/**
* The guild this manager belongs to
* @type {Guild}
*/
this.guild = guild;
}
/**
* The cache of this Manager
* @type {Collection<Snowflake, StageInstance>}
* @name StageInstanceManager#cache
*/
/**
* Options used to create a stage instance.
* @typedef {Object} StageInstanceCreateOptions
* @property {string} topic The topic of the stage instance
* @property {PrivacyLevel|number} [privacyLevel] The privacy level of the stage instance
*/
/**
* Data that can be resolved to a Stage Channel object. This can be:
* * A StageChannel
* * A Snowflake
* @typedef {StageChannel|Snowflake} StageChannelResolvable
*/
/**
* Creates a new stage instance.
* @param {StageChannelResolvable} channel The stage channel to associate the created stage instance to
* @param {StageInstanceCreateOptions} options The options to create the stage instance
* @returns {Promise<StageInstance>}
* @example
* // Create a stage instance
* guild.stageInstances.create('1234567890123456789', {
* topic: 'A very creative topic',
* privacyLevel: GuildPrivacyLevel.GuildOnly
* })
* .then(stageInstance => console.log(stageInstance))
* .catch(console.error);
*/
async create(channel, options) {
const channelId = this.guild.channels.resolveId(channel);
if (!channelId) throw new Error('STAGE_CHANNEL_RESOLVE');
if (typeof options !== 'object') throw new TypeError('INVALID_TYPE', 'options', 'object', true);
let { topic, privacyLevel } = options;
const data = await this.client.api['stage-instances'].post({
body: {
channel_id: channelId,
topic,
privacy_level: privacyLevel,
},
});
return this._add(data);
}
/**
* Fetches the stage instance associated with a stage channel, if it exists.
* @param {StageChannelResolvable} channel The stage channel whose associated stage instance is to be fetched
* @param {BaseFetchOptions} [options] Additional options for this fetch
* @returns {Promise<StageInstance>}
* @example
* // Fetch a stage instance
* guild.stageInstances.fetch('1234567890123456789')
* .then(stageInstance => console.log(stageInstance))
* .catch(console.error);
*/
async fetch(channel, { cache = true, force = false } = {}) {
const channelId = this.guild.channels.resolveId(channel);
if (!channelId) throw new Error('STAGE_CHANNEL_RESOLVE');
if (!force) {
const existing = this.cache.find(stageInstance => stageInstance.channelId === channelId);
if (existing) return existing;
}
const data = await this.client.api('stage-instances', channelId).get();
return this._add(data, cache);
}
/**
* Options used to edit an existing stage instance.
* @typedef {Object} StageInstanceEditOptions
* @property {string} [topic] The new topic of the stage instance
* @property {PrivacyLevel|number} [privacyLevel] The new privacy level of the stage instance
*/
/**
* Edits an existing stage instance.
* @param {StageChannelResolvable} channel The stage channel whose associated stage instance is to be edited
* @param {StageInstanceEditOptions} options The options to edit the stage instance
* @returns {Promise<StageInstance>}
* @example
* // Edit a stage instance
* guild.stageInstances.edit('1234567890123456789', { topic: 'new topic' })
* .then(stageInstance => console.log(stageInstance))
* .catch(console.error);
*/
async edit(channel, options) {
if (typeof options !== 'object') throw new TypeError('INVALID_TYPE', 'options', 'object', true);
const channelId = this.guild.channels.resolveId(channel);
if (!channelId) throw new Error('STAGE_CHANNEL_RESOLVE');
let { topic, privacyLevel } = options;
const data = await this.client.api('stage-instances', channelId).patch({
body: {
topic,
privacy_level: privacyLevel,
},
})
if (this.cache.has(data.id)) {
const clone = this.cache.get(data.id)._clone();
clone._patch(data);
return clone;
}
return this._add(data);
}
/**
* Deletes an existing stage instance.
* @param {StageChannelResolvable} channel The stage channel whose associated stage instance is to be deleted
* @returns {Promise<void>}
*/
async delete(channel) {
const channelId = this.guild.channels.resolveId(channel);
if (!channelId) throw new Error('STAGE_CHANNEL_RESOLVE');
await this.client.api('stage-instances', channelId).delete();
}
}
module.exports = StageInstanceManager;

View File

@@ -0,0 +1,267 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const { ChannelType, Routes } = require('discord-api-types/v9');
const CachedManager = require('./CachedManager');
const { TypeError } = require('../errors');
const ThreadChannel = require('../structures/ThreadChannel');
/**
* Manages API methods for {@link ThreadChannel} objects and stores their cache.
* @extends {CachedManager}
*/
class ThreadManager extends CachedManager {
constructor(channel, iterable) {
super(channel.client, ThreadChannel, iterable);
/**
* The channel this Manager belongs to
* @type {NewsChannel|TextChannel}
*/
this.channel = channel;
}
/**
* The cache of this Manager
* @type {Collection<Snowflake, ThreadChannel>}
* @name ThreadManager#cache
*/
_add(thread) {
const existing = this.cache.get(thread.id);
if (existing) return existing;
this.cache.set(thread.id, thread);
return thread;
}
/**
* Data that can be resolved to a Thread Channel object. This can be:
* * A ThreadChannel object
* * A Snowflake
* @typedef {ThreadChannel|Snowflake} ThreadChannelResolvable
*/
/**
* Resolves a {@link ThreadChannelResolvable} to a {@link ThreadChannel} object.
* @method resolve
* @memberof ThreadManager
* @instance
* @param {ThreadChannelResolvable} thread The ThreadChannel resolvable to resolve
* @returns {?ThreadChannel}
*/
/**
* Resolves a {@link ThreadChannelResolvable} to a {@link ThreadChannel} id.
* @method resolveId
* @memberof ThreadManager
* @instance
* @param {ThreadChannelResolvable} thread The ThreadChannel resolvable to resolve
* @returns {?Snowflake}
*/
/**
* Options for creating a thread. <warn>Only one of `startMessage` or `type` can be defined.</warn>
* @typedef {StartThreadOptions} ThreadCreateOptions
* @property {MessageResolvable} [startMessage] The message to start a thread from. <warn>If this is defined then type
* of thread gets automatically defined and cannot be changed. The provided `type` field will be ignored</warn>
* @property {ThreadChannelTypes|number} [type] The type of thread to create.
* Defaults to {@link ChannelType.GuildPublicThread} if created in a {@link TextChannel}
* <warn>When creating threads in a {@link NewsChannel} this is ignored and is always
* {@link ChannelType.GuildNewsThread}</warn>
* @property {boolean} [invitable] Whether non-moderators can add other non-moderators to the thread
* <info>Can only be set when type will be {@link ChannelType.GuildPrivateThread}</info>
*/
/**
* Creates a new thread in the channel.
* @param {ThreadCreateOptions} [options] Options to create a new thread
* @returns {Promise<ThreadChannel>}
* @example
* // Create a new public thread
* channel.threads
* .create({
* name: 'food-talk',
* autoArchiveDuration: 60,
* reason: 'Needed a separate thread for food',
* })
* .then(threadChannel => console.log(threadChannel))
* .catch(console.error);
* @example
* // Create a new private thread
* channel.threads
* .create({
* name: 'mod-talk',
* autoArchiveDuration: 60,
* type: ChannelType.GuildPrivateThread,
* reason: 'Needed a separate thread for moderation',
* })
* .then(threadChannel => console.log(threadChannel))
* .catch(console.error);
*/
async create({
name,
autoArchiveDuration = this.channel.defaultAutoArchiveDuration,
startMessage,
type,
invitable,
reason,
rateLimitPerUser,
} = {}) {
if (type && typeof type !== 'string' && typeof type !== 'number') {
throw new TypeError('INVALID_TYPE', 'type', 'ThreadChannelType or Number');
}
let resolvedType =
this.channel.type === ChannelType.GuildNews ? ChannelType.GuildNewsThread : ChannelType.GuildPublicThread;
let startMessageId;
if (startMessage) {
startMessageId = this.channel.messages.resolveId(startMessage);
if (!startMessageId) throw new TypeError('INVALID_TYPE', 'startMessage', 'MessageResolvable');
} else if (this.channel.type !== ChannelType.GuildNews) {
resolvedType = type ?? resolvedType;
}
if (autoArchiveDuration === 'MAX') {
autoArchiveDuration = 1440;
if (this.channel.guild.features.includes('SEVEN_DAY_THREAD_ARCHIVE')) {
autoArchiveDuration = 10080;
} else if (this.channel.guild.features.includes('THREE_DAY_THREAD_ARCHIVE')) {
autoArchiveDuration = 4320;
}
}
const data = await this.client.api.channels(this.channel.id).messages(startMessageId).threads.post({
body: {
name,
auto_archive_duration: autoArchiveDuration,
type: resolvedType,
invitable: resolvedType === ChannelType.GuildPrivateThread ? invitable : undefined,
rate_limit_per_user: rateLimitPerUser,
},
reason,
});
return this.client.actions.ThreadCreate.handle(data).thread;
}
/**
* The options for fetching multiple threads, the properties are mutually exclusive
* @typedef {Object} FetchThreadsOptions
* @property {FetchArchivedThreadOptions} [archived] The options used to fetch archived threads
* @property {boolean} [active] When true, fetches active threads. <warn>If `archived` is set, this is ignored!</warn>
*/
/**
* Obtains a thread from Discord, or the channel cache if it's already available.
* @param {ThreadChannelResolvable|FetchThreadsOptions} [options] The options to fetch threads. If it is a
* ThreadChannelResolvable then the specified thread will be fetched. Fetches all active threads if `undefined`
* @param {BaseFetchOptions} [cacheOptions] Additional options for this fetch. <warn>The `force` field gets ignored
* if `options` is not a {@link ThreadChannelResolvable}</warn>
* @returns {Promise<?(ThreadChannel|FetchedThreads)>}
* @example
* // Fetch a thread by its id
* channel.threads.fetch('831955138126104859')
* .then(channel => console.log(channel.name))
* .catch(console.error);
*/
fetch(options, { cache = true, force = false } = {}) {
if (!options) return this.fetchActive(cache);
const channel = this.client.channels.resolveId(options);
if (channel) return this.client.channels.fetch(channel, cache, force);
if (options.archived) {
return this.fetchArchived(options.archived, cache);
}
return this.fetchActive(cache);
}
/**
* Data that can be resolved to a Date object. This can be:
* * A Date object
* * A number representing a timestamp
* * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) string
* @typedef {Date|number|string} DateResolvable
*/
/**
* The options used to fetch archived threads.
* @typedef {Object} FetchArchivedThreadOptions
* @property {string} [type='public'] The type of threads to fetch, either `public` or `private`
* @property {boolean} [fetchAll=false] Whether to fetch **all** archived threads when type is `private`.
* Requires `MANAGE_THREADS` if true
* @property {DateResolvable|ThreadChannelResolvable} [before] Only return threads that were created before this Date
* or Snowflake. <warn>Must be a {@link ThreadChannelResolvable} when type is `private` and fetchAll is `false`</warn>
* @property {number} [limit] Maximum number of threads to return
*/
/**
* The data returned from a thread fetch that returns multiple threads.
* @typedef {Object} FetchedThreads
* @property {Collection<Snowflake, ThreadChannel>} threads The threads that were fetched, with any members returned
* @property {?boolean} hasMore Whether there are potentially additional threads that require a subsequent call
*/
/**
* Obtains a set of archived threads from Discord, requires `READ_MESSAGE_HISTORY` in the parent channel.
* @param {FetchArchivedThreadOptions} [options] The options to fetch archived threads
* @param {boolean} [cache=true] Whether to cache the new thread objects if they aren't already
* @returns {Promise<FetchedThreads>}
*/
async fetchArchived({ type = 'public', fetchAll = false, before, limit } = {}, cache = true) {
let path = this.client.api.channels(this.channel.id);
if (type === 'private' && !fetchAll) {
path = path.users('@me');
}
let timestamp;
let id;
const query = new URLSearchParams();
if (typeof before !== 'undefined') {
if (before instanceof ThreadChannel || /^\d{16,19}$/.test(String(before))) {
id = this.resolveId(before);
timestamp = this.resolve(before)?.archivedAt?.toISOString();
const toUse = type === 'private' && !fetchAll ? id : timestamp;
if (toUse) {
query.set('before', toUse);
}
} else {
try {
timestamp = new Date(before).toISOString();
if (type === 'public' || fetchAll) {
query.set('before', timestamp);
}
} catch {
throw new TypeError('INVALID_TYPE', 'before', 'DateResolvable or ThreadChannelResolvable');
}
}
}
if (limit) {
query.set('limit', limit);
}
const raw = await path.threads.archived(type).get({ query });
return this.constructor._mapThreads(raw, this.client, { parent: this.channel, cache });
}
/**
* Obtains the accessible active threads from Discord, requires `READ_MESSAGE_HISTORY` in the parent channel.
* @param {boolean} [cache=true] Whether to cache the new thread objects if they aren't already
* @returns {Promise<FetchedThreads>}
*/
async fetchActive(cache = true) {
const raw = await this.client.api.guilds(this.channel.guild.id).threads.active.get();
return this.constructor._mapThreads(raw, this.client, { parent: this.channel, cache });
}
static _mapThreads(rawThreads, client, { parent, guild, cache }) {
const threads = rawThreads.threads.reduce((coll, raw) => {
const thread = client.channels._add(raw, guild ?? parent?.guild, { cache });
if (parent && thread.parentId !== parent.id) return coll;
return coll.set(thread.id, thread);
}, new Collection());
// Discord sends the thread id as id in this object
for (const rawMember of rawThreads.members) client.channels.cache.get(rawMember.id)?.members._add(rawMember);
return {
threads,
hasMore: rawThreads.has_more ?? false,
};
}
}
module.exports = ThreadManager;

View File

@@ -0,0 +1,128 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const { Routes } = require('discord-api-types/v9');
const CachedManager = require('./CachedManager');
const { TypeError } = require('../errors');
const ThreadMember = require('../structures/ThreadMember');
/**
* Manages API methods for GuildMembers and stores their cache.
* @extends {CachedManager}
*/
class ThreadMemberManager extends CachedManager {
constructor(thread, iterable) {
super(thread.client, ThreadMember, iterable);
/**
* The thread this manager belongs to
* @type {ThreadChannel}
*/
this.thread = thread;
}
/**
* The cache of this Manager
* @type {Collection<Snowflake, ThreadMember>}
* @name ThreadMemberManager#cache
*/
_add(data, cache = true) {
const existing = this.cache.get(data.user_id);
if (cache) existing?._patch(data);
if (existing) return existing;
const member = new ThreadMember(this.thread, data);
if (cache) this.cache.set(data.user_id, member);
return member;
}
/**
* Data that resolves to give a ThreadMember object. This can be:
* * A ThreadMember object
* * A User resolvable
* @typedef {ThreadMember|UserResolvable} ThreadMemberResolvable
*/
/**
* Resolves a {@link ThreadMemberResolvable} to a {@link ThreadMember} object.
* @param {ThreadMemberResolvable} member The user that is part of the thread
* @returns {?GuildMember}
*/
resolve(member) {
const memberResolvable = super.resolve(member);
if (memberResolvable) return memberResolvable;
const userResolvable = this.client.users.resolveId(member);
if (userResolvable) return super.resolve(userResolvable);
return null;
}
/**
* Resolves a {@link ThreadMemberResolvable} to a {@link ThreadMember} id string.
* @param {ThreadMemberResolvable} member The user that is part of the guild
* @returns {?Snowflake}
*/
resolveId(member) {
const memberResolvable = super.resolveId(member);
if (memberResolvable) return memberResolvable;
const userResolvable = this.client.users.resolveId(member);
return this.cache.has(userResolvable) ? userResolvable : null;
}
/**
* Adds a member to the thread.
* @param {UserResolvable|'@me'} member The member to add
* @param {string} [reason] The reason for adding this member
* @returns {Promise<Snowflake>}
*/
async add(member, reason) {
const id = member === '@me' ? member : this.client.users.resolveId(member);
if (!id) throw new TypeError('INVALID_TYPE', 'member', 'UserResolvable');
await this.client.api.channels(this.thread.id, 'thread-members', id).put({ reason });
return id;
}
/**
* Remove a user from the thread.
* @param {Snowflake|'@me'} id The id of the member to remove
* @param {string} [reason] The reason for removing this member from the thread
* @returns {Promise<Snowflake>}
*/
async remove(id, reason) {
await this.client.api.channels(this.thread.id, 'thread-members', id).delete({ reason });
return id;
}
async _fetchOne(memberId, cache, force) {
if (!force) {
const existing = this.cache.get(memberId);
if (existing) return existing;
}
const data = await this.client.api.channels(this.thread.id, 'thread-members', memberId).get();
return this._add(data, cache);
}
async _fetchMany(cache) {
const raw = await this.client.api.channels(this.thread.id, 'thread-members');
return raw.reduce((col, member) => col.set(member.user_id, this._add(member, cache)), new Collection());
}
/**
* @typedef {BaseFetchOptions} ThreadMemberFetchOptions
* @property {UserResolvable} [member] The specific user to fetch from the thread
*/
/**
* Fetches member(s) for the thread from Discord, requires access to the `GUILD_MEMBERS` gateway intent.
* @param {ThreadMemberFetchOptions|boolean} [options] Additional options for this fetch, when a `boolean` is provided
* all members are fetched with `options.cache` set to the boolean value
* @returns {Promise<ThreadMember|Collection<Snowflake, ThreadMember>>}
*/
fetch({ member, cache = true, force = false } = {}) {
const id = this.resolveId(member);
return id ? this._fetchOne(id, cache, force) : this._fetchMany(member ?? cache);
}
}
module.exports = ThreadMemberManager;

140
src/managers/UserManager.js Normal file
View File

@@ -0,0 +1,140 @@
'use strict';
const { ChannelType, Routes } = require('discord-api-types/v9');
const CachedManager = require('./CachedManager');
const { GuildMember } = require('../structures/GuildMember');
const { Message } = require('../structures/Message');
const ThreadMember = require('../structures/ThreadMember');
const User = require('../structures/User');
/**
* Manages API methods for users and stores their cache.
* @extends {CachedManager}
*/
class UserManager extends CachedManager {
constructor(client, iterable) {
super(client, User, iterable);
}
/**
* The cache of this manager
* @type {Collection<Snowflake, User>}
* @name UserManager#cache
*/
/**
* Data that resolves to give a User object. This can be:
* * A User object
* * A Snowflake
* * A Message object (resolves to the message author)
* * A GuildMember object
* * A ThreadMember object
* @typedef {User|Snowflake|Message|GuildMember|ThreadMember} UserResolvable
*/
/**
* The DM between the client's user and a user
* @param {Snowflake} userId The user id
* @returns {?DMChannel}
* @private
*/
dmChannel(userId) {
return this.client.channels.cache.find(c => c.type === ChannelType.DM && c.recipient.id === userId) ?? null;
}
/**
* Creates a {@link DMChannel} between the client and a user.
* @param {UserResolvable} user The UserResolvable to identify
* @param {BaseFetchOptions} [options] Additional options for this fetch
* @returns {Promise<DMChannel>}
*/
async createDM(user, { cache = true, force = false } = {}) {
const id = this.resolveId(user);
if (!force) {
const dmChannel = this.dmChannel(id);
if (dmChannel && !dmChannel.partial) return dmChannel;
}
const data = await this.client.api.users('@me').channels.post({ body: { recipient_id: id } });
return this.client.channels._add(data, null, { cache });
}
/**
* Deletes a {@link DMChannel} (if one exists) between the client and a user. Resolves with the channel if successful.
* @param {UserResolvable} user The UserResolvable to identify
* @returns {Promise<DMChannel>}
*/
async deleteDM(user) {
const id = this.resolveId(user);
const dmChannel = this.dmChannel(id);
if (!dmChannel) throw new Error('USER_NO_DM_CHANNEL');
await this.client.channels(dmChannel.id).delete();
this.client.channels._remove(dmChannel.id);
return dmChannel;
}
/**
* Obtains a user from Discord, or the user cache if it's already available.
* @param {UserResolvable} user The user to fetch
* @param {BaseFetchOptions} [options] Additional options for this fetch
* @returns {Promise<User>}
*/
async fetch(user, { cache = true, force = false } = {}) {
const id = this.resolveId(user);
if (!force) {
const existing = this.cache.get(id);
if (existing && !existing.partial) return existing;
}
const data = await this.client.api.users(id).get();
const userObject = this._add(data, cache);
if(!this.user.bot) await userObject.getProfile().catch(() => {});
return userObject;
}
/**
* Fetches a user's flags.
* @param {UserResolvable} user The UserResolvable to identify
* @param {BaseFetchOptions} [options] Additional options for this fetch
* @returns {Promise<UserFlagsBitField>}
*/
async fetchFlags(user, options) {
return (await this.fetch(user, options)).flags;
}
/**
* Sends a message to a user.
* @param {UserResolvable} user The UserResolvable to identify
* @param {string|MessagePayload|MessageOptions} options The options to provide
* @returns {Promise<Message>}
*/
async send(user, options) {
return (await this.createDM(user)).send(options);
}
/**
* Resolves a {@link UserResolvable} to a {@link User} object.
* @param {UserResolvable} user The UserResolvable to identify
* @returns {?User}
*/
resolve(user) {
if (user instanceof GuildMember || user instanceof ThreadMember) return user.user;
if (user instanceof Message) return user.author;
return super.resolve(user);
}
/**
* Resolves a {@link UserResolvable} to a {@link User} id.
* @param {UserResolvable} user The UserResolvable to identify
* @returns {?Snowflake}
*/
resolveId(user) {
if (user instanceof ThreadMember) return user.id;
if (user instanceof GuildMember) return user.user.id;
if (user instanceof Message) return user.author.id;
return super.resolveId(user);
}
}
module.exports = UserManager;

View File

@@ -0,0 +1,37 @@
'use strict';
const CachedManager = require('./CachedManager');
const VoiceState = require('../structures/VoiceState');
/**
* Manages API methods for VoiceStates and stores their cache.
* @extends {CachedManager}
*/
class VoiceStateManager extends CachedManager {
constructor(guild, iterable) {
super(guild.client, VoiceState, iterable);
/**
* The guild this manager belongs to
* @type {Guild}
*/
this.guild = guild;
}
/**
* The cache of this manager
* @type {Collection<Snowflake, VoiceState>}
* @name VoiceStateManager#cache
*/
_add(data, cache = true) {
const existing = this.cache.get(data.user_id);
if (existing) return existing._patch(data);
const entry = new this.holds(this.guild, data);
if (cache) this.cache.set(data.user_id, entry);
return entry;
}
}
module.exports = VoiceStateManager;