diff --git a/src/client/actions/InviteCreate.js b/src/client/actions/InviteCreate.js index 34cfa8455072..6aee0b95db6d 100644 --- a/src/client/actions/InviteCreate.js +++ b/src/client/actions/InviteCreate.js @@ -1,7 +1,6 @@ 'use strict'; const Action = require('./Action'); -const Invite = require('../../structures/Invite'); const { Events } = require('../../util/Constants'); class InviteCreateAction extends Action { @@ -12,7 +11,8 @@ class InviteCreateAction extends Action { if (!channel) return false; const inviteData = Object.assign(data, { channel, guild }); - const invite = new Invite(client, inviteData); + const invite = guild.invites.add(inviteData); + /** * Emitted when an invite is created. * This event only triggers if the client has `MANAGE_GUILD` permissions for the guild, diff --git a/src/client/actions/InviteDelete.js b/src/client/actions/InviteDelete.js index 96e22aa03275..c6c039ca36f3 100644 --- a/src/client/actions/InviteDelete.js +++ b/src/client/actions/InviteDelete.js @@ -13,6 +13,7 @@ class InviteDeleteAction extends Action { const inviteData = Object.assign(data, { channel, guild }); const invite = new Invite(client, inviteData); + guild.invites.cache.delete(invite.code); /** * Emitted when an invite is deleted. diff --git a/src/errors/Messages.js b/src/errors/Messages.js index 6869cc3cece3..8d97ec7aa497 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -113,6 +113,10 @@ const Messages = { VANITY_URL: 'This guild does not have the VANITY_URL feature enabled.', + INVITE_RESOLVE_CODE: 'Could not resolve the code to fetch the invite.', + + INVITE_NOT_FOUND: 'Could not find the requested invite.', + DELETE_GROUP_DM_CHANNEL: "Bots don't have access to Group DM Channels and cannot delete them", FETCH_GROUP_DM_CHANNEL: "Bots don't have access to Group DM Channels and cannot fetch them", diff --git a/src/managers/GuildInviteManager.js b/src/managers/GuildInviteManager.js new file mode 100644 index 000000000000..e9b47cdea367 --- /dev/null +++ b/src/managers/GuildInviteManager.js @@ -0,0 +1,199 @@ +'use strict'; + +const BaseManager = require('./BaseManager'); +const { Error } = require('../errors'); +const Invite = require('../structures/Invite'); +const Collection = require('../util/Collection'); +const DataResolver = require('../util/DataResolver'); + +/** + * Manages API methods for GuildInvites and stores their cache. + * @extends {BaseManager} + */ +class GuildInviteManager extends BaseManager { + constructor(guild, iterable) { + super(guild.client, iterable, Invite); + + /** + * The guild this Manager belongs to + * @type {Guild} + */ + this.guild = guild; + } + + /** + * The cache of this Manager + * @type {Collection} + * @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 + */ + + /** + * 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 {boolean} cache 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>} + * @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 {GuildChannelResolvable} channel The options for creating the invite from a channel. + * @param {CreateInviteOptions} [options={}] The options for creating the invite from a channel. + * @returns {Promise} + * @example + * // Create an invite to a selected channel + * guild.invites.create('599942732013764608') + * .then(console.log) + * .catch(console.error); + */ + async create( + channel, + { temporary = false, maxAge = 86400, maxUses = 0, 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({ + data: { + 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} + */ + async delete(invite, reason) { + const code = DataResolver.resolveInviteCode(invite); + + await this.client.api.invites(code).delete({ reason }); + } +} + +module.exports = GuildInviteManager; diff --git a/src/structures/Guild.js b/src/structures/Guild.js index d56df8afa5c7..5cb24ffe52c5 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -5,7 +5,6 @@ const GuildAuditLogs = require('./GuildAuditLogs'); const GuildPreview = require('./GuildPreview'); const GuildTemplate = require('./GuildTemplate'); const Integration = require('./Integration'); -const Invite = require('./Invite'); const Webhook = require('./Webhook'); const WelcomeScreen = require('./WelcomeScreen'); const { Error, TypeError } = require('../errors'); @@ -586,35 +585,6 @@ class Guild extends AnonymousGuild { .then(data => new GuildTemplate(this.client, data)); } - /** - * Fetches a collection of invites to this guild. - * Resolves with a collection mapping invites by their codes. - * @returns {Promise>} - * @example - * // Fetch invites - * guild.fetchInvites() - * .then(invites => console.log(`Fetched ${invites.size} invites`)) - * .catch(console.error); - * @example - * // Fetch invite creator by their id - * guild.fetchInvites() - * .then(invites => console.log(invites.find(invite => invite.inviter.id === '84484653687267328'))) - * .catch(console.error); - */ - fetchInvites() { - return this.client.api - .guilds(this.id) - .invites.get() - .then(inviteItems => { - const invites = new Collection(); - for (const inviteItem of inviteItems) { - const invite = new Invite(this.client, inviteItem); - invites.set(invite.code, invite); - } - return invites; - }); - } - /** * Obtains a guild preview for this guild from Discord. * @returns {Promise} diff --git a/src/structures/GuildAuditLogs.js b/src/structures/GuildAuditLogs.js index 1bfe246fed64..6680af2ce488 100644 --- a/src/structures/GuildAuditLogs.js +++ b/src/structures/GuildAuditLogs.js @@ -475,7 +475,7 @@ class GuildAuditLogsEntry { if (me.permissions.has(Permissions.FLAGS.MANAGE_GUILD)) { let change = this.changes.find(c => c.key === 'code'); change = change.new ?? change.old; - return guild.fetchInvites().then(invites => { + return guild.invites.fetch().then(invites => { this.target = invites.find(i => i.code === change); }); } else { diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index 756be7bea4ee..c0e9705136de 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -1,7 +1,6 @@ 'use strict'; const Channel = require('./Channel'); -const Invite = require('./Invite'); const PermissionOverwrites = require('./PermissionOverwrites'); const { Error } = require('../errors'); const PermissionOverwriteManager = require('../managers/PermissionOverwriteManager'); @@ -470,46 +469,18 @@ class GuildChannel extends Channel { * .then(invite => console.log(`Created an invite with a code of ${invite.code}`)) * .catch(console.error); */ - createInvite({ - temporary = false, - maxAge = 86400, - maxUses = 0, - unique, - targetUser, - targetApplication, - targetType, - reason, - } = {}) { - return this.client.api - .channels(this.id) - .invites.post({ - data: { - 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, - }) - .then(invite => new Invite(this.client, invite)); + createInvite(options) { + return this.guild.invites.create(this.id, options); } /** * Fetches a collection of invites to this guild channel. * Resolves with a collection mapping invites by their codes. + * @param {boolean} [cache=true] Whether or not to cache the fetched invites * @returns {Promise>} */ - async fetchInvites() { - const inviteItems = await this.client.api.channels(this.id).invites.get(); - const invites = new Collection(); - for (const inviteItem of inviteItems) { - const invite = new Invite(this.client, inviteItem); - invites.set(invite.code, invite); - } - return invites; + fetchInvites(cache = true) { + return this.guild.invites.fetch({ channelID: this.id, cache }); } /** diff --git a/typings/index.d.ts b/typings/index.d.ts index 3365888f1800..1f186af86a68 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -876,6 +876,7 @@ declare module 'discord.js' { public approximatePresenceCount: number | null; public available: boolean; public bans: GuildBanManager; + public invites: GuildInviteManager; public channels: GuildChannelManager; public commands: GuildApplicationCommandManager; public defaultMessageNotifications: DefaultMessageNotificationLevel | number; @@ -924,7 +925,6 @@ declare module 'discord.js' { public equals(guild: Guild): boolean; public fetchAuditLogs(options?: GuildAuditLogsFetchOptions): Promise; public fetchIntegrations(): Promise>; - public fetchInvites(): Promise>; public fetchOwner(options?: FetchOwnerOptions): Promise; public fetchPreview(): Promise; public fetchTemplates(): Promise>; @@ -1038,7 +1038,7 @@ declare module 'discord.js' { public createInvite(options?: CreateInviteOptions): Promise; public edit(data: ChannelData, reason?: string): Promise; public equals(channel: GuildChannel): boolean; - public fetchInvites(): Promise>; + public fetchInvites(cache?: boolean): Promise>; public lockPermissions(): Promise; public permissionsFor(memberOrRole: GuildMember | Role): Readonly; public permissionsFor(memberOrRole: GuildMemberResolvable | RoleResolvable): Readonly | null; @@ -2550,6 +2550,15 @@ declare module 'discord.js' { public remove(user: UserResolvable, reason?: string): Promise; } + export class GuildInviteManager extends DataManager { + constructor(guild: Guild, iterable?: Iterable); + public guild: Guild; + public create(channel: GuildChannelResolvable, options?: CreateInviteOptions): Promise; + public fetch(options: InviteResolvable | FetchInviteOptions): Promise; + public fetch(options?: FetchInvitesOptions): Promise>; + public delete(invite: InviteResolvable, reason?: string): Promise; + } + export class GuildMemberRoleManager extends DataManager { constructor(member: GuildMember); public readonly hoist: Role | null; @@ -3324,6 +3333,15 @@ declare module 'discord.js' { cache: boolean; } + interface FetchInviteOptions extends BaseFetchOptions { + code: string; + } + + interface FetchInvitesOptions { + channelID?: Snowflake; + cache?: boolean; + } + interface FetchGuildOptions extends BaseFetchOptions { guild: GuildResolvable; }