mirror of
https://github.com/danbulant/discord.js
synced 2026-06-16 13:11:31 +00:00
interactions wip
This commit is contained in:
parent
2685b960d7
commit
c6f87aa32d
15 changed files with 335 additions and 7 deletions
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"root": true,
|
||||
"extends": ["eslint:recommended", "plugin:prettier/recommended"],
|
||||
"plugins": ["import"],
|
||||
"parserOptions": {
|
||||
|
|
|
|||
|
|
@ -110,9 +110,9 @@
|
|||
"src/client/voice/receiver/PacketHandler.js": false,
|
||||
"src/client/voice/receiver/Receiver.js": false,
|
||||
"src/client/voice/util/PlayInterface.js": false,
|
||||
"src/client/voice/util/Secretbox.js": false,
|
||||
"src/client/voice/util/Silence.js": false,
|
||||
"src/client/voice/util/VolumeInterface.js": false
|
||||
"src/client/voice/util/VolumeInterface.js": false,
|
||||
"src/util/Sodium.js": false
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
@ -103,6 +104,24 @@ class Client extends BaseClient {
|
|||
? ShardClientUtil.singleton(this, process.env.SHARDING_MANAGER_MODE)
|
||||
: null;
|
||||
|
||||
/**
|
||||
* The interaction client.
|
||||
* @type {InteractionClient}
|
||||
*/
|
||||
this.interactionClient = new InteractionClient(
|
||||
options,
|
||||
interaction => {
|
||||
/**
|
||||
* Emitted when an interaction is created.
|
||||
* @event Client#interactionCreate
|
||||
* @param {Interaction} interaction The interaction which was created.
|
||||
*/
|
||||
this.client.emit(Events.INTERACTION_CREATE, interaction);
|
||||
return null;
|
||||
},
|
||||
this,
|
||||
);
|
||||
|
||||
/**
|
||||
* All of the {@link User} objects that have been cached at any point, mapped by their IDs
|
||||
* @type {UserManager}
|
||||
|
|
|
|||
131
src/client/InteractionClient.js
Normal file
131
src/client/InteractionClient.js
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
'use strict';
|
||||
|
||||
const BaseClient = require('./BaseClient');
|
||||
const Interaction = require('../structures/Interaction');
|
||||
const { ApplicationCommandOptionType, InteractionType, InteractionResponseType } = require('../util/Constants');
|
||||
|
||||
let sodium;
|
||||
|
||||
/**
|
||||
* Interaction client is used for interactions.
|
||||
*
|
||||
* ```js
|
||||
* const client = new InteractionClient({
|
||||
* token: ABC,
|
||||
* publicKey: XYZ,
|
||||
* }, async (interaction) => {
|
||||
* if (will take a long time) {
|
||||
* doSomethingLong.then((d) => {
|
||||
* interaction.reply({
|
||||
* content: 'wow that took long',
|
||||
* });
|
||||
* });
|
||||
* // return null to signal that we will be replying via `interaction.reply`.
|
||||
* return null;
|
||||
* }
|
||||
* return { content: 'hi!' };
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
class InteractionClient extends BaseClient {
|
||||
/**
|
||||
* @param {Options} options Options for the client.
|
||||
* @param {Handler} handler Handler to handle things.
|
||||
* @param {undefined} client For internal use.
|
||||
*/
|
||||
constructor(options, handler, client) {
|
||||
super(options);
|
||||
this.client = client || this;
|
||||
this.handler = handler;
|
||||
this.publicKey = options.publicKey ? Buffer.from(options.publicKey, 'hex') : undefined;
|
||||
}
|
||||
|
||||
getCommands(guildID) {
|
||||
let path = this.client.api.applications('@me');
|
||||
if (guildID) {
|
||||
path = path.guilds(guildID);
|
||||
}
|
||||
return path.commands.get();
|
||||
}
|
||||
|
||||
createCommand(command, guildID) {
|
||||
let path = this.client.api.applications('@me');
|
||||
if (guildID) {
|
||||
path = path.guilds(guildID);
|
||||
}
|
||||
return path.commands.post({
|
||||
data: {
|
||||
name: command.name,
|
||||
description: command.description,
|
||||
options: command.options.map(function m(o) {
|
||||
return {
|
||||
type: ApplicationCommandOptionType[o.type],
|
||||
name: o.name,
|
||||
description: o.description,
|
||||
default: o.default,
|
||||
required: o.required,
|
||||
choices: o.choices,
|
||||
options: o.options.map(m),
|
||||
};
|
||||
}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
deleteCommand(commandID, guildID) {
|
||||
let path = this.client.api.applications('@me');
|
||||
if (guildID) {
|
||||
path = path.guilds(guildID);
|
||||
}
|
||||
return path.commands(commandID).delete();
|
||||
}
|
||||
|
||||
async handle(data) {
|
||||
switch (data.type) {
|
||||
case InteractionType.PING:
|
||||
return {
|
||||
type: InteractionResponseType.PONG,
|
||||
};
|
||||
case InteractionType.APPLICATION_COMMAND: {
|
||||
try {
|
||||
const interaction = new Interaction(this.client, data);
|
||||
const result = await this.handler(interaction);
|
||||
if (result === null) {
|
||||
return {
|
||||
type: InteractionResponseType.ACKNOWLEDGE,
|
||||
};
|
||||
}
|
||||
// handle result as message resolvable here, probably DRY this branch with code in
|
||||
// `Interaction#reply`, except `Interaction#reply` obviously does a POST and this just
|
||||
// returns the data.
|
||||
throw new Error('fucc');
|
||||
} catch (e) {
|
||||
this.client.emit('error', e);
|
||||
return {
|
||||
type: InteractionResponseType.ACKNOWLEDGE,
|
||||
};
|
||||
}
|
||||
}
|
||||
default:
|
||||
throw new RangeError('Invalid interaction data');
|
||||
}
|
||||
}
|
||||
|
||||
async handleFromHTTP(body, signature) {
|
||||
if (sodium === undefined) {
|
||||
sodium = require('../util/Sodium');
|
||||
}
|
||||
if (!sodium.methods.verify(Buffer.from(signature, 'hex'), Buffer.from(body), this.publicKey)) {
|
||||
throw new Error('Invalid signature');
|
||||
}
|
||||
const data = JSON.parse(body);
|
||||
const result = await this.handle(data);
|
||||
return JSON.stringify(result);
|
||||
}
|
||||
|
||||
async handleFromGateway(data) {
|
||||
await this.handle(data);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = InteractionClient;
|
||||
|
|
@ -37,6 +37,7 @@ class ActionsManager {
|
|||
this.register(require('./GuildIntegrationsUpdate'));
|
||||
this.register(require('./WebhooksUpdate'));
|
||||
this.register(require('./TypingStart'));
|
||||
this.register(require('./InteractionCreate'));
|
||||
}
|
||||
|
||||
register(Action) {
|
||||
|
|
|
|||
15
src/client/actions/InteractionCreate.js
Normal file
15
src/client/actions/InteractionCreate.js
Normal file
|
|
@ -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;
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
const { Writable } = require('stream');
|
||||
const secretbox = require('../util/Secretbox');
|
||||
const secretbox = require('../../../util/Sodium');
|
||||
const Silence = require('../util/Silence');
|
||||
const VolumeInterface = require('../util/VolumeInterface');
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
5
src/client/websocket/handlers/INTERACTION_CREATE.js
Normal file
5
src/client/websocket/handlers/INTERACTION_CREATE.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = (client, packet) => {
|
||||
client.actions.InteractionCreate.handle(packet.d);
|
||||
};
|
||||
|
|
@ -6,6 +6,7 @@ module.exports = {
|
|||
// "Root" classes (starting points)
|
||||
BaseClient: require('./client/BaseClient'),
|
||||
Client: require('./client/Client'),
|
||||
InteractionClient: require('./client/InteractionClient'),
|
||||
Shard: require('./sharding/Shard'),
|
||||
ShardClientUtil: require('./sharding/ShardClientUtil'),
|
||||
ShardingManager: require('./sharding/ShardingManager'),
|
||||
|
|
|
|||
|
|
@ -74,6 +74,16 @@ class APIMessage {
|
|||
return this.target instanceof Message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the target is an interaction
|
||||
* @type {boolean}
|
||||
* @readonly
|
||||
*/
|
||||
get isInteraction() {
|
||||
const Interaction = require('./Interaction');
|
||||
return this.target instanceof Interaction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the content of this message.
|
||||
* @returns {?(string|string[])}
|
||||
|
|
@ -166,6 +176,8 @@ class APIMessage {
|
|||
if (this.isMessage) {
|
||||
// eslint-disable-next-line eqeqeq
|
||||
flags = this.options.flags != null ? new MessageFlags(this.options.flags).bitfield : this.target.flags.bitfield;
|
||||
} else if (this.isInteraction) {
|
||||
flags = this.options.ephemeral ? MessageFlags.EPHEMERAL : undefined;
|
||||
}
|
||||
|
||||
let allowedMentions =
|
||||
|
|
|
|||
112
src/structures/Interaction.js
Normal file
112
src/structures/Interaction.js
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
'use strict';
|
||||
|
||||
const APIMessage = require('./APIMessage');
|
||||
const Base = require('./Base');
|
||||
const { InteractionResponseType } = require('../util/Constants');
|
||||
const Snowflake = require('../util/Snowflake');
|
||||
|
||||
/**
|
||||
* Represents an interaction, see {@link InteractionClient}.
|
||||
* @extends {Base}
|
||||
*/
|
||||
class Interaction extends Base {
|
||||
constructor(client, data) {
|
||||
super(client);
|
||||
this._patch(data);
|
||||
}
|
||||
|
||||
_patch(data) {
|
||||
/**
|
||||
* The ID of this interaction.
|
||||
* @type {Snowflake}
|
||||
*/
|
||||
this.id = data.id;
|
||||
|
||||
/**
|
||||
* The token of this interaction.
|
||||
* @type {string}
|
||||
*/
|
||||
this.token = data.token;
|
||||
|
||||
/**
|
||||
* The ID of the invoked command.
|
||||
* @type {Snowflake}
|
||||
*/
|
||||
this.commandID = data.data.id;
|
||||
|
||||
/**
|
||||
* The name of the invoked command.
|
||||
* @type {string}
|
||||
*/
|
||||
this.commandName = data.data.name;
|
||||
|
||||
/**
|
||||
* The options passed to the command.
|
||||
* @type {Object}
|
||||
*/
|
||||
this.options = data.data.options;
|
||||
|
||||
/**
|
||||
* The channel this interaction was sent in.
|
||||
* @type {Channel}
|
||||
*/
|
||||
this.channel = this.client.channels.cache.get(data.channel_id);
|
||||
|
||||
/**
|
||||
* The guild this interaction was sent in, if any.
|
||||
* @type {?Guild}
|
||||
*/
|
||||
this.guild = data.guild_id ? this.client.guilds.cache.get(data.guild_id) : null;
|
||||
|
||||
/**
|
||||
* If this interaction was sent in a guild, the member which sent it.
|
||||
* @type {?Member}
|
||||
*/
|
||||
this.member = data.member ? this.guild.members.add(data.member, false) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The timestamp the emoji was created at, or null if unicode
|
||||
* @type {?number}
|
||||
* @readonly
|
||||
*/
|
||||
get createdTimestamp() {
|
||||
if (!this.id) return null;
|
||||
return Snowflake.deconstruct(this.id).timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* The time the emoji was created at, or null if unicode
|
||||
* @type {?Date}
|
||||
* @readonly
|
||||
*/
|
||||
get createdAt() {
|
||||
if (!this.id) return null;
|
||||
return new Date(this.createdTimestamp);
|
||||
}
|
||||
|
||||
async reply(content, options) {
|
||||
let apiMessage;
|
||||
|
||||
if (content instanceof APIMessage) {
|
||||
apiMessage = content.resolveData();
|
||||
} else {
|
||||
apiMessage = APIMessage.create(this, content, options).resolveData();
|
||||
if (Array.isArray(apiMessage.data.content)) {
|
||||
throw new Error();
|
||||
}
|
||||
}
|
||||
|
||||
const { data, files } = await apiMessage.resolveFiles();
|
||||
|
||||
return this.client.api.interactions(this.id, this.token).callback.post({
|
||||
data: {
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data,
|
||||
},
|
||||
files,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Interaction;
|
||||
|
|
@ -77,14 +77,14 @@ exports.DefaultOptions = {
|
|||
/**
|
||||
* HTTP options
|
||||
* @typedef {Object} HTTPOptions
|
||||
* @property {number} [version=7] API version to use
|
||||
* @property {number} [version=8] API version to use
|
||||
* @property {string} [api='https://discord.com/api'] Base url of the API
|
||||
* @property {string} [cdn='https://cdn.discordapp.com'] Base url of the CDN
|
||||
* @property {string} [invite='https://discord.gg'] Base url of invites
|
||||
* @property {string} [template='https://discord.new'] Base url of templates
|
||||
*/
|
||||
http: {
|
||||
version: 7,
|
||||
version: 8,
|
||||
api: 'https://discord.com/api',
|
||||
cdn: 'https://cdn.discordapp.com',
|
||||
invite: 'https://discord.gg',
|
||||
|
|
@ -282,6 +282,7 @@ exports.Events = {
|
|||
SHARD_READY: 'shardReady',
|
||||
SHARD_RESUME: 'shardResume',
|
||||
INVALIDATED: 'invalidated',
|
||||
INTERACTION_CREATE: 'interactionCreate',
|
||||
RAW: 'raw',
|
||||
};
|
||||
|
||||
|
|
@ -345,6 +346,7 @@ exports.PartialTypes = keyMirror(['USER', 'CHANNEL', 'GUILD_MEMBER', 'MESSAGE',
|
|||
* * VOICE_STATE_UPDATE
|
||||
* * VOICE_SERVER_UPDATE
|
||||
* * WEBHOOKS_UPDATE
|
||||
* * INTERACTION_CREATE
|
||||
* @typedef {string} WSEventType
|
||||
*/
|
||||
exports.WSEvents = keyMirror([
|
||||
|
|
@ -384,6 +386,7 @@ exports.WSEvents = keyMirror([
|
|||
'VOICE_STATE_UPDATE',
|
||||
'VOICE_SERVER_UPDATE',
|
||||
'WEBHOOKS_UPDATE',
|
||||
'INTERACTION_CREATE',
|
||||
]);
|
||||
|
||||
/**
|
||||
|
|
@ -676,6 +679,29 @@ exports.WebhookTypes = [
|
|||
'Channel Follower',
|
||||
];
|
||||
|
||||
exports.ApplicationCommandOptionType = {
|
||||
SUB_COMMAND: 1,
|
||||
SUB_COMMAND_GROUP: 2,
|
||||
STRING: 3,
|
||||
INTEGER: 4,
|
||||
BOOLEAN: 5,
|
||||
USER: 6,
|
||||
CHANNEL: 7,
|
||||
ROLE: 8,
|
||||
};
|
||||
|
||||
exports.InteractionType = {
|
||||
PING: 1,
|
||||
APPLICATION_COMMAND: 2,
|
||||
};
|
||||
|
||||
exports.InteractionResponseType = {
|
||||
PONG: 1,
|
||||
ACKNOWLEDGE: 2,
|
||||
CHANNEL_MESSAGE: 3,
|
||||
CHANNEL_MESSAGE_WITH_SOURCE: 4,
|
||||
};
|
||||
|
||||
function keyMirror(arr) {
|
||||
let tmp = Object.create(null);
|
||||
for (const value of arr) tmp[value] = value;
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ class MessageFlags extends BitField {}
|
|||
* * `SUPPRESS_EMBEDS`
|
||||
* * `SOURCE_MESSAGE_DELETED`
|
||||
* * `URGENT`
|
||||
* * `EPHEMERAL`
|
||||
* @type {Object}
|
||||
* @see {@link https://discord.com/developers/docs/resources/channel#message-object-message-flags}
|
||||
*/
|
||||
|
|
@ -31,6 +32,7 @@ MessageFlags.FLAGS = {
|
|||
SUPPRESS_EMBEDS: 1 << 2,
|
||||
SOURCE_MESSAGE_DELETED: 1 << 3,
|
||||
URGENT: 1 << 4,
|
||||
EPHEMERAL: 1 << 6,
|
||||
};
|
||||
|
||||
module.exports = MessageFlags;
|
||||
|
|
|
|||
|
|
@ -5,16 +5,19 @@ const libs = {
|
|||
open: sodium.api.crypto_secretbox_open_easy,
|
||||
close: sodium.api.crypto_secretbox_easy,
|
||||
random: n => sodium.randombytes_buf(n),
|
||||
verify: sodium.api.crypto_sign_verify_detached,
|
||||
}),
|
||||
'libsodium-wrappers': sodium => ({
|
||||
open: sodium.crypto_secretbox_open_easy,
|
||||
close: sodium.crypto_secretbox_easy,
|
||||
random: n => sodium.randombytes_buf(n),
|
||||
verify: sodium.crypto_sign_verify_detached,
|
||||
}),
|
||||
tweetnacl: tweetnacl => ({
|
||||
open: tweetnacl.secretbox.open,
|
||||
close: tweetnacl.secretbox,
|
||||
random: n => tweetnacl.randomBytes(n),
|
||||
verify: (s, d, p) => tweetnacl.sign.detached.verify(d, s, p),
|
||||
}),
|
||||
};
|
||||
|
||||
Loading…
Reference in a new issue