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: interactions #5448

Merged
merged 94 commits into from May 7, 2021
Merged
Show file tree
Hide file tree
Changes from 61 commits
Commits
Show all changes
94 commits
Select commit Hold shift + click to select a range
6481028
feat: interactions
vaporoxx Mar 30, 2021
c58e5be
chore: rewording
vaporoxx Mar 30, 2021
bb3105c
fix: correct type
vaporoxx Mar 30, 2021
25f76e9
fix: dont pass the application
vaporoxx Mar 31, 2021
06e1e46
fix: add missing await
vaporoxx Mar 31, 2021
e5cb998
types: add missing guild property
vaporoxx Mar 31, 2021
9c250a0
fix: static methods
vaporoxx Mar 31, 2021
b1ed7e7
fix: fix: static methods
vaporoxx Mar 31, 2021
3985659
refactor: use reduce on the returned array
vaporoxx Mar 31, 2021
12566f5
refactor: use flag property
vaporoxx Mar 31, 2021
7007bdc
refactor: commandPath property (hopefully this works)
vaporoxx Mar 31, 2021
e4355e2
docs: some fixes
vaporoxx Mar 31, 2021
d5a94b3
fix: InteractionTypes is an enum, not a keyMirror
vaporoxx Mar 31, 2021
050e41c
Merge branch 'main' into feat-interactions
vaporoxx Mar 31, 2021
47cf6e0
feat: new message type
vaporoxx Mar 31, 2021
56ba478
fix: damn
vaporoxx Mar 31, 2021
938e2f0
fix: damn #2
vaporoxx Mar 31, 2021
eabd028
chore: revert lockfile version change
vaporoxx Mar 31, 2021
3d35543
fix: resolved.members is undefined in dms
vaporoxx Mar 31, 2021
dfbfe5d
feat: add methods to the manager
vaporoxx Apr 2, 2021
36d3441
fix: merge fucked up order
vaporoxx Apr 2, 2021
df68348
refactor: shorten manager getter
vaporoxx Apr 2, 2021
e1db18b
fix: use enum and not enum values as type
vaporoxx Apr 2, 2021
520816d
docs: add missing readonly tag
vaporoxx Apr 2, 2021
55e25d1
feat: more message types!
vaporoxx Apr 3, 2021
77416df
feat: make interaction.user non-nullable
vaporoxx Apr 3, 2021
22d9ce5
refactor: remove unnecessary fallbacks
vaporoxx Apr 3, 2021
e6e2fb8
Merge branch 'main' into feat-interactions
vaporoxx Apr 3, 2021
74ece3d
Merge branch 'main' into feat-interactions
vaporoxx Apr 3, 2021
68bb5ca
feat: enforce strings (#4880)
vaporoxx Apr 3, 2021
c6d8558
feat: support editing/deleting the initial response
vaporoxx Apr 3, 2021
ff5a22e
fix: LOADING is a message flag, part 1
vaporoxx Apr 3, 2021
0a3344e
fix: LOADING is a message flag, part 2
vaporoxx Apr 3, 2021
a55505d
Merge branch 'main' into feat-interactions
vaporoxx Apr 3, 2021
c580d8f
feat: allow sending files
vaporoxx Apr 4, 2021
5e9f5ff
refactor: use properties from the data object
vaporoxx Apr 4, 2021
e953dd0
feat: replied property
vaporoxx Apr 4, 2021
c50c0cb
feat: separate .replied from .deferred
vaporoxx Apr 4, 2021
ad53b3c
style: oops
vaporoxx Apr 4, 2021
fdc1128
refactor: don't use string literal
vaporoxx Apr 4, 2021
5a8c9ad
docs: add missing @returns tag
vaporoxx Apr 4, 2021
5e1bb97
feat: fetchReply
vaporoxx Apr 5, 2021
b011586
fix: stuff
vaporoxx Apr 6, 2021
06b4fa6
feat: try to return the command when deleting from the manager
vaporoxx Apr 7, 2021
23d6f9c
fix: sort methods correctly
vaporoxx Apr 7, 2021
f2217d3
feat: command permissions (discord/discord-api-docs#2737)
vaporoxx Apr 7, 2021
dd8c145
chore: apply suggestions from code review
vaporoxx Apr 13, 2021
45f40f4
fix: lint
vaporoxx Apr 13, 2021
7c7f2c2
feat: make CommandInteraction extendable
vaporoxx Apr 13, 2021
60029f1
fix: discord keeps sending interactions after application left the guild
vaporoxx Apr 13, 2021
f930f05
refactor: give editPermissions a better name
vaporoxx Apr 13, 2021
d19d70f
feat: allow types as numbers
vaporoxx Apr 13, 2021
cf98736
refactor: use this.constructor
vaporoxx Apr 13, 2021
6a1e2d2
docs: fix grammar
vaporoxx Apr 13, 2021
2c08679
refactor: use enum
vaporoxx Apr 13, 2021
1179daf
fix: use arrow functions everywhere
vaporoxx Apr 14, 2021
0811df1
refactor: use new endpoint
vaporoxx Apr 14, 2021
c5ed978
feat: better handling of uncached guilds
vaporoxx Apr 14, 2021
24fc783
fix: add missing typings
vaporoxx Apr 14, 2021
cc2dbf5
Merge branch 'main' into feat-interactions
vaporoxx Apr 15, 2021
356b53d
fix: Snowflake -> SnowflakeUtil
vaporoxx Apr 15, 2021
ad99a05
fix: dont crash when passing a foreign user id
vaporoxx Apr 17, 2021
132e1c8
feat: throw custom error if the user has already deferred/replied
vaporoxx Apr 17, 2021
ef08cc8
docs: seems like this is actually correct
vaporoxx Apr 17, 2021
cd3572d
fix: import Error class
vaporoxx Apr 18, 2021
6448b21
feat: add support for the bulk edit permissions endpoint
vaporoxx Apr 22, 2021
ddf31f8
docs: add more links
vaporoxx Apr 22, 2021
c420e5d
fix: how can you even fuck this up
vaporoxx Apr 22, 2021
3965843
fix: data isn't iterable
vaporoxx Apr 22, 2021
b8d649a
feat: add missing fields
vaporoxx Apr 22, 2021
da08846
Merge branch 'main' into feat-interactions
vaporoxx Apr 23, 2021
3169cfe
feat: return raw objects instead of null
vaporoxx Apr 23, 2021
c10be41
docs: add references to webhook methods
vaporoxx Apr 23, 2021
5ef3e93
chore: rewording
vaporoxx Apr 23, 2021
c5cb86c
docs: fix @see tags
vaporoxx Apr 26, 2021
de1a6a2
docs: add examples
vaporoxx Apr 26, 2021
e939f5c
types: better overloads
vaporoxx Apr 26, 2021
46a325a
style: remove trailing space
vaporoxx Apr 26, 2021
35a1cb9
Merge branch 'main' into feat-interactions
vaporoxx Apr 30, 2021
6450348
fix: use data interface
vaporoxx Apr 30, 2021
99ff03e
fix: stop webhook clients from keeping the process alive
vaporoxx Apr 30, 2021
7070dc3
refactor: use Webhook#fetchMessage
vaporoxx Apr 30, 2021
a38043c
feat: new MENTIONABLE option type
vaporoxx Apr 30, 2021
9955597
feat: add MessageInteraction
vaporoxx Apr 30, 2021
d523d5f
fix: use WebhookMessageOptions for interactions
vaporoxx Apr 30, 2021
572c4af
feat: return raw objects if guild is not in cache
vaporoxx May 1, 2021
69dfce3
docs: Message#interaction is nullable
vaporoxx May 1, 2021
9e3b213
chore: use better types
vaporoxx May 3, 2021
ab114f7
docs: put typedef tag at the top
vaporoxx May 3, 2021
76ae544
fix: dont use object spread
vaporoxx May 3, 2021
80d8a38
fix: should return actual raw data
vaporoxx May 3, 2021
0655892
types: omit webhook-only properties
vaporoxx May 3, 2021
4e99dab
refactor: remove duplicate code
vaporoxx May 4, 2021
0c0749e
refactor: import enums from discord-api-types
vaporoxx May 4, 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
52 changes: 52 additions & 0 deletions docs/examples/commands.md
@@ -0,0 +1,52 @@
# Slash Commands

In this example, you'll get to know how to create commands and listen to incoming interactions.

## Creating a Command

First off, we need to create a command so that users can use it. We will create an `echo` command which simply returns what the user inputted. Note that global commands can take up to an hour to appear in the client, so if you want to test a new command, you should create it for one guild first.

```js
// The data for our command
const commandData = {
name: 'echo',
description: 'Replies with your input!',
options: [{
name: 'input',
type: 'STRING',
description: 'The input which should be echoed back',
required: true,
}],
};

client.once('ready', () => {
// Creating a global command
client.application.commands.create(commandData);

// Creating a guild-specific command
client.guilds.cache.get('id').commands.create(commandData);
vaporoxx marked this conversation as resolved.
Show resolved Hide resolved
});
```

And that's it! As soon as your client gets ready, it will register the `echo` command.
vaporoxx marked this conversation as resolved.
Show resolved Hide resolved

## Handling Commands

Now let's implement a simple handler for it:

```js
client.on('interaction', interaction => {
// If the interaction isn't a slash command, return
if (!interaction.isCommand()) return;

// Check if it is the correct command
if (interaction.commandName === 'echo') {
// Get the input of the user
const input = interaction.options[0].value;
vaporoxx marked this conversation as resolved.
Show resolved Hide resolved
// Reply to the command
interaction.reply(input);
}
});
```

The `interaction` event will get emitted every time the client receives an interaction. Only our own slash commands trigger this event, so there is no need to implement a check for commands that belong to other bots.
2 changes: 2 additions & 0 deletions docs/index.yml
Expand Up @@ -28,3 +28,5 @@
path: moderation.md
- name: Webhook
path: webhook.js
- name: Slash Commands
path: commands.md
5 changes: 5 additions & 0 deletions esm/discord.mjs
Expand Up @@ -28,8 +28,10 @@ export const {
UserFlags,
Util,
version,
ApplicationCommandManager,
BaseGuildEmojiManager,
ChannelManager,
GuildApplicationCommandManager,
GuildChannelManager,
GuildEmojiManager,
GuildEmojiRoleManager,
Expand All @@ -49,6 +51,7 @@ export const {
resolveString,
splitMessage,
Application,
ApplicationCommand,
Base,
Activity,
APIMessage,
Expand All @@ -59,6 +62,7 @@ export const {
ClientApplication,
ClientUser,
Collector,
CommandInteraction,
DMChannel,
Emoji,
Guild,
Expand All @@ -70,6 +74,7 @@ export const {
GuildTemplate,
Integration,
IntegrationApplication,
Interaction,
Invite,
Message,
MessageAttachment,
Expand Down
23 changes: 23 additions & 0 deletions src/client/websocket/handlers/INTERACTION_CREATE.js
@@ -0,0 +1,23 @@
'use strict';

const { Events, InteractionTypes } = require('../../../util/Constants');
let Structures;

module.exports = (client, { d: data }) => {
if (data.type === InteractionTypes.APPLICATION_COMMAND) {
if (!Structures) Structures = require('../../../util/Structures');
const CommandInteraction = Structures.get('CommandInteraction');

const interaction = new CommandInteraction(client, data);

/**
* Emitted when an interaction is created.
* @event Client#interaction
* @param {Interaction} interaction The interaction which was created
*/
client.emit(Events.INTERACTION_CREATE, interaction);
vaporoxx marked this conversation as resolved.
Show resolved Hide resolved
return;
}

client.emit(Events.DEBUG, `[INTERACTION] Received interaction with unknown type: ${data.type}`);
};
5 changes: 5 additions & 0 deletions src/index.js
Expand Up @@ -33,8 +33,10 @@ module.exports = {
version: require('../package.json').version,

// Managers
ApplicationCommandManager: require('./managers/ApplicationCommandManager'),
BaseGuildEmojiManager: require('./managers/BaseGuildEmojiManager'),
ChannelManager: require('./managers/ChannelManager'),
GuildApplicationCommandManager: require('./managers/GuildApplicationCommandManager'),
GuildChannelManager: require('./managers/GuildChannelManager'),
GuildEmojiManager: require('./managers/GuildEmojiManager'),
GuildEmojiRoleManager: require('./managers/GuildEmojiRoleManager'),
Expand All @@ -58,6 +60,7 @@ module.exports = {

// Structures
Application: require('./structures/interfaces/Application'),
ApplicationCommand: require('./structures/ApplicationCommand'),
Base: require('./structures/Base'),
Activity: require('./structures/Presence').Activity,
APIMessage: require('./structures/APIMessage'),
Expand All @@ -71,6 +74,7 @@ module.exports = {
return require('./structures/ClientUser');
},
Collector: require('./structures/interfaces/Collector'),
CommandInteraction: require('./structures/CommandInteraction'),
DMChannel: require('./structures/DMChannel'),
Emoji: require('./structures/Emoji'),
Guild: require('./structures/Guild'),
Expand All @@ -82,6 +86,7 @@ module.exports = {
GuildTemplate: require('./structures/GuildTemplate'),
Integration: require('./structures/Integration'),
IntegrationApplication: require('./structures/IntegrationApplication'),
Interaction: require('./structures/Interaction'),
Invite: require('./structures/Invite'),
Message: require('./structures/Message'),
MessageAttachment: require('./structures/MessageAttachment'),
Expand Down
201 changes: 201 additions & 0 deletions src/managers/ApplicationCommandManager.js
@@ -0,0 +1,201 @@
'use strict';

const BaseManager = require('./BaseManager');
const { TypeError } = require('../errors');
const ApplicationCommand = require('../structures/ApplicationCommand');
const Collection = require('../util/Collection');
const { ApplicationCommandPermissionTypes } = require('../util/Constants');

/**
* Manages API methods for application commands and stores their cache.
* @extends {BaseManager}
*/
class ApplicationCommandManager extends BaseManager {
constructor(client, iterable) {
super(client, iterable, ApplicationCommand);
}

/**
* The cache of this manager
* @type {Collection<Snowflake, ApplicationCommand>}
* @name ApplicationCommandManager#cache
*/

add(data, cache) {
return super.add(data, cache, { extras: [this.guild] });
}

/**
* The APIRouter path to the commands
* @type {Object}
* @readonly
* @private
*/
get commandPath() {
let path = this.client.api.applications(this.client.application.id);
if (this.guild) path = path.guilds(this.guild.id);
return path.commands;
}

/**
* Data that resolves to give an ApplicationCommand object. This can be:
* * An ApplicationCommand object
* * A Snowflake
* @typedef {ApplicationCommand|Snowflake} ApplicationCommandResolvable
*/

/**
* Obtains one or multiple application commands from Discord, or the cache if it's already available.
* @param {Snowflake} [id] ID of the application command
* @param {boolean} [cache=true] Whether to cache the new application commands if they weren't already
* @param {boolean} [force=false] Whether to skip the cache check and request the API
* @returns {Promise<ApplicationCommand|Collection<Snowflake, ApplicationCommand>>}
*/
async fetch(id, cache = true, force = false) {
if (id) {
if (!force) {
const existing = this.cache.get(id);
if (existing) return existing;
}
const command = await this.commandPath(id).get();
return this.add(command, cache);
}

const data = await this.commandPath.get();
return data.reduce((coll, command) => coll.set(command.id, this.add(command, cache)), new Collection());
}

/**
* Creates an application command.
* @param {ApplicationCommandData} command The command
* @returns {Promise<ApplicationCommand>}
*/
async create(command) {
vaporoxx marked this conversation as resolved.
Show resolved Hide resolved
const data = await this.commandPath.post({
data: this.constructor.transformCommand(command),
});
return this.add(data);
}

/**
* Sets all the commands for this application or guild.
* @param {ApplicationCommandData[]} commands The commands
* @returns {Promise<Collection<Snowflake, ApplicationCommand>>}
*/
async set(commands) {
const data = await this.commandPath.put({
data: commands.map(c => this.constructor.transformCommand(c)),
});
return data.reduce((coll, command) => coll.set(command.id, this.add(command)), new Collection());
}

/**
* Edits an application command.
* @param {ApplicationCommandResolvable} command The command to edit
* @param {ApplicationCommandData} data The data to update the command with
* @returns {Promise<ApplicationCommand>}
*/
async edit(command, data) {
const id = this.resolveID(command);
if (!id) throw new TypeError('INVALID_TYPE', 'command', 'ApplicationCommandResolvable');

const raw = {};
if (data.name) raw.name = data.name;
if (data.description) raw.description = data.description;
if (data.options) raw.options = data.options.map(o => ApplicationCommand.transformOption(o));
if (data.defaultPermission) raw.default_permission = data.defaultPermission;
vaporoxx marked this conversation as resolved.
Show resolved Hide resolved

const patched = await this.commandPath(id).patch({ data: raw });
return this.add(patched);
}

/**
* Deletes an application command.
* @param {ApplicationCommandResolvable} command The command to delete
* @returns {Promise<?ApplicationCommand>}
*/
async delete(command) {
const id = this.resolveID(command);
if (!id) throw new TypeError('INVALID_TYPE', 'command', 'ApplicationCommandResolvable');

await this.commandPath(id).delete();

const cached = this.cache.get(id);
this.cache.delete(id);
return cached ?? null;
}

/**
* Fetches the permissions for one or multiple commands.
* @param {ApplicationCommandResolvable} [command] The command to get the permissions from
* @returns {Promise<ApplicationCommandPermissions[]|Collection<Snowflake, ApplicationCommandPermissions[]>>}
*/
async fetchPermissions(command) {
if (command) {
const id = this.resolveID(command);
if (!id) throw new TypeError('INVALID_TYPE', 'command', 'ApplicationCommandResolvable');

const data = await this.commandPath(id).permissions.get();
return data.permissions.map(o => this.constructor.transformPermissions(o, true));
}

const commands = await this.commandPath.permissions.get();
return commands.reduce(
(coll, data) =>
coll.set(
data.id,
data.permissions.map(o => this.constructor.transformPermissions(o, true)),
),
new Collection(),
);
}

/**
* Sets the permissions for a command.
* @param {ApplicationCommandResolvable} command The command to edit the permissions for
* @param {ApplicationCommandPermissions[]} permissions The new permissions for the command
* @returns {Promise<?ApplicationCommand>}
*/
async setPermissions(command, permissions) {
vaporoxx marked this conversation as resolved.
Show resolved Hide resolved
const id = this.resolveID(command);
if (!id) throw new TypeError('INVALID_TYPE', 'command', 'ApplicationCommandResolvable');

await this.commandPath(id).permissions.put({
data: { permissions: permissions.map(p => this.constructor.transformPermissions(p)) },
});
return this.cache.get(id) ?? null;
}

/**
* Transforms an {@link ApplicationCommandData} object into something that can be used with the API.
* @param {ApplicationCommandData} command The command to transform
* @returns {Object}
* @private
*/
static transformCommand(command) {
return {
...command,
options: command.options?.map(o => ApplicationCommand.transformOption(o)),
default_permission: command.defaultPermission,
};
}

/**
* Transforms an {@link ApplicationCommandPermissionData} object into something that can be used with the API.
* @param {ApplicationCommandPermissionData} permissions The permissions to transform
* @param {boolean} [received] Whether these permissions have been received from Discord
* @returns {Object}
* @private
*/
static transformPermissions(permissions, received) {
return {
...permissions,
type:
permissions.type === 'number' && !received
? permissions.type
: ApplicationCommandPermissionTypes[permissions.type],
};
}
}

module.exports = ApplicationCommandManager;
21 changes: 21 additions & 0 deletions src/managers/GuildApplicationCommandManager.js
@@ -0,0 +1,21 @@
'use strict';

const ApplicationCommandManager = require('./ApplicationCommandManager');

/**
* An extension for guild-specific application commands.
* @extends {ApplicationCommandManager}
*/
class GuildApplicationCommandManager extends ApplicationCommandManager {
constructor(guild, iterable) {
super(guild.client, iterable);

/**
* The guild that this manager belongs to
* @type {Guild}
*/
this.guild = guild;
}
}

module.exports = GuildApplicationCommandManager;