feat(Sticker): updates, sticker packs, and guild stickers (#5867)

Co-authored-by: SpaceEEC <spaceeec@yahoo.com>
Co-authored-by: Antonio Román <kyradiscord@gmail.com>
Co-authored-by: Tiemen <ThaTiemsz@users.noreply.github.com>
Co-authored-by: Vlad Frangu <kingdgrizzle@gmail.com>
Co-authored-by: BannerBomb <BannerBomb55@gmail.com>
Co-authored-by: Noel <icrawltogo@gmail.com>
Co-authored-by: Sugden <28943913+NotSugden@users.noreply.github.com>
This commit is contained in:
Advaith
2021-07-19 16:17:21 -07:00
committed by GitHub
parent 76888e6c1b
commit 54d6a3a070
23 changed files with 771 additions and 48 deletions

View File

@@ -14,6 +14,8 @@ const ClientPresence = require('../structures/ClientPresence');
const GuildPreview = require('../structures/GuildPreview');
const GuildTemplate = require('../structures/GuildTemplate');
const Invite = require('../structures/Invite');
const Sticker = require('../structures/Sticker');
const StickerPack = require('../structures/StickerPack');
const VoiceRegion = require('../structures/VoiceRegion');
const Webhook = require('../structures/Webhook');
const Widget = require('../structures/Widget');
@@ -318,6 +320,33 @@ class Client extends BaseClient {
});
}
/**
* Obtains a sticker from Discord.
* @param {Snowflake} id The sticker's id
* @returns {Promise<Sticker>}
* @example
* client.fetchSticker('id')
* .then(sticker => console.log(`Obtained sticker with name: ${sticker.name}`))
* .catch(console.error);
*/
async fetchSticker(id) {
const data = await this.api.stickers(id).get();
return new Sticker(this, data);
}
/**
* Obtains the list of sticker packs available to Nitro subscribers from Discord.
* @returns {Promise<Collection<Snowflake, StickerPack>>}
* @example
* client.fetchPremiumStickerPacks()
* .then(packs => console.log(`Available sticker packs are: ${packs.map(pack => pack.name).join(', ')}`))
* .catch(console.error);
*/
async fetchPremiumStickerPacks() {
const data = await this.api('sticker-packs').get();
return new Collection(data.sticker_packs.map(p => [p.id, new StickerPack(this, p)]));
}
/**
* Sweeps all text-based channels' messages and removes the ones older than the max message lifetime.
* If the message has been edited, the time of the edit is used rather than the time of the original message.

View File

@@ -0,0 +1,20 @@
'use strict';
const Action = require('./Action');
const { Events } = require('../../util/Constants');
class GuildStickerCreateAction extends Action {
handle(guild, createdSticker) {
const already = guild.stickers.cache.has(createdSticker.id);
const sticker = guild.stickers._add(createdSticker);
/**
* Emitted whenever a custom sticker is created in a guild.
* @event Client#stickerCreate
* @param {Sticker} sticker The sticker that was created
*/
if (!already) this.client.emit(Events.GUILD_STICKER_CREATE, sticker);
return { sticker };
}
}
module.exports = GuildStickerCreateAction;

View File

@@ -0,0 +1,20 @@
'use strict';
const Action = require('./Action');
const { Events } = require('../../util/Constants');
class GuildStickerDeleteAction extends Action {
handle(sticker) {
sticker.guild.stickers.cache.delete(sticker.id);
sticker.deleted = true;
/**
* Emitted whenever a custom sticker is deleted in a guild.
* @event Client#stickerDelete
* @param {Sticker} sticker The sticker that was deleted
*/
this.client.emit(Events.GUILD_STICKER_DELETE, sticker);
return { sticker };
}
}
module.exports = GuildStickerDeleteAction;

View File

@@ -0,0 +1,20 @@
'use strict';
const Action = require('./Action');
const { Events } = require('../../util/Constants');
class GuildStickerUpdateAction extends Action {
handle(current, data) {
const old = current._update(data);
/**
* Emitted whenever a custom sticker is updated in a guild.
* @event Client#stickerUpdate
* @param {Sticker} oldSticker The old sticker
* @param {Sticker} newSticker The new sticker
*/
this.client.emit(Events.GUILD_STICKER_UPDATE, old, current);
return { sticker: current };
}
}
module.exports = GuildStickerUpdateAction;

View File

@@ -0,0 +1,34 @@
'use strict';
const Action = require('./Action');
class GuildStickersUpdateAction extends Action {
handle(data) {
const guild = this.client.guilds.cache.get(data.guild_id);
if (!guild?.stickers) return;
const deletions = new Map(guild.stickers.cache);
for (const sticker of data.stickers) {
// Determine type of sticker event
const cachedSticker = guild.stickers.cache.get(sticker.id);
if (cachedSticker) {
deletions.delete(sticker.id);
if (!cachedSticker.equals(sticker)) {
// Sticker updated
this.client.actions.GuildStickerUpdate.handle(cachedSticker, sticker);
}
} else {
// Sticker added
this.client.actions.GuildStickerCreate.handle(guild, sticker);
}
}
for (const sticker of deletions.values()) {
// Sticker deleted
this.client.actions.GuildStickerDelete.handle(sticker);
}
}
}
module.exports = GuildStickersUpdateAction;

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.GuildStickersUpdate.handle(packet.d);
};

View File

@@ -105,8 +105,9 @@ const Messages = {
EMOJI_TYPE: 'Emoji must be a string or GuildEmoji/ReactionEmoji',
EMOJI_MANAGED: 'Emoji is managed and has no Author.',
MISSING_MANAGE_EMOJIS_PERMISSION: guild =>
`Client must have Manage Emoji permission in guild ${guild} to see emoji authors.`,
MISSING_MANAGE_EMOJIS_AND_STICKERS_PERMISSION: guild =>
`Client must have Manage Emojis and Stickers permission in guild ${guild} to see emoji authors.`,
NOT_GUILD_STICKER: 'Sticker is a standard (non-guild) sticker and has no author.',
REACTION_RESOLVE_USER: "Couldn't resolve the user id to remove from the reaction.",

View File

@@ -45,6 +45,7 @@ module.exports = {
GuildMemberManager: require('./managers/GuildMemberManager'),
GuildMemberRoleManager: require('./managers/GuildMemberRoleManager'),
GuildManager: require('./managers/GuildManager'),
GuildStickerManager: require('./managers/GuildStickerManager'),
ReactionManager: require('./managers/ReactionManager'),
ReactionUserManager: require('./managers/ReactionUserManager'),
MessageManager: require('./managers/MessageManager'),

View File

@@ -0,0 +1,160 @@
'use strict';
const CachedManager = require('./CachedManager');
const { TypeError } = require('../errors');
const MessagePayload = require('../structures/MessagePayload');
const Sticker = require('../structures/Sticker');
const Collection = require('../util/Collection');
/**
* Manages API methods for Guild Stickers and stores their cache.
* @extends {CachedManager}
*/
class GuildStickerManager extends CachedManager {
constructor(guild, iterable) {
super(guild.client, Sticker, iterable);
/**
* The guild this manager belongs to
* @type {Guild}
*/
this.guild = guild;
}
/**
* The cache of Guild Stickers
* @type {Collection<Snowflake, Sticker>}
* @name GuildStickerManager#cache
*/
_add(data, cache) {
return super._add(data, cache, { extras: [this.guild] });
}
/**
* Creates a new custom sticker in the guild.
* @param {BufferResolvable|Stream|FileOptions|MessageAttachment} file The file for the sticker
* @param {string} name The name for the sticker
* @param {string} tags The Discord name of a unicode emoji representing the sticker's expression
* @param {Object} [options] Options
* @param {?string} [options.description] The description for the sticker
* @param {string} [options.reason] Reason for creating the sticker
* @returns {Promise<Sticker>} The created sticker
* @example
* // Create a new sticker from a url
* guild.stickers.create('https://i.imgur.com/w3duR07.png', 'rip')
* .then(sticker => console.log(`Created new sticker with name ${sticker.name}!`))
* .catch(console.error);
* @example
* // Create a new sticker from a file on your computer
* guild.stickers.create('./memes/banana.png', 'banana')
* .then(sticker => console.log(`Created new sticker with name ${sticker.name}!`))
* .catch(console.error);
*/
async create(file, name, tags, { description, reason } = {}) {
file = { ...(await MessagePayload.resolveFile(file)), key: 'file' };
if (!file) throw new TypeError('REQ_RESOURCE_TYPE');
const data = { name, tags, description: description ?? '' };
return this.client.api
.guilds(this.guild.id)
.stickers.post({ data, files: [file], reason, dontUsePayloadJSON: true })
.then(sticker => this.client.actions.GuildStickerCreate.handle(this.guild, sticker).sticker);
}
/**
* Data that resolves to give a Sticker object. This can be:
* * An Sticker object
* * A Snowflake
* @typedef {Sticker|Snowflake} StickerResolvable
*/
/**
* Resolves an StickerResolvable to a Sticker object.
* @method resolve
* @memberof GuildStickerManager
* @instance
* @param {StickerResolvable} sticker The Sticker resolvable to identify
* @returns {?Sticker}
*/
/**
* Resolves an StickerResolvable to an Sticker id string.
* @method resolveId
* @memberof GuildStickerManager
* @instance
* @param {StickerResolvable} sticker The Sticker resolvable to identify
* @returns {?Snowflake}
*/
/**
* Edits a sticker.
* @param {StickerResolvable} sticker The sticker to edit
* @param {GuildStickerEditData} [data] The new data for the sticker
* @param {string} [reason] Reason for editing this sticker
* @returns {Promise<Sticker>}
*/
async edit(sticker, data, reason) {
const stickerId = this.resolveId(sticker);
if (!stickerId) throw new TypeError('INVALID_TYPE', 'sticker', 'StickerResolvable');
const d = await this.client.api.guilds(this.guild.id).stickers(stickerId).patch({
data,
reason,
});
const existing = this.cache.get(stickerId);
if (existing) {
const clone = existing._clone();
clone._patch(d);
return clone;
}
return this._add(d);
}
/**
* Deletes a sticker.
* @param {StickerResolvable} sticker The sticker to delete
* @param {string} [reason] Reason for deleting this sticker
* @returns {Promise<void>}
*/
async delete(sticker, reason) {
sticker = this.resolveId(sticker);
if (!sticker) throw new TypeError('INVALID_TYPE', 'sticker', 'StickerResolvable');
await this.client.api.guilds(this.guild.id).stickers(sticker).delete({ reason });
}
/**
* Obtains one or more stickers from Discord, or the sticker cache if they're already available.
* @param {Snowflake} [id] The Sticker's id
* @param {BaseFetchOptions} [options] Additional options for this fetch
* @returns {Promise<Sticker|Collection<Snowflake, Sticker>>}
* @example
* // Fetch all stickers from the guild
* message.guild.stickers.fetch()
* .then(stickers => console.log(`There are ${stickers.size} stickers.`))
* .catch(console.error);
* @example
* // Fetch a single sticker
* message.guild.stickers.fetch('222078108977594368')
* .then(sticker => console.log(`The sticker name is: ${sticker.name}`))
* .catch(console.error);
*/
async fetch(id, { cache = true, force = false } = {}) {
if (id) {
if (!force) {
const existing = this.cache.get(id);
if (existing) return existing;
}
const sticker = await this.client.api.guilds(this.guild.id).stickers(id).get();
return this._add(sticker, cache);
}
const data = await this.client.api.guilds(this.guild.id).stickers.get();
return new Collection(data.map(sticker => [sticker.id, this._add(sticker, cache)]));
}
}
module.exports = GuildStickerManager;

View File

@@ -49,8 +49,16 @@ class APIRequest {
let body;
if (this.options.files && this.options.files.length) {
body = new FormData();
for (const file of this.options.files) if (file && file.file) body.append(file.name, file.file, file.name);
if (typeof this.options.data !== 'undefined') body.append('payload_json', JSON.stringify(this.options.data));
for (const file of this.options.files) {
if (file?.file) body.append(file.key ?? file.name, file.file, file.name);
}
if (typeof this.options.data !== 'undefined') {
if (this.options.dontUsePayloadJSON) {
for (const [key, value] of Object.entries(this.options.data)) body.append(key, value);
} else {
body.append('payload_json', JSON.stringify(this.options.data));
}
}
headers = Object.assign(headers, body.getHeaders());
// eslint-disable-next-line eqeqeq
} else if (this.options.data != null) {

View File

@@ -14,6 +14,7 @@ const GuildChannelManager = require('../managers/GuildChannelManager');
const GuildEmojiManager = require('../managers/GuildEmojiManager');
const GuildInviteManager = require('../managers/GuildInviteManager');
const GuildMemberManager = require('../managers/GuildMemberManager');
const GuildStickerManager = require('../managers/GuildStickerManager');
const PresenceManager = require('../managers/PresenceManager');
const RoleManager = require('../managers/RoleManager');
const StageInstanceManager = require('../managers/StageInstanceManager');
@@ -404,6 +405,20 @@ class Guild extends AnonymousGuild {
emojis: data.emojis,
});
}
if (!this.stickers) {
/**
* A manager of the stickers belonging to this guild
* @type {GuildStickerManager}
*/
this.stickers = new GuildStickerManager(this);
if (data.stickers) for (const sticker of data.stickers) this.stickers._add(sticker);
} else if (data.stickers) {
this.client.actions.GuildStickersUpdate.handle({
guild_id: this.id,
stickers: data.stickers,
});
}
}
/**

View File

@@ -2,6 +2,7 @@
const Integration = require('./Integration');
const StageInstance = require('./StageInstance');
const Sticker = require('./Sticker');
const Webhook = require('./Webhook');
const Collection = require('../util/Collection');
const { OverwriteTypes, PartialTypes } = require('../util/Constants');
@@ -21,6 +22,7 @@ const Util = require('../util/Util');
* * MESSAGE
* * INTEGRATION
* * STAGE_INSTANCE
* * STICKER
* @typedef {string} AuditLogTargetType
*/
@@ -41,6 +43,7 @@ const Targets = {
MESSAGE: 'MESSAGE',
INTEGRATION: 'INTEGRATION',
STAGE_INSTANCE: 'STAGE_INSTANCE',
STICKER: 'STICKER',
UNKNOWN: 'UNKNOWN',
};
@@ -85,6 +88,9 @@ const Targets = {
* * STAGE_INSTANCE_CREATE: 83
* * STAGE_INSTANCE_UPDATE: 84
* * STAGE_INSTANCE_DELETE: 85
* * STICKER_CREATE: 90
* * STICKER_UPDATE: 91
* * STICKER_DELETE: 92
* @typedef {?(number|string)} AuditLogAction
*/
@@ -133,6 +139,9 @@ const Actions = {
STAGE_INSTANCE_CREATE: 83,
STAGE_INSTANCE_UPDATE: 84,
STAGE_INSTANCE_DELETE: 85,
STICKER_CREATE: 90,
STICKER_UPDATE: 91,
STICKER_DELETE: 92,
};
/**
@@ -197,9 +206,10 @@ class GuildAuditLogs {
* * A message
* * An integration
* * A stage instance
* * A sticker
* * An object with an id key if target was deleted
* * An object where the keys represent either the new value or the old value
* @typedef {?(Object|Guild|Channel|User|Role|Invite|Webhook|GuildEmoji|Message|Integration|StageInstance)}
* @typedef {?(Object|Guild|Channel|User|Role|Invite|Webhook|GuildEmoji|Message|Integration|StageInstance|Sticker)}
* AuditLogEntryTarget
*/
@@ -219,6 +229,7 @@ class GuildAuditLogs {
if (target < 80) return Targets.MESSAGE;
if (target < 83) return Targets.INTEGRATION;
if (target < 86) return Targets.STAGE_INSTANCE;
if (target < 100) return Targets.STICKER;
return Targets.UNKNOWN;
}
@@ -250,6 +261,7 @@ class GuildAuditLogs {
Actions.MESSAGE_PIN,
Actions.INTEGRATION_CREATE,
Actions.STAGE_INSTANCE_CREATE,
Actions.STICKER_CREATE,
].includes(action)
) {
return 'CREATE';
@@ -272,6 +284,7 @@ class GuildAuditLogs {
Actions.MESSAGE_UNPIN,
Actions.INTEGRATION_DELETE,
Actions.STAGE_INSTANCE_DELETE,
Actions.STICKER_DELETE,
].includes(action)
) {
return 'DELETE';
@@ -291,6 +304,7 @@ class GuildAuditLogs {
Actions.EMOJI_UPDATE,
Actions.INTEGRATION_UPDATE,
Actions.STAGE_INSTANCE_UPDATE,
Actions.STICKER_UPDATE,
].includes(action)
) {
return 'UPDATE';
@@ -533,6 +547,19 @@ class GuildAuditLogsEntry {
},
),
);
} else if (targetType === Targets.STICKER) {
this.target =
guild.stickers.cache.get(data.target_id) ??
new Sticker(
guild.client,
this.changes.reduce(
(o, c) => {
o[c.key] = c.new ?? c.old;
return o;
},
{ id: data.target_id },
),
);
} else if (data.target_id) {
this.target = guild[`${targetType.toLowerCase()}s`]?.cache.get(data.target_id) ?? { id: data.target_id };
}

View File

@@ -59,7 +59,7 @@ class GuildEmoji extends BaseGuildEmoji {
*/
get deletable() {
if (!this.guild.me) throw new Error('GUILD_UNCACHED_ME');
return !this.managed && this.guild.me.permissions.has(Permissions.FLAGS.MANAGE_EMOJIS);
return !this.managed && this.guild.me.permissions.has(Permissions.FLAGS.MANAGE_EMOJIS_AND_STICKERS);
}
/**
@@ -80,8 +80,8 @@ class GuildEmoji extends BaseGuildEmoji {
throw new Error('EMOJI_MANAGED');
} else {
if (!this.guild.me) throw new Error('GUILD_UNCACHED_ME');
if (!this.guild.me.permissions.has(Permissions.FLAGS.MANAGE_EMOJIS)) {
throw new Error('MISSING_MANAGE_EMOJIS_PERMISSION', this.guild);
if (!this.guild.me.permissions.has(Permissions.FLAGS.MANAGE_EMOJIS_AND_STICKERS)) {
throw new Error('MISSING_MANAGE_EMOJIS_AND_STICKERS_PERMISSION', this.guild);
}
}
const data = await this.client.api.guilds(this.guild.id).emojis(this.id).get();

View File

@@ -153,15 +153,12 @@ class Message extends Base {
}
/**
* A collection of stickers in the message
* A collection of (partial) stickers in the message
* @type {Collection<Snowflake, Sticker>}
*/
this.stickers = new Collection();
if (data.stickers) {
for (const sticker of data.stickers) {
this.stickers.set(sticker.id, new Sticker(this.client, sticker));
}
}
this.stickers = new Collection(
(data.sticker_items ?? data.stickers)?.map(s => [s.id, new Sticker(this.client, s)]),
);
/**
* The timestamp the message was sent at

View File

@@ -191,6 +191,7 @@ class MessagePayload {
flags,
message_reference,
attachments: this.options.attachments,
sticker_ids: this.options.stickers?.map(sticker => sticker.id ?? sticker),
};
return this;
}

View File

@@ -1,7 +1,7 @@
'use strict';
const Base = require('./Base');
const { StickerFormatTypes } = require('../util/Constants');
const { StickerFormatTypes, StickerTypes } = require('../util/Constants');
const SnowflakeUtil = require('../util/SnowflakeUtil');
/**
@@ -9,8 +9,17 @@ const SnowflakeUtil = require('../util/SnowflakeUtil');
* @extends {Base}
*/
class Sticker extends Base {
/**
* @param {Client} client The instantiating client
* @param {APISticker | APIStickerItem} sticker The data for the sticker
*/
constructor(client, sticker) {
super(client);
this._patch(sticker);
}
_patch(sticker) {
/**
* The sticker's id
* @type {Snowflake}
@@ -18,16 +27,16 @@ class Sticker extends Base {
this.id = sticker.id;
/**
* The sticker image's id
* @type {string}
* The description of the sticker
* @type {?string}
*/
this.asset = sticker.asset;
this.description = sticker.description ?? null;
/**
* The description of the sticker
* @type {string}
* The type of the sticker
* @type {?StickerType}
*/
this.description = sticker.description;
this.type = StickerTypes[sticker.type] ?? null;
/**
* The format of the sticker
@@ -42,16 +51,40 @@ class Sticker extends Base {
this.name = sticker.name;
/**
* The id of the pack the sticker is from
* @type {Snowflake}
* The id of the pack the sticker is from, for standard stickers
* @type {?Snowflake}
*/
this.packId = sticker.pack_id;
this.packId = sticker.pack_id ?? null;
/**
* An array of tags for the sticker, if any
* @type {string[]}
* An array of tags for the sticker
* @type {?string[]}
*/
this.tags = sticker.tags?.split(', ') ?? [];
this.tags = sticker.tags?.split(', ') ?? null;
/**
* Whether or not the guild sticker is available
* @type {?boolean}
*/
this.available = sticker.available ?? null;
/**
* The id of the guild that owns this sticker
* @type {?Snowflake}
*/
this.guildId = sticker.guild_id ?? null;
/**
* The user that uploaded the guild sticker
* @type {?User}
*/
this.user = sticker.user ? this.client.users.add(sticker.user) : null;
/**
* The standard sticker's sort order within its pack
* @type {?number}
*/
this.sortValue = sticker.sort_value ?? null;
}
/**
@@ -72,17 +105,141 @@ class Sticker extends Base {
return new Date(this.createdTimestamp);
}
/**
* Whether this sticker is partial
* @type {boolean}
* @readonly
*/
get partial() {
return !this.type;
}
/**
* The guild that owns this sticker
* @type {?Guild}
* @readonly
*/
get guild() {
return this.client.guilds.resolve(this.guildId);
}
/**
* A link to the sticker
* <info>If the sticker's format is LOTTIE, it returns the URL of the Lottie json file.
* Lottie json files must be converted in order to be displayed in Discord.</info>
* <info>If the sticker's format is LOTTIE, it returns the URL of the Lottie json file.</info>
* @type {string}
*/
get url() {
return `${this.client.options.http.cdn}/stickers/${this.id}/${this.asset}.${
this.format === 'LOTTIE' ? 'json' : 'png'
}`;
return this.client.rest.cdn.Sticker(this.id, this.format);
}
/**
* Fetches this sticker.
* @returns {Promise<Sticker>}
*/
async fetch() {
const data = await this.client.api.stickers(this.id).get();
this._patch(data);
return this;
}
/**
* Fetches the pack this sticker is part of from Discord, if this is a Nitro sticker.
* @returns {Promise<?StickerPack>}
*/
async fetchPack() {
return (this.packId && (await this.client.fetchPremiumStickerPacks()).get(this.packId)) ?? null;
}
/**
* Fetches the user who uploaded this sticker, if this is a guild sticker.
* @returns {Promise<?User>}
*/
async fetchUser() {
if (this.partial) await this.fetch();
if (!this.guildID) throw new Error('NOT_GUILD_STICKER');
const data = await this.client.api.guilds(this.guildId).stickers(this.id).get();
this._patch(data);
return this.user;
}
/**
* Data for editing a sticker.
* @typedef {Object} GuildStickerEditData
* @property {string} [name] The name of the sticker
* @property {?string} [description] The description of the sticker
* @property {string} [tags] The Discord name of a unicode emoji representing the sticker's expression
*/
/**
* Edits the sticker.
* @param {GuildStickerEditData} [data] The new data for the sticker
* @param {string} [reason] Reason for editing this sticker
* @returns {Promise<Sticker>}
* @example
* // Update the name of a sticker
* sticker.edit({ name: 'new name' })
* .then(s => console.log(`Updated the name of the sticker to ${s.name}`))
* .catch(console.error);
*/
edit(data, reason) {
return this.guild.stickers.edit(this, data, reason);
}
/**
* Deletes the sticker.
* @returns {Promise<Sticker>}
* @param {string} [reason] Reason for deleting this sticker
* @example
* // Delete a message
* sticker.delete()
* .then(s => console.log(`Deleted sticker ${s.name}`))
* .catch(console.error);
*/
async delete(reason) {
await this.guild.stickers.delete(this, reason);
return this;
}
/**
* Whether this sticker is the same as another one.
* @param {Sticker|APISticker} other The sticker to compare it to
* @returns {boolean} Whether the sticker is equal to the given sticker or not
*/
equals(other) {
if (other instanceof Sticker) {
return (
other.id === this.id &&
other.description === this.description &&
other.type === this.type &&
other.format === this.format &&
other.name === this.name &&
other.packId === this.packId &&
other.tags.length === this.tags.length &&
other.tags.every(tag => this.tags.includes(tag)) &&
other.available === this.available &&
other.guildId === this.guildId &&
other.sortValue === this.sortValue
);
} else {
return (
other.id === this.id &&
other.description === this.description &&
other.name === this.name &&
other.tags === this.tags.join(', ')
);
}
}
}
module.exports = Sticker;
/**
* @external APISticker
* @see {@link https://discord.com/developers/docs/resources/sticker#sticker-object}
*/
/**
* @external APIStickerItem
* @see {@link https://discord.com/developers/docs/resources/sticker#sticker-item-object}
*/

View File

@@ -0,0 +1,104 @@
'use strict';
const Base = require('./Base');
const Sticker = require('./Sticker');
const Collection = require('../util/Collection');
const SnowflakeUtil = require('../util/SnowflakeUtil');
/**
* Represents a pack of standard stickers.
* @extends {Base}
*/
class StickerPack extends Base {
/**
* @param {Client} client The instantiating client
* @param {APIStickerPack} pack The data for the sticker pack
*/
constructor(client, pack) {
super(client);
/**
* The Sticker pack's id
* @type {Snowflake}
*/
this.id = pack.id;
/**
* The stickers in the pack
* @type {Collection<Snowflake, Sticker>}
*/
this.stickers = new Collection(pack.stickers.map(s => [s.id, new Sticker(client, s)]));
/**
* The name of the sticker pack
* @type {string}
*/
this.name = pack.name;
/**
* The id of the pack's SKU
* @type {Snowflake}
*/
this.skuId = pack.sku_id;
/**
* The id of a sticker in the pack which is shown as the pack's icon
* @type {?Snowflake}
*/
this.coverStickerId = pack.cover_sticker_id ?? null;
/**
* The description of the sticker pack
* @type {string}
*/
this.description = pack.description;
/**
* The id of the sticker pack's banner image
* @type {Snowflake}
*/
this.bannerId = pack.banner_asset_id;
}
/**
* The timestamp the sticker was created at
* @type {number}
* @readonly
*/
get createdTimestamp() {
return SnowflakeUtil.deconstruct(this.id).timestamp;
}
/**
* The time the sticker was created at
* @type {Date}
* @readonly
*/
get createdAt() {
return new Date(this.createdTimestamp);
}
/**
* The sticker which is shown as the pack's icon
* @type {?Sticker}
* @readonly
*/
get coverSticker() {
return this.coverStickerId && this.stickers.get(this.coverStickerId);
}
/**
* The URL to this sticker pack's banner.
* @param {StaticImageURLOptions} [options={}] Options for the Image URL
* @returns {string}
*/
bannerURL({ format, size } = {}) {
return this.client.rest.cdn.StickerPackBanner(this.bannerId, format, size);
}
}
module.exports = StickerPack;
/**
* @external APIStickerPack
* @see {@link https://discord.com/developers/docs/resources/sticker#sticker-pack-object}
*/

View File

@@ -65,6 +65,7 @@ class TextBasedChannel {
* @property {FileOptions[]|BufferResolvable[]|MessageAttachment[]} [files] Files to send with the message
* @property {MessageActionRow[]|MessageActionRowOptions[]} [components]
* Action rows containing interactive components for the message (buttons, select menus)
* @property {StickerResolvable[]} [stickers=[]] Stickers to send in the message
*/
/**

View File

@@ -59,6 +59,8 @@ exports.Endpoints = {
makeImageUrl(`${root}/app-icons/${clientId}/${hash}`, { size, format }),
AppAsset: (clientId, hash, { format = 'webp', size } = {}) =>
makeImageUrl(`${root}/app-assets/${clientId}/${hash}`, { size, format }),
StickerPackBanner: (bannerId, format = 'webp', size) =>
makeImageUrl(`${root}/app-assets/710982414301790216/store/${bannerId}`, { size, format }),
GDMIcon: (channelId, hash, format = 'webp', size) =>
makeImageUrl(`${root}/channel-icons/${channelId}/${hash}`, { size, format }),
Splash: (guildId, hash, format = 'webp', size) =>
@@ -67,6 +69,8 @@ exports.Endpoints = {
makeImageUrl(`${root}/discovery-splashes/${guildId}/${hash}`, { size, format }),
TeamIcon: (teamId, hash, { format = 'webp', size } = {}) =>
makeImageUrl(`${root}/team-icons/${teamId}/${hash}`, { size, format }),
Sticker: (stickerId, stickerFormat) =>
`${root}/stickers/${stickerId}.${stickerFormat === 'LOTTIE' ? 'json' : 'png'}`,
};
},
invite: (root, code) => `${root}/${code}`,
@@ -178,6 +182,9 @@ exports.Events = {
STAGE_INSTANCE_CREATE: 'stageInstanceCreate',
STAGE_INSTANCE_UPDATE: 'stageInstanceUpdate',
STAGE_INSTANCE_DELETE: 'stageInstanceDelete',
GUILD_STICKER_CREATE: 'stickerCreate',
GUILD_STICKER_DELETE: 'stickerDelete',
GUILD_STICKER_UPDATE: 'stickerUpdate',
};
exports.ShardEvents = {
@@ -253,6 +260,7 @@ exports.PartialTypes = keyMirror(['USER', 'CHANNEL', 'GUILD_MEMBER', 'MESSAGE',
* * STAGE_INSTANCE_CREATE
* * STAGE_INSTANCE_UPDATE
* * STAGE_INSTANCE_DELETE
* * GUILD_STICKERS_UPDATE
* @typedef {string} WSEventType
*/
exports.WSEvents = keyMirror([
@@ -305,6 +313,7 @@ exports.WSEvents = keyMirror([
'STAGE_INSTANCE_CREATE',
'STAGE_INSTANCE_UPDATE',
'STAGE_INSTANCE_DELETE',
'GUILD_STICKERS_UPDATE',
]);
/**
@@ -765,6 +774,8 @@ exports.APIErrors = {
INVALID_FORM_BODY: 50035,
INVITE_ACCEPTED_TO_GUILD_NOT_CONTAINING_BOT: 50036,
INVALID_API_VERSION: 50041,
FILE_UPLOADED_EXCEEDS_MAXIMUM_SIZE: 50045,
INVALID_FILE_UPLOADED: 50046,
CANNOT_SELF_REDEEM_GIFT: 50054,
PAYMENT_SOURCE_REQUIRED: 50070,
CANNOT_DELETE_COMMUNITY_REQUIRED_CHANNEL: 50074,
@@ -780,7 +791,14 @@ exports.APIErrors = {
MESSAGE_ALREADY_HAS_THREAD: 160004,
THREAD_LOCKED: 160005,
MAXIMUM_ACTIVE_THREADS: 160006,
MAXIMUM_ACTIVE_ANNOUCEMENT_THREAD: 160007,
MAXIMUM_ACTIVE_ANNOUNCEMENT_THREADS: 160007,
INVALID_JSON_FOR_UPLOADED_LOTTIE_FILE: 170001,
UPLOADED_LOTTIES_CANNOT_CONTAIN_RASTERIZED_IMAGES: 170002,
STICKER_MAXIMUM_FRAMERATE_EXCEEDED: 170003,
STICKER_FRAME_COUNT_EXCEEDS_MAXIMUM_OF_1000_FRAMES: 170004,
LOTTIE_ANIMATION_MAXIMUM_DIMENSIONS_EXCEEDED: 170005,
STICKER_FRAME_RATE_IS_TOO_SMALL_OR_TOO_LARGE: 170006,
STICKER_ANIMATION_DURATION_EXCEEDS_MAXIMUM_OF_5_SECONDS: 170007,
};
/**
@@ -809,6 +827,14 @@ exports.WebhookTypes = createEnum([null, 'Incoming', 'Channel Follower']);
/**
* The value set for a sticker's type:
* * STANDARD
* * GUILD
* @typedef {string} StickerFormatType
*/
exports.StickerTypes = createEnum([null, 'STANDARD', 'GUILD']);
/**
* The value set for a sticker's format type:
* * PNG
* * APNG
* * LOTTIE

View File

@@ -28,7 +28,7 @@ class Intents extends BitField {}
* * `GUILDS`
* * `GUILD_MEMBERS`
* * `GUILD_BANS`
* * `GUILD_EMOJIS`
* * `GUILD_EMOJIS_AND_STICKERS`
* * `GUILD_INTEGRATIONS`
* * `GUILD_WEBHOOKS`
* * `GUILD_INVITES`
@@ -47,7 +47,7 @@ Intents.FLAGS = {
GUILDS: 1 << 0,
GUILD_MEMBERS: 1 << 1,
GUILD_BANS: 1 << 2,
GUILD_EMOJIS: 1 << 3,
GUILD_EMOJIS_AND_STICKERS: 1 << 3,
GUILD_INTEGRATIONS: 1 << 4,
GUILD_WEBHOOKS: 1 << 5,
GUILD_INVITES: 1 << 6,

View File

@@ -87,12 +87,13 @@ class Permissions extends BitField {
* * `MANAGE_NICKNAMES` (change other members' nicknames)
* * `MANAGE_ROLES`
* * `MANAGE_WEBHOOKS`
* * `MANAGE_EMOJIS`
* * `MANAGE_EMOJIS_AND_STICKERS`
* * `USE_APPLICATION_COMMANDS`
* * `REQUEST_TO_SPEAK`
* * `MANAGE_THREADS`
* * `USE_PUBLIC_THREADS`
* * `USE_PRIVATE_THREADS`
* * `USE_EXTERNAL_STICKERS` (use stickers from different guilds)
* @type {Object<string, bigint>}
* @see {@link https://discord.com/developers/docs/topics/permissions}
*/
@@ -127,12 +128,13 @@ Permissions.FLAGS = {
MANAGE_NICKNAMES: 1n << 27n,
MANAGE_ROLES: 1n << 28n,
MANAGE_WEBHOOKS: 1n << 29n,
MANAGE_EMOJIS: 1n << 30n,
MANAGE_EMOJIS_AND_STICKERS: 1n << 30n,
USE_APPLICATION_COMMANDS: 1n << 31n,
REQUEST_TO_SPEAK: 1n << 32n,
MANAGE_THREADS: 1n << 34n,
USE_PUBLIC_THREADS: 1n << 35n,
USE_PRIVATE_THREADS: 1n << 36n,
USE_EXTERNAL_STICKERS: 1n << 37n,
};
/**

5
typings/enums.d.ts vendored
View File

@@ -126,6 +126,11 @@ export enum StickerFormatTypes {
LOTTIE = 3,
}
export enum StickerTypes {
STANDARD = 1,
GUILD = 2,
}
export enum VerificationLevels {
NONE = 0,
LOW = 1,

110
typings/index.d.ts vendored
View File

@@ -25,6 +25,9 @@ import {
APIOverwrite,
APIPartialEmoji,
APIRole,
APISticker,
APIStickerItem,
APIStickerPack,
APIUser,
GatewayVoiceServerUpdateDispatchData,
GatewayVoiceStateUpdateDispatchData,
@@ -52,6 +55,7 @@ import {
PremiumTiers,
PrivacyLevels,
StickerFormatTypes,
StickerTypes,
VerificationLevels,
WebhookTypes,
} from './enums';
@@ -290,6 +294,8 @@ export class Client<Ready extends boolean = boolean> extends BaseClient {
public fetchInvite(invite: InviteResolvable): Promise<Invite>;
public fetchGuildTemplate(template: GuildTemplateResolvable): Promise<GuildTemplate>;
public fetchVoiceRegions(): Promise<Collection<string, VoiceRegion>>;
public fetchSticker(id: Snowflake): Promise<Sticker>;
public fetchPremiumStickerPacks(): Promise<Collection<Snowflake, StickerPack>>;
public fetchWebhook(id: Snowflake, token?: string): Promise<Webhook>;
public fetchWidget(guild: GuildResolvable): Promise<Widget>;
public generateInvite(options?: InviteGenerationOptions): string;
@@ -552,6 +558,7 @@ export class Guild extends AnonymousGuild {
public readonly shard: WebSocketShard;
public shardId: number;
public stageInstances: StageInstanceManager;
public stickers: GuildStickerManager;
public readonly systemChannel: TextChannel | null;
public systemChannelFlags: Readonly<SystemChannelFlags>;
public systemChannelId: Snowflake | null;
@@ -645,6 +652,7 @@ export class GuildAuditLogsEntry {
| Message
| Integration
| StageInstance
| Sticker
| { id: Snowflake }
| null;
public targetType: GuildAuditLogsTarget;
@@ -1550,17 +1558,44 @@ export class StageInstance extends Base {
}
export class Sticker extends Base {
public constructor(client: Client, data: unknown);
public asset: string;
public constructor(client: Client, data: APISticker | APIStickerItem);
public readonly createdTimestamp: number;
public readonly createdAt: Date;
public description: string;
public available: boolean | null;
public description: string | null;
public format: StickerFormatType;
public readonly guild: Guild | null;
public guildId: Snowflake | null;
public id: Snowflake;
public name: string;
public packId: Snowflake;
public tags: string[];
public packId: Snowflake | null;
public readonly partial: boolean;
public sortValue: number | null;
public tags: string[] | null;
public type: StickerType | null;
public user: User | null;
public readonly url: string;
public fetch(): Promise<Sticker>;
public fetchPack(): Promise<StickerPack | null>;
public fetchUser(): Promise<User | null>;
public edit(data?: GuildStickerEditData, reason?: string): Promise<Sticker>;
public delete(reason?: string): Promise<Sticker>;
public equals(other: Sticker | unknown): boolean;
}
export class StickerPack extends Base {
public constructor(client: Client, data: APIStickerPack);
public readonly createdTimestamp: number;
public readonly createdAt: Date;
public bannerId: Snowflake;
public readonly coverSticker: Sticker | null;
public coverStickerId: Snowflake | null;
public description: string;
public id: Snowflake;
public name: string;
public skuId: Snowflake;
public stickers: Collection<Snowflake, Sticker>;
public bannerURL(options?: StaticImageURLOptions): string;
}
export class StoreChannel extends GuildChannel {
@@ -2029,10 +2064,12 @@ export const Constants: {
Icon: (userId: Snowflake | number, hash: string, format: 'default' | AllowedImageFormat, size: number) => string;
AppIcon: (userId: Snowflake | number, hash: string, format: AllowedImageFormat, size: number) => string;
AppAsset: (userId: Snowflake | number, hash: string, format: AllowedImageFormat, size: number) => string;
StickerPackBanner: (bannerId: Snowflake, format: AllowedImageFormat, size: number) => string;
GDMIcon: (userId: Snowflake | number, hash: string, format: AllowedImageFormat, size: number) => string;
Splash: (guildId: Snowflake | number, hash: string, format: AllowedImageFormat, size: number) => string;
DiscoverySplash: (guildId: Snowflake | number, hash: string, format: AllowedImageFormat, size: number) => string;
TeamIcon: (teamId: Snowflake | number, hash: string, format: AllowedImageFormat, size: number) => string;
Sticker: (stickerId: Snowflake, stickerFormat: StickerFormatType) => string;
};
};
WSCodes: {
@@ -2062,6 +2099,7 @@ export const Constants: {
MessageTypes: MessageType[];
SystemMessageTypes: SystemMessageType[];
ActivityTypes: typeof ActivityTypes;
StickerTypes: typeof StickerTypes;
StickerFormatTypes: typeof StickerFormatTypes;
OverwriteTypes: typeof OverwriteTypes;
ExplicitContentFilterLevels: typeof ExplicitContentFilterLevels;
@@ -2313,6 +2351,21 @@ export class GuildInviteManager extends DataManager<Snowflake, Invite, InviteRes
public delete(invite: InviteResolvable, reason?: string): Promise<Invite>;
}
export class GuildStickerManager extends CachedManager<Snowflake, Sticker, StickerResolvable> {
public constructor(guild: Guild, iterable?: Iterable<any>);
public guild: Guild;
public create(
file: BufferResolvable | Stream | FileOptions | MessageAttachment,
name: string,
tags: string,
options?: GuildStickerCreateOptions,
): Promise<Sticker>;
public edit(sticker: StickerResolvable, data?: GuildStickerEditData, reason?: string): Promise<Sticker>;
public delete(sticker: StickerResolvable, reason?: string): Promise<void>;
public fetch(id: Snowflake, options?: BaseFetchOptions): Promise<Sticker>;
public fetch(id?: Snowflake, options?: BaseFetchOptions): Promise<Collection<Snowflake, Sticker>>;
}
export class GuildMemberRoleManager extends DataManager<Snowflake, Role, RoleResolvable> {
public constructor(member: GuildMember);
public readonly hoist: Role | null;
@@ -2648,6 +2701,8 @@ export interface APIErrors {
INVALID_FORM_BODY: 50035;
INVITE_ACCEPTED_TO_GUILD_NOT_CONTAINING_BOT: 50036;
INVALID_API_VERSION: 50041;
FILE_UPLOADED_EXCEEDS_MAXIMUM_SIZE: 50045;
INVALID_FILE_UPLOADED: 50046;
CANNOT_SELF_REDEEM_GIFT: 50054;
PAYMENT_SOURCE_REQUIRED: 50070;
CANNOT_DELETE_COMMUNITY_REQUIRED_CHANNEL: 50074;
@@ -2663,7 +2718,14 @@ export interface APIErrors {
MESSAGE_ALREADY_HAS_THREAD: 160004;
THREAD_LOCKED: 160005;
MAXIMUM_ACTIVE_THREADS: 160006;
MAXIMUM_ACTIVE_ANNOUCEMENT_THREAD: 160007;
MAXIMUM_ACTIVE_ANNOUNCEMENT_THREADS: 160007;
INVALID_JSON_FOR_UPLOADED_LOTTIE_FILE: 170001;
UPLOADED_LOTTIES_CANNOT_CONTAIN_RASTERIZED_IMAGES: 170002;
STICKER_MAXIMUM_FRAMERATE_EXCEEDED: 170003;
STICKER_FRAME_COUNT_EXCEEDS_MAXIMUM_OF_1000_FRAMES: 170004;
LOTTIE_ANIMATION_MAXIMUM_DIMENSIONS_EXCEEDED: 170005;
STICKER_FRAME_RATE_IS_TOO_SMALL_OR_TOO_LARGE: 170006;
STICKER_ANIMATION_DURATION_EXCEEDS_MAXIMUM_OF_5_SECONDS: 170007;
}
export interface ApplicationAsset {
@@ -2923,6 +2985,9 @@ export interface ClientEvents {
stageInstanceCreate: [stageInstance: StageInstance];
stageInstanceUpdate: [oldStageInstance: StageInstance | null, newStageInstance: StageInstance];
stageInstanceDelete: [stageInstance: StageInstance];
stickerCreate: [sticker: Sticker];
stickerDelete: [sticker: Sticker];
stickerUpdate: [oldSticker: Sticker, newSticker: Sticker];
}
export interface ClientOptions {
@@ -3133,6 +3198,9 @@ export interface ConstantsEvents {
STAGE_INSTANCE_CREATE: 'stageInstanceCreate';
STAGE_INSTANCE_UPDATE: 'stageInstanceUpdate';
STAGE_INSTANCE_DELETE: 'stageInstanceDelete';
GUILD_STICKER_CREATE: 'stickerCreate';
GUILD_STICKER_DELETE: 'stickerDelete';
GUILD_STICKER_UPDATE: 'stickerUpdate';
}
export interface ConstantsOpcodes {
@@ -3363,6 +3431,9 @@ export interface GuildAuditLogsActions {
STAGE_INSTANCE_CREATE?: number;
STAGE_INSTANCE_UPDATE?: number;
STAGE_INSTANCE_DELETE?: number;
STICKER_CREATE?: number;
STICKER_UPDATE?: number;
STICKER_DELETE?: number;
}
export type GuildAuditLogsActionType = 'CREATE' | 'DELETE' | 'UPDATE' | 'ALL';
@@ -3388,6 +3459,7 @@ export interface GuildAuditLogsTargets {
MESSAGE?: string;
INTEGRATION?: string;
STAGE_INSTANCE?: string;
STICKER?: string;
UNKNOWN?: string;
}
@@ -3483,6 +3555,17 @@ export interface GuildEmojiEditData {
roles?: Collection<Snowflake, Role> | RoleResolvable[];
}
export interface GuildStickerCreateOptions {
description?: string | null;
reason?: string;
}
export interface GuildStickerEditData {
name?: string;
description?: string | null;
tags?: string;
}
export type GuildFeatures =
| 'ANIMATED_ICON'
| 'BANNER'
@@ -3617,7 +3700,7 @@ export type IntentsString =
| 'GUILDS'
| 'GUILD_MEMBERS'
| 'GUILD_BANS'
| 'GUILD_EMOJIS'
| 'GUILD_EMOJIS_AND_STICKERS'
| 'GUILD_INTEGRATIONS'
| 'GUILD_WEBHOOKS'
| 'GUILD_INVITES'
@@ -3836,6 +3919,7 @@ export interface MessageOptions {
allowedMentions?: MessageMentionOptions;
files?: (FileOptions | BufferResolvable | Stream | MessageAttachment)[];
reply?: ReplyOptions;
stickers?: StickerResolvable[];
}
export type MessageReactionResolvable =
@@ -3983,12 +4067,13 @@ export type PermissionString =
| 'MANAGE_NICKNAMES'
| 'MANAGE_ROLES'
| 'MANAGE_WEBHOOKS'
| 'MANAGE_EMOJIS'
| 'MANAGE_EMOJIS_AND_STICKERS'
| 'USE_APPLICATION_COMMANDS'
| 'REQUEST_TO_SPEAK'
| 'MANAGE_THREADS'
| 'USE_PUBLIC_THREADS'
| 'USE_PRIVATE_THREADS';
| 'USE_PRIVATE_THREADS'
| 'USE_EXTERNAL_STICKERS';
export type RecursiveArray<T> = ReadonlyArray<T | RecursiveArray<T>>;
@@ -4164,6 +4249,10 @@ export type Status = number;
export type StickerFormatType = keyof typeof StickerFormatTypes;
export type StickerResolvable = Sticker | Snowflake;
export type StickerType = keyof typeof StickerTypes;
export type SystemChannelFlagsString =
| 'SUPPRESS_JOIN_NOTIFICATIONS'
| 'SUPPRESS_PREMIUM_SUBSCRIPTIONS'
@@ -4341,7 +4430,8 @@ export type WSEventType =
| 'INTERACTION_CREATE'
| 'STAGE_INSTANCE_CREATE'
| 'STAGE_INSTANCE_UPDATE'
| 'STAGE_INSTANCE_DELETE';
| 'STAGE_INSTANCE_DELETE'
| 'GUILD_STICKERS_UPDATE';
export type Serialized<T> = T extends symbol | bigint | (() => any)
? never