Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 47 commits into from Jul 19, 2021
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
23ce3c3
feat(Sticker): update sticker properties
advaith1 Jun 16, 2021
61c09c7
feat: add StickerPack
advaith1 Jun 17, 2021
9cc3e87
feat(Sticker): add fetchPack()
advaith1 Jun 17, 2021
dd810cf
types: fix order
advaith1 Jun 17, 2021
059964c
use client.guilds.resolve
advaith1 Jun 17, 2021
ac897e5
fix: rename StickerBanner to StickerPackBanner
advaith1 Jun 17, 2021
afa882b
feat(Client): add fetchSticker(id)
advaith1 Jun 18, 2021
ee953d9
Edit fetchSticker desc
advaith1 Jun 19, 2021
3545ca2
style: switch from .then to async/await
advaith1 Jun 19, 2021
2e84a12
style: use data variables instead of inline API reqs
advaith1 Jun 19, 2021
8f3c562
refactor: move sticker url to CDN constant
advaith1 Jun 20, 2021
58c95e3
refactor: rename fetchNitroStickerPacks to fetchPremiumStickerPacks
advaith1 Jun 20, 2021
b7f0fc8
refactor(Permissions): `MANAGE_EMOJIS_AND_STICKERS`
advaith1 Jun 25, 2021
f1c8f19
fix(StickerPack): `cover_sticker_id` is optional
advaith1 Jun 25, 2021
3814ef3
fix: banner fields are not nullable
advaith1 Jun 25, 2021
7dd8a60
refactor: switch to msg.sticker_items, add fetch() and fetchUser()
advaith1 Jun 29, 2021
16c21cb
Apply suggestions from code review
advaith1 Jun 30, 2021
c65a377
types: switch to old nullable syntax
advaith1 Jul 1, 2021
55abf97
fix partial check
advaith1 Jul 1, 2021
6c9f2be
Apply suggestions from code review
advaith1 Jul 3, 2021
a761659
fix: partial check but for real this time
advaith1 Jul 3, 2021
d97cdd0
Merge remote-tracking branch 'upstream/master' into sticker-updates
advaith1 Jul 3, 2021
a08b490
feat: sending stickers
advaith1 Jul 3, 2021
594ba64
feat: add GuildStickerManager and events
advaith1 Jul 4, 2021
ddc9b54
Apply suggestions from code review
advaith1 Jul 5, 2021
af64a00
Merge branch 'master' into sticker-updates
advaith1 Jul 5, 2021
a7bb38e
fix(GuildStickerManager): crawl missed an `ID`
advaith1 Jul 5, 2021
dbce477
fix(Sticker): check tags in API equals
advaith1 Jul 5, 2021
61fc74e
types: add typings for GSM and events
advaith1 Jul 5, 2021
c202904
fix(GuildStickerManager): extend CachedManager
advaith1 Jul 5, 2021
e95ec9a
Merge branch 'master' into sticker-updates
advaith1 Jul 7, 2021
ced9bf3
feat(ApiErrors): add sticker error codes
advaith1 Jul 7, 2021
41fb4bb
feat(GuildAuditLogs): add sticker audit logs
advaith1 Jul 7, 2021
54133d3
style: use for-of and object.entries
advaith1 Jul 7, 2021
eca5e4d
Merge branch 'master' into sticker-updates
advaith1 Jul 7, 2021
60ae9ff
style: use optional chaining
advaith1 Jul 14, 2021
31ca245
style: return new collection directly
advaith1 Jul 14, 2021
8193e0f
feat(Permissions): add USE_EXTERNAL_STICKERS
advaith1 Jul 17, 2021
a3b9a52
feat(ApiErrors): add 170007
advaith1 Jul 17, 2021
737ed2b
Merge branch 'master' into sticker-updates
advaith1 Jul 17, 2021
e26591b
refactor: rename add to _add
advaith1 Jul 18, 2021
8a6cb78
feat: support old messages and edit Message#stickers desc
advaith1 Jul 19, 2021
8a32e87
Merge branch 'master' into sticker-updates
advaith1 Jul 19, 2021
d17fe5d
feat: add more error codes
advaith1 Jul 19, 2021
c98dda1
fix: missed one `add`
advaith1 Jul 19, 2021
5ab68fc
docs: sticker object API docs links and API types
advaith1 Jul 19, 2021
e5023ed
Merge branch 'master' into sticker-updates
advaith1 Jul 19, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
29 changes: 29 additions & 0 deletions src/client/Client.js
Expand Up @@ -13,6 +13,8 @@ const ShardClientUtil = require('../sharding/ShardClientUtil');
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');
Expand Down Expand Up @@ -310,6 +312,33 @@ class Client extends BaseClient {
});
}

/**
* Obtains a sticker from Discord.
* @param {Snowflake} id The sticker's ID
advaith1 marked this conversation as resolved.
Show resolved Hide resolved
* @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)]));
iCrawl marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* 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.
Expand Down
5 changes: 3 additions & 2 deletions src/errors/Messages.js
Expand Up @@ -98,8 +98,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.`,
vladfrangu marked this conversation as resolved.
Show resolved Hide resolved
NOT_GUILD_STICKER: 'Sticker is a standard (non-guild) sticker and has no User.',
advaith1 marked this conversation as resolved.
Show resolved Hide resolved

REACTION_RESOLVE_USER: "Couldn't resolve the user ID to remove from the reaction.",

Expand Down
6 changes: 3 additions & 3 deletions src/structures/GuildEmoji.js
Expand Up @@ -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);
}

/**
Expand All @@ -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();
Expand Down
7 changes: 1 addition & 6 deletions src/structures/Message.js
Expand Up @@ -146,12 +146,7 @@ class Message extends Base {
* A collection of 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?.map(s => [s.id, new Sticker(this.client, s)]));

/**
* The timestamp the message was sent at
Expand Down
113 changes: 96 additions & 17 deletions src/structures/Sticker.js
@@ -1,33 +1,42 @@
'use strict';

const Base = require('./Base');
const { StickerFormatTypes } = require('../util/Constants');
const { StickerFormatTypes, StickerTypes } = require('../util/Constants');
const SnowflakeUtil = require('../util/SnowflakeUtil');

/**
* Represents a Sticker.
* @extends {Base}
*/
class Sticker extends Base {
/**
* @param {Client} client The instantiating client
* @param {Object} sticker The data for the sticker
iCrawl marked this conversation as resolved.
Show resolved Hide resolved
*/
constructor(client, sticker) {
super(client);

this._patch(sticker);
}

_patch(sticker) {
/**
* The ID of the sticker
advaith1 marked this conversation as resolved.
Show resolved Hide resolved
* @type {Snowflake}
*/
this.id = sticker.id;

/**
* The ID of the sticker's image
* @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
Expand All @@ -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
advaith1 marked this conversation as resolved.
Show resolved Hide resolved
* @type {?Snowflake}
*/
this.packID = sticker.pack_id;
this.packID = sticker.pack_id ?? null;
advaith1 marked this conversation as resolved.
Show resolved Hide resolved

/**
* An array of tags for the sticker, if any
* An array of tags for the sticker
* @type {string[]}
advaith1 marked this conversation as resolved.
Show resolved Hide resolved
*/
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
advaith1 marked this conversation as resolved.
Show resolved Hide resolved
* @type {?Snowflake}
*/
this.guildID = sticker.guild_id ?? null;
advaith1 marked this conversation as resolved.
Show resolved Hide resolved

/**
* The user that uploaded the guild sticker
* @type {?User}
*/
this.user = sticker.user ? this.client.users.add(sticker.user) : null;
iCrawl marked this conversation as resolved.
Show resolved Hide resolved

/**
* The standard sticker's sort order within its pack
* @type {?number}
*/
this.sortValue = sticker.sort_value ?? null;
}

/**
Expand All @@ -72,16 +105,62 @@ class Sticker extends Base {
return new Date(this.createdTimestamp);
}

/**
* Whether this sticker is partial
* @type {boolean}
* @readonly
*/
get partial() {
return 'username' in this;
iCrawl marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* The guild that owns this sticker
* @type {?Guild}
* @readonly
*/
get guild() {
return this.client.guilds.resolve(this.guildID);
advaith1 marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* 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>}
advaith1 marked this conversation as resolved.
Show resolved Hide resolved
*/
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;
vladfrangu marked this conversation as resolved.
Show resolved Hide resolved
advaith1 marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* 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();
advaith1 marked this conversation as resolved.
Show resolved Hide resolved
this._patch(data);
return this.user;
}
}

Expand Down
99 changes: 99 additions & 0 deletions src/structures/StickerPack.js
@@ -0,0 +1,99 @@
'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 {Object} pack The data for the sticker pack
iCrawl marked this conversation as resolved.
Show resolved Hide resolved
*/
constructor(client, pack) {
super(client);
/**
* The ID of the sticker pack
advaith1 marked this conversation as resolved.
Show resolved Hide resolved
* @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)]));
iCrawl marked this conversation as resolved.
Show resolved Hide resolved

/**
* The name of the sticker pack
* @type {string}
*/
this.name = pack.name;

/**
* The ID of the pack's SKU
advaith1 marked this conversation as resolved.
Show resolved Hide resolved
* @type {Snowflake}
*/
this.skuID = pack.sku_id;
advaith1 marked this conversation as resolved.
Show resolved Hide resolved

/**
* The ID of a sticker in the pack which is shown as the pack's icon
advaith1 marked this conversation as resolved.
Show resolved Hide resolved
* @type {?Snowflake}
*/
this.coverStickerID = pack.cover_sticker_id ?? null;
advaith1 marked this conversation as resolved.
Show resolved Hide resolved

/**
* The description of the sticker pack
* @type {string}
*/
this.description = pack.description;

/**
* The ID of the sticker pack's banner image
advaith1 marked this conversation as resolved.
Show resolved Hide resolved
* @type {Snowflake}
*/
this.bannerID = pack.banner_asset_id;
advaith1 marked this conversation as resolved.
Show resolved Hide resolved
advaith1 marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* 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);
advaith1 marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* 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);
advaith1 marked this conversation as resolved.
Show resolved Hide resolved
}
}

module.exports = StickerPack;
12 changes: 12 additions & 0 deletions src/util/Constants.js
Expand Up @@ -169,6 +169,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) =>
advaith1 marked this conversation as resolved.
Show resolved Hide resolved
makeImageUrl(`${root}/app-assets/710982414301790216/store/${bannerID}`, { size, format }),
kyranet marked this conversation as resolved.
Show resolved Hide resolved
advaith1 marked this conversation as resolved.
Show resolved Hide resolved
GDMIcon: (channelID, hash, format = 'webp', size) =>
makeImageUrl(`${root}/channel-icons/${channelID}/${hash}`, { size, format }),
Splash: (guildID, hash, format = 'webp', size) =>
Expand All @@ -177,6 +179,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) =>
advaith1 marked this conversation as resolved.
Show resolved Hide resolved
`${root}/stickers/${stickerID}.${stickerFormat === 'LOTTIE' ? 'json' : 'png'}`,
advaith1 marked this conversation as resolved.
Show resolved Hide resolved
};
},
invite: (root, code) => `${root}/${code}`,
Expand Down Expand Up @@ -742,6 +746,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
Expand Down