refactor!: Invite classes (#10888)

BREAKING CHANGE: Invites are now split up into classes based on their type.
This commit is contained in:
Jiralite
2025-07-17 13:34:25 +01:00
committed by GitHub
parent aee6d311c0
commit 43a995bef0
15 changed files with 595 additions and 426 deletions

View File

@@ -14,7 +14,6 @@ const { ShardClientUtil } = require('../sharding/ShardClientUtil.js');
const { ClientPresence } = require('../structures/ClientPresence.js');
const { GuildPreview } = require('../structures/GuildPreview.js');
const { GuildTemplate } = require('../structures/GuildTemplate.js');
const { Invite } = require('../structures/Invite.js');
const { SoundboardSound } = require('../structures/SoundboardSound.js');
const { Sticker } = require('../structures/Sticker.js');
const { StickerPack } = require('../structures/StickerPack.js');
@@ -24,6 +23,7 @@ const { Widget } = require('../structures/Widget.js');
const { resolveInviteCode, resolveGuildTemplateCode } = require('../util/DataResolver.js');
const { Events } = require('../util/Events.js');
const { IntentsBitField } = require('../util/IntentsBitField.js');
const { createInvite } = require('../util/Invites.js');
const { Options } = require('../util/Options.js');
const { PermissionsBitField } = require('../util/PermissionsBitField.js');
const { Status } = require('../util/Status.js');
@@ -459,6 +459,7 @@ class Client extends BaseClient {
* Options used when fetching an invite from Discord.
*
* @typedef {Object} ClientFetchInviteOptions
* @property {boolean} [withCounts] Whether to include approximate member counts
* @property {Snowflake} [guildScheduledEventId] The id of the guild scheduled event to include with
* the invite
*/
@@ -474,14 +475,16 @@ class Client extends BaseClient {
* .then(invite => console.log(`Obtained invite with code: ${invite.code}`))
* .catch(console.error);
*/
async fetchInvite(invite, options) {
async fetchInvite(invite, { withCounts, guildScheduledEventId } = {}) {
const code = resolveInviteCode(invite);
const query = makeURLSearchParams({
with_counts: true,
guild_scheduled_event_id: options?.guildScheduledEventId,
with_counts: withCounts,
guild_scheduled_event_id: guildScheduledEventId,
});
const data = await this.rest.get(Routes.invite(code), { query });
return new Invite(this, data);
return createInvite(this, data);
}
/**

View File

@@ -15,7 +15,7 @@ module.exports = (client, { d: data }) => {
* <info>This event requires the {@link PermissionFlagsBits.ManageChannels} permission for the channel.</info>
*
* @event Client#inviteCreate
* @param {Invite} invite The invite that was created
* @param {GuildInvite} invite The invite that was created
*/
client.emit(Events.InviteCreate, invite);
};

View File

@@ -1,6 +1,6 @@
'use strict';
const { Invite } = require('../../../structures/Invite.js');
const { GuildInvite } = require('../../../structures/GuildInvite.js');
const { Events } = require('../../../util/Events.js');
module.exports = (client, { d: data }) => {
@@ -9,7 +9,7 @@ module.exports = (client, { d: data }) => {
if (!channel) return;
const inviteData = Object.assign(data, { channel, guild });
const invite = new Invite(client, inviteData);
const invite = new GuildInvite(client, inviteData);
guild.invites.cache.delete(invite.code);
@@ -18,7 +18,7 @@ module.exports = (client, { d: data }) => {
* <info>This event requires the {@link PermissionFlagsBits.ManageChannels} permission for the channel.</info>
*
* @event Client#inviteDelete
* @param {Invite} invite The invite that was deleted
* @param {GuildInvite} invite The invite that was deleted
*/
client.emit(Events.InviteDelete, invite);
};

View File

@@ -124,6 +124,7 @@ exports.BaseGuildEmoji = require('./structures/BaseGuildEmoji.js').BaseGuildEmoj
exports.BaseGuildTextChannel = require('./structures/BaseGuildTextChannel.js').BaseGuildTextChannel;
exports.BaseGuildVoiceChannel = require('./structures/BaseGuildVoiceChannel.js').BaseGuildVoiceChannel;
exports.BaseInteraction = require('./structures/BaseInteraction.js').BaseInteraction;
exports.BaseInvite = require('./structures/BaseInvite.js').BaseInvite;
exports.BaseSelectMenuComponent = require('./structures/BaseSelectMenuComponent.js').BaseSelectMenuComponent;
exports.ButtonComponent = require('./structures/ButtonComponent.js').ButtonComponent;
exports.ButtonInteraction = require('./structures/ButtonInteraction.js').ButtonInteraction;
@@ -151,12 +152,14 @@ exports.Emoji = require('./structures/Emoji.js').Emoji;
exports.Entitlement = require('./structures/Entitlement.js').Entitlement;
exports.FileComponent = require('./structures/FileComponent.js').FileComponent;
exports.ForumChannel = require('./structures/ForumChannel.js').ForumChannel;
exports.GroupDMInvite = require('./structures/GroupDMInvite.js').GroupDMInvite;
exports.Guild = require('./structures/Guild.js').Guild;
exports.GuildAuditLogs = require('./structures/GuildAuditLogs.js').GuildAuditLogs;
exports.GuildAuditLogsEntry = require('./structures/GuildAuditLogsEntry.js').GuildAuditLogsEntry;
exports.GuildBan = require('./structures/GuildBan.js').GuildBan;
exports.GuildChannel = require('./structures/GuildChannel.js').GuildChannel;
exports.GuildEmoji = require('./structures/GuildEmoji.js').GuildEmoji;
exports.GuildInvite = require('./structures/GuildInvite.js').GuildInvite;
exports.GuildMember = require('./structures/GuildMember.js').GuildMember;
exports.GuildOnboarding = require('./structures/GuildOnboarding.js').GuildOnboarding;
exports.GuildOnboardingPrompt = require('./structures/GuildOnboardingPrompt.js').GuildOnboardingPrompt;
@@ -175,7 +178,6 @@ exports.InteractionCallbackResponse =
require('./structures/InteractionCallbackResponse.js').InteractionCallbackResponse;
exports.InteractionCollector = require('./structures/InteractionCollector.js').InteractionCollector;
exports.InteractionWebhook = require('./structures/InteractionWebhook.js').InteractionWebhook;
exports.Invite = require('./structures/Invite.js').Invite;
exports.InviteGuild = require('./structures/InviteGuild.js').InviteGuild;
exports.MediaChannel = require('./structures/MediaChannel.js').MediaChannel;
exports.MediaGalleryComponent = require('./structures/MediaGalleryComponent.js').MediaGalleryComponent;

View File

@@ -3,7 +3,7 @@
const { Collection } = require('@discordjs/collection');
const { Routes } = require('discord-api-types/v10');
const { DiscordjsError, ErrorCodes } = require('../errors/index.js');
const { Invite } = require('../structures/Invite.js');
const { GuildInvite } = require('../structures/GuildInvite.js');
const { resolveInviteCode } = require('../util/DataResolver.js');
const { CachedManager } = require('./CachedManager.js');
@@ -14,7 +14,7 @@ const { CachedManager } = require('./CachedManager.js');
*/
class GuildInviteManager extends CachedManager {
constructor(guild, iterable) {
super(guild.client, Invite, iterable);
super(guild.client, GuildInvite, iterable);
/**
* The guild this Manager belongs to
@@ -27,7 +27,7 @@ class GuildInviteManager extends CachedManager {
/**
* The cache of this Manager
*
* @type {Collection<string, Invite>}
* @type {Collection<string, GuildInvite>}
* @name GuildInviteManager#cache
*/
@@ -36,35 +36,44 @@ class GuildInviteManager extends CachedManager {
}
/**
* Data that resolves to give an Invite object. This can be:
* Data that resolves to give a `GuildInvite`. This can be:
*
* - An invite code
* - An invite URL
*
* @typedef {string} InviteResolvable
* @typedef {string} GuildInviteResolvable
*/
/**
* Data that can be resolved to a channel that an invite can be created on. This can be:
* A guild channel where an invite may be created on. This can be:
* - TextChannel
* - VoiceChannel
* - AnnouncementChannel
* - StageChannel
* - ForumChannel
* - MediaChannel
*
* @typedef {TextChannel|VoiceChannel|AnnouncementChannel|StageChannel|ForumChannel|MediaChannel}
* GuildInvitableChannel
*/
/**
* Data that can be resolved to a guild channel where an invite may be created on. This can be:
* - GuildInvitableChannel
* - Snowflake
*
* @typedef {TextChannel|VoiceChannel|AnnouncementChannel|StageChannel|ForumChannel|MediaChannel|Snowflake}
* @typedef {GuildInvitableChannel|Snowflake}
* GuildInvitableChannelResolvable
*/
/**
* Resolves an InviteResolvable to an Invite object.
* Resolves an `GuildInviteResolvable` to a `GuildInvite` object.
*
* @method resolve
* @memberof GuildInviteManager
* @instance
* @param {InviteResolvable} invite The invite resolvable to resolve
* @returns {?Invite}
* @param {GuildInviteResolvable} invite The invite resolvable to resolve
* @returns {?GuildInvite}
*/
/**
@@ -98,8 +107,9 @@ class GuildInviteManager extends CachedManager {
/**
* Fetches invite(s) from Discord.
*
* @param {InviteResolvable|FetchInviteOptions|FetchInvitesOptions} [options] Options for fetching guild invite(s)
* @returns {Promise<Invite|Collection<string, Invite>>}
* @param {GuildInviteResolvable|FetchInviteOptions|FetchInvitesOptions} [options]
* Options for fetching guild invite(s)
* @returns {Promise<GuildInvite|Collection<string, GuildInvite>>}
* @example
* // Fetch all invites from a guild
* guild.invites.fetch()
@@ -183,7 +193,7 @@ class GuildInviteManager extends CachedManager {
*
* @param {GuildInvitableChannelResolvable} channel The options for creating the invite from a channel.
* @param {InviteCreateOptions} [options={}] The options for creating the invite from a channel.
* @returns {Promise<Invite>}
* @returns {Promise<GuildInvite>}
* @example
* // Create an invite to a selected channel
* guild.invites.create('599942732013764608')
@@ -209,7 +219,7 @@ class GuildInviteManager extends CachedManager {
},
reason,
});
return new Invite(this.client, invite);
return new GuildInvite(this.client, invite);
}
/**

View File

@@ -10,8 +10,8 @@ const { ShardClientUtil } = require('../sharding/ShardClientUtil.js');
const { Guild } = require('../structures/Guild.js');
const { GuildChannel } = require('../structures/GuildChannel.js');
const { GuildEmoji } = require('../structures/GuildEmoji.js');
const { GuildInvite } = require('../structures/GuildInvite.js');
const { GuildMember } = require('../structures/GuildMember.js');
const { Invite } = require('../structures/Invite.js');
const { OAuth2Guild } = require('../structures/OAuth2Guild.js');
const { Role } = require('../structures/Role.js');
const { resolveImage } = require('../util/DataResolver.js');
@@ -119,7 +119,7 @@ class GuildManager extends CachedManager {
guild instanceof GuildMember ||
guild instanceof GuildEmoji ||
guild instanceof Role ||
(guild instanceof Invite && guild.guild)
(guild instanceof GuildInvite && guild.guild)
) {
return super.resolve(guild.guild);
}
@@ -142,7 +142,7 @@ class GuildManager extends CachedManager {
guild instanceof GuildMember ||
guild instanceof GuildEmoji ||
guild instanceof Role ||
(guild instanceof Invite && guild.guild)
(guild instanceof GuildInvite && guild.guild)
) {
return super.resolveId(guild.guild.id);
}

View File

@@ -0,0 +1,187 @@
'use strict';
const { RouteBases } = require('discord-api-types/v10');
const { Base } = require('./Base.js');
/**
* The base invite class.
*
* @extends {Base}
*/
class BaseInvite extends Base {
constructor(client, data) {
super(client);
/**
* The type of this invite.
*
* @type {InviteType}
*/
this.type = data.type;
/**
* The invite code.
*
* @type {string}
*/
this.code = data.code;
this._patch(data);
}
_patch(data) {
if ('inviter_id' in data) {
/**
* The id of the user that created this invite.
*
* @type {?Snowflake}
*/
this.inviterId = data.inviter_id;
} else {
this.inviterId ??= null;
}
if ('inviter' in data) {
this.client.users._add(data.inviter);
this.inviterId ??= data.inviter.id;
}
if ('max_age' in data) {
/**
* The maximum age of the invite in seconds. `0` for no expiry.
*
* @type {?number}
*/
this.maxAge = data.max_age;
} else {
this.maxAge ??= null;
}
if ('created_at' in data) {
/**
* The timestamp this invite was created at.
*
* @type {?number}
*/
this.createdTimestamp = Date.parse(data.created_at);
} else {
this.createdTimestamp ??= null;
}
if ('expires_at' in data) {
this._expiresTimestamp = data.expires_at && Date.parse(data.expires_at);
} else {
this._expiresTimestamp ??= null;
}
if ('channel_id' in data) {
/**
* The id of the channel this invite is for.
*
* @type {?Snowflake}
*/
this.channelId = data.channel_id;
}
if ('approximate_member_count' in data) {
/**
* The approximate total number of members.
*
* @type {?number}
*/
this.approximateMemberCount = data.approximate_member_count;
} else {
this.approximateMemberCount ??= null;
}
}
/**
* The user that created this invite.
*
* @type {?User}
* @readonly
*/
get inviter() {
return this.inviterId && this.client.users.resolve(this.inviterId);
}
/**
* The creation date of this invite.
*
* @type {?Date}
* @readonly
*/
get createdAt() {
return this.createdTimestamp && new Date(this.createdTimestamp);
}
/**
* The timestamp this invite expires at.
*
* @type {?number}
* @readonly
*/
get expiresTimestamp() {
return (
this._expiresTimestamp ??
(this.createdTimestamp && this.maxAge ? this.createdTimestamp + this.maxAge * 1_000 : null)
);
}
/**
* The expiry date of this invite.
*
* @type {?Date}
* @readonly
*/
get expiresAt() {
return this.expiresTimestamp && new Date(this.expiresTimestamp);
}
/**
* The URL to the invite.
*
* @type {string}
* @readonly
*/
get url() {
return `${RouteBases.invite}/${this.code}`;
}
/**
* A regular expression that matches Discord invite links.
* The `code` group property is present on the `exec()` result of this expression.
*
* @type {RegExp}
* @memberof BaseInvite
*/
static InvitesPattern = /discord(?:(?:app)?\.com\/invite|\.gg(?:\/invite)?)\/(?<code>[\w-]{2,255})/i;
/**
* When concatenated with a string, this automatically concatenates the invite's URL instead of the object.
*
* @returns {string}
* @example
* // Logs: Invite: https://discord.gg/djs
* console.log(`Invite: ${invite}`);
*/
toString() {
return this.url;
}
toJSON() {
return super.toJSON({
url: true,
expiresTimestamp: true,
uses: false,
channel: 'channelId',
inviter: 'inviterId',
});
}
valueOf() {
return this.code;
}
}
exports.BaseInvite = BaseInvite;

View File

@@ -0,0 +1,37 @@
'use strict';
const { BaseInvite } = require('./BaseInvite.js');
/**
* A channel invite leading to a group direct message channel.
*
* @extends {BaseInvite}
*/
class GroupDMInvite extends BaseInvite {
/**
* The approximate total number of members of in the group direct message channel.
* <info>This is only available when the invite was fetched through {@link Client#fetchInvite}.</info>
*
* @name GroupDMInvite#approximateMemberCount
* @type {?number}
*/
_patch(data) {
super._patch(data);
if ('channel' in data) {
/**
* The channel this invite is for.
*
* @type {?PartialGroupDMChannel}
*/
this.channel =
this.client.channels._add(data.channel, null, { cache: false }) ??
this.client.channels.cache.get(this.channelId);
this.channelId ??= data.channel.id;
}
}
}
exports.GroupDMInvite = GroupDMInvite;

View File

@@ -5,10 +5,10 @@ const { AuditLogOptionsType, AuditLogEvent } = require('discord-api-types/v10');
const { Partials } = require('../util/Partials.js');
const { flatten } = require('../util/Util.js');
const { AutoModerationRule } = require('./AutoModerationRule.js');
const { GuildInvite } = require('./GuildInvite.js');
const { GuildOnboardingPrompt } = require('./GuildOnboardingPrompt.js');
const { GuildScheduledEvent } = require('./GuildScheduledEvent.js');
const { Integration } = require('./Integration.js');
const { Invite } = require('./Invite.js');
const { StageInstance } = require('./StageInstance.js');
const { Sticker } = require('./Sticker.js');
const { Webhook } = require('./Webhook.js');
@@ -337,7 +337,7 @@ class GuildAuditLogsEntry {
this.target =
guild.invites.cache.get(inviteChange.new ?? inviteChange.old) ??
new Invite(guild.client, changesReduce(this.changes, { guild }));
new GuildInvite(guild.client, changesReduce(this.changes, { guild }));
} else if (targetType === Targets.Message) {
// Discord sends a channel id for the MessageBulkDelete action type.
this.target =

View File

@@ -0,0 +1,211 @@
'use strict';
const { Routes, PermissionFlagsBits, InviteType } = require('discord-api-types/v10');
const { DiscordjsError, ErrorCodes } = require('../errors/index.js');
const { BaseInvite } = require('./BaseInvite.js');
const { GuildScheduledEvent } = require('./GuildScheduledEvent.js');
const { IntegrationApplication } = require('./IntegrationApplication.js');
const { InviteGuild } = require('./InviteGuild.js');
/**
* A channel invite leading to a guild.
*
* @extends {BaseInvite}
*/
class GuildInvite extends BaseInvite {
constructor(client, data) {
super(client, data);
// Type may be missing from audit logs.
this.type = InviteType.Guild;
/**
* The id of the guild this invite is for.
*
* @type {Snowflake}
*/
// Guild id may be missing from audit logs.
this.guildId = data.guild_id ?? data.guild.id;
/**
* The maximum age of the invite in seconds. `0` for no expiry.
* <info>This is only available when the invite was fetched through {@link GuildInviteManager#fetch}
* or created through {@link GuildInviteManager#create}.</info>
*
* @name GuildInvite#maxAge
* @type {?number}
*/
/**
* The approximate total number of members of the guild.
* <info>This is only available when the invite was fetched through {@link Client#fetchInvite}.</info>
*
* @name GuildInvite#approximateMemberCount
* @type {?number}
*/
}
_patch(data) {
super._patch(data);
if ('guild' in data) {
/**
* The guild the invite is for. May include welcome screen data.
*
* @type {?(Guild|InviteGuild)}
*/
this.guild = this.client.guilds.cache.get(data.guild.id) ?? new InviteGuild(this.client, data.guild);
} else {
this.guild ??= null;
}
if ('channel' in data) {
/**
* The channel this invite is for.
*
* @type {?GuildInvitableChannel}
*/
this.channel =
this.client.channels._add(data.channel, this.guild, { cache: false }) ??
this.client.channels.cache.get(this.channelId);
this.channelId ??= data.channel.id;
}
if ('target_type' in data) {
/**
* The target type.
*
* @type {?InviteTargetType}
*/
this.targetType = data.target_type;
} else {
this.targetType ??= null;
}
if ('target_user' in data) {
/**
* The user whose stream to display for this voice channel stream invite.
*
* @type {?User}
*/
this.targetUser = this.client.users._add(data.target_user);
} else {
this.targetUser ??= null;
}
if ('target_application' in data) {
/**
* The embedded application to open for this voice channel embedded application invite.
*
* @type {?IntegrationApplication}
*/
this.targetApplication = new IntegrationApplication(this.client, data.target_application);
} else {
this.targetApplication ??= null;
}
if ('guild_scheduled_event' in data) {
/**
* The guild scheduled event data if there is a {@link GuildScheduledEvent} in the channel.
*
* @type {?GuildScheduledEvent}
*/
this.guildScheduledEvent = new GuildScheduledEvent(this.client, data.guild_scheduled_event);
} else {
this.guildScheduledEvent ??= null;
}
if ('uses' in data) {
/**
* How many times this invite has been used.
* <info>This is only available when the invite was fetched through {@link GuildInviteManager#fetch}
* or created through {@link GuildInviteManager#create}.</info>
*
* @type {?number}
*/
this.uses = data.uses;
} else {
this.uses ??= null;
}
if ('max_uses' in data) {
/**
* The maximum uses of this invite.
* <info>This is only available when the invite was fetched through {@link GuildInviteManager#fetch}
* or created through {@link GuildInviteManager#create}.</info>
*
* @type {?number}
*/
this.maxUses = data.max_uses;
} else {
this.maxUses ??= null;
}
if ('temporary' in data) {
/**
* Whether this invite grants temporary membership.
* <info>This is only available when the invite was fetched through {@link GuildInviteManager#fetch}
* or created through {@link GuildInviteManager#create}.</info>
*
* @type {?boolean}
*/
this.temporary = data.temporary ?? null;
} else {
this.temporary ??= null;
}
if ('approximate_presence_count' in data) {
/**
* The approximate number of online members of the guild.
* <info>This is only available when the invite was fetched through {@link Client#fetchInvite}.</info>
*
* @type {?number}
*/
this.approximatePresenceCount = data.approximate_presence_count;
} else {
this.approximatePresenceCount ??= null;
}
}
/**
* Whether the invite is deletable by the client user.
*
* @type {boolean}
* @readonly
*/
get deletable() {
const guild = this.guild;
if (!guild || !this.client.guilds.cache.has(guild.id)) return false;
if (!guild.members.me) throw new DiscordjsError(ErrorCodes.GuildUncachedMe);
return Boolean(
this.channel?.permissionsFor(this.client.user).has(PermissionFlagsBits.ManageChannels, false) ||
guild.members.me.permissions.has(PermissionFlagsBits.ManageGuild),
);
}
/**
* Delete this invite.
*
* @param {string} [reason] Reason for deleting this invite
* @returns {Promise<void>}
*/
async delete(reason) {
await this.client.rest.delete(Routes.invite(this.code), { reason });
}
toJSON() {
return super.toJSON({
url: true,
expiresTimestamp: true,
presenceCount: false,
memberCount: false,
uses: false,
channel: 'channelId',
inviter: 'inviterId',
guild: 'guildId',
});
}
}
exports.GuildInvite = GuildInvite;

View File

@@ -1,345 +0,0 @@
'use strict';
const { RouteBases, Routes, PermissionFlagsBits } = require('discord-api-types/v10');
const { DiscordjsError, ErrorCodes } = require('../errors/index.js');
const { Base } = require('./Base.js');
const { GuildScheduledEvent } = require('./GuildScheduledEvent.js');
const { IntegrationApplication } = require('./IntegrationApplication.js');
/**
* Represents an invitation to a guild channel.
*
* @extends {Base}
*/
class Invite extends Base {
/**
* A regular expression that matches Discord invite links.
* The `code` group property is present on the `exec()` result of this expression.
*
* @type {RegExp}
* @memberof Invite
*/
static InvitesPattern = /discord(?:(?:app)?\.com\/invite|\.gg(?:\/invite)?)\/(?<code>[\w-]{2,255})/i;
constructor(client, data) {
super(client);
/**
* The type of this invite
*
* @type {InviteType}
*/
this.type = data.type;
this._patch(data);
}
_patch(data) {
const { InviteGuild } = require('./InviteGuild.js');
/**
* The guild the invite is for including welcome screen data if present
*
* @type {?(Guild|InviteGuild)}
*/
this.guild ??= null;
if (data.guild) {
this.guild = this.client.guilds.cache.get(data.guild.id) ?? new InviteGuild(this.client, data.guild);
}
if ('code' in data) {
/**
* The code for this invite
*
* @type {string}
*/
this.code = data.code;
}
if ('approximate_presence_count' in data) {
/**
* The approximate number of online members of the guild this invite is for
* <info>This is only available when the invite was fetched through {@link Client#fetchInvite}.</info>
*
* @type {?number}
*/
this.presenceCount = data.approximate_presence_count;
} else {
this.presenceCount ??= null;
}
if ('approximate_member_count' in data) {
/**
* The approximate total number of members of the guild this invite is for
* <info>This is only available when the invite was fetched through {@link Client#fetchInvite}.</info>
*
* @type {?number}
*/
this.memberCount = data.approximate_member_count;
} else {
this.memberCount ??= null;
}
if ('temporary' in data) {
/**
* Whether or not this invite only grants temporary membership
* <info>This is only available when the invite was fetched through {@link GuildInviteManager#fetch}
* or created through {@link GuildInviteManager#create}.</info>
*
* @type {?boolean}
*/
this.temporary = data.temporary ?? null;
} else {
this.temporary ??= null;
}
if ('max_age' in data) {
/**
* The maximum age of the invite, in seconds, 0 if never expires
* <info>This is only available when the invite was fetched through {@link GuildInviteManager#fetch}
* or created through {@link GuildInviteManager#create}.</info>
*
* @type {?number}
*/
this.maxAge = data.max_age;
} else {
this.maxAge ??= null;
}
if ('uses' in data) {
/**
* How many times this invite has been used
* <info>This is only available when the invite was fetched through {@link GuildInviteManager#fetch}
* or created through {@link GuildInviteManager#create}.</info>
*
* @type {?number}
*/
this.uses = data.uses;
} else {
this.uses ??= null;
}
if ('max_uses' in data) {
/**
* The maximum uses of this invite
* <info>This is only available when the invite was fetched through {@link GuildInviteManager#fetch}
* or created through {@link GuildInviteManager#create}.</info>
*
* @type {?number}
*/
this.maxUses = data.max_uses;
} else {
this.maxUses ??= null;
}
if ('inviter_id' in data) {
/**
* The user's id who created this invite
*
* @type {?Snowflake}
*/
this.inviterId = data.inviter_id;
} else {
this.inviterId ??= null;
}
if ('inviter' in data) {
this.client.users._add(data.inviter);
this.inviterId = data.inviter.id;
}
if ('target_user' in data) {
/**
* The user whose stream to display for this voice channel stream invite
*
* @type {?User}
*/
this.targetUser = this.client.users._add(data.target_user);
} else {
this.targetUser ??= null;
}
if ('target_application' in data) {
/**
* The embedded application to open for this voice channel embedded application invite
*
* @type {?IntegrationApplication}
*/
this.targetApplication = new IntegrationApplication(this.client, data.target_application);
} else {
this.targetApplication ??= null;
}
if ('target_type' in data) {
/**
* The target type
*
* @type {?InviteTargetType}
*/
this.targetType = data.target_type;
} else {
this.targetType ??= null;
}
if ('channel_id' in data) {
/**
* The id of the channel this invite is for
*
* @type {?Snowflake}
*/
this.channelId = data.channel_id;
}
if ('channel' in data) {
/**
* The channel this invite is for
*
* @type {?BaseChannel}
*/
this.channel =
this.client.channels._add(data.channel, this.guild, { cache: false }) ??
this.client.channels.resolve(this.channelId);
this.channelId ??= data.channel.id;
}
if ('created_at' in data) {
/**
* The timestamp this invite was created at
*
* @type {?number}
*/
this.createdTimestamp = Date.parse(data.created_at);
} else {
this.createdTimestamp ??= null;
}
if ('expires_at' in data) {
this._expiresTimestamp = data.expires_at && Date.parse(data.expires_at);
} else {
this._expiresTimestamp ??= null;
}
if ('guild_scheduled_event' in data) {
/**
* The guild scheduled event data if there is a {@link GuildScheduledEvent} in the channel this invite is for
*
* @type {?GuildScheduledEvent}
*/
this.guildScheduledEvent = new GuildScheduledEvent(this.client, data.guild_scheduled_event);
} else {
this.guildScheduledEvent ??= null;
}
}
/**
* The time the invite was created at
*
* @type {?Date}
* @readonly
*/
get createdAt() {
return this.createdTimestamp && new Date(this.createdTimestamp);
}
/**
* Whether the invite is deletable by the client user
*
* @type {boolean}
* @readonly
*/
get deletable() {
const guild = this.guild;
if (!guild || !this.client.guilds.cache.has(guild.id)) return false;
if (!guild.members.me) throw new DiscordjsError(ErrorCodes.GuildUncachedMe);
return (
this.channel?.permissionsFor(this.client.user).has(PermissionFlagsBits.ManageChannels, false) ||
guild.members.me.permissions.has(PermissionFlagsBits.ManageGuild)
);
}
/**
* The timestamp the invite will expire at
*
* @type {?number}
* @readonly
*/
get expiresTimestamp() {
return (
this._expiresTimestamp ??
(this.createdTimestamp && this.maxAge ? this.createdTimestamp + this.maxAge * 1_000 : null)
);
}
/**
* The time the invite will expire at
*
* @type {?Date}
* @readonly
*/
get expiresAt() {
return this.expiresTimestamp && new Date(this.expiresTimestamp);
}
/**
* The user who created this invite
*
* @type {?User}
* @readonly
*/
get inviter() {
return this.inviterId && this.client.users.resolve(this.inviterId);
}
/**
* The URL to the invite
*
* @type {string}
* @readonly
*/
get url() {
return `${RouteBases.invite}/${this.code}`;
}
/**
* Deletes this invite.
*
* @param {string} [reason] Reason for deleting this invite
* @returns {Promise<Invite>}
*/
async delete(reason) {
await this.client.rest.delete(Routes.invite(this.code), { reason });
return this;
}
/**
* When concatenated with a string, this automatically concatenates the invite's URL instead of the object.
*
* @returns {string}
* @example
* // Logs: Invite: https://discord.gg/A1b2C3
* console.log(`Invite: ${invite}`);
*/
toString() {
return this.url;
}
toJSON() {
return super.toJSON({
url: true,
expiresTimestamp: true,
presenceCount: false,
memberCount: false,
uses: false,
channel: 'channelId',
inviter: 'inviterId',
guild: 'guildId',
});
}
valueOf() {
return this.code;
}
}
exports.Invite = Invite;

View File

@@ -5,7 +5,7 @@ const fs = require('node:fs/promises');
const path = require('node:path');
const { fetch } = require('undici');
const { DiscordjsError, DiscordjsTypeError, ErrorCodes } = require('../errors/index.js');
const { Invite } = require('../structures/Invite.js');
const { BaseInvite } = require('../structures/BaseInvite.js');
/**
* Data that can be resolved to give an invite code. This can be:
@@ -43,7 +43,7 @@ function resolveCode(data, regex) {
* @private
*/
function resolveInviteCode(data) {
return resolveCode(data, Invite.InvitesPattern);
return resolveCode(data, BaseInvite.InvitesPattern);
}
/**

View File

@@ -0,0 +1,31 @@
'use strict';
const { InviteType } = require('discord-api-types/v10');
const { BaseInvite } = require('../structures/BaseInvite.js');
const { GroupDMInvite } = require('../structures/GroupDMInvite.js');
const { GuildInvite } = require('../structures/GuildInvite.js');
/**
* Any invite.
*
* @typedef {GuildInvite|GroupDMInvite} Invite
*/
const InviteTypeToClass = {
[InviteType.Guild]: GuildInvite,
[InviteType.GroupDM]: GroupDMInvite,
};
/**
* Creates an invite.
*
* @param {Client} client The client
* @param {Object} data The data
* @returns {BaseInvite}
* @ignore
*/
function createInvite(client, data) {
return new (InviteTypeToClass[data.type] ?? BaseInvite)(client, data);
}
exports.createInvite = createInvite;

View File

@@ -615,8 +615,8 @@ export class BaseGuildTextChannel extends GuildChannel {
public nsfw: boolean;
public threads: GuildTextThreadManager<AllowedThreadTypeForAnnouncementChannel | AllowedThreadTypeForTextChannel>;
public topic: string | null;
public createInvite(options?: InviteCreateOptions): Promise<Invite>;
public fetchInvites(cache?: boolean): Promise<Collection<string, Invite>>;
public createInvite(options?: InviteCreateOptions): Promise<GuildInvite>;
public fetchInvites(cache?: boolean): Promise<Collection<string, GuildInvite>>;
public setDefaultAutoArchiveDuration(
defaultAutoArchiveDuration: ThreadAutoArchiveDuration,
reason?: string,
@@ -644,8 +644,8 @@ export class BaseGuildVoiceChannel extends GuildChannel {
public rtcRegion: string | null;
public userLimit: number;
public videoQualityMode: VideoQualityMode | null;
public createInvite(options?: InviteCreateOptions): Promise<Invite>;
public fetchInvites(cache?: boolean): Promise<Collection<string, Invite>>;
public createInvite(options?: InviteCreateOptions): Promise<GuildInvite>;
public fetchInvites(cache?: boolean): Promise<Collection<string, GuildInvite>>;
public setBitrate(bitrate: number, reason?: string): Promise<this>;
public setRTCRegion(rtcRegion: string | null, reason?: string): Promise<this>;
public setUserLimit(userLimit: number, reason?: string): Promise<this>;
@@ -931,6 +931,10 @@ export class Client<Ready extends boolean = boolean> extends BaseClient<ClientEv
public destroy(): Promise<void>;
public deleteWebhook(id: Snowflake, options?: WebhookDeleteOptions): Promise<void>;
public fetchGuildPreview(guild: GuildResolvable): Promise<GuildPreview>;
public fetchInvite(
invite: InviteResolvable,
options: ClientFetchInviteOptions & { withCounts: true },
): Promise<Invite<true>>;
public fetchInvite(invite: InviteResolvable, options?: ClientFetchInviteOptions): Promise<Invite>;
public fetchGuildTemplate(template: GuildTemplateResolvable): Promise<GuildTemplate>;
public fetchVoiceRegions(): Promise<Collection<string, VoiceRegion>>;
@@ -2018,37 +2022,49 @@ export class InteractionWebhook {
public fetchMessage(message: Snowflake | '@original'): Promise<Message>;
}
export class Invite extends Base {
private constructor(client: Client<true>, data: unknown);
public channel: NonThreadGuildBasedChannel | PartialGroupDMChannel | null;
public channelId: Snowflake | null;
public code: string;
public get deletable(): boolean;
export class BaseInvite<WithCounts extends boolean = boolean> extends Base {
protected constructor(client: Client<true>, data: unknown);
public readonly type: InviteType;
public readonly code: string;
public readonly inviterId: Snowflake | null;
public get inviter(): User | null;
public maxAge: number | null;
public get createdAt(): Date | null;
public createdTimestamp: number | null;
public get expiresAt(): Date | null;
public get expiresTimestamp(): number | null;
public guild: Guild | InviteGuild | null;
public get inviter(): User | null;
public inviterId: Snowflake | null;
public maxAge: number | null;
public maxUses: number | null;
public memberCount: number;
public presenceCount: number;
public targetApplication: IntegrationApplication | null;
public targetUser: User | null;
public targetType: InviteTargetType | null;
public temporary: boolean | null;
public type: InviteType;
public readonly channelId: Snowflake | null;
public approximateMemberCount: WithCounts extends true ? number : null;
public get url(): string;
public uses: number | null;
public delete(reason?: string): Promise<Invite>;
public toJSON(): unknown;
public toString(): string;
public static InvitesPattern: RegExp;
public guildScheduledEvent: GuildScheduledEvent | null;
public toString(): string;
public toJSON(): unknown;
}
export class GuildInvite<WithCounts extends boolean = boolean> extends BaseInvite<WithCounts> {
public readonly type: InviteType.Guild;
public guild: Guild | InviteGuild | null;
public readonly guildId: Snowflake;
public channel: NonThreadGuildBasedChannel | null;
public targetType: InviteTargetType | null;
public targetUser: User | null;
public targetApplication: IntegrationApplication | null;
public guildScheduledEvent: GuildScheduledEvent | null;
public uses: number | null;
public maxUses: number | null;
public temporary: boolean | null;
public approximatePresenceCount: WithCounts extends true ? number : null;
public get deletable(): boolean;
public delete(reason?: string): Promise<void>;
}
export class GroupDMInvite<WithCounts extends boolean = boolean> extends BaseInvite<WithCounts> {
public readonly type: InviteType.GroupDM;
public channel: PartialGroupDMChannel | null;
}
export type Invite<WithCounts extends boolean = boolean> = GroupDMInvite<WithCounts> | GuildInvite<WithCounts>;
export class InviteGuild extends AnonymousGuild {
private constructor(client: Client<true>, data: APIPartialGuild);
public welcomeScreen: WelcomeScreen | null;
@@ -2663,8 +2679,8 @@ export abstract class ThreadOnlyChannel extends GuildChannel {
public setAvailableTags(tags: readonly GuildForumTagData[], reason?: string): Promise<this>;
public setDefaultReactionEmoji(emojiId: DefaultReactionEmoji | null, reason?: string): Promise<this>;
public setDefaultThreadRateLimitPerUser(rateLimit: number, reason?: string): Promise<this>;
public createInvite(options?: InviteCreateOptions): Promise<Invite>;
public fetchInvites(cache?: boolean): Promise<Collection<string, Invite>>;
public createInvite(options?: InviteCreateOptions): Promise<GuildInvite>;
public fetchInvites(cache?: boolean): Promise<Collection<string, GuildInvite>>;
public setDefaultAutoArchiveDuration(
defaultAutoArchiveDuration: ThreadAutoArchiveDuration,
reason?: string,
@@ -4393,13 +4409,13 @@ export class GuildBanManager extends CachedManager<Snowflake, GuildBan, GuildBan
): Promise<BulkBanResult>;
}
export class GuildInviteManager extends DataManager<string, Invite, InviteResolvable> {
export class GuildInviteManager extends DataManager<string, GuildInvite, GuildInviteResolvable> {
private constructor(guild: Guild, iterable?: Iterable<unknown>);
public guild: Guild;
public create(channel: GuildInvitableChannelResolvable, options?: InviteCreateOptions): Promise<Invite>;
public fetch(options: FetchInviteOptions | InviteResolvable): Promise<Invite>;
public fetch(options?: FetchInvitesOptions): Promise<Collection<string, Invite>>;
public delete(invite: InviteResolvable, reason?: string): Promise<Invite>;
public create(channel: GuildInvitableChannelResolvable, options?: InviteCreateOptions): Promise<GuildInvite>;
public fetch(options: FetchInviteOptions | InviteResolvable): Promise<GuildInvite>;
public fetch(options?: FetchInvitesOptions): Promise<Collection<string, GuildInvite>>;
public delete(invite: InviteResolvable, reason?: string): Promise<void>;
}
export class GuildScheduledEventManager extends CachedManager<
@@ -5206,7 +5222,7 @@ export interface Caches {
// TODO: GuildChannelManager: [manager: typeof GuildChannelManager, holds: typeof GuildChannel];
GuildEmojiManager: [manager: typeof GuildEmojiManager, holds: typeof GuildEmoji];
GuildForumThreadManager: [manager: typeof GuildForumThreadManager, holds: typeof ThreadChannel<true>];
GuildInviteManager: [manager: typeof GuildInviteManager, holds: typeof Invite];
GuildInviteManager: [manager: typeof GuildInviteManager, holds: typeof GuildInvite];
// TODO: GuildManager: [manager: typeof GuildManager, holds: typeof Guild];
GuildMemberManager: [manager: typeof GuildMemberManager, holds: typeof GuildMember];
GuildMessageManager: [manager: typeof GuildMessageManager, holds: typeof Message<true>];
@@ -5367,8 +5383,8 @@ export interface ClientEventTypes {
guildUpdate: [oldGuild: Guild, newGuild: Guild];
interactionCreate: [interaction: Interaction];
invalidated: [];
inviteCreate: [invite: Invite];
inviteDelete: [invite: Invite];
inviteCreate: [invite: GuildInvite];
inviteDelete: [invite: GuildInvite];
messageCreate: [message: OmitPartialGroupDMChannel<Message>];
messageDelete: [message: OmitPartialGroupDMChannel<Message | PartialMessage>];
messageDeleteBulk: [
@@ -5430,6 +5446,7 @@ export interface ClientEventTypes {
export interface ClientFetchInviteOptions {
guildScheduledEventId?: Snowflake;
withCounts?: boolean;
}
export interface ClientOptions extends WebhookClientOptions {
@@ -5989,7 +6006,7 @@ export interface GuildAuditLogsEntryTargetField<TAction extends AuditLogEvent> {
GuildOnboardingPrompt: GuildOnboardingPrompt | { [x: string]: unknown; id: Snowflake };
GuildScheduledEvent: GuildScheduledEvent;
Integration: Integration;
Invite: Invite;
Invite: GuildInvite;
Message: TAction extends AuditLogEvent.MessageBulkDelete ? GuildTextBasedChannel | { id: Snowflake } : User | null;
Role: Role | { id: Snowflake };
SoundboardSound: SoundboardSound | { id: Snowflake };
@@ -6176,7 +6193,14 @@ export interface GuildMemberEditOptions {
roles?: ReadonlyCollection<Snowflake, Role> | readonly RoleResolvable[];
}
export type GuildResolvable = Guild | GuildEmoji | GuildMember | Invite | NonThreadGuildBasedChannel | Role | Snowflake;
export type GuildResolvable =
| Guild
| GuildEmoji
| GuildInvite
| GuildMember
| NonThreadGuildBasedChannel
| Role
| Snowflake;
export interface GuildPruneMembersOptions {
count?: boolean;
@@ -6411,14 +6435,9 @@ export interface InviteGenerationOptions {
scopes: readonly OAuth2Scopes[];
}
export type GuildInvitableChannelResolvable =
| AnnouncementChannel
| ForumChannel
| MediaChannel
| Snowflake
| StageChannel
| TextChannel
| VoiceChannel;
export type GuildInvitableChannel = AnnouncementChannel | ForumChannel | MediaChannel | TextChannel | VoiceChannel;
export type GuildInvitableChannelResolvable = GuildInvitableChannel | Snowflake;
export interface InviteCreateOptions {
maxAge?: number;
@@ -6432,6 +6451,7 @@ export interface InviteCreateOptions {
}
export type InviteResolvable = string;
export type GuildInviteResolvable = string;
export interface LifetimeFilterOptions<Key, Value> {
excludeFromSweep?(value: Value, key: Key, collection: LimitedCollection<Key, Value>): boolean;
@@ -7004,7 +7024,7 @@ export interface SweeperDefinitions {
emojis: [Snowflake, GuildEmoji];
entitlements: [Snowflake, Entitlement];
guildMembers: [Snowflake, GuildMember];
invites: [string, Invite, true];
invites: [string, GuildInvite, true];
messages: [Snowflake, Message, true];
presences: [Snowflake, Presence];
reactions: [Snowflake | string, MessageReaction];

View File

@@ -199,6 +199,8 @@ import type {
User,
VoiceBasedChannel,
VoiceChannel,
Invite,
GuildInvite,
} from './index.js';
import {
ActionRowBuilder,
@@ -262,6 +264,10 @@ if (client.isReady()) {
expectType<Client>(client);
}
expectType<Promise<Invite>>(client.fetchInvite('https://discord.gg/djs'));
expectType<Promise<Invite<true>>>(client.fetchInvite('https://discord.gg/djs', { withCounts: true }));
expectNotType<Promise<Invite<true>>>(client.fetchInvite('https://discord.gg/djs', { withCounts: false }));
const testGuildId = '222078108977594368'; // DJS
const testUserId = '987654321098765432'; // example id
const globalCommandId = '123456789012345678'; // example id
@@ -407,8 +413,15 @@ client.on('interactionCreate', async interaction => {
}
});
client.on('inviteCreate', ({ client }) => expectType<Client<true>>(client));
client.on('inviteDelete', ({ client }) => expectType<Client<true>>(client));
client.on('inviteCreate', invite => {
expectType<GuildInvite>(invite);
expectType<Client<true>>(invite.client);
});
client.on('inviteDelete', invite => {
expectType<GuildInvite>(invite);
expectType<Client<true>>(invite.client);
});
// This is to check that stuff is the right type
declare const assertIsMessage: (m: Promise<Message>) => void;