From 88133d0d774e703700fd92ea0a3a63cc78ac4f22 Mon Sep 17 00:00:00 2001 From: SouSinner <62366719+SouSinner@users.noreply.github.com> Date: Fri, 27 Mar 2020 20:57:28 +0100 Subject: [PATCH] feat(GuildPreview): implement support for "preview" endpoint (#3965) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(GuildPreview): method — fetchGuildPreview * feat(GuildPreview): structure — GuildPreview * feat(GuildPreview): update typings * fix(GuildPreview): remove typedef for Features — already exists * refactor(GuildPreview): update JSDocs & method * feat(GuildPreview): implement DiscoverySplash function * fix(GuildPreview): description & error handling for id * fix(GuildPreview): misleading description, assign emojis correctly * feat(GuildPreview): func DisplaySplash & GuildPreviewEmoji interface * fix(Typings): satisfy TSLint * fix(GuildPreview): toJSON - returns a value now * feat(GuildPreview): add fetchPreview method on instance of Guild * feat(GuildPreview): update typings * fix: missing client constructor * fix: typo in typings * feat(BaseEmoji): implement BaseEmoji — parent for emoji instances * feat(BaseEmoji): refactor - GuildEmoji extends BaseEmoji now * feat(BaseEmoji): refactor - adjust emojis prop to BaseEmoji instance * feat(BaseEmoji): not documented fully - GuildPreviewEmoji * feat(BaseEmoji): update typings * fix: remove duplicate typing properties - inherited * fix: remove redundant methods & properties - inherited / already set * fix: let -> const * fix: typings - put BaseEmoji after BaseClient * fix: remove _clone method - redundant * refactor(GuildPreview): emojis should be a collection * refactor: rename base class, move relevant props there and expose roles * fix(GuildPreview): update emojis in _patch * fix(Typings): remove empty line, add Client#fetchGuildPreview * feat: export GuildPreview * fix(Typings): add GuildPreview#discoverSplash, icon, and splash Co-authored-by: LxveFades Co-authored-by: Crawl Co-authored-by: SpaceEEC --- src/client/Client.js | 15 +++ src/index.js | 2 + src/structures/BaseGuildEmoji.js | 58 ++++++++++ src/structures/Guild.js | 12 +++ src/structures/GuildEmoji.js | 58 ++-------- src/structures/GuildPreview.js | 157 ++++++++++++++++++++++++++++ src/structures/GuildPreviewEmoji.js | 26 +++++ src/util/Constants.js | 2 + typings/index.d.ts | 55 ++++++++-- 9 files changed, 328 insertions(+), 57 deletions(-) create mode 100644 src/structures/BaseGuildEmoji.js create mode 100644 src/structures/GuildPreview.js create mode 100644 src/structures/GuildPreviewEmoji.js diff --git a/src/client/Client.js b/src/client/Client.js index b63f5a515..4a12e8505 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -11,6 +11,7 @@ const GuildManager = require('../managers/GuildManager'); const UserManager = require('../managers/UserManager'); const ShardClientUtil = require('../sharding/ShardClientUtil'); const ClientApplication = require('../structures/ClientApplication'); +const GuildPreview = require('../structures/GuildPreview'); const Invite = require('../structures/Invite'); const VoiceRegion = require('../structures/VoiceRegion'); const Webhook = require('../structures/Webhook'); @@ -338,6 +339,20 @@ class Client extends BaseClient { .then(app => new ClientApplication(this, app)); } + /** + * Obtains a guild preview from Discord, only available for public guilds. + * @param {GuildResolvable} guild The guild to fetch the preview for + * @returns {Promise} + */ + fetchGuildPreview(guild) { + const id = this.guilds.resolveID(guild); + if (!id) throw new TypeError('INVALID_TYPE', 'guild', 'GuildResolvable'); + return this.api + .guilds(id) + .preview.get() + .then(data => new GuildPreview(this, data)); + } + /** * Generates a link that can be used to invite the bot to a guild. * @param {PermissionResolvable} [permissions] Permissions to request diff --git a/src/index.js b/src/index.js index 406d4f72e..a8b3969d8 100644 --- a/src/index.js +++ b/src/index.js @@ -57,6 +57,7 @@ module.exports = { Base: require('./structures/Base'), Activity: require('./structures/Presence').Activity, APIMessage: require('./structures/APIMessage'), + BaseGuildEmoji: require('./structures/BaseGuildEmoji'), CategoryChannel: require('./structures/CategoryChannel'), Channel: require('./structures/Channel'), ClientApplication: require('./structures/ClientApplication'), @@ -72,6 +73,7 @@ module.exports = { GuildChannel: require('./structures/GuildChannel'), GuildEmoji: require('./structures/GuildEmoji'), GuildMember: require('./structures/GuildMember'), + GuildPreview: require('./structures/GuildPreview'), Integration: require('./structures/Integration'), Invite: require('./structures/Invite'), Message: require('./structures/Message'), diff --git a/src/structures/BaseGuildEmoji.js b/src/structures/BaseGuildEmoji.js new file mode 100644 index 000000000..d3665271c --- /dev/null +++ b/src/structures/BaseGuildEmoji.js @@ -0,0 +1,58 @@ +'use strict'; + +const Emoji = require('./Emoji'); + +/** + * Parent class for {@link GuildEmoji} and {@link GuildPreviewEmoji}. + * @extends {Emoji} + */ +class BaseGuildEmoji extends Emoji { + constructor(client, data, guild) { + super(client, data); + + /** + * The guild this emoji is a part of + * @type {Guild|GuildPreview} + */ + this.guild = guild; + + /** + * Array of role ids this emoji is active for + * @name BaseGuildEmoji#_roles + * @type {Snowflake[]} + * @private + */ + Object.defineProperty(this, '_roles', { value: [], writable: true }); + + this._patch(data); + } + + _patch(data) { + if (data.name) this.name = data.name; + + /** + * Whether or not this emoji requires colons surrounding it + * @type {boolean} + * @name GuildEmoji#requiresColons + */ + if (typeof data.require_colons !== 'undefined') this.requiresColons = data.require_colons; + + /** + * Whether this emoji is managed by an external service + * @type {boolean} + * @name GuildEmoji#managed + */ + if (typeof data.managed !== 'undefined') this.managed = data.managed; + + /** + * Whether this emoji is available + * @type {boolean} + * @name GuildEmoji#available + */ + if (typeof data.available !== 'undefined') this.available = data.available; + + if (data.roles) this._roles = data.roles; + } +} + +module.exports = BaseGuildEmoji; diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 733a1fdb6..ec86c4232 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -2,6 +2,7 @@ const Base = require('./Base'); const GuildAuditLogs = require('./GuildAuditLogs'); +const GuildPreview = require('./GuildPreview'); const Integration = require('./Integration'); const Invite = require('./Invite'); const VoiceRegion = require('./VoiceRegion'); @@ -709,6 +710,17 @@ class Guild extends Base { }); } + /** + * Obtains a guild preview for this guild from Discord, only available for public guilds. + * @returns {Promise} + */ + fetchPreview() { + return this.client.api + .guilds(this.id) + .preview.get() + .then(data => new GuildPreview(this.client, data)); + } + /** * Fetches the vanity url invite code to this guild. * Resolves with a string matching the vanity url invite code, not the full url. diff --git a/src/structures/GuildEmoji.js b/src/structures/GuildEmoji.js index ea4de4558..d4189e9f2 100644 --- a/src/structures/GuildEmoji.js +++ b/src/structures/GuildEmoji.js @@ -1,65 +1,29 @@ 'use strict'; -const Emoji = require('./Emoji'); +const BaseGuildEmoji = require('./BaseGuildEmoji'); const { Error } = require('../errors'); const GuildEmojiRoleManager = require('../managers/GuildEmojiRoleManager'); const Permissions = require('../util/Permissions'); /** * Represents a custom emoji. - * @extends {Emoji} + * @extends {BaseGuildEmoji} */ -class GuildEmoji extends Emoji { +class GuildEmoji extends BaseGuildEmoji { /** + * @name GuildEmoji + * @kind constructor + * @memberof GuildEmoji * @param {Client} client The instantiating client * @param {Object} data The data for the guild emoji * @param {Guild} guild The guild the guild emoji is part of */ - constructor(client, data, guild) { - super(client, data); - /** - * The guild this emoji is part of - * @type {Guild} - */ - this.guild = guild; - - /** - * The ID of this emoji - * @type {Snowflake} - * @name GuildEmoji#id - */ - - this._roles = []; - this._patch(data); - } - - _patch(data) { - if (data.name) this.name = data.name; - - /** - * Whether or not this emoji requires colons surrounding it - * @type {boolean} - * @name GuildEmoji#requiresColons - */ - if (typeof data.require_colons !== 'undefined') this.requiresColons = data.require_colons; - - /** - * Whether this emoji is managed by an external service - * @type {boolean} - * @name GuildEmoji#managed - */ - if (typeof data.managed !== 'undefined') this.managed = data.managed; - - /** - * Whether this emoji is available - * @type {boolean} - * @name GuildEmoji#available - */ - if (typeof data.available !== 'undefined') this.available = data.available; - - if (data.roles) this._roles = data.roles; - } + /** + * The guild this emoji is part of + * @type {Guild} + * @name GuildEmoji#guild + */ _clone() { const clone = super._clone(); diff --git a/src/structures/GuildPreview.js b/src/structures/GuildPreview.js new file mode 100644 index 000000000..681ff607c --- /dev/null +++ b/src/structures/GuildPreview.js @@ -0,0 +1,157 @@ +'use strict'; + +const Base = require('./Base'); +const GuildPreviewEmoji = require('./GuildPreviewEmoji'); +const Collection = require('../util/Collection'); + +/** + * Represents the data about the guild any bot can preview, connected to the specified public guild. + * @extends {Base} + */ +class GuildPreview extends Base { + constructor(client, data) { + super(client); + + if (!data) return; + + this._patch(data); + } + + /** + * Builds the public guild with the provided data. + * @param {*} data The raw data of the public guild + * @private + */ + _patch(data) { + /** + * The id of this public guild + * @type {string} + */ + this.id = data.id; + + /** + * The name of this public guild + * @type {string} + */ + this.name = data.name; + + /** + * The icon of this public guild + * @type {?string} + */ + this.icon = data.icon; + + /** + * The splash icon of this public guild + * @type {?string} + */ + this.splash = data.splash; + + /** + * The discovery splash icon of this public guild + * @type {?string} + */ + this.discoverySplash = data.discovery_splash; + + /** + * An array of enabled guild features + * @type {Features[]} + */ + this.features = data.features; + + /** + * The approximate count of members in this public guild + * @type {number} + */ + this.approximateMemberCount = data.approximate_member_count; + + /** + * The approximate count of online members in this public guild + * @type {number} + */ + this.approximatePresenceCount = data.approximate_presence_count; + + /** + * The description for this public guild + * @type {?string} + */ + this.description = data.description; + + if (!this.emojis) { + /** + * Collection of emojis belonging to this public guild + * @type {Collection} + */ + this.emojis = new Collection(); + } else { + this.emojis.clear(); + } + for (const emoji of data.emojis) { + this.emojis.set(emoji.id, new GuildPreviewEmoji(this.client, emoji, this)); + } + } + + /** + * The URL to this public guild's splash. + * @param {ImageURLOptions} [options={}] Options for the Image URL + * @returns {?string} + */ + splashURL({ format, size } = {}) { + if (!this.splash) return null; + return this.client.rest.cdn.Splash(this.id, this.splash, format, size); + } + + /** + * The URL to this public guild's discovery splash. + * @param {ImageURLOptions} [options={}] Options for the Image URL + * @returns {?string} + */ + discoverySplashURL({ format, size } = {}) { + if (!this.discoverySplash) return null; + return this.client.rest.cdn.DiscoverySplash(this.id, this.discoverySplash, format, size); + } + + /** + * The URL to this public 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 public guild. + * @returns {Promise} + */ + fetch() { + return this.client.api + .guilds(this.id) + .preview.get() + .then(data => { + this._patch(data); + return this; + }); + } + + /** + * 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 ${previewGuild}!`); + */ + toString() { + return this.name; + } + + toJSON() { + const json = super.toJSON(); + json.iconURL = this.iconURL(); + json.splashURL = this.splashURL(); + return json; + } +} + +module.exports = GuildPreview; diff --git a/src/structures/GuildPreviewEmoji.js b/src/structures/GuildPreviewEmoji.js new file mode 100644 index 000000000..4c709031c --- /dev/null +++ b/src/structures/GuildPreviewEmoji.js @@ -0,0 +1,26 @@ +'use strict'; + +const BaseGuildEmoji = require('./BaseGuildEmoji'); + +/** + * Represents an instance of an emoji belonging to a public guild obtained through Discord's preview endpoint. + * @extends {BaseGuildEmoji} + */ +class GuildPreviewEmoji extends BaseGuildEmoji { + /** + * The public guild this emoji is part of + * @type {GuildPreview} + * @name GuildPreviewEmoji#guild + */ + + /** + * Set of roles this emoji is active for + * @type {Set} + * @readonly + */ + get roles() { + return new Set(this._roles); + } +} + +module.exports = GuildPreviewEmoji; diff --git a/src/util/Constants.js b/src/util/Constants.js index 508ede0eb..baad53084 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -141,6 +141,8 @@ exports.Endpoints = { makeImageUrl(`${root}/channel-icons/${channelID}/${hash}`, { size, format }), Splash: (guildID, hash, format = 'webp', size) => makeImageUrl(`${root}/splashes/${guildID}/${hash}`, { size, format }), + DiscoverySplash: (guildID, hash, format = 'webp', size) => + makeImageUrl(`${root}/discovery-splashes/${guildID}/${hash}`, { size, format }), TeamIcon: (teamID, hash, { format = 'webp', size } = {}) => makeImageUrl(`${root}/team-icons/${teamID}/${hash}`, { size, format }), }; diff --git a/typings/index.d.ts b/typings/index.d.ts index 5c09205e9..cf6c05ba6 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -108,6 +108,19 @@ declare module 'discord.js' { public toJSON(...props: { [key: string]: boolean | string }[]): object; } + export class BaseGuildEmoji extends Emoji { + constructor(client: Client, data: object, guild: Guild); + private _roles: string[]; + + public available: boolean; + public readonly createdAt: Date; + public readonly createdTimestamp: number; + public guild: Guild | GuildPreview; + public id: Snowflake; + public managed: boolean; + public requiresColons: boolean; + } + class BroadcastDispatcher extends VolumeMixin(StreamDispatcher) { public broadcast: VoiceBroadcast; } @@ -168,6 +181,7 @@ declare module 'discord.js' { public ws: WebSocketManager; public destroy(): void; public fetchApplication(): Promise; + public fetchGuildPreview(guild: GuildResolvable): Promise; public fetchInvite(invite: InviteResolvable): Promise; public fetchVoiceRegions(): Promise>; public fetchWebhook(id: Snowflake, token?: string): Promise; @@ -296,7 +310,8 @@ declare module 'discord.js' { AppIcon: (userID: string | number, hash: string, format: AllowedImageFormat, size: number) => string; AppAsset: (userID: string | number, hash: string, format: AllowedImageFormat, size: number) => string; GDMIcon: (userID: string | number, hash: string, format: AllowedImageFormat, size: number) => string; - Splash: (userID: string | number, hash: string, format: AllowedImageFormat, size: number) => string; + Splash: (guildID: string | number, hash: string, format: AllowedImageFormat, size: number) => string; + DiscoverySplash: (guildID: string | number, hash: string, format: AllowedImageFormat, size: number) => string; TeamIcon: (teamID: string | number, hash: string, format: AllowedImageFormat, size: number) => string; }; }; @@ -636,6 +651,7 @@ declare module 'discord.js' { public fetchEmbed(): Promise; public fetchIntegrations(): Promise>; public fetchInvites(): Promise>; + public fetchPreview(): Promise; public fetchVanityCode(): Promise; public fetchVoiceRegions(): Promise>; public fetchWebhooks(): Promise>; @@ -748,17 +764,10 @@ declare module 'discord.js' { ): Promise; } - export class GuildEmoji extends Emoji { - constructor(client: Client, data: object, guild: Guild); - private _roles: string[]; - - public available: boolean; + export class GuildEmoji extends BaseGuildEmoji { public readonly deletable: boolean; public guild: Guild; - public id: Snowflake; - public managed: boolean; - public requiresColons: boolean; - public roles: GuildEmojiRoleManager; + public readonly roles: GuildEmojiRoleManager; public readonly url: string; public delete(reason?: string): Promise; public edit(data: GuildEmojiEditData, reason?: string): Promise; @@ -807,6 +816,32 @@ declare module 'discord.js' { public valueOf(): string; } + export class GuildPreview extends Base { + constructor(client: Client, data: object); + public approximateMemberCount: number; + public approximatePresenceCount: number; + public description?: string; + public discoverySplash: string | null; + public emojis: Collection; + public features: GuildFeatures; + public icon: string | null; + public id: string; + public name: string; + public splash: string | null; + public discoverySplashURL(options?: ImageURLOptions): string | null; + public iconURL(options?: ImageURLOptions & { dynamic?: boolean }): string | null; + public splashURL(options?: ImageURLOptions): string | null; + public fetch(): Promise; + public toJSON(): object; + public toString(): string; + } + + export class GuildPreviewEmoji extends BaseGuildEmoji { + constructor(client: Client, data: object, guild: GuildPreview); + public guild: GuildPreview; + public readonly roles: Set; + } + export class HTTPError extends Error { constructor(message: string, name: string, code: number, method: string, path: string); public code: number;