From 603d922a33ebe423ae486987df1ed5af1c1cc668 Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 7 Nov 2022 21:45:00 +0100 Subject: [PATCH] allow /bridge config from revolt --- bot/src/bot/commands/configuration/bridge.ts | 392 ++++++++++++------ bridge/src/discord/commands.ts | 2 +- bridge/src/index.ts | 2 +- bridge/src/types/BridgeConfig.ts | 20 - .../src/misc/bridge_config_keys.ts | 0 lib/src/types/BridgeConfig.ts | 9 +- 6 files changed, 280 insertions(+), 145 deletions(-) delete mode 100644 bridge/src/types/BridgeConfig.ts rename bridge/src/types/ConfigKeys.ts => lib/src/misc/bridge_config_keys.ts (100%) diff --git a/bot/src/bot/commands/configuration/bridge.ts b/bot/src/bot/commands/configuration/bridge.ts index e6b51f3..2f653b1 100644 --- a/bot/src/bot/commands/configuration/bridge.ts +++ b/bot/src/bot/commands/configuration/bridge.ts @@ -1,210 +1,358 @@ import { Message } from "@janderedev/revolt.js"; import { ulid } from "ulid"; import { SendableEmbed } from "revolt-api"; +import { CONFIG_KEYS } from "automod/dist/misc/bridge_config_keys"; import { dbs } from "../../.."; import CommandCategory from "../../../struct/commands/CommandCategory"; import SimpleCommand from "../../../struct/commands/SimpleCommand"; import MessageCommandContext from "../../../struct/MessageCommandContext"; import { DEFAULT_PREFIX } from "../../modules/command_handler"; -import { embed, EmbedColor, isBotManager, isModerator, NO_MANAGER_MSG } from "../../util"; +import { + embed, + EmbedColor, + isBotManager, + isModerator, + NO_MANAGER_MSG, +} from "../../util"; -const DISCORD_INVITE_URL = 'https://discord.com/api/oauth2/authorize?client_id=965692929643524136&permissions=536996864&scope=bot%20applications.commands'; // todo: read this from env or smth +const DISCORD_INVITE_URL = + "https://discord.com/api/oauth2/authorize?client_id=965692929643524136&permissions=536996864&scope=bot%20applications.commands"; // todo: read this from env or smth export default { - name: 'bridge', + name: "bridge", aliases: null, - description: 'Bridge a channel with Discord', + description: "Bridge a channel with Discord", category: CommandCategory.Misc, run: async (message: MessageCommandContext, args: string[]) => { - switch(args[0]?.toLowerCase()) { - case 'link': { - if (!await isBotManager(message)) return message.reply(NO_MANAGER_MSG); + switch (args[0]?.toLowerCase()) { + case "link": { + if (!(await isBotManager(message))) + return message.reply(NO_MANAGER_MSG); - const count = await dbs.BRIDGE_CONFIG.count({ revolt: message.channel_id }); - if (count) return message.reply(`This channel is already bridged.`); + const count = await dbs.BRIDGE_CONFIG.count({ + revolt: message.channel_id, + }); + if (count) + return message.reply(`This channel is already bridged.`); // Invalidate previous bridge request - await dbs.BRIDGE_REQUESTS.remove({ revolt: message.channel_id }); + await dbs.BRIDGE_REQUESTS.remove({ + revolt: message.channel_id, + }); const reqId = ulid(); await dbs.BRIDGE_REQUESTS.insert({ id: reqId, revolt: message.channel_id, - expires: Date.now() + (1000 * 60 * 15), + expires: Date.now() + 1000 * 60 * 15, }); - let text = `### Link request created.\n` + + let text = + `### Link request created.\n` + `Request ID: \`${reqId}\`\n\n` + `[Invite the bridge bot to your Discord server](<${DISCORD_INVITE_URL}>) ` + `and run \`/bridge confirm ${reqId}\` in the channel you wish to link.\n` + `This request expires in 15 minutes.`; - if (!message.channel!.havePermission('Masquerade') - || !message.channel!.havePermission('SendEmbeds') - || !message.channel!.havePermission('UploadFiles')) { - text += '\n\n> :warning: I currently don\'t have all required permissions in this ' + - 'channel for the bridge to work. Please make sure to grant the "Masquerade", ' + - '"Upload Files" and "Send Embeds" permission.' + if ( + !message.channel!.havePermission("Masquerade") || + !message.channel!.havePermission("SendEmbeds") || + !message.channel!.havePermission("UploadFiles") + ) { + text += + "\n\n> :warning: I currently don't have all required permissions in this " + + 'channel for the bridge to work. Please make sure to grant the "Masquerade", ' + + '"Upload Files" and "Send Embeds" permission.'; } await message.reply(text, false); break; } - case 'unlink': { - if (!await isBotManager(message)) return message.reply(NO_MANAGER_MSG); + case "unlink": { + if (!(await isBotManager(message))) + return message.reply(NO_MANAGER_MSG); - const res = await dbs.BRIDGE_CONFIG.remove({ revolt: message.channel_id }); + const res = await dbs.BRIDGE_CONFIG.remove({ + revolt: message.channel_id, + }); if (res.deletedCount) await message.reply(`Channel unlinked!`); - else await message.reply(`Unable to unlink; no channel linked.`); + else + await message.reply(`Unable to unlink; no channel linked.`); break; } - case 'unlink_all': { - if (!await isBotManager(message)) return message.reply(NO_MANAGER_MSG); + case "unlink_all": { + if (!(await isBotManager(message))) + return message.reply(NO_MANAGER_MSG); - const query = { revolt: { $in: message.channel?.server?.channel_ids || [] } }; - if (args[1] == 'CONFIRM') { + const query = { + revolt: { $in: message.channel?.server?.channel_ids || [] }, + }; + if (args[1] == "CONFIRM") { const res = await dbs.BRIDGE_CONFIG.remove(query); if (res.deletedCount) { - await message.reply(`All channels have been unlinked. (Count: **${res.deletedCount}**)`); + await message.reply( + `All channels have been unlinked. (Count: **${res.deletedCount}**)` + ); } else { - await message.reply(`No bridged channels found; nothing to delete.`); + await message.reply( + `No bridged channels found; nothing to delete.` + ); } } else { const res = await dbs.BRIDGE_CONFIG.count(query); - if (!res) await message.reply(`No bridged channels found; nothing to delete.`); + if (!res) + await message.reply( + `No bridged channels found; nothing to delete.` + ); else { - await message.reply(`${res} bridged channels found. ` - + `Run \`${DEFAULT_PREFIX}bridge unlink_all CONFIRM\` to confirm deletion.`); + await message.reply( + `${res} bridged channels found. ` + + `Run \`${DEFAULT_PREFIX}bridge unlink_all CONFIRM\` to confirm deletion.` + ); } } break; } - case 'list': { - if (!await isBotManager(message)) return message.reply(NO_MANAGER_MSG); + case "list": { + if (!(await isBotManager(message))) + return message.reply(NO_MANAGER_MSG); - const links = await dbs.BRIDGE_CONFIG.find({ revolt: { $in: message.channel?.server?.channel_ids || [] } }); + const links = await dbs.BRIDGE_CONFIG.find({ + revolt: { $in: message.channel?.server?.channel_ids || [] }, + }); await message.reply({ - content: '#', + content: "#", embeds: [ { title: `Bridges in ${message.channel?.server?.name}`, - description: `**${links.length}** bridged channels found.\n\n` - + links.map(l => `<#${l.revolt}> **->** ${l.discord}`).join('\n'), - } - ] + description: + `**${links.length}** bridged channels found.\n\n` + + links + .map( + (l) => + `<#${l.revolt}> **->** ${l.discord}` + ) + .join("\n"), + }, + ], }); break; } - case 'info': { + case "info": { try { if (!message.reply_ids) { - return await message.reply('Please run this command again while replying to a message.'); - } - - if (message.reply_ids.length > 1 && !await isModerator(message, false)) { return await message.reply( - 'To avoid spam, only moderators are allowed to query bridge info for more than one message at a time.' + "Please run this command again while replying to a message." ); } - const messages = (await Promise.allSettled( - message.reply_ids?.map(m => message.channel!.fetchMessage(m)) || [] - )) - .filter(m => m.status == 'fulfilled') - .map(m => (m as PromiseFulfilledResult).value); - - if (!messages.length) { - return await message.reply('Something went wrong; could not fetch the target message(s).'); + if ( + message.reply_ids.length > 1 && + !(await isModerator(message, false)) + ) { + return await message.reply( + "To avoid spam, only moderators are allowed to query bridge info for more than one message at a time." + ); } - const embeds: SendableEmbed[] = await Promise.all(messages.map(async msg => { - const bridgeData = await dbs.BRIDGED_MESSAGES.findOne({ - 'revolt.messageId': msg._id, - }); + const messages = ( + await Promise.allSettled( + message.reply_ids?.map((m) => + message.channel!.fetchMessage(m) + ) || [] + ) + ) + .filter((m) => m.status == "fulfilled") + .map( + (m) => (m as PromiseFulfilledResult).value + ); - const embed: SendableEmbed = bridgeData ? { - url: msg.url, - title: `Message ${bridgeData?.origin == 'revolt' ? `by ${msg.author?.username}` : 'from Discord'}`, - colour: '#7e96ff', - description: `**Origin:** ${bridgeData.origin == 'revolt' ? 'Revolt' : 'Discord'}\n` + - `**Bridge Status:** ${ - bridgeData.origin == 'revolt' - ? (bridgeData.discord.messageId ? 'Bridged' : 'Unbridged') - : (bridgeData.revolt.messageId ? 'Bridged' : (bridgeData.revolt.nonce ? 'ID unknown' : 'Unbridged')) - }\n` + - `### Bridge Data\n` + - `Origin: \`${bridgeData.origin}\`\n` + - `Discord ID: \`${bridgeData.discord.messageId}\`\n` + - `Revolt ID: \`${bridgeData.revolt.messageId}\`\n` + - `Revolt Nonce: \`${bridgeData.revolt.nonce}\`\n` + - `Discord Channel: \`${bridgeData.channels?.discord}\`\n` + - `Revolt Channel: \`${bridgeData.channels?.revolt}\``, - } : { - url: msg.url, - title: `Message by ${msg.author?.username}`, - description: 'This message has not been bridged.', - colour: '#7e96ff', - } + if (!messages.length) { + return await message.reply( + "Something went wrong; could not fetch the target message(s)." + ); + } - return embed; - })); + const embeds: SendableEmbed[] = await Promise.all( + messages.map(async (msg) => { + const bridgeData = + await dbs.BRIDGED_MESSAGES.findOne({ + "revolt.messageId": msg._id, + }); + + const embed: SendableEmbed = bridgeData + ? { + url: msg.url, + title: `Message ${ + bridgeData?.origin == "revolt" + ? `by ${msg.author?.username}` + : "from Discord" + }`, + colour: "#7e96ff", + description: + `**Origin:** ${ + bridgeData.origin == "revolt" + ? "Revolt" + : "Discord" + }\n` + + `**Bridge Status:** ${ + bridgeData.origin == "revolt" + ? bridgeData.discord.messageId + ? "Bridged" + : "Unbridged" + : bridgeData.revolt.messageId + ? "Bridged" + : bridgeData.revolt.nonce + ? "ID unknown" + : "Unbridged" + }\n` + + `### Bridge Data\n` + + `Origin: \`${bridgeData.origin}\`\n` + + `Discord ID: \`${bridgeData.discord.messageId}\`\n` + + `Revolt ID: \`${bridgeData.revolt.messageId}\`\n` + + `Revolt Nonce: \`${bridgeData.revolt.nonce}\`\n` + + `Discord Channel: \`${bridgeData.channels?.discord}\`\n` + + `Revolt Channel: \`${bridgeData.channels?.revolt}\``, + } + : { + url: msg.url, + title: `Message by ${msg.author?.username}`, + description: + "This message has not been bridged.", + colour: "#7e96ff", + }; + + return embed; + }) + ); await message.reply({ embeds }, false); - } catch(e) { + } catch (e) { console.error(e); - message.reply(''+e)?.catch(() => {}); + message.reply("" + e)?.catch(() => {}); } break; } - case 'status': { - const link = await dbs.BRIDGE_CONFIG.findOne({ revolt: message.channel_id }); + case "status": { + const link = await dbs.BRIDGE_CONFIG.findOne({ + revolt: message.channel_id, + }); - if (!link) return await message.reply({ - embeds: [ - embed( - 'This channel is **not** bridged, and no message data is being sent to Discord.', - 'Bridge status', - EmbedColor.Success - ) - ] - }); - else return await message.reply({ - embeds: [ - embed( - 'This channel is bridged to Discord. Please refer to the [Privacy Policy]() for more info.', - 'Bridge Status', - EmbedColor.Success, - ) - ] - }); + if (!link) + return await message.reply({ + embeds: [ + embed( + "This channel is **not** bridged, and no message data is being sent to Discord.", + "Bridge status", + EmbedColor.Success + ), + ], + }); + else + return await message.reply({ + embeds: [ + embed( + "This channel is bridged to Discord. Please refer to the [Privacy Policy]() for more info.", + "Bridge Status", + EmbedColor.Success + ), + ], + }); } - case 'help': { + case "config": { + const [_, configKey, newVal]: (string | undefined)[] = args; + + if (!configKey) { + return await message.reply({ + embeds: [ + { + title: "Bridge Configuration", + description: + `To modify a configuration option, run ${DEFAULT_PREFIX}bridge config [true|false].\n\n` + + `**Available configuration keys:**` + + Object.keys(CONFIG_KEYS).map( + (key) => `\n- ${key}` + ), + }, + ], + }); + } + + if (!Object.keys(CONFIG_KEYS).includes(configKey)) { + return await message.reply("Unknown configuration key."); + } + + const key = CONFIG_KEYS[configKey as keyof typeof CONFIG_KEYS]; + + if (!newVal) { + const bridgeConfig = await dbs.BRIDGE_CONFIG.findOne({ + revolt: message.channel_id, + }); + return await message.reply({ + embeds: [ + { + title: "Bridge Configuration: " + configKey, + description: `**${key.friendlyName}**\n${ + key.description + }\n\nCurrent value: **${ + bridgeConfig?.config?.[ + configKey as keyof typeof CONFIG_KEYS + ] + }**`, + }, + ], + }); + } + + if (newVal != "true" && newVal != "false") { + return await message.reply( + "Value needs to be either `true` or `false`." + ); + } + + await dbs.BRIDGE_CONFIG.update( + { revolt: message.channel_id }, + { + $set: { [`config.${configKey}`]: newVal == "true" }, + $setOnInsert: { revolt: message.channel_id }, + }, + { upsert: true } + ); + return await message.reply( + `Configuration key **${configKey}** has been updated to **${newVal}**.` + ); + } + case "help": { await message.reply({ - content: '#', + content: "#", embeds: [ { - title: 'Discord Bridge', - description: `Bridges allow you to link your Revolt server to a Discord server ` - + `by relaying all messages.\n\n` - + `To link a channel, first run \`${DEFAULT_PREFIX}bridge link\` on Revolt. ` - + `This will provide you with a link ID.\n` - + `On Discord, first [add the Bridge bot to your server](<${DISCORD_INVITE_URL}>), ` - + `then run the command: \`/bridge confirm [ID]\`.\n\n` - + `You can list all bridges in a Revolt server by running \`${DEFAULT_PREFIX}bridge list\`\n\n` - + `To unlink a channel, run \`/bridge unlink\` from either Discord or Revolt. If you wish to ` - + `unbridge all channels in a Revolt server, run \`${DEFAULT_PREFIX}bridge unlink_all\`.\n` - + `To view bridge info about a particular message, run \`${DEFAULT_PREFIX}bridge info\` ` - + `while replying to the message.` - } - ] + title: "Discord Bridge", + description: + `Bridges allow you to link your Revolt server to a Discord server ` + + `by relaying all messages.\n\n` + + `To link a channel, first run \`${DEFAULT_PREFIX}bridge link\` on Revolt. ` + + `This will provide you with a link ID.\n` + + `On Discord, first [add the Bridge bot to your server](<${DISCORD_INVITE_URL}>), ` + + `then run the command: \`/bridge confirm [ID]\`.\n\n` + + `You can list all bridges in a Revolt server by running \`${DEFAULT_PREFIX}bridge list\`\n\n` + + `To unlink a channel, run \`/bridge unlink\` from either Discord or Revolt. If you wish to ` + + `unbridge all channels in a Revolt server, run \`${DEFAULT_PREFIX}bridge unlink_all\`.\n` + + `To view bridge info about a particular message, run \`${DEFAULT_PREFIX}bridge info\` ` + + `while replying to the message.\n` + + `You can customize how the bridge behaves using \`${DEFAULT_PREFIX}bridge config\`.`, + }, + ], }); break; } default: { - await message.reply(`Run \`${DEFAULT_PREFIX}bridge help\` for help.`); + await message.reply( + `Run \`${DEFAULT_PREFIX}bridge help\` for help.` + ); } } - } + }, } as SimpleCommand; diff --git a/bridge/src/discord/commands.ts b/bridge/src/discord/commands.ts index a53b0eb..d9c23c8 100644 --- a/bridge/src/discord/commands.ts +++ b/bridge/src/discord/commands.ts @@ -7,7 +7,7 @@ import { BRIDGED_MESSAGES, BRIDGE_CONFIG, BRIDGE_REQUESTS, BRIDGE_USER_CONFIG, l import { MessageEmbed, TextChannel } from "discord.js"; import { revoltFetchMessage, revoltFetchUser } from "../util"; import { client as revoltClient } from "../revolt/client"; -import { CONFIG_KEYS } from "../types/ConfigKeys"; +import { CONFIG_KEYS } from "automod/dist/misc/bridge_config_keys"; const PRIVACY_POLICY_URL = "https://github.com/janderedev/automod/wiki/Privacy-Policy"; diff --git a/bridge/src/index.ts b/bridge/src/index.ts index 54df0ce..1cab5c6 100644 --- a/bridge/src/index.ts +++ b/bridge/src/index.ts @@ -4,7 +4,7 @@ import { getDb } from './db'; import { login as loginRevolt } from './revolt/client'; import { login as loginDiscord } from './discord/client'; import { ICollection } from 'monk'; -import BridgeConfig from './types/BridgeConfig'; +import BridgeConfig from "automod/dist/types/BridgeConfig"; import BridgedMessage from './types/BridgedMessage'; import BridgeRequest from './types/BridgeRequest'; import DiscordBridgedEmoji from './types/DiscordBridgedEmoji'; diff --git a/bridge/src/types/BridgeConfig.ts b/bridge/src/types/BridgeConfig.ts deleted file mode 100644 index df0ec54..0000000 --- a/bridge/src/types/BridgeConfig.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { CONFIG_KEYS } from "./ConfigKeys"; - -export default class { - // Revolt channel ID - revolt?: string; - - // Discord channel ID - discord?: string; - - // Discord webhook - discordWebhook?: { - id: string; - token: string; - }; - - config?: { [key in keyof typeof CONFIG_KEYS]: boolean | undefined }; - - // If true, messages by users who have opted out of bridging will be deleted. - disallowIfOptedOut?: boolean; -} diff --git a/bridge/src/types/ConfigKeys.ts b/lib/src/misc/bridge_config_keys.ts similarity index 100% rename from bridge/src/types/ConfigKeys.ts rename to lib/src/misc/bridge_config_keys.ts diff --git a/lib/src/types/BridgeConfig.ts b/lib/src/types/BridgeConfig.ts index 03a1fde..83ec1f6 100644 --- a/lib/src/types/BridgeConfig.ts +++ b/lib/src/types/BridgeConfig.ts @@ -1,3 +1,5 @@ +import { CONFIG_KEYS } from "../misc/bridge_config_keys"; + export default class { // Revolt channel ID revolt?: string; @@ -9,5 +11,10 @@ export default class { discordWebhook?: { id: string; token: string; - } + }; + + config?: { [key in keyof typeof CONFIG_KEYS]: boolean | undefined }; + + // If true, messages by users who have opted out of bridging will be deleted. + disallowIfOptedOut?: boolean; }