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,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
*/
/**