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 35 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 @@ -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 @@ -304,6 +306,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)]));
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
4 changes: 4 additions & 0 deletions src/client/actions/ActionsManager.js
Expand Up @@ -47,6 +47,10 @@ class ActionsManager {
this.register(require('./StageInstanceCreate'));
this.register(require('./StageInstanceUpdate'));
this.register(require('./StageInstanceDelete'));
this.register(require('./GuildStickerCreate'));
this.register(require('./GuildStickerDelete'));
this.register(require('./GuildStickerUpdate'));
this.register(require('./GuildStickersUpdate'));
}

register(Action) {
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 @@ -107,8 +107,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 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
161 changes: 161 additions & 0 deletions src/managers/GuildStickerManager.js
@@ -0,0 +1,161 @@
'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);

/**
advaith1 marked this conversation as resolved.
Show resolved Hide resolved
* 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();
const stickers = new Collection(data.map(sticker => [sticker.id, this.add(sticker, cache)]));
iCrawl marked this conversation as resolved.
Show resolved Hide resolved
return stickers;
}
}

module.exports = GuildStickerManager;
12 changes: 10 additions & 2 deletions src/rest/APIRequest.js
Expand Up @@ -43,8 +43,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.file) body.append(file.key ?? file.name, file.file, file.name);
advaith1 marked this conversation as resolved.
Show resolved Hide resolved
}
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