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

interactions #5106

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"root": true,
"extends": ["eslint:recommended", "plugin:prettier/recommended"],
"plugins": ["import"],
"parserOptions": {
Expand Down
4 changes: 4 additions & 0 deletions esm/discord.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export default Discord;
export const {
BaseClient,
Client,
InteractionClient,
Shard,
ShardClientUtil,
ShardingManager,
Expand Down Expand Up @@ -49,6 +50,7 @@ export const {
resolveString,
splitMessage,
Application,
ApplicationCommand,
Base,
Activity,
APIMessage,
Expand All @@ -58,6 +60,7 @@ export const {
ClientApplication,
ClientUser,
Collector,
CommandInteraction,
DMChannel,
Emoji,
Guild,
Expand All @@ -68,6 +71,7 @@ export const {
GuildPreview,
GuildTemplate,
Integration,
Interaction,
Invite,
Message,
MessageAttachment,
Expand Down
13 changes: 13 additions & 0 deletions src/client/Client.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

const BaseClient = require('./BaseClient');
const InteractionClient = require('./InteractionClient');
const ActionsManager = require('./actions/ActionsManager');
const ClientVoiceManager = require('./voice/ClientVoiceManager');
const WebSocketManager = require('./websocket/WebSocketManager');
Expand Down Expand Up @@ -102,6 +103,12 @@ class Client extends BaseClient {
? ShardClientUtil.singleton(this, process.env.SHARDING_MANAGER_MODE)
: null;

/**
* The interaction client.
* @type {InteractionClient}
*/
this.interactionClient = new InteractionClient(options, this);

/**
* All of the {@link User} objects that have been cached at any point, mapped by their IDs
* @type {UserManager}
Expand Down Expand Up @@ -151,6 +158,12 @@ class Client extends BaseClient {
*/
this.user = null;

/**
* Application that this client is associated with.
* @type {?Snowflake}
*/
this.applicationID = null;

/**
* Time at which the client was last regarded as being in the `READY` state
* (each time the client disconnects and successfully reconnects, this will be overwritten)
Expand Down
221 changes: 221 additions & 0 deletions src/client/InteractionClient.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
'use strict';

const BaseClient = require('./BaseClient');
const ApplicationCommand = require('../structures/ApplicationCommand');
const CommandInteraction = require('../structures/CommandInteraction');
const { Events, ApplicationCommandOptionType, InteractionType, InteractionResponseType } = require('../util/Constants');
const MessageFlags = require('../util/MessageFlags');
let sodium;

function transformCommand(command) {
return {
...command,
options: command.options.map(function m(o) {
return {
...o,
type: ApplicationCommandOptionType[o.type],
options: o.options?.map(m),
};
}),
};
}

/**
* Interaction client is used for interactions.
*
* @example
* const client = new InteractionClient({
* token: ABC,
* publicKey: XYZ,
* });
*
* client.on('interactionCreate', (interaction) => {
* // automatically handles long responses
* if (will take a long time) {
* interaction.defer();
* doSomethingLong.then((d) => {
* interaction.webhook.send({
* content: `fancy data: ${d}`,
* });
* });
* } else {
* interaction.reply('hi!');
* }
* });
* ```
*/
class InteractionClient extends BaseClient {
/**
* @param {Options} options Options for the client.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this Options type doesn't exist, would probably be good to give it a better name too

* @param {undefined} client For internal use.
devsnek marked this conversation as resolved.
Show resolved Hide resolved
devsnek marked this conversation as resolved.
Show resolved Hide resolved
*/
constructor(options, client) {
super(options);

Object.defineProperty(this, 'token', {
value: options.token,
writable: true,
});

if (client) {
this.client = client;
} else {
this.client = this;
this.interactionClient = this;
devsnek marked this conversation as resolved.
Show resolved Hide resolved

Object.defineProperty(this, 'applicationID', {
value: options.clientID,
writable: true,
});

Object.defineProperty(this, 'publicKey', {
value: options.publicKey ? Buffer.from(options.publicKey, 'hex') : undefined,
writable: true,
});
}
}

/**
* Fetch registered slash commands.
* @param {Snowflake} [guildID] Optional guild ID.
* @returns {ApplicationCommand[]}
*/
async fetchCommands(guildID) {
let path = this.client.api.applications(this.client.applicationID);
if (guildID) {
path = path.guilds(guildID);
}
const commands = await path.commands.get();
return commands.map(c => new ApplicationCommand(this, c, guildID));
}

/**
* Set all the commands for the application or guild.
* @param {Object[]} commands The command descriptor.
* @param {Snowflake} [guildID] Optional guild ID.
* @returns {ApplicationCommand[]} The commands.
*/
async setCommands(commands, guildID) {
devsnek marked this conversation as resolved.
Show resolved Hide resolved
let path = this.client.api.applications(this.client.applicationID);
if (guildID) {
path = path.guilds(guildID);
}
const cs = await path.commands.put({
data: commands.map(transformCommand),
});
return cs.map(c => new ApplicationCommand(this, c, guildID));
}

/**
* Create a command.
* @param {Object} command The command descriptor.
* @param {Snowflake} [guildID] Optional guild ID.
* @returns {ApplicationCommand} The created command.
*/
async createCommand(command, guildID) {
let path = this.client.api.applications(this.client.applicationID);
if (guildID) {
path = path.guilds(guildID);
}
const c = await path.commands.post({
data: transformCommand(command),
});
return new ApplicationCommand(this, c, guildID);
}

async handle(data) {
switch (data.type) {
case InteractionType.PING:
return {
type: InteractionResponseType.PONG,
};
case InteractionType.APPLICATION_COMMAND: {
let resolve;
const directPromise = new Promise(r => {
resolve = r;
});

const syncHandle = {
devsnek marked this conversation as resolved.
Show resolved Hide resolved
defer(ephemeral) {
resolve({
type: InteractionResponseType.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE,
data: {
flags: ephemeral ? MessageFlags.FLAGS.EPHEMERAL : MessageFlags.FLAGS.NONE,
},
});
},
reply(resolved) {
resolve({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
devsnek marked this conversation as resolved.
Show resolved Hide resolved
data: resolved.data,
});
},
};

const interaction = new CommandInteraction(this.client, data, syncHandle);

/**
* Emitted when an interaction is created.
* @event Client#interactionCreate
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there should also be a docstring to indicate this event is emitted on InteractionClient

or perhaps just emit the event on the InteractionClient only

* @param {Interaction} interaction The interaction which was created.
*/
this.client.emit(Events.INTERACTION_CREATE, interaction);

const r = await directPromise;
return r;
}
default:
this.client.emit('debug', `[INTERACTION] unknown type ${data.type}`);
return undefined;
}
}

/**
* An express-like middleware factory which can be used
* with webhook interactions.
* @returns {Function} The middleware function.
*/
middleware() {
devsnek marked this conversation as resolved.
Show resolved Hide resolved
return async (req, res) => {
const timestamp = req.get('x-signature-timestamp');
const signature = req.get('x-signature-ed25519');
devsnek marked this conversation as resolved.
Show resolved Hide resolved

const chunks = [];
for await (const chunk of req) {
chunks.push(chunk);
}
const body = Buffer.concat(chunks);

if (sodium === undefined) {
devsnek marked this conversation as resolved.
Show resolved Hide resolved
sodium = require('../util/Sodium');
}

if (
!sodium.methods.verify(
Buffer.from(signature, 'hex'),
Buffer.concat([Buffer.from(timestamp), body]),
this.publicKey,
)
devsnek marked this conversation as resolved.
Show resolved Hide resolved
) {
res.status(401).end();
return;
}

const data = JSON.parse(body.toString());

const result = await this.handle(data);
res.status(200).end(JSON.stringify(result));
};
}

async handleFromGateway(data) {
const result = await this.handle(data);

await this.client.api.interactions(data.id, data.token).callback.post({
data: result,
query: { wait: true },
devsnek marked this conversation as resolved.
Show resolved Hide resolved
});
}
}

module.exports = InteractionClient;
1 change: 1 addition & 0 deletions src/client/actions/ActionsManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class ActionsManager {
this.register(require('./GuildIntegrationsUpdate'));
this.register(require('./WebhooksUpdate'));
this.register(require('./TypingStart'));
this.register(require('./InteractionCreate'));
}

register(Action) {
Expand Down
15 changes: 15 additions & 0 deletions src/client/actions/InteractionCreate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
'use strict';

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

class InteractionCreateAction extends Action {
handle(data) {
this.client.interactionClient.handleFromGateway(data).catch(e => {
this.client.emit('error', e);
});

return {};
}
}

module.exports = InteractionCreateAction;
10 changes: 5 additions & 5 deletions src/client/voice/dispatcher/StreamDispatcher.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use strict';

const { Writable } = require('stream');
const secretbox = require('../util/Secretbox');
const sodium = require('../../../util/Sodium');
const Silence = require('../util/Silence');
const VolumeInterface = require('../util/VolumeInterface');

Expand Down Expand Up @@ -259,12 +259,12 @@ class StreamDispatcher extends Writable {
this._nonce++;
if (this._nonce > MAX_NONCE_SIZE) this._nonce = 0;
this._nonceBuffer.writeUInt32BE(this._nonce, 0);
return [secretbox.methods.close(buffer, this._nonceBuffer, secret_key), this._nonceBuffer.slice(0, 4)];
return [sodium.methods.close(buffer, this._nonceBuffer, secret_key), this._nonceBuffer.slice(0, 4)];
} else if (mode === 'xsalsa20_poly1305_suffix') {
const random = secretbox.methods.random(24);
return [secretbox.methods.close(buffer, random, secret_key), random];
const random = sodium.methods.random(24);
return [sodium.methods.close(buffer, random, secret_key), random];
} else {
return [secretbox.methods.close(buffer, nonce, secret_key)];
return [sodium.methods.close(buffer, nonce, secret_key)];
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/client/voice/receiver/PacketHandler.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
'use strict';

const EventEmitter = require('events');
const sodium = require('../../../util/Sodium');
const Speaking = require('../../../util/Speaking');
const secretbox = require('../util/Secretbox');
const { SILENCE_FRAME } = require('../util/Silence');

// The delay between packets when a user is considered to have stopped speaking
Expand Down Expand Up @@ -58,7 +58,7 @@ class PacketHandler extends EventEmitter {
}

// Open packet
let packet = secretbox.methods.open(buffer.slice(12, end), this.nonce, secret_key);
let packet = sodium.methods.open(buffer.slice(12, end), this.nonce, secret_key);
if (!packet) return new Error('Failed to decrypt voice packet');
packet = Buffer.from(packet);

Expand Down
5 changes: 5 additions & 0 deletions src/client/websocket/handlers/INTERACTION_CREATE.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use strict';

module.exports = (client, packet) => {
client.actions.InteractionCreate.handle(packet.d);
};
2 changes: 2 additions & 0 deletions src/client/websocket/handlers/READY.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ module.exports = (client, { d: data }, shard) => {
client.users.cache.set(clientUser.id, clientUser);
}

client.applicationID = data.application.id;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
client.applicationID = data.application.id;
client.applicationID = data.application.id;
client.interactionClient.applicationID = data.application.id;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
client.applicationID = data.application.id;
client.applicationID = client.interactionClient.applicationID = data.application.id;


for (const guild of data.guilds) {
guild.shardID = shard.id;
client.guilds.add(guild);
Expand Down