Skip to content

Commit

Permalink
feat(Sticker): updates, sticker packs, and guild stickers (#5867)
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
8 people committed Jul 19, 2021
1 parent 76888e6 commit 54d6a3a
Show file tree
Hide file tree
Showing 23 changed files with 771 additions and 48 deletions.
29 changes: 29 additions & 0 deletions src/client/Client.js
Expand Up @@ -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');
Expand Down Expand Up @@ -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.
Expand Down
20 changes: 20 additions & 0 deletions src/client/actions/GuildStickerCreate.js
@@ -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;
20 changes: 20 additions & 0 deletions src/client/actions/GuildStickerDelete.js
@@ -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;
20 changes: 20 additions & 0 deletions src/client/actions/GuildStickerUpdate.js
@@ -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;
34 changes: 34 additions & 0 deletions src/client/actions/GuildStickersUpdate.js
@@ -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;
5 changes: 5 additions & 0 deletions src/client/websocket/handlers/GUILD_STICKERS_UPDATE.js
@@ -0,0 +1,5 @@
'use strict';

module.exports = (client, packet) => {
client.actions.GuildStickersUpdate.handle(packet.d);
};
5 changes: 3 additions & 2 deletions src/errors/Messages.js
Expand Up @@ -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.",

Expand Down
1 change: 1 addition & 0 deletions src/index.js
Expand Up @@ -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'),
Expand Down
160 changes: 160 additions & 0 deletions src/managers/GuildStickerManager.js
@@ -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;
12 changes: 10 additions & 2 deletions src/rest/APIRequest.js
Expand Up @@ -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) {
Expand Down
15 changes: 15 additions & 0 deletions src/structures/Guild.js
Expand Up @@ -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');
Expand Down Expand Up @@ -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,
});
}
}

/**
Expand Down

0 comments on commit 54d6a3a

Please sign in to comment.