diff --git a/packages/discord.js/src/managers/GuildInviteManager.js b/packages/discord.js/src/managers/GuildInviteManager.js index 960dbf364411..bf3fefe29689 100644 --- a/packages/discord.js/src/managers/GuildInviteManager.js +++ b/packages/discord.js/src/managers/GuildInviteManager.js @@ -1,12 +1,12 @@ 'use strict'; +const { Buffer } = require('node:buffer'); const { Collection } = require('@discordjs/collection'); const { Routes } = require('discord-api-types/v10'); const { DiscordjsError, ErrorCodes } = require('../errors/index.js'); const { GuildInvite } = require('../structures/GuildInvite.js'); -const { resolveInviteCode } = require('../util/DataResolver.js'); +const { resolveInviteCode, resolveFile } = require('../util/DataResolver.js'); const { CachedManager } = require('./CachedManager.js'); - /** * Manages API methods for GuildInvites and stores their cache. * @@ -104,6 +104,18 @@ class GuildInviteManager extends CachedManager { * @property {boolean} [cache=true] Whether or not to cache the fetched invites */ + /** + * Job status for target users of an invite + * + * @typedef {Object} TargetUsersJobStatusForInvite + * @property {InviteTargetUsersJobStatus} status The status of job processing the users + * @property {number} totalUsers The total number of users provided in the list + * @property {number} processedUsers The total number of users processed so far + * @property {Date|null} createdAt The time when the job was created + * @property {Date|null} completedAt The time when the job was successfully completed + * @property {string|null} errorMessage The error message if the job failed + */ + /** * Fetches invite(s) from Discord. * @@ -191,7 +203,7 @@ class GuildInviteManager extends CachedManager { /** * Create an invite to the guild from the provided channel. * - * @param {GuildInvitableChannelResolvable} channel The options for creating the invite from a channel. + * @param {GuildInvitableChannelResolvable} channel The channel where invite should be created. * @param {InviteCreateOptions} [options={}] The options for creating the invite from a channel. * @returns {Promise} * @example @@ -202,23 +214,38 @@ class GuildInviteManager extends CachedManager { */ async create( channel, - { temporary, maxAge, maxUses, unique, targetUser, targetApplication, targetType, reason } = {}, + { + temporary, + maxAge, + maxUses, + unique, + targetUser, + targetApplication, + targetType, + roles, + targetUsersFile, + reason, + } = {}, ) { const id = this.guild.channels.resolveId(channel); if (!id) throw new DiscordjsError(ErrorCodes.GuildChannelResolve); - + const options = { + temporary, + max_age: maxAge, + max_uses: maxUses, + unique, + target_user_id: this.client.users.resolveId(targetUser), + target_application_id: targetApplication?.id ?? targetApplication?.applicationId ?? targetApplication, + role_ids: roles?.map(role => this.guild.roles.resolveId(role)), + target_type: targetType, + }; const invite = await this.client.rest.post(Routes.channelInvites(id), { - 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, - }, + body: targetUsersFile ? await this._createInviteFormData({ targetUsersFile, ...options }) : options, + // This is necessary otherwise rest stringifies the body + passThroughBody: Boolean(targetUsersFile), reason, }); + return new GuildInvite(this.client, invite); } @@ -234,6 +261,78 @@ class GuildInviteManager extends CachedManager { await this.client.rest.delete(Routes.invite(code), { reason }); } + + /** + * Get target users for an invite + * + * @param {InviteResolvable} invite The invite to get the target users + * @returns {Promise} The csv file containing target users + */ + async fetchTargetUsers(invite) { + const code = resolveInviteCode(invite); + const arrayBuff = await this.client.rest.get(Routes.inviteTargetUsers(code)); + + return Buffer.from(arrayBuff); + } + + /** + * Updates target users for an invite + * + * @param {InviteResolvable} invite The invite to update the target users + * @param {UserResolvable[]|BufferResolvable} targetUsersFile An array of users or a csv file with a single column of user IDs + * for all the users able to accept this invite + * @returns {Promise} + */ + async updateTargetUsers(invite, targetUsersFile) { + const code = resolveInviteCode(invite); + + return this.client.rest.put(Routes.inviteTargetUsers(code), { + body: await this._createInviteFormData({ targetUsersFile }), + // This is necessary otherwise rest stringifies the body + passThroughBody: true, + }); + } + + /** + * Get status of the job processing target users of an invite + * + * @param {InviteResolvable} invite The invite to get the target users for + * @returns {Promise} The target users + */ + async fetchTargetUsersJobStatus(invite) { + const code = resolveInviteCode(invite); + const job = await this.client.rest.get(Routes.inviteTargetUsersJobStatus(code)); + return { + status: job.status, + totalUsers: job.total_users, + processedUsers: job.processed_users, + createdAt: job.created_at ? new Date(job.created_at) : null, + completedAt: job.completed_at ? new Date(job.completed_at) : null, + errorMessage: job.error_message ?? null, + }; + } + + /** + * Creates form data body payload for invite + * + * @param {InviteCreateOptions} options The options for creating invite + * @returns {Promise} + * @private + */ + async _createInviteFormData({ targetUsersFile, ...rest } = {}) { + const formData = new FormData(); + let usersCsv; + if (Array.isArray(targetUsersFile)) { + usersCsv = targetUsersFile.map(user => this.client.users.resolveId(user)).join('\n'); + } else { + const resolved = await resolveFile(targetUsersFile); + usersCsv = resolved.data.toString('utf8'); + } + + formData.append('target_users_file', new Blob([usersCsv], { type: 'text/csv' }), 'users.csv'); + formData.append('payload_json', JSON.stringify(rest)); + return formData; + } } exports.GuildInviteManager = GuildInviteManager; diff --git a/packages/discord.js/src/structures/BaseGuildTextChannel.js b/packages/discord.js/src/structures/BaseGuildTextChannel.js index ae7eb073e5ab..b2c8a2658214 100644 --- a/packages/discord.js/src/structures/BaseGuildTextChannel.js +++ b/packages/discord.js/src/structures/BaseGuildTextChannel.js @@ -161,6 +161,9 @@ class BaseGuildTextChannel extends GuildChannel { * required if `targetType` is {@link InviteTargetType.Stream}, the application must have the * {@link InviteTargetType.EmbeddedApplication} flag * @property {InviteTargetType} [targetType] The type of the target for this voice channel invite + * @property {UserResolvable[]|BufferResolvable} [targetUsersFile] An array of users or a csv file with a single column of user IDs + * for all the users able to accept this invite + * @property {RoleResolvable[]} [roles] The roles in the guild given to users that accept this invite * @property {string} [reason] The reason for creating the invite */ diff --git a/packages/discord.js/src/structures/GuildInvite.js b/packages/discord.js/src/structures/GuildInvite.js index 179aac0b2523..b6f53bd740d1 100644 --- a/packages/discord.js/src/structures/GuildInvite.js +++ b/packages/discord.js/src/structures/GuildInvite.js @@ -1,5 +1,6 @@ 'use strict'; +const { Collection } = require('@discordjs/collection'); const { Routes, PermissionFlagsBits, InviteType } = require('discord-api-types/v10'); const { DiscordjsError, ErrorCodes } = require('../errors/index.js'); const { InviteFlagsBitField } = require('../util/InviteFlagsBitField.js'); @@ -7,6 +8,7 @@ const { BaseInvite } = require('./BaseInvite.js'); const { GuildScheduledEvent } = require('./GuildScheduledEvent.js'); const { IntegrationApplication } = require('./IntegrationApplication.js'); const { InviteGuild } = require('./InviteGuild.js'); +const { Role } = require('./Role.js'); /** * A channel invite leading to a guild. @@ -178,6 +180,17 @@ class GuildInvite extends BaseInvite { } else { this.approximatePresenceCount ??= null; } + + if ('roles' in data) { + /** + * The roles assigned to the user upon accepting the invite. + * + * @type {Collection|null} + */ + this.roles = new Collection(data.roles.map(role => [role.id, new Role(this.client, role, this.guild)])); + } else { + this.roles ??= null; + } } /** @@ -206,6 +219,35 @@ class GuildInvite extends BaseInvite { await this.client.rest.delete(Routes.invite(this.code), { reason }); } + /** + * Update target users of this invite. + * + * @param {UserResolvable[]|BufferResolvable} targetUsersFile An array of users or a csv file with a single column of user IDs + * for all the users able to accept this invite + * @returns {Promise} + */ + updateTargetUsers(targetUsersFile) { + return this.guild.invites.updateTargetUsers(this.code, targetUsersFile); + } + + /** + * Get target users of this invite + * + * @returns {Promise} + */ + fetchTargetUsers() { + return this.guild.invites.fetchTargetUsers(this.code); + } + + /** + * Get status of the job processing target users of this invite + * + * @returns {Promise} + */ + fetchTargetUsersJobStatus() { + return this.guild.invites.fetchTargetUsersJobStatus(this.code); + } + toJSON() { return super.toJSON({ url: true, @@ -216,6 +258,7 @@ class GuildInvite extends BaseInvite { channel: 'channelId', inviter: 'inviterId', guild: 'guildId', + roles: 'roles', }); } } diff --git a/packages/discord.js/src/util/APITypes.js b/packages/discord.js/src/util/APITypes.js index 1e11f869279b..6129872253ac 100644 --- a/packages/discord.js/src/util/APITypes.js +++ b/packages/discord.js/src/util/APITypes.js @@ -519,6 +519,11 @@ * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/InviteFlags} */ +/** + * @external InviteTargetUsersJobStatus + * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/InviteTargetUsersJobStatus} + */ + /** * @external InviteType * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/InviteType} diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index 8db068601377..1c09c08e8183 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -162,6 +162,7 @@ import { InteractionType, InviteFlags, InviteTargetType, + InviteTargetUsersJobStatus, InviteType, Locale, LocalizationMap, @@ -2060,6 +2061,7 @@ export class GuildInvite extends BaseInvit public guild: Guild | InviteGuild | null; public readonly guildId: Snowflake; public channel: NonThreadGuildBasedChannel | null; + public roles: Collection | null; public targetType: InviteTargetType | null; public targetUser: User | null; public targetApplication: IntegrationApplication | null; @@ -2070,6 +2072,9 @@ export class GuildInvite extends BaseInvit public approximatePresenceCount: WithCounts extends true ? number : null; public get deletable(): boolean; public delete(reason?: string): Promise; + public updateTargetUsers(targetUsersFile: BufferResolvable | readonly UserResolvable[]): Promise; + public fetchTargetUsersJobStatus(): Promise; + public fetchTargetUsers(): Promise; } export type InviteFlagsString = keyof typeof InviteFlags; @@ -4513,13 +4518,28 @@ export class GuildBanManager extends CachedManager; } +export interface TargetUsersJobStatusForInvite { + completedAt: Date | null; + createdAt: Date | null; + errorMessage: string | null; + processedUsers: number; + status: InviteTargetUsersJobStatus; + totalUsers: number; +} export class GuildInviteManager extends DataManager { private constructor(guild: Guild, iterable?: Iterable); public guild: Guild; public create(channel: GuildInvitableChannelResolvable, options?: InviteCreateOptions): Promise; + public updateTargetUsers( + invite: InviteResolvable, + targetUsersFile: BufferResolvable | readonly UserResolvable[], + ): Promise; + public fetchTargetUsers(invite: InviteResolvable): Promise; + public fetchTargetUsersJobStatus(invite: InviteResolvable): Promise; public fetch(options: FetchInviteOptions | InviteResolvable): Promise; public fetch(options?: FetchInvitesOptions): Promise>; public delete(invite: InviteResolvable, reason?: string): Promise; + private _createInviteFormData(options: InviteCreateOptions): Promise; } export class GuildScheduledEventManager extends CachedManager< @@ -6582,9 +6602,11 @@ export interface InviteCreateOptions { maxAge?: number; maxUses?: number; reason?: string; + roles?: readonly RoleResolvable[]; targetApplication?: ApplicationResolvable; targetType?: InviteTargetType; targetUser?: UserResolvable; + targetUsersFile?: BufferResolvable | readonly UserResolvable[]; temporary?: boolean; unique?: boolean; }