feat: add support for guild templates (#4907)

Co-authored-by: Noel <buechler.noel@outlook.com>
This commit is contained in:
izexi 2020-11-21 14:09:56 +00:00 committed by GitHub
parent eaecd0e8b7
commit 2b2994badc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 339 additions and 4 deletions

View file

@ -65,6 +65,7 @@ export const {
GuildEmoji,
GuildMember,
GuildPreview,
GuildTemplate,
Integration,
Invite,
Message,

View file

@ -12,6 +12,7 @@ const UserManager = require('../managers/UserManager');
const ShardClientUtil = require('../sharding/ShardClientUtil');
const ClientApplication = require('../structures/ClientApplication');
const GuildPreview = require('../structures/GuildPreview');
const GuildTemplate = require('../structures/GuildTemplate');
const Invite = require('../structures/Invite');
const VoiceRegion = require('../structures/VoiceRegion');
const Webhook = require('../structures/Webhook');
@ -254,6 +255,23 @@ class Client extends BaseClient {
.then(data => new Invite(this, data));
}
/**
* Obtains a template from Discord.
* @param {GuildTemplateResolvable} template Template code or URL
* @returns {Promise<GuildTemplate>}
* @example
* client.fetchGuildTemplate('https://discord.new/FKvmczH2HyUf')
* .then(template => console.log(`Obtained template with code: ${template.code}`))
* .catch(console.error);
*/
fetchGuildTemplate(template) {
const code = DataResolver.resolveGuildTemplateCode(template);
return this.api.guilds
.templates(code)
.get()
.then(data => new GuildTemplate(this, data));
}
/**
* Obtains a webhook from Discord.
* @param {Snowflake} id ID of the webhook

View file

@ -77,6 +77,7 @@ module.exports = {
GuildEmoji: require('./structures/GuildEmoji'),
GuildMember: require('./structures/GuildMember'),
GuildPreview: require('./structures/GuildPreview'),
GuildTemplate: require('./structures/GuildTemplate'),
Integration: require('./structures/Integration'),
Invite: require('./structures/Invite'),
Message: require('./structures/Message'),

View file

@ -4,6 +4,7 @@ const { deprecate } = require('util');
const Base = require('./Base');
const GuildAuditLogs = require('./GuildAuditLogs');
const GuildPreview = require('./GuildPreview');
const GuildTemplate = require('./GuildTemplate');
const Integration = require('./Integration');
const Invite = require('./Invite');
const VoiceRegion = require('./VoiceRegion');
@ -733,6 +734,20 @@ class Guild extends Base {
);
}
/**
* Fetches a collection of templates from this guild.
* Resolves with a collection mapping templates by their codes.
* @returns {Promise<Collection<string, GuildTemplate>>}
*/
fetchTemplates() {
return this.client.api
.guilds(this.id)
.templates.get()
.then(templates =>
templates.reduce((col, data) => col.set(data.code, new GuildTemplate(this.client, data)), new Collection()),
);
}
/**
* The data for creating an integration.
* @typedef {Object} IntegrationData
@ -753,6 +768,19 @@ class Guild extends Base {
.then(() => this);
}
/**
* Creates a template for the guild.
* @param {string} name The name for the template
* @param {string} [description] The description for the template
* @returns {Promise<GuildTemplate>}
*/
createTemplate(name, description) {
return this.client.api
.guilds(this.id)
.templates.post({ data: { name, description } })
.then(data => new GuildTemplate(this.client, data));
}
/**
* Fetches a collection of invites to this guild.
* Resolves with a collection mapping invites by their codes.

View file

@ -0,0 +1,224 @@
'use strict';
const Base = require('./Base');
const { Events } = require('../util/Constants');
const DataResolver = require('../util/DataResolver');
/**
* Represents the template for a guild.
* @extends {Base}
*/
class GuildTemplate extends Base {
/**
* @param {Client} client The instantiating client
* @param {Object} data The raw data for the template
*/
constructor(client, data) {
super(client);
this._patch(data);
}
/**
* Builds or updates the template with the provided data.
* @param {Object} data The raw data for the template
* @returns {GuildTemplate}
* @private
*/
_patch(data) {
/**
* The unique code of this template
* @type {string}
*/
this.code = data.code;
/**
* The name of this template
* @type {string}
*/
this.name = data.name;
/**
* The description of this template
* @type {?string}
*/
this.description = data.description;
/**
* The amount of times this template has been used
* @type {number}
*/
this.usageCount = data.usage_count;
/**
* The ID of the user that created this template
* @type {Snowflake}
*/
this.creatorID = data.creator_id;
/**
* The user that created this template
* @type {User}
*/
this.creator = this.client.users.add(data.creator);
/**
* The time of when this template was created at
* @type {Date}
*/
this.createdAt = new Date(data.created_at);
/**
* The time of when this template was last synced to the guild
* @type {Date}
*/
this.updatedAt = new Date(data.updated_at);
/**
* The ID of the guild that this template belongs to
* @type {Snowflake}
*/
this.guildID = data.source_guild_id;
/**
* The data of the guild that this template would create
* @type {Object}
* @see {@link https://discord.com/developers/docs/resources/guild#guild-resource}
*/
this.serializedGuild = data.serialized_source_guild;
/**
* Whether this template has unsynced changes
* @type {?boolean}
*/
this.unSynced = 'is_dirty' in data ? Boolean(data.is_dirty) : null;
return this;
}
/**
* Creates a guild based from this template.
* <warn>This is only available to bots in fewer than 10 guilds.</warn>
* @param {string} name The name of the guild
* @param {BufferResolvable|Base64Resolvable} [icon] The icon for the guild
* @returns {Promise<Guild>}
*/
async createGuild(name, icon) {
const { client } = this;
const data = await client.api.guilds.templates(this.code).post({
data: {
name,
icon: await DataResolver.resolveImage(icon),
},
});
// eslint-disable-next-line consistent-return
return new Promise(resolve => {
const createdGuild = client.guilds.cache.get(data.id);
if (createdGuild) return resolve(createdGuild);
const resolveGuild = guild => {
client.off(Events.GUILD_CREATE, handleGuild);
client.decrementMaxListeners();
resolve(guild);
};
const handleGuild = guild => {
if (guild.id === data.id) {
client.clearTimeout(timeout);
resolveGuild(guild);
}
};
client.incrementMaxListeners();
client.on(Events.GUILD_CREATE, handleGuild);
const timeout = client.setTimeout(() => resolveGuild(client.guilds.add(data)), 10000);
});
}
/**
* Updates the metadata on this template.
* @param {Object} options Options for the template
* @param {string} [options.name] The name of this template
* @param {string} [options.description] The description of this template
* @returns {Promise<GuildTemplate>}
*/
edit({ name, description } = {}) {
return this.client.api
.guilds(this.guildID)
.templates(this.code)
.patch({ data: { name, description } })
.then(data => this._patch(data));
}
/**
* Deletes this template.
* @returns {Promise<GuildTemplate>}
*/
delete() {
return this.client.api
.guilds(this.guildID)
.templates(this.code)
.delete()
.then(() => this);
}
/**
* Syncs this template to the current state of the guild.
* @returns {Promise<GuildTemplate>}
*/
sync() {
return this.client.api
.guilds(this.guildID)
.templates(this.code)
.put()
.then(data => this._patch(data));
}
/**
* The timestamp of when this template was created at
* @type {number}
* @readonly
*/
get createdTimestamp() {
return this.createdAt.getTime();
}
/**
* The timestamp of when this template was last synced to the guild
* @type {number}
* @readonly
*/
get updatedTimestamp() {
return this.updatedAt.getTime();
}
/**
* The guild that this template belongs to
* @type {?Guild}
* @readonly
*/
get guild() {
return this.client.guilds.get(this.guildID) || null;
}
/**
* The URL of this template
* @type {string}
* @readonly
*/
get url() {
return `${this.client.options.http.template}/${this.code}`;
}
/**
* When concatenated with a string, this automatically returns the templates's code instead of the template object.
* @returns {string}
* @example
* // Logs: Template: FKvmczH2HyUf
* console.log(`Template: ${guildTemplate}!`);
*/
toString() {
return this.code;
}
}
module.exports = GuildTemplate;

View file

@ -81,12 +81,14 @@ exports.DefaultOptions = {
* @property {string} [api='https://discord.com/api'] Base url of the API
* @property {string} [cdn='https://cdn.discordapp.com'] Base url of the CDN
* @property {string} [invite='https://discord.gg'] Base url of invites
* @property {string} [template='https://discord.new'] Base url of templates
*/
http: {
version: 7,
api: 'https://discord.com/api',
cdn: 'https://cdn.discordapp.com',
invite: 'https://discord.gg',
template: 'https://discord.new',
},
};
@ -520,6 +522,7 @@ exports.VerificationLevels = ['NONE', 'LOW', 'MEDIUM', 'HIGH', 'VERY_HIGH'];
* * UNKNOWN_EMOJI
* * UNKNOWN_WEBHOOK
* * UNKNOWN_BAN
* * UNKNOWN_GUILD_TEMPLATE
* * BOT_PROHIBITED_ENDPOINT
* * BOT_ONLY_ENDPOINT
* * CHANNEL_HIT_WRITE_RATELIMIT
@ -532,6 +535,7 @@ exports.VerificationLevels = ['NONE', 'LOW', 'MEDIUM', 'HIGH', 'VERY_HIGH'];
* * MAXIMUM_CHANNELS
* * MAXIMUM_ATTACHMENTS
* * MAXIMUM_INVITES
* * GUILD_ALREADY_HAS_TEMPLATE
* * UNAUTHORIZED
* * ACCOUNT_VERIFICATION_REQUIRED
* * REQUEST_ENTITY_TOO_LARGE
@ -584,6 +588,7 @@ exports.APIErrors = {
UNKNOWN_EMOJI: 10014,
UNKNOWN_WEBHOOK: 10015,
UNKNOWN_BAN: 10026,
UNKNOWN_GUILD_TEMPLATE: 10057,
BOT_PROHIBITED_ENDPOINT: 20001,
BOT_ONLY_ENDPOINT: 20002,
CHANNEL_HIT_WRITE_RATELIMIT: 20028,
@ -596,6 +601,7 @@ exports.APIErrors = {
MAXIMUM_CHANNELS: 30013,
MAXIMUM_ATTACHMENTS: 30015,
MAXIMUM_INVITES: 30016,
GUILD_ALREADY_HAS_TEMPLATE: 30031,
UNAUTHORIZED: 40001,
ACCOUNT_VERIFICATION_REQUIRED: 40002,
REQUEST_ENTITY_TOO_LARGE: 40005,

View file

@ -24,16 +24,40 @@ class DataResolver {
* @typedef {string} InviteResolvable
*/
/**
* Data that can be resolved to give an template code. This can be:
* * A template code
* * A template URL
* @typedef {string} GuildTemplateResolvable
*/
/**
* Resolves the string to a code based on the passed regex.
* @param {string} data The string to resolve
* @param {RegExp} regex The RegExp used to extract the code
* @returns {string}
*/
static resolveCode(data, regex) {
const match = regex.exec(data);
return match ? match[1] || data : data;
}
/**
* Resolves InviteResolvable to an invite code.
* @param {InviteResolvable} data The invite resolvable to resolve
* @returns {string}
*/
static resolveInviteCode(data) {
const inviteRegex = /discord(?:(?:app)?\.com\/invite|\.gg(?:\/invite)?)\/([\w-]{2,255})/i;
const match = inviteRegex.exec(data);
if (match && match[1]) return match[1];
return data;
return this.resolveCode(data, /discord(?:(?:app)?\.com\/invite|\.gg(?:\/invite)?)\/([\w-]{2,255})/i);
}
/**
* Resolves GuildTemplateResolvable to a template code.
* @param {GuildTemplateResolvable} data The template resolvable to resolve
* @returns {string}
*/
static resolveGuildTemplateCode(data) {
return this.resolveCode(data, /discord(?:app)?\.(?:com\/template|new)\/([\w-]{2,255})/i);
}
/**

33
typings/index.d.ts vendored
View file

@ -214,6 +214,7 @@ declare module 'discord.js' {
public fetchApplication(): Promise<ClientApplication>;
public fetchGuildPreview(guild: GuildResolvable): Promise<GuildPreview>;
public fetchInvite(invite: InviteResolvable): Promise<Invite>;
public fetchGuildTemplate(template: GuildTemplateResolvable): Promise<GuildTemplate>;
public fetchVoiceRegions(): Promise<Collection<string, VoiceRegion>>;
public fetchWebhook(id: Snowflake, token?: string): Promise<Webhook>;
public generateInvite(options?: InviteGenerationOptions | PermissionResolvable): Promise<string>;
@ -524,10 +525,12 @@ declare module 'discord.js' {
export class DataResolver {
public static resolveBase64(data: Base64Resolvable): string;
public static resolveCode(data: string, regx: RegExp): string;
public static resolveFile(resource: BufferResolvable | Stream): Promise<Buffer | Stream>;
public static resolveFileAsBuffer(resource: BufferResolvable | Stream): Promise<Buffer>;
public static resolveImage(resource: BufferResolvable | Base64Resolvable): Promise<string>;
public static resolveInviteCode(data: InviteResolvable): string;
public static resolveGuildTemplateCode(data: GuildTemplateResolvable): string;
}
export class DiscordAPIError extends Error {
@ -634,6 +637,7 @@ declare module 'discord.js' {
public addMember(user: UserResolvable, options: AddGuildMemberOptions): Promise<GuildMember>;
public bannerURL(options?: ImageURLOptions): string | null;
public createIntegration(data: IntegrationData, reason?: string): Promise<Guild>;
public createTemplate(name: string, description?: string): Promise<GuildTemplate>;
public delete(): Promise<Guild>;
public discoverySplashURL(options?: ImageURLOptions): string | null;
public edit(data: GuildEditData, reason?: string): Promise<Guild>;
@ -646,6 +650,7 @@ declare module 'discord.js' {
public fetchIntegrations(options?: FetchIntegrationsOptions): Promise<Collection<string, Integration>>;
public fetchInvites(): Promise<Collection<string, Invite>>;
public fetchPreview(): Promise<GuildPreview>;
public fetchTemplates(): Promise<Collection<GuildTemplate['code'], GuildTemplate>>;
public fetchVanityCode(): Promise<string>;
public fetchVanityData(): Promise<{ code: string; uses: number }>;
public fetchVoiceRegions(): Promise<Collection<string, VoiceRegion>>;
@ -852,6 +857,29 @@ declare module 'discord.js' {
public toString(): string;
}
export class GuildTemplate extends Base {
constructor(client: Client, data: object);
public readonly createdTimestamp: number;
public readonly updatedTimestamp: number;
public readonly url: string;
public code: string;
public name: string;
public description: string | null;
public usageCount: number;
public creator: User;
public creatorID: Snowflake;
public createdAt: Date;
public updatedAt: Date;
public guild: Guild | null;
public guildID: Snowflake;
public serializedGuild: object;
public unSynced: boolean | null;
public createGuild(name: string, icon?: BufferResolvable | Base64Resolvable): Promise<Guild>;
public delete(): Promise<GuildTemplate>;
public edit(options?: { name?: string; description?: string }): Promise<GuildTemplate>;
public sync(): Promise<GuildTemplate>;
}
export class GuildPreviewEmoji extends BaseGuildEmoji {
constructor(client: Client, data: object, guild: GuildPreview);
public guild: GuildPreview;
@ -2101,6 +2129,7 @@ declare module 'discord.js' {
UNKNOWN_EMOJI: 10014;
UNKNOWN_WEBHOOK: 10015;
UNKNOWN_BAN: 10026;
UNKNOWN_GUILD_TEMPLATE: 10057;
BOT_PROHIBITED_ENDPOINT: 20001;
BOT_ONLY_ENDPOINT: 20002;
CHANNEL_HIT_WRITE_RATELIMIT: 20028;
@ -2113,6 +2142,7 @@ declare module 'discord.js' {
MAXIMUM_CHANNELS: 30013;
MAXIMUM_ATTACHMENTS: 30015;
MAXIMUM_INVITES: 30016;
GUILD_ALREADY_HAS_TEMPLATE: 30031;
UNAUTHORIZED: 40001;
ACCOUNT_VERIFICATION_REQUIRED: 40002;
REQUEST_ENTITY_TOO_LARGE: 40005;
@ -2637,6 +2667,7 @@ declare module 'discord.js' {
host?: string;
cdn?: string;
invite?: string;
template?: string;
}
type ImageSize = 16 | 32 | 64 | 128 | 256 | 512 | 1024 | 2048 | 4096;
@ -2694,6 +2725,8 @@ declare module 'discord.js' {
type InviteResolvable = string;
type GuildTemplateResolvable = string;
type MembershipStates = 'INVITED' | 'ACCEPTED';
type MessageAdditions = MessageEmbed | MessageAttachment | (MessageEmbed | MessageAttachment)[];