diff --git a/esm/discord.mjs b/esm/discord.mjs index befac0f11003..c966234d99d6 100644 --- a/esm/discord.mjs +++ b/esm/discord.mjs @@ -32,6 +32,7 @@ export const { BaseGuildEmojiManager, ChannelManager, GuildApplicationCommandManager, + GuildBanManager, GuildChannelManager, GuildEmojiManager, GuildEmojiRoleManager, @@ -67,6 +68,7 @@ export const { Emoji, Guild, GuildAuditLogs, + GuildBan, GuildChannel, GuildEmoji, GuildMember, diff --git a/src/client/actions/ActionsManager.js b/src/client/actions/ActionsManager.js index 4055795aa92c..3a239595e9d1 100644 --- a/src/client/actions/ActionsManager.js +++ b/src/client/actions/ActionsManager.js @@ -21,6 +21,7 @@ class ActionsManager { this.register(require('./InviteDelete')); this.register(require('./GuildMemberRemove')); this.register(require('./GuildMemberUpdate')); + this.register(require('./GuildBanAdd')); this.register(require('./GuildBanRemove')); this.register(require('./GuildRoleCreate')); this.register(require('./GuildRoleDelete')); diff --git a/src/client/actions/GuildBanAdd.js b/src/client/actions/GuildBanAdd.js new file mode 100644 index 000000000000..2efe1634737d --- /dev/null +++ b/src/client/actions/GuildBanAdd.js @@ -0,0 +1,20 @@ +'use strict'; + +const Action = require('./Action'); +const { Events } = require('../../util/Constants'); + +class GuildBanAdd extends Action { + handle(data) { + const client = this.client; + const guild = client.guilds.cache.get(data.guild_id); + + /** + * Emitted whenever a member is banned from a guild. + * @event Client#guildBanAdd + * @param {GuildBan} ban The ban that occurred + */ + if (guild) client.emit(Events.GUILD_BAN_ADD, guild.bans.add(data)); + } +} + +module.exports = GuildBanAdd; diff --git a/src/client/actions/GuildBanRemove.js b/src/client/actions/GuildBanRemove.js index fc28e113f66b..515473599197 100644 --- a/src/client/actions/GuildBanRemove.js +++ b/src/client/actions/GuildBanRemove.js @@ -1,20 +1,24 @@ 'use strict'; const Action = require('./Action'); +const GuildBan = require('../../structures/GuildBan'); const { Events } = require('../../util/Constants'); class GuildBanRemove extends Action { handle(data) { const client = this.client; const guild = client.guilds.cache.get(data.guild_id); - const user = client.users.add(data.user); + /** * Emitted whenever a member is unbanned from a guild. * @event Client#guildBanRemove - * @param {Guild} guild The guild that the unban occurred in - * @param {User} user The user that was unbanned + * @param {GuildBan} ban The ban that was removed */ - if (guild && user) client.emit(Events.GUILD_BAN_REMOVE, guild, user); + if (guild) { + const ban = guild.bans.cache.get(data.user.id) ?? new GuildBan(client, data, guild); + guild.bans.cache.delete(ban.user.id); + client.emit(Events.GUILD_BAN_REMOVE, ban); + } } } diff --git a/src/client/websocket/handlers/GUILD_BAN_ADD.js b/src/client/websocket/handlers/GUILD_BAN_ADD.js index 5d4a0965c7b5..d8dc0f9da746 100644 --- a/src/client/websocket/handlers/GUILD_BAN_ADD.js +++ b/src/client/websocket/handlers/GUILD_BAN_ADD.js @@ -1,16 +1,5 @@ 'use strict'; -const { Events } = require('../../../util/Constants'); - -module.exports = (client, { d: data }) => { - const guild = client.guilds.cache.get(data.guild_id); - const user = client.users.add(data.user); - - /** - * Emitted whenever a member is banned from a guild. - * @event Client#guildBanAdd - * @param {Guild} guild The guild that the ban occurred in - * @param {User} user The user that was banned - */ - if (guild && user) client.emit(Events.GUILD_BAN_ADD, guild, user); +module.exports = (client, packet) => { + client.actions.GuildBanAdd.handle(packet.d); }; diff --git a/src/index.js b/src/index.js index 3d4f0519a111..92fd0789bd40 100644 --- a/src/index.js +++ b/src/index.js @@ -37,6 +37,7 @@ module.exports = { BaseGuildEmojiManager: require('./managers/BaseGuildEmojiManager'), ChannelManager: require('./managers/ChannelManager'), GuildApplicationCommandManager: require('./managers/GuildApplicationCommandManager'), + GuildBanManager: require('./managers/GuildBanManager'), GuildChannelManager: require('./managers/GuildChannelManager'), GuildEmojiManager: require('./managers/GuildEmojiManager'), GuildEmojiRoleManager: require('./managers/GuildEmojiRoleManager'), @@ -79,6 +80,7 @@ module.exports = { Emoji: require('./structures/Emoji'), Guild: require('./structures/Guild'), GuildAuditLogs: require('./structures/GuildAuditLogs'), + GuildBan: require('./structures/GuildBan'), GuildChannel: require('./structures/GuildChannel'), GuildEmoji: require('./structures/GuildEmoji'), GuildMember: require('./structures/GuildMember'), diff --git a/src/managers/GuildBanManager.js b/src/managers/GuildBanManager.js new file mode 100644 index 000000000000..cb2dd6d03712 --- /dev/null +++ b/src/managers/GuildBanManager.js @@ -0,0 +1,177 @@ +'use strict'; + +const BaseManager = require('./BaseManager'); +const GuildBan = require('../structures/GuildBan'); +const GuildMember = require('../structures/GuildMember'); +const Collection = require('../util/Collection'); + +/** + * Manages API methods for GuildBans and stores their cache. + * @extends {BaseManager} + */ +class GuildBanManager extends BaseManager { + constructor(guild, iterable) { + super(guild.client, iterable, GuildBan); + + /** + * The guild this Manager belongs to + * @type {Guild} + */ + this.guild = guild; + } + + /** + * The cache of this Manager + * @type {Collection} + * @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 {Object} FetchBanOptions + * @property {UserResolvable} user The ban to fetch + * @property {boolean} [cache=true] Whether or not to cache the fetched ban + * @property {boolean} [force=false] Whether to skip the cache check and request the API + */ + + /** + * 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|Promise>} + * @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 }); + if (options.user) { + 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()); + } + + /** + * Bans a user from the guild. + * @param {UserResolvable} user The user to ban + * @param {Object} [options] Options for the ban + * @param {number} [options.days=0] Number of days of messages to delete, must be between 0 and 7, inclusive + * @param {string} [options.reason] Reason for banning + * @returns {Promise} 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(user => console.log(`Banned ${user.username ?? user.id ?? user} from ${guild.name}`)) + * .catch(console.error); + */ + async create(user, options = { days: 0 }) { + 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({ + data: { + reason: options.reason, + delete_message_days: options.days, + }, + }); + 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} + * @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; diff --git a/src/managers/GuildMemberManager.js b/src/managers/GuildMemberManager.js index 882d4d584cbb..f4ac65ac9af5 100644 --- a/src/managers/GuildMemberManager.js +++ b/src/managers/GuildMemberManager.js @@ -216,29 +216,15 @@ class GuildMemberManager extends BaseManager { * @returns {Promise} 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(user => console.log(`Banned ${user.username || user.id || user} from ${guild.name}`)) + * .then(user => console.log(`Banned ${user.username ?? user.id ?? user} from ${guild.name}`)) * .catch(console.error); */ ban(user, options = { days: 0 }) { - if (typeof options !== 'object') return Promise.reject(new TypeError('INVALID_TYPE', 'options', 'object', true)); - if (options.days) options.delete_message_days = options.days; - const id = this.client.users.resolveID(user); - if (!id) return Promise.reject(new Error('BAN_RESOLVE_ID', true)); - return this.client.api - .guilds(this.guild.id) - .bans[id].put({ data: options }) - .then(() => { - if (user instanceof GuildMember) return user; - const _user = this.client.users.resolve(id); - if (_user) { - const member = this.resolve(_user); - return member || _user; - } - return id; - }); + return this.guild.bans.create(user, options); } /** @@ -246,6 +232,7 @@ class GuildMemberManager extends BaseManager { * @param {UserResolvable} user The user to unban * @param {string} [reason] Reason for unbanning user * @returns {Promise} + * Internally calls the GuildBanManager#remove method. * @example * // Unban a user by ID (or with a user/guild member object) * guild.members.unban('84484653687267328') @@ -253,12 +240,7 @@ class GuildMemberManager extends BaseManager { * .catch(console.error); */ unban(user, reason) { - const id = this.client.users.resolveID(user); - if (!id) return Promise.reject(new Error('BAN_RESOLVE_ID')); - return this.client.api - .guilds(this.guild.id) - .bans[id].delete({ reason }) - .then(() => this.client.users.resolve(user)); + return this.guild.bans.remove(user, reason); } _fetchSingle({ user, cache, force = false }) { diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 5e8b76c3573e..47ec2414fffd 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -10,6 +10,7 @@ const VoiceRegion = require('./VoiceRegion'); const Webhook = require('./Webhook'); const { Error, TypeError } = require('../errors'); const GuildApplicationCommandManager = require('../managers/GuildApplicationCommandManager'); +const GuildBanManager = require('../managers/GuildBanManager'); const GuildChannelManager = require('../managers/GuildChannelManager'); const GuildEmojiManager = require('../managers/GuildEmojiManager'); const GuildMemberManager = require('../managers/GuildMemberManager'); @@ -61,6 +62,12 @@ class Guild extends Base { */ this.channels = new GuildChannelManager(this); + /** + * A manager of the bans belonging to this guild + * @type {GuildBanManager} + */ + this.bans = new GuildBanManager(this); + /** * A manager of the roles belonging to this guild * @type {RoleManager} @@ -632,50 +639,6 @@ class Guild extends Base { }); } - /** - * An object containing information about a guild member's ban. - * @typedef {Object} BanInfo - * @property {User} user User that was banned - * @property {?string} reason Reason the user was banned - */ - - /** - * Fetches information on a banned user from this guild. - * @param {UserResolvable} user The User to fetch the ban info of - * @returns {Promise} - */ - fetchBan(user) { - const id = this.client.users.resolveID(user); - if (!id) throw new Error('FETCH_BAN_RESOLVE_ID'); - return this.client.api - .guilds(this.id) - .bans(id) - .get() - .then(ban => ({ - reason: ban.reason, - user: this.client.users.add(ban.user), - })); - } - - /** - * Fetches a collection of banned users in this guild. - * @returns {Promise>} - */ - fetchBans() { - return this.client.api - .guilds(this.id) - .bans.get() - .then(bans => - bans.reduce((collection, ban) => { - collection.set(ban.user.id, { - reason: ban.reason, - user: this.client.users.add(ban.user), - }); - return collection; - }, new Collection()), - ); - } - /** * Fetches a collection of integrations to this guild. * Resolves with a collection mapping integrations by their ids. diff --git a/src/structures/GuildBan.js b/src/structures/GuildBan.js new file mode 100644 index 000000000000..f83ca496b22e --- /dev/null +++ b/src/structures/GuildBan.js @@ -0,0 +1,63 @@ +'use strict'; + +const Base = require('./Base'); + +/** + * Represents a ban in a guild on Discord. + * @extends {Base} + */ +class GuildBan extends Base { + /** + * @param {Client} client The instantiating client + * @param {Object} data The data for the ban + * @param {Guild} guild The guild in which the ban is + */ + constructor(client, data, guild) { + super(client); + + /** + * The guild in which the ban is + * @type {Guild} + */ + this.guild = guild; + + this._patch(data); + } + + _patch(data) { + /** + * The user this ban applies to + * @type {User} + */ + this.user = this.client.users.add(data.user, true); + + if ('reason' in data) { + /** + * The reason for the ban + * @type {?string} + */ + this.reason = data.reason; + } + } + + /** + * Whether this GuildBan is a partial + * If the reason is not provided the value is null + * @type {boolean} + * @readonly + */ + get partial() { + return !('reason' in this); + } + + /** + * Fetches this GuildBan. + * @param {boolean} [force=false] Whether to skip the cache check and request the API + * @returns {Promise} + */ + fetch(force = false) { + return this.guild.bans.fetch({ user: this.user, cache: true, force }); + } +} + +module.exports = GuildBan; diff --git a/typings/index.d.ts b/typings/index.d.ts index c44de9184354..88be93f89d3a 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -696,6 +696,7 @@ declare module 'discord.js' { public approximatePresenceCount: number | null; public available: boolean; public banner: string | null; + public bans: GuildBanManager; public channels: GuildChannelManager; public commands: GuildApplicationCommandManager; public readonly createdAt: Date; @@ -757,8 +758,6 @@ declare module 'discord.js' { public equals(guild: Guild): boolean; public fetch(): Promise; public fetchAuditLogs(options?: GuildAuditLogsFetchOptions): Promise; - public fetchBan(user: UserResolvable): Promise<{ user: User; reason: string }>; - public fetchBans(): Promise>; public fetchIntegrations(): Promise>; public fetchInvites(): Promise>; public fetchOwner(options?: FetchOwnerOptions): Promise; @@ -844,6 +843,15 @@ declare module 'discord.js' { public toJSON(): object; } + export class GuildBan extends Base { + constructor(client: Client, data: object, guild: Guild); + public guild: Guild; + public user: User; + public readonly partial: boolean; + public reason?: string | null; + public fetch(force?: boolean): Promise; + } + export class GuildChannel extends Channel { constructor(guild: Guild, data?: object); private memberPermissions(member: GuildMember): Readonly; @@ -2158,6 +2166,15 @@ declare module 'discord.js' { public unban(user: UserResolvable, reason?: string): Promise; } + export class GuildBanManager extends BaseManager { + constructor(guild: Guild, iterable?: Iterable); + public guild: Guild; + public create(user: UserResolvable, options?: BanOptions): Promise; + public fetch(options: UserResolvable | FetchBanOptions): Promise; + public fetch(options?: FetchBansOptions): Promise>; + public remove(user: UserResolvable, reason?: string): Promise; + } + export class GuildMemberRoleManager { constructor(member: GuildMember); public readonly cache: Collection; @@ -2560,8 +2577,8 @@ declare module 'discord.js' { emojiDelete: [emoji: GuildEmoji]; emojiUpdate: [oldEmoji: GuildEmoji, newEmoji: GuildEmoji]; error: [error: Error]; - guildBanAdd: [guild: Guild, user: User]; - guildBanRemove: [guild: Guild, user: User]; + guildBanAdd: [ban: GuildBan]; + guildBanRemove: [ban: GuildBan]; guildCreate: [guild: Guild]; guildDelete: [guild: Guild]; guildUnavailable: [guild: Guild]; @@ -2776,6 +2793,16 @@ declare module 'discord.js' { CommandInteraction: typeof CommandInteraction; } + interface FetchBanOptions { + user: UserResolvable; + cache?: boolean; + force?: boolean; + } + + interface FetchBansOptions { + cache: boolean; + } + interface FetchMemberOptions { user: UserResolvable; cache?: boolean; @@ -2870,6 +2897,8 @@ declare module 'discord.js' { UNKNOWN?: string; } + type GuildBanResolvable = GuildBan | UserResolvable; + type GuildChannelResolvable = Snowflake | GuildChannel; interface GuildCreateChannelOptions {