diff --git a/esm/discord.mjs b/esm/discord.mjs index c966234d99d6..2d2dceeac40a 100644 --- a/esm/discord.mjs +++ b/esm/discord.mjs @@ -56,6 +56,7 @@ export const { Base, Activity, APIMessage, + BaseGuild, BaseGuildEmoji, BaseGuildVoiceChannel, CategoryChannel, @@ -85,6 +86,7 @@ export const { MessageMentions, MessageReaction, NewsChannel, + OAuth2Guild, PermissionOverwrites, Presence, ClientPresence, diff --git a/src/index.js b/src/index.js index 92fd0789bd40..b70c20739af5 100644 --- a/src/index.js +++ b/src/index.js @@ -65,6 +65,7 @@ module.exports = { Base: require('./structures/Base'), Activity: require('./structures/Presence').Activity, APIMessage: require('./structures/APIMessage'), + BaseGuild: require('./structures/BaseGuild'), BaseGuildEmoji: require('./structures/BaseGuildEmoji'), BaseGuildVoiceChannel: require('./structures/BaseGuildVoiceChannel'), CategoryChannel: require('./structures/CategoryChannel'), @@ -97,6 +98,7 @@ module.exports = { MessageMentions: require('./structures/MessageMentions'), MessageReaction: require('./structures/MessageReaction'), NewsChannel: require('./structures/NewsChannel'), + OAuth2Guild: require('./structures/OAuth2Guild'), PermissionOverwrites: require('./structures/PermissionOverwrites'), Presence: require('./structures/Presence').Presence, ClientPresence: require('./structures/ClientPresence'), diff --git a/src/managers/GuildManager.js b/src/managers/GuildManager.js index f30879d2edc4..770191654104 100644 --- a/src/managers/GuildManager.js +++ b/src/managers/GuildManager.js @@ -6,7 +6,9 @@ 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 Collection = require('../util/Collection'); const { ChannelTypes, Events, @@ -231,25 +233,41 @@ class GuildManager extends BaseManager { } /** - * Obtains a guild from Discord, or the guild cache if it's already available. - * @param {Snowflake} id ID of the guild - * @param {boolean} [cache=true] Whether to cache the new guild object if it isn't already - * @param {boolean} [force=false] Whether to skip the cache check and request the API - * @returns {Promise} - * @example - * // Fetch a guild by its id - * client.guilds.fetch('222078108977594368') - * .then(guild => console.log(guild.name)) - * .catch(console.error); + * Options used to fetch a single guild. + * @typedef {Object} FetchGuildOptions + * @property {GuildResolvable} guild The guild to fetch + * @property {boolean} [cache=true] Whether or not to cache the fetched guild + * @property {boolean} [force=false] Whether to skip the cache check and request the API */ - async fetch(id, cache = true, force = false) { - if (!force) { - const existing = this.cache.get(id); - if (existing) return existing; + + /** + * 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=100] Maximum number of guilds to request (1-100) + */ + + /** + * Obtains one or multiple guilds from Discord, or the guild cache if it's already available. + * @param {GuildResolvable|FetchGuildOptions|FetchGuildsOptions} [options] ID of the guild or options + * @returns {Promise>} + */ + 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: { with_counts: true } }); + return this.add(data, options.cache); } - const data = await this.client.api.guilds(id).get({ query: { with_counts: true } }); - return this.add(data, cache); + const data = await this.client.api.users('@me').guilds.get({ query: options }); + return data.reduce((coll, guild) => coll.set(guild.id, new OAuth2Guild(this.client, guild)), new Collection()); } } diff --git a/src/structures/BaseGuild.js b/src/structures/BaseGuild.js new file mode 100644 index 000000000000..561375f3c0d0 --- /dev/null +++ b/src/structures/BaseGuild.js @@ -0,0 +1,115 @@ +'use strict'; + +const Base = require('./Base'); +const SnowflakeUtil = require('../util/SnowflakeUtil'); + +/** + * The base class for {@link Guild} and {@link OAuth2Guild}. + * @extends {Base} + */ +class BaseGuild extends Base { + constructor(client, data) { + super(client); + + /** + * The ID of this guild + * @type {Snowflake} + */ + this.id = data.id; + + /** + * The name of this guild + * @type {string} + */ + this.name = data.name; + + /** + * The icon hash of this guild + * @type {?string} + */ + this.icon = data.icon; + + /** + * An array of features available to this guild + * @type {Features[]} + */ + this.features = data.features; + } + + /** + * The timestamp this guild was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return SnowflakeUtil.deconstruct(this.id).timestamp; + } + + /** + * The time this guild was created at + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * The acronym that shows up in place of a guild icon + * @type {string} + * @readonly + */ + get nameAcronym() { + return this.name + .replace(/'s /g, ' ') + .replace(/\w+/g, e => e[0]) + .replace(/\s/g, ''); + } + + /** + * Whether this guild is partnered + * @type {boolean} + * @readonly + */ + get partnered() { + return this.features.includes('PARTNERED'); + } + + /** + * Whether this guild is verified + * @type {boolean} + * @readonly + */ + get verified() { + return this.features.includes('VERIFIED'); + } + + /** + * The URL to this guild's icon. + * @param {ImageURLOptions} [options={}] Options for the Image URL + * @returns {?string} + */ + iconURL({ format, size, dynamic } = {}) { + if (!this.icon) return null; + return this.client.rest.cdn.Icon(this.id, this.icon, format, size, dynamic); + } + + /** + * Fetches this guild. + * @returns {Promise} + */ + async fetch() { + const data = await this.client.api.guilds(this.id).get({ query: { with_counts: true } }); + return this.client.guilds.add(data); + } + + /** + * When concatenated with a string, this automatically returns the guild's name instead of the Guild object. + * @returns {string} + */ + toString() { + return this.name; + } +} + +module.exports = BaseGuild; diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 80a2b0ea9b31..6e52dbee173d 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -1,6 +1,6 @@ 'use strict'; -const Base = require('./Base'); +const BaseGuild = require('./BaseGuild'); const GuildAuditLogs = require('./GuildAuditLogs'); const GuildPreview = require('./GuildPreview'); const GuildTemplate = require('./GuildTemplate'); @@ -27,7 +27,6 @@ const { NSFWLevels, } = require('../util/Constants'); const DataResolver = require('../util/DataResolver'); -const SnowflakeUtil = require('../util/SnowflakeUtil'); const SystemChannelFlags = require('../util/SystemChannelFlags'); const Util = require('../util/Util'); @@ -35,15 +34,11 @@ const Util = require('../util/Util'); * Represents a guild (or a server) on Discord. * It's recommended to see if a guild is available before performing operations or reading data from it. You can * check this with `guild.available`. - * @extends {Base} + * @extends {BaseGuild} */ -class Guild extends Base { - /** - * @param {Client} client The instantiating client - * @param {Object} data The data for the guild - */ +class Guild extends BaseGuild { constructor(client, data) { - super(client); + super(client, data); /** * A manager of the application commands belonging to this guild @@ -100,12 +95,6 @@ class Guild extends Base { * @type {boolean} */ this.available = false; - - /** - * The Unique ID of the guild, useful for comparisons - * @type {Snowflake} - */ - this.id = data.id; } else { this._patch(data); if (!data.channels) this.available = false; @@ -133,17 +122,11 @@ class Guild extends Base { * @private */ _patch(data) { - /** - * The name of the guild - * @type {string} - */ + this.id = data.id; this.name = data.name; - - /** - * The hash of the guild icon - * @type {?string} - */ this.icon = data.icon; + this.features = data.features; + this.available = !data.unavailable; /** * The hash of the guild invite splash image @@ -204,12 +187,6 @@ class Guild extends Base { * @typedef {string} Features */ - /** - * An array of guild features available to the guild - * @type {Features[]} - */ - this.features = data.features; - /** * The ID of the application that created this guild (if applicable) * @type {?Snowflake} @@ -378,10 +355,6 @@ class Guild extends Base { */ this.banner = data.banner; - this.id = data.id; - this.available = !data.unavailable; - this.features = data.features || this.features || []; - /** * The ID of the rules channel for the guild * @type {?Snowflake} @@ -463,24 +436,6 @@ class Guild extends Base { return this.client.rest.cdn.Banner(this.id, this.banner, format, size); } - /** - * The timestamp the guild was created at - * @type {number} - * @readonly - */ - get createdTimestamp() { - return SnowflakeUtil.deconstruct(this.id).timestamp; - } - - /** - * The time the guild was created at - * @type {Date} - * @readonly - */ - get createdAt() { - return new Date(this.createdTimestamp); - } - /** * The time the client user joined the guild * @type {Date} @@ -490,46 +445,6 @@ class Guild extends Base { return new Date(this.joinedTimestamp); } - /** - * If this guild is partnered - * @type {boolean} - * @readonly - */ - get partnered() { - return this.features.includes('PARTNERED'); - } - - /** - * If this guild is verified - * @type {boolean} - * @readonly - */ - get verified() { - return this.features.includes('VERIFIED'); - } - - /** - * The URL to this guild's icon. - * @param {ImageURLOptions} [options={}] Options for the Image URL - * @returns {?string} - */ - iconURL({ format, size, dynamic } = {}) { - if (!this.icon) return null; - return this.client.rest.cdn.Icon(this.id, this.icon, format, size, dynamic); - } - - /** - * The acronym that shows up in place of a guild icon. - * @type {string} - * @readonly - */ - get nameAcronym() { - return this.name - .replace(/'s /g, ' ') - .replace(/\w+/g, e => e[0]) - .replace(/\s/g, ''); - } - /** * The URL to this guild's invite splash image. * @param {ImageURLOptions} [options={}] Options for the Image URL @@ -626,20 +541,6 @@ class Guild extends Base { ); } - /** - * Fetches this guild. - * @returns {Promise} - */ - fetch() { - return this.client.api - .guilds(this.id) - .get({ query: { with_counts: true } }) - .then(data => { - this._patch(data); - return this; - }); - } - /** * Fetches a collection of integrations to this guild. * Resolves with a collection mapping integrations by their ids. @@ -1409,17 +1310,6 @@ class Guild extends Base { ); } - /** - * When concatenated with a string, this automatically returns the guild's name instead of the Guild object. - * @returns {string} - * @example - * // Logs: Hello from My Guild! - * console.log(`Hello from ${guild}!`); - */ - toString() { - return this.name; - } - toJSON() { const json = super.toJSON({ available: false, diff --git a/src/structures/OAuth2Guild.js b/src/structures/OAuth2Guild.js new file mode 100644 index 000000000000..3f44ad47122d --- /dev/null +++ b/src/structures/OAuth2Guild.js @@ -0,0 +1,28 @@ +'use strict'; + +const BaseGuild = require('./BaseGuild'); +const Permissions = require('../util/Permissions'); + +/** + * A partial guild received when using {@link GuildManager#fetch} to fetch multiple guilds. + * @extends {BaseGuild} + */ +class OAuth2Guild extends BaseGuild { + constructor(client, data) { + super(client, data); + + /** + * Whether the client user is the owner of the guild + * @type {boolean} + */ + this.owner = data.owner; + + /** + * The permissions that the client user has in this guild + * @type {Readonly} + */ + this.permissions = new Permissions(BigInt(data.permissions)).freeze(); + } +} + +module.exports = OAuth2Guild; diff --git a/typings/index.d.ts b/typings/index.d.ts index 56df4e014c22..b9088e04871e 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -227,6 +227,21 @@ declare module 'discord.js' { public toJSON(...props: { [key: string]: boolean | string }[]): object; } + export class BaseGuild extends Base { + public readonly createdAt: Date; + public readonly createdTimestamp: number; + public features: GuildFeatures[]; + public icon: string | null; + public id: Snowflake; + public name: string; + public readonly nameAcronym: string; + public readonly partnered: boolean; + public readonly verified: boolean; + public fetch(): Promise; + public iconURL(options?: ImageURLOptions & { dynamic?: boolean }): string | null; + public toString(): string; + } + export class BaseGuildEmoji extends Emoji { constructor(client: Client, data: object, guild: Guild); private _roles: string[]; @@ -702,7 +717,7 @@ declare module 'discord.js' { public toString(): string; } - export class Guild extends Base { + export class Guild extends BaseGuild { constructor(client: Client, data: object); private _sortedRoles(): Collection; private _sortedChannels(channel: Channel): Collection; @@ -719,17 +734,12 @@ declare module 'discord.js' { public bans: GuildBanManager; public channels: GuildChannelManager; public commands: GuildApplicationCommandManager; - public readonly createdAt: Date; - public readonly createdTimestamp: number; public defaultMessageNotifications: DefaultMessageNotifications | number; public deleted: boolean; public description: string | null; public discoverySplash: string | null; public emojis: GuildEmojiManager; public explicitContentFilter: ExplicitContentFilterLevel; - public features: GuildFeatures[]; - public icon: string | null; - public id: Snowflake; public readonly joinedAt: Date; public joinedTimestamp: number; public large: boolean; @@ -739,11 +749,8 @@ declare module 'discord.js' { public memberCount: number; public members: GuildMemberManager; public mfaLevel: number; - public name: string; - public readonly nameAcronym: string; public nsfwLevel: NSFWLevel; public ownerID: Snowflake; - public readonly partnered: boolean; public preferredLocale: string; public premiumSubscriptionCount: number | null; public premiumTier: PremiumTier; @@ -763,7 +770,6 @@ declare module 'discord.js' { public vanityURLCode: string | null; public vanityURLUses: number | null; public verificationLevel: VerificationLevel; - public readonly verified: boolean; public readonly voiceStates: VoiceStateManager; public readonly widgetChannel: TextChannel | null; public widgetChannelID: Snowflake | null; @@ -776,7 +782,6 @@ declare module 'discord.js' { public discoverySplashURL(options?: ImageURLOptions): string | null; public edit(data: GuildEditData, reason?: string): Promise; public equals(guild: Guild): boolean; - public fetch(): Promise; public fetchAuditLogs(options?: GuildAuditLogsFetchOptions): Promise; public fetchIntegrations(): Promise>; public fetchInvites(): Promise>; @@ -787,7 +792,6 @@ declare module 'discord.js' { public fetchVoiceRegions(): Promise>; public fetchWebhooks(): Promise>; public fetchWidget(): Promise; - public iconURL(options?: ImageURLOptions & { dynamic?: boolean }): string | null; public leave(): Promise; public setAFKChannel(afkChannel: ChannelResolvable | null, reason?: string): Promise; public setAFKTimeout(afkTimeout: number, reason?: string): Promise; @@ -817,7 +821,6 @@ declare module 'discord.js' { public setWidget(widget: GuildWidgetData, reason?: string): Promise; public splashURL(options?: ImageURLOptions): string | null; public toJSON(): object; - public toString(): string; } export class GuildAuditLogs { @@ -1345,6 +1348,11 @@ declare module 'discord.js' { public addFollower(channel: GuildChannelResolvable, reason?: string): Promise; } + export class OAuth2Guild extends BaseGuild { + public owner: boolean; + public permissions: Readonly; + } + export class PartialGroupDMChannel extends Channel { constructor(client: Client, data: object); public name: string; @@ -2167,7 +2175,8 @@ declare module 'discord.js' { export class GuildManager extends BaseManager { constructor(client: Client, iterable?: Iterable); public create(name: string, options?: GuildCreateOptions): Promise; - public fetch(id: Snowflake, cache?: boolean, force?: boolean): Promise; + public fetch(options: Snowflake | FetchGuildOptions): Promise; + public fetch(options?: FetchGuildsOptions): Promise>; } export class GuildMemberManager extends BaseManager { @@ -2829,9 +2838,16 @@ declare module 'discord.js' { cache: boolean; } - interface GuildChannelOverwriteOptions { - reason?: string; - type?: number; + interface FetchGuildOptions { + guild: GuildResolvable; + cache?: boolean; + force?: boolean; + } + + interface FetchGuildsOptions { + before?: Snowflake; + after?: Snowflake; + limit?: number; } interface FetchMemberOptions { @@ -2930,6 +2946,11 @@ declare module 'discord.js' { type GuildBanResolvable = GuildBan | UserResolvable; + interface GuildChannelOverwriteOptions { + reason?: string; + type?: number; + } + type GuildChannelResolvable = Snowflake | GuildChannel; interface GuildCreateChannelOptions {