Compare commits

...

2 commits

Author SHA1 Message Date
6e85237cab interaction command support 2024-05-17 14:33:01 -06:00
73590257c8 global command self delete react 2024-05-16 14:43:51 -06:00
6 changed files with 2451 additions and 1801 deletions

File diff suppressed because it is too large Load diff

View file

@ -1,15 +1,20 @@
const Dysnomia = require("@projectdysnomia/dysnomia");
const logger = require("./lib/logger.js");
const fs = require("fs");
const {resolve} = require("path");
const fs = require("node:fs");
const {resolve} = require("node:path");
const sqlite3 = require("sqlite3");
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",
@ -17,9 +22,11 @@ const bot = new Dysnomia.Client(config.token, {
gateway: {
intents: Object.values(Dysnomia.Constants.Intents),
},
restMode: true,
});
const commands = new Dysnomia.Collection();
const interactionCommands = new Dysnomia.Collection();
const database = new sqlite3.Database(resolve(__dirname, "..", "database.db"));
@ -33,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}'`);
}
}
@ -41,21 +51,20 @@ 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}'`);
}
bot.on("messageCreate", async (msg) => {
try {
// fix DMs cause of gateway v8 changes
if (
!(msg.channel instanceof Dysnomia.Channel) &&
@ -69,8 +78,17 @@ bot.on("messageCreate", async (msg) => {
if (!(msg.channel instanceof Dysnomia.Channel)) return;
await CommandDispatcher(msg);
} catch (err) {
const stack = (err?.stack ?? err.message).split("\n");
const error = stack.shift();
logger.error(
"hf:main",
`Failed to dispatch command: ${error}\n\t${stack.join("\n\t")}`
);
}
});
bot.on("messageUpdate", (msg, oldMsg) => {
bot.on("messageUpdate", async (msg, oldMsg) => {
try {
const oneDay = Date.now() - 86400000;
if (
msg.timestamp > oneDay &&
@ -78,7 +96,75 @@ 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");
const error = stack.shift();
logger.error(
"hf:main",
`Failed to dispatch command update: ${error}\n\t${stack.join("\n\t")}`
);
}
});
bot.on("messageReactionAdd", async (msg, reaction, reactor) => {
if (msg.author.id !== bot.user.id) return;
if (reaction.name !== "\u274c") return;
try {
let channel = msg.channel;
if (!(channel instanceof Dysnomia.Channel)) {
const newChannel = hf.bot.getChannel(channel.id);
if (newChannel) {
channel = newChannel;
} else {
channel = await hf.bot.getRESTChannel(channel.id);
}
}
if (!msg.messageReference) {
msg = await channel.getMessage(msg.id);
}
if (!msg.messageReference) return;
const ref = await channel.getMessage(msg.messageReference.messageID);
if (!ref) return;
if (ref.author.id !== reactor.id) return;
await msg.delete("Command sender requested output deletion.");
} catch (err) {
const stack = (err?.stack ?? err.message).split("\n");
const error = stack.shift();
logger.error(
"hf:main",
`Failed to self-delete message: ${error}\n\t${stack.join("\n\t")}`
);
}
});
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"
)}`
);
}
});
@ -98,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) => {

View 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;

View 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};

View file

@ -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("#")
);
@ -155,40 +155,3 @@ events.add("messageCreate", "codePreviews", async function (msg) {
}
}
});
// TODO: maybe all command outputs should have this ability
events.add(
"messageReactionAdd",
"codePreviews",
async function (msg, reaction, reactor) {
if (!msg.guildID) return;
if (!(await hasFlag(msg.guildID, "codePreviews"))) return;
if (reaction.name != "\u274c") return;
let channel = msg.channel;
if (!channel.name) {
channel = hf.bot.getChannel(channel.id);
}
if (!msg.messageReference) {
msg = await channel.getMessage(msg.id);
}
if (!msg.messageReference) return;
const ref = await channel.getMessage(msg.messageReference.messageID);
if (!ref) return;
if (
ref.author.id != reactor.id &&
!channel.permissionsOf(reactor.id).has("manageMessages")
)
return;
if (
!REGEX_GITHUB.test(ref.content) &&
!REGEX_GITLAB.test(ref.content) &&
!REGEX_GITEA.test(ref.content)
)
return;
await msg.delete("Author requested code preview deletion");
}
);

View file

@ -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,
};
}
};