diff --git a/bridge/src/discord/commands.ts b/bridge/src/discord/commands.ts index b456a61..f6d800b 100644 --- a/bridge/src/discord/commands.ts +++ b/bridge/src/discord/commands.ts @@ -3,7 +3,7 @@ import { client } from "./client"; import { REST } from '@discordjs/rest'; import { Routes } from 'discord-api-types/v9'; -import { BRIDGED_MESSAGES, BRIDGE_CONFIG, BRIDGE_REQUESTS, logger } from ".."; +import { BRIDGED_MESSAGES, BRIDGE_CONFIG, BRIDGE_REQUESTS, BRIDGE_USER_CONFIG, logger } from ".."; import { MessageEmbed, TextChannel } from "discord.js"; import { revoltFetchMessage, revoltFetchUser } from "../util"; import { client as revoltClient } from "../revolt/client"; @@ -36,7 +36,20 @@ const COMMANDS: any[] = [ name: 'help', description: 'Usage instructions', type: 1, - } + }, + { + name: 'opt_out', + description: 'Opt out of having your messages bridged', + type: 1, + options: [ + { + name: 'opt_out', + description: 'Whether you wish to opt out of having your messages bridged', + optional: true, + type: 5 // Boolean + }, + ], + }, ], }, { @@ -168,6 +181,46 @@ client.on('interactionCreate', async interaction => { await interaction.reply({ embeds: [ embed ], ephemeral: true }); break; + case 'opt_out': + const optOut = interaction.options.getBoolean('opt_out', false); + if (optOut == null) { + const userConfig = await BRIDGE_USER_CONFIG.findOne({ id: interaction.user.id }); + if (userConfig?.optOut) { + return await interaction.reply({ + ephemeral: true, + content: 'You are currently **opted out** of message bridging. ' + + 'Users on Revolt **will not** see your username, avatar or message content.' + }); + } else { + return await interaction.reply({ + ephemeral: true, + content: 'You are currently **not** opted out of message bridging. ' + + 'All your messages in a bridged channel will be sent to the associated Revolt channel.' + }); + } + } else { + await BRIDGE_USER_CONFIG.update( + { id: interaction.user.id }, + { + $setOnInsert: { id: interaction.user.id }, + $set: { optOut }, + }, + { upsert: true } + ); + + return await interaction.reply({ + ephemeral: true, + content: `You have **opted ${optOut ? 'out of' : 'into'}** message bridging. ` + + ( + optOut + ? 'Your username, avatar and message content will no longer be visible on Revolt.\n' + + 'Please note that some servers may be configured to automatically delete your messages.' + : 'All your messages in a bridged channel will be sent to the associated Revolt channel.' + ), + }); + } + break; + default: await interaction.reply('Unknown subcommand'); } diff --git a/bridge/src/discord/events.ts b/bridge/src/discord/events.ts index 563d312..1fe3a19 100644 --- a/bridge/src/discord/events.ts +++ b/bridge/src/discord/events.ts @@ -1,4 +1,4 @@ -import { BRIDGED_MESSAGES, BRIDGE_CONFIG, logger } from ".."; +import { BRIDGED_MESSAGES, BRIDGE_CONFIG, BRIDGE_USER_CONFIG, logger } from ".."; import { client } from "./client"; import { AUTUMN_URL, client as revoltClient } from "../revolt/client"; import axios from 'axios'; @@ -27,6 +27,7 @@ client.on('messageDelete', async message => { ]); if (!bridgedMsg?.revolt) return logger.debug(`Discord: Message has not been bridged; ignoring deletion`); + if (!bridgedMsg.ignore) return logger.debug(`Discord: Message marked as ignore`); if (!bridgeCfg?.revolt) return logger.debug(`Discord: No Revolt channel associated`); const targetMsg = await revoltFetchMessage(bridgedMsg.revolt.messageId, revoltClient.channels.get(bridgeCfg.revolt)); @@ -51,6 +52,7 @@ client.on('messageUpdate', async (oldMsg, newMsg) => { ]); if (!bridgedMsg) return logger.debug(`Discord: Message has not been bridged; ignoring edit`); + if (!bridgedMsg.ignore) return logger.debug(`Discord: Message marked as ignore`); if (!bridgeCfg?.revolt) return logger.debug(`Discord: No Revolt channel associated`); if (newMsg.webhookId && newMsg.webhookId == bridgeCfg.discordWebhook?.id) { return logger.debug(`Discord: Message was sent by bridge; ignoring edit`); @@ -69,12 +71,13 @@ client.on('messageUpdate', async (oldMsg, newMsg) => { client.on('messageCreate', async message => { try { logger.debug(`[M] Discord: ${message.content}`); - const [ bridgeCfg, bridgedReply ] = await Promise.all([ + const [ bridgeCfg, bridgedReply, userConfig ] = await Promise.all([ BRIDGE_CONFIG.findOne({ discord: message.channelId }), (message.reference?.messageId ? BRIDGED_MESSAGES.findOne({ "discord.messageId": message.reference.messageId }) : undefined ), + BRIDGE_USER_CONFIG.findOne({ id: message.author.id }), ]); if (message.webhookId && bridgeCfg?.discordWebhook?.id == message.webhookId) { @@ -98,6 +101,11 @@ client.on('messageCreate', async message => { } } + if (bridgeCfg.disallowIfOptedOut && userConfig?.optOut && message.deletable) { + await message.delete(); + return; + } + // Setting a known nonce allows us to ignore bridged // messages while still letting other AutoMod messages pass. const nonce = ulid(); @@ -105,7 +113,7 @@ client.on('messageCreate', async message => { await BRIDGED_MESSAGES.update( { "discord.messageId": message.id }, { - $setOnInsert: { + $setOnInsert: userConfig?.optOut ? {} : { origin: 'discord', discord: { messageId: message.id, @@ -116,12 +124,32 @@ client.on('messageCreate', async message => { channels: { discord: message.channelId, revolt: bridgeCfg.revolt, - } + }, + ignore: userConfig?.optOut, } }, { upsert: true } ); + if (userConfig?.optOut) { + const msg = await channel.sendMessage({ + content: `$\\color{#565656}\\small{\\textsf{Message content redacted}}$`, + masquerade: { + name: 'AutoMod Bridge', + }, + nonce: nonce, + }); + + await BRIDGED_MESSAGES.update( + { "discord.messageId": message.id }, + { + $set: { "revolt.messageId": msg._id }, + } + ); + + return; + } + const autumnUrls: string[] = []; // todo: upload all attachments at once instead of sequentially diff --git a/bridge/src/index.ts b/bridge/src/index.ts index b2b5a56..54df0ce 100644 --- a/bridge/src/index.ts +++ b/bridge/src/index.ts @@ -8,6 +8,7 @@ import BridgeConfig from './types/BridgeConfig'; import BridgedMessage from './types/BridgedMessage'; import BridgeRequest from './types/BridgeRequest'; import DiscordBridgedEmoji from './types/DiscordBridgedEmoji'; +import BridgeUserConfig from './types/BridgeUserConfig'; config(); @@ -17,6 +18,7 @@ const BRIDGED_MESSAGES: ICollection = db.get('bridged_messages') const BRIDGE_CONFIG: ICollection = db.get('bridge_config'); const BRIDGE_REQUESTS: ICollection = db.get('bridge_requests'); const BRIDGED_EMOJIS: ICollection = db.get('bridged_emojis'); +const BRIDGE_USER_CONFIG: ICollection = db.get('bridge_user_config'); for (const v of [ 'REVOLT_TOKEN', 'DISCORD_TOKEN', 'DB_STRING' ]) { if (!process.env[v]) { @@ -33,4 +35,4 @@ for (const v of [ 'REVOLT_TOKEN', 'DISCORD_TOKEN', 'DB_STRING' ]) { ]); })(); -export { logger, db, BRIDGED_MESSAGES, BRIDGE_CONFIG, BRIDGE_REQUESTS, BRIDGED_EMOJIS } +export { logger, db, BRIDGED_MESSAGES, BRIDGE_CONFIG, BRIDGE_REQUESTS, BRIDGED_EMOJIS, BRIDGE_USER_CONFIG } diff --git a/bridge/src/types/BridgeConfig.ts b/bridge/src/types/BridgeConfig.ts index 03a1fde..017be83 100644 --- a/bridge/src/types/BridgeConfig.ts +++ b/bridge/src/types/BridgeConfig.ts @@ -10,4 +10,7 @@ export default class { id: string; token: string; } + + // If true, messages by users who have opted out of bridging will be deleted. + disallowIfOptedOut?: boolean; } diff --git a/bridge/src/types/BridgeUserConfig.ts b/bridge/src/types/BridgeUserConfig.ts new file mode 100644 index 0000000..a4fcd1d --- /dev/null +++ b/bridge/src/types/BridgeUserConfig.ts @@ -0,0 +1,5 @@ +export default class { + platform: 'discord'; // Todo: Revolt users too? + id: string; + optOut?: boolean; +} diff --git a/bridge/src/types/BridgedMessage.ts b/bridge/src/types/BridgedMessage.ts index 698d542..1df269f 100644 --- a/bridge/src/types/BridgedMessage.ts +++ b/bridge/src/types/BridgedMessage.ts @@ -15,4 +15,6 @@ export default class { discord: string; revolt: string; } + + ignore?: boolean; }