Skip to content

Instantly share code, notes, and snippets.

@sudojunior
Created July 19, 2021 15:22
Show Gist options
  • Save sudojunior/00bb89b85a9f6c7f2a10b9882f95bbeb to your computer and use it in GitHub Desktop.
Save sudojunior/00bb89b85a9f6c7f2a10b9882f95bbeb to your computer and use it in GitHub Desktop.
(slash-create) Demonstrating possible use for ephemeral messages through 'whispers'. Understandably, DMs are the default and better alternative - but it was worth exploring.
const { Client } = require("discord.js");
const { GatewayServer, SlashCreator, Command, CommandOptionType, InteractionResponseFlags } = require("slash-create");
const { Endpoints } = require("slash-create/lib/constants");
const client = new Client();
const creator = new SlashCreator({
applicationID: "APP_ID",
publicKey: "PUBLIC_KEY",
token: "BOT_TOKEN"
});
creator.withServer(new GatewayServer(
(handler) => client.ws.on('INTERACTION_CREATE', handler)
));
creator.on('commandRun', async (command, promise, context) => {
await promise;
whispers.set(context.user.id, {
...(whispers.get(context.user.id) || {}),
context
});
});
client.on("debug", (msg) => console.log("[CLIENT]", msg));
/**
* @typedef {Object} WhisperContext
* @property {import("slash-create").CommandContext} context
* @property {string} lastWhisperFrom
*/
/**
* @type {Map<string, WhisperContext>}
*/
const whispers = new Map();
class UnknownCommand extends Command {
constructor(creator) {
super(creator, {
unknown: true,
});
}
/**
* @param {import("slash-create").CommandContext} ctx
* @return {Promise<void>}
*/
async run(ctx) {
ctx.send(`I was not able to find '${ctx.commandName}'`);
}
}
class GreetingCommand extends Command {
constructor(creator) {
super(creator, {
name: "hello",
description: "Salutations!",
options: [{
type: CommandOptionType.STRING,
name: "food",
description: "What's your favorite food?"
}]
});
}
/**
* @param {import("slash-create").CommandContext} ctx
*/
async run(ctx) {
const { food } = ctx.options;
const message = `Hello, ${ctx.user.username}! ${food ? `I too like ${food}!` : ''}`;
await ctx.send(message, {ephemeral:true});
}
}
class WhisperCommand extends Command {
constructor(creator) {
super(creator, {
name: "whisper",
description: "Whisper to someone",
options: [{
type: CommandOptionType.USER,
name: "to",
description: "Who should I whisper to?",
required: true
}, {
type: CommandOptionType.STRING,
name: "message",
description: "What should I whisper?",
required: true
}],
deferEphemeral: true,
});
}
/**
* @param {import("slash-create").CommandContext} ctx
* @return {Promise<void>}
*/
async run(ctx) {
const { to, message } = ctx.options;
const whisper = whispers.get(to);
if(to === ctx.user.id) {
await ctx.send(`You can't whisper to yourself, ${ctx.user.username}!`, {ephemeral:true});
return;
}
if (whisper === undefined) {
const message = `I was not able to find '${to}'.`;
await ctx.send(message, {ephemeral:true});
return;
}
const { context } = whisper;
try {
const msg = `**${ctx.user.username}:** ${message}`;
await creator.requestHandler.request(
"POST", Endpoints.FOLLOWUP_MESSAGE(creator.options.applicationID, context.interactionToken), true,
{
content: msg,
flags: InteractionResponseFlags.EPHEMERAL
});
await ctx.send(`Whisper sent to **${context.user.username}**!`, {ephemeral:true});
whisper.lastWhisperFrom = ctx.user.id;
whispers.set(context.user.id, whisper);
} catch (err) {
await ctx.send(`Something went wrong: ${err.message}`);
}
}
}
class ReplyCommand extends Command {
constructor(creator) {
super(creator, {
name: "reply",
description: "Reply to a message",
options: [{
type: CommandOptionType.STRING,
name: "message",
description: "What should I reply with?",
required: true
}],
deferEphemeral: true,
});
}
/**
* @param {import("slash-create").CommandContext} ctx
* @return {Promise<void>}
*/
async run(ctx) {
const currentUserWhisper = whispers.get(ctx.user.id);
if (currentUserWhisper === undefined) {
const message = `I was not able to find your last whisper.`;
await ctx.send(message);
return;
}
const { lastWhisperFrom } = currentUserWhisper;
const whisper = whispers.get(lastWhisperFrom);
if (whisper === undefined) {
const message = `I was not able to find '${lastWhisperFrom}'.`;
await ctx.send(message, {ephemeral:true});
return;
}
if(whisper.context.expired) {
const message = `I was not able to find '${lastWhisperFrom}'.`;
await ctx.send(message, {ephemeral:true});
return;
}
try {
const { context } = whisper;
const message = `**${ctx.user.username}:** ${ctx.options.message}`;
await creator.requestHandler.request(
"POST", Endpoints.FOLLOWUP_MESSAGE(creator.options.applicationID, context.interactionToken), true,
{
content: message,
flags: InteractionResponseFlags.EPHEMERAL
});
await ctx.send(`Reply sent to ${lastWhisperFrom}`, {ephemeral:true});
whisper.lastWhisperFrom = ctx.user.id;
whispers.set(context.user.id, whisper);
} catch (e) {
await ctx.send(`Something went wrong: ${e.message}`);
}
}
}
creator
.on("debug", console.log.bind(console, "[SLASH]"))
.registerCommands([
UnknownCommand,
GreetingCommand,
WhisperCommand,
ReplyCommand
])
.syncCommands();
client.login(creator.options.token);

In order to be able to send a 'whisper' to another user through slash commands, a few conditions must be met.

  1. The recipient of the message must have used a slash command.
  2. This slash command must within 15 minutes (or for those that don't understand, before the last interaction token EXPIRES).
  3. While for this example it may not be the case, it should be limited per guild to prevent unncessary spam.
Client A (sudojunior) Client B (Sans)

As you can see there are some problems with this...

  • The context of the last interaction remains visible (regardless of the selected source, Discord shows the context of the command you last ran).
  • As mentioned above, this could in theory be used across guild entities which might be considered as a security risk.
  • As with the given example, it is not perfect code - but it is based on the concept of what users have wanted as in channel whispers / DMs.

If you run this code, I would encourage you to modify it so that it's cache is consistent - but also to heed caution when applying this methodology.

I should also give my credit to CoPilot, for writing the initial WhisperCommand class - while it didn't understand the context of how to handle and respond to the request, it was very accurate at figuring out the description, options and intended flow of the response (even if it was giving throw new Error as a way for a middleware to catch it).

@sudojunior
Copy link
Author

ctx.sendFollowUp(msg, opts) now supports the ephemeral parameter which was added in 10ba62d from Snazzah/slash-create#83 and since added in v3.4.0.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment