interaction command support
This commit is contained in:
parent
73590257c8
commit
6e85237cab
6 changed files with 2376 additions and 1743 deletions
3770
pnpm-lock.yaml
3770
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
65
src/index.js
65
src/index.js
|
@ -7,9 +7,14 @@ const {instead, before} = require("spitroast");
|
|||
|
||||
const config = require("../config.json");
|
||||
const apikeys = require("../apikeys.json");
|
||||
const Command = require("./lib/command.js");
|
||||
|
||||
const events = require("./lib/events.js");
|
||||
const {formatUsername} = require("./lib/utils.js");
|
||||
const timer = require("./lib/timer.js");
|
||||
const Command = require("./lib/command.js");
|
||||
const CommandDispatcher = require("./lib/commandDispatcher.js");
|
||||
const InteractionCommand = require("./lib/interactionCommand.js");
|
||||
const {InteractionDispatcher} = require("./lib/interactionDispatcher.js");
|
||||
|
||||
const bot = new Dysnomia.Client(config.token, {
|
||||
defaultImageFormat: "png",
|
||||
|
@ -21,6 +26,7 @@ const bot = new Dysnomia.Client(config.token, {
|
|||
});
|
||||
|
||||
const commands = new Dysnomia.Collection();
|
||||
const interactionCommands = new Dysnomia.Collection();
|
||||
|
||||
const database = new sqlite3.Database(resolve(__dirname, "..", "database.db"));
|
||||
|
||||
|
@ -34,6 +40,9 @@ function registerCommand(cmdObj) {
|
|||
aliases.length > 0 ? ` (aliases: ${aliases.join(", ")})` : ""
|
||||
}`
|
||||
);
|
||||
} else if (cmdObj instanceof InteractionCommand) {
|
||||
interactionCommands.set(cmdObj.name, cmdObj);
|
||||
logger.info("hf:cmd", `Registered interaction command '${cmdObj.name}'`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -42,15 +51,13 @@ global.hf = {
|
|||
config,
|
||||
apikeys,
|
||||
commands,
|
||||
interactionCommands,
|
||||
registerCommand,
|
||||
events,
|
||||
timer,
|
||||
database,
|
||||
};
|
||||
|
||||
const CommandDispatcher = require("./lib/commandDispatcher.js");
|
||||
const {formatUsername} = require("./lib/utils.js");
|
||||
|
||||
for (const file of fs.readdirSync(resolve(__dirname, "modules"))) {
|
||||
require(resolve(__dirname, "modules", file));
|
||||
logger.info("hf:modules", `Loaded module: '${file}'`);
|
||||
|
@ -80,7 +87,7 @@ bot.on("messageCreate", async (msg) => {
|
|||
);
|
||||
}
|
||||
});
|
||||
bot.on("messageUpdate", (msg, oldMsg) => {
|
||||
bot.on("messageUpdate", async (msg, oldMsg) => {
|
||||
try {
|
||||
const oneDay = Date.now() - 86400000;
|
||||
if (
|
||||
|
@ -89,7 +96,7 @@ bot.on("messageUpdate", (msg, oldMsg) => {
|
|||
oldMsg &&
|
||||
oldMsg.content !== msg.content
|
||||
) {
|
||||
CommandDispatcher(msg);
|
||||
await CommandDispatcher(msg);
|
||||
}
|
||||
} catch (err) {
|
||||
const stack = (err?.stack ?? err.message).split("\n");
|
||||
|
@ -135,6 +142,31 @@ bot.on("messageReactionAdd", async (msg, reaction, reactor) => {
|
|||
);
|
||||
}
|
||||
});
|
||||
bot.on("interactionCreate", async (interaction) => {
|
||||
try {
|
||||
if (!(interaction.channel instanceof Dysnomia.Channel)) {
|
||||
const newChannel = hf.bot.getChannel(interaction.channel.id);
|
||||
if (newChannel) {
|
||||
interaction.channel = newChannel;
|
||||
} else {
|
||||
interaction.channel = await hf.bot.getRESTChannel(
|
||||
interaction.channel.id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await InteractionDispatcher(interaction);
|
||||
} catch (err) {
|
||||
const stack = (err?.stack ?? err.message).split("\n");
|
||||
const error = stack.shift();
|
||||
logger.error(
|
||||
"hf:main",
|
||||
`Failed to dispatch interaction command: ${error}\n\t${stack.join(
|
||||
"\n\t"
|
||||
)}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
bot.once("ready", async () => {
|
||||
logger.info("hf:main", "Connected to Discord.");
|
||||
|
@ -152,6 +184,27 @@ bot.once("ready", async () => {
|
|||
bot.on("ready", () => {
|
||||
logger.info("hf:main", "Reconnected to Discord.");
|
||||
});
|
||||
|
||||
const commands = await bot.getCommands();
|
||||
for (const command of interactionCommands.values()) {
|
||||
const hasCommand = commands.find((c) => c.name == command.name);
|
||||
if (hasCommand) continue;
|
||||
|
||||
await bot.createCommand({
|
||||
name: command.name,
|
||||
type: command.type,
|
||||
description: command.helpText,
|
||||
options: Object.values(command.options),
|
||||
defaultMemberPermissions: command.permissions,
|
||||
dmPermission: !command.guildOnly,
|
||||
});
|
||||
}
|
||||
|
||||
for (const command of commands) {
|
||||
if (interactionCommands.has(command.name)) continue;
|
||||
|
||||
await bot.deleteCommand(command.id);
|
||||
}
|
||||
});
|
||||
|
||||
bot.on("error", (err) => {
|
||||
|
|
26
src/lib/interactionCommand.js
Normal file
26
src/lib/interactionCommand.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
const {ApplicationCommandTypes, ApplicationCommandOptionTypes} =
|
||||
require("@projectdysnomia/dysnomia").Constants;
|
||||
|
||||
class InteractionCommand {
|
||||
constructor(name) {
|
||||
this.name = name;
|
||||
this.type = ApplicationCommandTypes.CHAT_INPUT;
|
||||
this.helpText = "No description provided.";
|
||||
this.guildOnly = false;
|
||||
this.options = {
|
||||
send: {
|
||||
type: ApplicationCommandOptionTypes.BOOLEAN,
|
||||
name: "send",
|
||||
description: "Should the output be ephemeral or not",
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
callback() {
|
||||
return "Callback not overwritten.";
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = InteractionCommand;
|
127
src/lib/interactionDispatcher.js
Normal file
127
src/lib/interactionDispatcher.js
Normal file
|
@ -0,0 +1,127 @@
|
|||
const logger = require("./logger.js");
|
||||
const {MessageFlags} = require("@projectdysnomia/dysnomia").Constants;
|
||||
const {pastelize, getTopColor} = require("./utils.js");
|
||||
|
||||
function getOption(interaction, command, key) {
|
||||
return (
|
||||
interaction.data.options?.find((o) => o.name === key)?.value ??
|
||||
command.options[key].default
|
||||
);
|
||||
}
|
||||
|
||||
async function runCommand(interaction, command) {
|
||||
if (command.ownerOnly && interaction.user.id != hf.config.owner_id) {
|
||||
return {
|
||||
content: "No\n\nSent from my iPhone.",
|
||||
flags: MessageFlags.EPHEMERAL,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
command.elevatedOnly &&
|
||||
!hf.config.elevated.includes(interaction.user.id)
|
||||
) {
|
||||
return {
|
||||
content: "No\n\nSent from my iPhone.",
|
||||
flags: MessageFlags.EPHEMERAL,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const ret = command.callback(interaction);
|
||||
if (ret instanceof Promise) {
|
||||
return await ret;
|
||||
} else {
|
||||
return ret;
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error("hf:cmd:" + command.name, err + "\n" + err.stack);
|
||||
return {
|
||||
content: ":warning: An internal error occurred.",
|
||||
flags: MessageFlags.EPHEMERAL,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function InteractionDispatcher(interaction) {
|
||||
const command = hf.interactionCommands.get(interaction.data.name);
|
||||
const shouldSend = getOption(interaction, command, "send");
|
||||
|
||||
try {
|
||||
const response = await runCommand(interaction, command);
|
||||
if (response != null) {
|
||||
if (response.file) {
|
||||
const newFile = response.file;
|
||||
delete response.file;
|
||||
if (newFile.contents) {
|
||||
newFile.file = newFile.contents;
|
||||
delete newFile.contents;
|
||||
}
|
||||
if (newFile.name) {
|
||||
newFile.filename = newFile.name;
|
||||
delete newFile.name;
|
||||
}
|
||||
const files = response.attachments ?? [];
|
||||
files.push(newFile);
|
||||
response.attachments = files;
|
||||
}
|
||||
if (response.files) {
|
||||
response.attachments = response.files;
|
||||
delete response.files;
|
||||
for (const attachment of response.attachments) {
|
||||
if (attachment.contents) {
|
||||
attachment.file = attachment.contents;
|
||||
delete attachment.contents;
|
||||
}
|
||||
if (attachment.name) {
|
||||
attachment.filename = attachment.name;
|
||||
delete attachment.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (response.embed) {
|
||||
response.embeds = [...(response.embeds ?? []), response.embed];
|
||||
delete response.embed;
|
||||
}
|
||||
if (response.embeds) {
|
||||
for (const embed of response.embeds) {
|
||||
embed.color =
|
||||
embed.color ??
|
||||
getTopColor(interaction, hf.bot.user.id, pastelize(hf.bot.user.id));
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await interaction.createMessage(
|
||||
Object.assign(
|
||||
typeof response === "string" ? {content: response} : response,
|
||||
{
|
||||
flags: shouldSend ? response.flags : MessageFlags.EPHEMERAL,
|
||||
allowedMentions: {
|
||||
repliedUser: false,
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
} catch (err) {
|
||||
await interaction.createMessage({
|
||||
content: `:warning: An error has occurred:\n\`\`\`${err}\`\`\``,
|
||||
flags: MessageFlags.EPHEMERAL,
|
||||
allowedMentions: {
|
||||
repliedUser: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
await interaction.createMessage({
|
||||
content: `:warning: An error has occurred:\n\`\`\`${err}\`\`\``,
|
||||
flags: MessageFlags.EPHEMERAL,
|
||||
allowedMentions: {
|
||||
repliedUser: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {InteractionDispatcher, getOption};
|
|
@ -28,7 +28,7 @@ async function processFile(link, spoiler = false) {
|
|||
const file = await res.text();
|
||||
const lines = file.replace(/\r/g, "").split("\n");
|
||||
|
||||
const fileName = link.substring(
|
||||
const fileName = decodeURI(link).substring(
|
||||
link.lastIndexOf("/") + 1,
|
||||
link.indexOf("#") == -1 ? link.length : link.indexOf("#")
|
||||
);
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
const {MessageFlags} = require("@projectdysnomia/dysnomia").Constants;
|
||||
const {MessageFlags, ApplicationCommandOptionTypes} =
|
||||
require("@projectdysnomia/dysnomia").Constants;
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const httpSignature = require("@peertube/http-signature");
|
||||
|
@ -7,6 +8,8 @@ const events = require("../lib/events.js");
|
|||
const logger = require("../lib/logger.js");
|
||||
const {hasFlag} = require("../lib/guildSettings.js");
|
||||
const {parseHtmlEntities, getUploadLimit} = require("../lib/utils.js");
|
||||
const InteractionCommand = require("../lib/interactionCommand.js");
|
||||
const {getOption} = require("../lib/interactionDispatcher.js");
|
||||
|
||||
const FRIENDLY_USERAGENT =
|
||||
"HiddenPhox/fedimbed (https://gitdab.com/Cynosphere/HiddenPhox)";
|
||||
|
@ -157,7 +160,7 @@ async function processUrl(msg, url, spoiler = false) {
|
|||
invalidUrl = true;
|
||||
}
|
||||
|
||||
if (invalidUrl) return;
|
||||
if (invalidUrl) return {};
|
||||
|
||||
// some lemmy instances have old reddit frontend subdomains
|
||||
// but these frontends are just frontends and dont actually expose the API
|
||||
|
@ -661,7 +664,7 @@ async function processUrl(msg, url, spoiler = false) {
|
|||
"fedimbed",
|
||||
`Bailing trying to re-embed "${url}": Failed to get author.`
|
||||
);
|
||||
return;
|
||||
return {};
|
||||
}
|
||||
|
||||
// Start constructing embed
|
||||
|
@ -806,12 +809,16 @@ async function processUrl(msg, url, spoiler = false) {
|
|||
let sendWait = false;
|
||||
if (videos.length > 0 || audios.length > 0 || images.length > 4) {
|
||||
sendWait = true;
|
||||
await msg.addReaction("\uD83D\uDCE4");
|
||||
if (msg) await msg.addReaction("\uD83D\uDCE4");
|
||||
}
|
||||
|
||||
const embeds = [];
|
||||
const files = [];
|
||||
|
||||
const guild =
|
||||
msg.channel?.guild ??
|
||||
(msg.guildID ? hf.bot.guilds.get(msg.guildID) : false);
|
||||
|
||||
if (images.length > 0) {
|
||||
if (images.length <= 4) {
|
||||
for (const attachment of images) {
|
||||
|
@ -830,7 +837,7 @@ async function processUrl(msg, url, spoiler = false) {
|
|||
},
|
||||
}).then((res) => Number(res.headers.get("Content-Length")));
|
||||
|
||||
if (size <= getUploadLimit(msg.channel.guild)) {
|
||||
if (size <= getUploadLimit(guild)) {
|
||||
const file = await fetch(attachment.url, {
|
||||
headers: {
|
||||
"User-Agent": FRIENDLY_USERAGENT,
|
||||
|
@ -864,7 +871,7 @@ async function processUrl(msg, url, spoiler = false) {
|
|||
},
|
||||
}).then((res) => Number(res.headers.get("Content-Length")));
|
||||
|
||||
if (size <= getUploadLimit(msg.channel.guild)) {
|
||||
if (size <= getUploadLimit(guild)) {
|
||||
const file = await fetch(attachment.url, {
|
||||
headers: {
|
||||
"User-Agent": FRIENDLY_USERAGENT,
|
||||
|
@ -937,7 +944,7 @@ async function processUrl(msg, url, spoiler = false) {
|
|||
},
|
||||
}).then((res) => Number(res.headers.get("Content-Length")));
|
||||
|
||||
if (size <= getUploadLimit(msg.channel.guild)) {
|
||||
if (size <= getUploadLimit(guild)) {
|
||||
const file = await fetch(attachment.url, {
|
||||
headers: {
|
||||
"User-Agent": FRIENDLY_USERAGENT,
|
||||
|
@ -968,7 +975,7 @@ async function processUrl(msg, url, spoiler = false) {
|
|||
},
|
||||
}).then((res) => Number(res.headers.get("Content-Length")));
|
||||
|
||||
if (size <= getUploadLimit(msg.channel.guild)) {
|
||||
if (size <= getUploadLimit(guild)) {
|
||||
const file = await fetch(attachment.url, {
|
||||
headers: {
|
||||
"User-Agent": FRIENDLY_USERAGENT,
|
||||
|
@ -995,8 +1002,8 @@ async function processUrl(msg, url, spoiler = false) {
|
|||
}
|
||||
}
|
||||
|
||||
await msg.channel
|
||||
.createMessage({
|
||||
return {
|
||||
response: {
|
||||
content:
|
||||
cw != "" &&
|
||||
(images.length > 0 || videos.length > 0 || audios.length > 0)
|
||||
|
@ -1012,16 +1019,9 @@ async function processUrl(msg, url, spoiler = false) {
|
|||
messageReference: {
|
||||
messageID: msg.id,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
if (sendWait) {
|
||||
msg.removeReaction("\uD83D\uDCE4");
|
||||
}
|
||||
|
||||
if ((msg.flags & MessageFlags.SUPPRESS_EMBEDS) === 0) {
|
||||
msg.edit({flags: MessageFlags.SUPPRESS_EMBEDS}).catch(() => {});
|
||||
}
|
||||
});
|
||||
},
|
||||
sendWait,
|
||||
};
|
||||
}
|
||||
|
||||
events.add("messageCreate", "fedimbed", async function (msg) {
|
||||
|
@ -1054,15 +1054,100 @@ events.add("messageCreate", "fedimbed", async function (msg) {
|
|||
"fedimbed",
|
||||
`Hit "${service}" for "${url}", processing now.`
|
||||
);
|
||||
await processUrl(msg, url, hasSpoiler).catch((err) => {
|
||||
try {
|
||||
const {response, sendWait} = await processUrl(msg, url, hasSpoiler);
|
||||
await msg.channel.createMessage(response).then(() => {
|
||||
if (sendWait) {
|
||||
msg.removeReaction("\uD83D\uDCE4");
|
||||
}
|
||||
|
||||
if ((msg.flags & MessageFlags.SUPPRESS_EMBEDS) === 0) {
|
||||
msg.edit({flags: MessageFlags.SUPPRESS_EMBEDS}).catch(() => {});
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
"fedimbed",
|
||||
`Error processing "${url}":\n` + err.stack
|
||||
);
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const fedimbedCommand = new InteractionCommand("fedimbed");
|
||||
fedimbedCommand.helpText =
|
||||
"Better embeds for fediverse (Mastodon, Pleroma, etc) posts";
|
||||
fedimbedCommand.options.url = {
|
||||
name: "url",
|
||||
type: ApplicationCommandOptionTypes.STRING,
|
||||
description: "URL to attempt to parse for re-embedding",
|
||||
required: true,
|
||||
default: "",
|
||||
};
|
||||
fedimbedCommand.options.spoiler = {
|
||||
name: "spoiler",
|
||||
type: ApplicationCommandOptionTypes.BOOLEAN,
|
||||
description: "Send embed spoilered",
|
||||
required: false,
|
||||
default: false,
|
||||
};
|
||||
fedimbedCommand.callback = async function (interaction) {
|
||||
let url = getOption(interaction, fedimbedCommand, "url");
|
||||
const spoiler = getOption(interaction, fedimbedCommand, "spoiler");
|
||||
|
||||
url = url
|
||||
.replace(/\|/g, "")
|
||||
.replace(/^\]\(/, "")
|
||||
.replace(/\s*[\S]*?\)$/, "")
|
||||
.trim()
|
||||
.replace("@\u200b", "@")
|
||||
.replace("@%E2%80%8B", "@");
|
||||
let urlObj;
|
||||
try {
|
||||
urlObj = new URL(url);
|
||||
} catch (err) {
|
||||
return {
|
||||
content: `Failed to parse URL:\`\`\`\n${err}\`\`\``,
|
||||
flags: MessageFlags.EPHEMERAL,
|
||||
};
|
||||
}
|
||||
|
||||
let hasService = false;
|
||||
for (const service of Object.keys(PATH_REGEX)) {
|
||||
const regex = PATH_REGEX[service];
|
||||
if (urlObj && regex.test(urlObj.pathname)) {
|
||||
hasService = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasService) {
|
||||
try {
|
||||
const {response} = await processUrl(interaction, url, spoiler);
|
||||
|
||||
if (!response)
|
||||
return {
|
||||
content: "Failed to process URL.",
|
||||
flags: MessageFlags.EPHEMERAL,
|
||||
};
|
||||
|
||||
delete response.messageReference;
|
||||
return response;
|
||||
} catch (err) {
|
||||
logger.error("fedimbed", `Error processing "${url}":\n` + err.stack);
|
||||
return {
|
||||
content: "Failed to process URL.",
|
||||
flags: MessageFlags.EPHEMERAL,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
content: "Did not get a valid service for this URL.",
|
||||
flags: MessageFlags.EPHEMERAL,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue