Skip to content

Commit

Permalink
interactions
Browse files Browse the repository at this point in the history
  • Loading branch information
devsnek committed Mar 25, 2021
1 parent d744e51 commit 5a7b88a
Show file tree
Hide file tree
Showing 22 changed files with 757 additions and 9 deletions.
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.
* @param {undefined} client For internal use.
*/
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;

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) {
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 = {
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,
data: resolved.data,
});
},
};

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

/**
* Emitted when an interaction is created.
* @event Client#interactionCreate
* @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() {
return async (req, res) => {
const timestamp = req.get('x-signature-timestamp');
const signature = req.get('x-signature-ed25519');

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

if (sodium === undefined) {
sodium = require('../util/Sodium');
}

if (
!sodium.methods.verify(
Buffer.from(signature, 'hex'),
Buffer.concat([Buffer.from(timestamp), body]),
this.publicKey,
)
) {
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 },
});
}
}

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;

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

0 comments on commit 5a7b88a

Please sign in to comment.