-
-
Notifications
You must be signed in to change notification settings - Fork 3.9k
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
interactions #5106
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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. | ||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 or perhaps just emit the event on the |
||
* @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; |
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; |
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); | ||
}; |
Original file line number | Diff line number | Diff line change | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -12,6 +12,8 @@ module.exports = (client, { d: data }, shard) => { | |||||||||||
client.users.cache.set(clientUser.id, clientUser); | ||||||||||||
} | ||||||||||||
|
||||||||||||
client.applicationID = data.application.id; | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
|
||||||||||||
for (const guild of data.guilds) { | ||||||||||||
guild.shardID = shard.id; | ||||||||||||
client.guilds.add(guild); | ||||||||||||
|
There was a problem hiding this comment.
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