add option to bridge nicknames/masquerade data

This commit is contained in:
Jan 2022-11-07 21:17:18 +01:00
parent 0408c4b0a5
commit 55f5e339e1
Signed by: Lea
GPG key ID: 5D5E18ACB990F57A
5 changed files with 392 additions and 179 deletions

View file

@ -7,18 +7,20 @@ 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";
const PRIVACY_POLICY_URL = 'https://github.com/janderedev/automod/wiki/Privacy-Policy';
const PRIVACY_POLICY_URL =
"https://github.com/janderedev/automod/wiki/Privacy-Policy";
const COMMANDS: any[] = [
{
name: 'bridge',
description: 'Confirm or delete Revolt bridges',
name: "bridge",
description: "Confirm or delete Revolt bridges",
type: 1, // Slash command
options: [
{
name: 'confirm',
description: 'Confirm a bridge initiated from Revolt',
name: "confirm",
description: "Confirm a bridge initiated from Revolt",
type: 1, // Subcommand
options: [
{
@ -26,63 +28,93 @@ const COMMANDS: any[] = [
description: "The bridge request ID",
required: true,
type: 3,
}
},
],
},
{
name: 'unlink',
description: 'Unbridge the current channel',
name: "unlink",
description: "Unbridge the current channel",
type: 1,
},
{
name: 'help',
description: 'Usage instructions',
name: "help",
description: "Usage instructions",
type: 1,
},
{
name: 'opt_out',
description: 'Opt out of having your messages bridged',
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',
name: "opt_out",
description:
"Whether you wish to opt out of having your messages bridged",
optional: true,
type: 5 // Boolean
type: 5, // Boolean
},
],
},
{
name: 'status',
description: 'Find out whether this channel is bridged to Revolt',
type: 1
}
name: "status",
description:
"Find out whether this channel is bridged to Revolt",
type: 1,
},
{
name: "config",
description: "Bridge configuration options for this channel",
type: 1,
options: [
{
name: "key",
description: "The configuration option to change",
type: 3, // String
required: true,
choices: Object.entries(CONFIG_KEYS).map((conf) => ({
name: conf[1].friendlyName,
value: conf[0],
})),
},
{
name: "value",
description:
"The new value for the option. Leave empty to get current state",
type: 5, // Boolean
required: false,
},
],
},
],
},
{
name: 'Message Info',
description: '',
name: "Message Info",
description: "",
type: 3, // Message context menu
}
},
];
const rest = new REST({ version: '9' }).setToken(process.env['DISCORD_TOKEN']!);
const rest = new REST({ version: "9" }).setToken(process.env["DISCORD_TOKEN"]!);
client.once('ready', async () => {
client.once("ready", async () => {
try {
logger.info(`Refreshing application commands.`);
if (process.env.NODE_ENV != 'production' && process.env.DEV_GUILD) {
if (process.env.NODE_ENV != "production" && process.env.DEV_GUILD) {
await rest.put(
Routes.applicationGuildCommands(client.user!.id, process.env.DEV_GUILD),
{ body: COMMANDS },
Routes.applicationGuildCommands(
client.user!.id,
process.env.DEV_GUILD
),
{ body: COMMANDS }
);
logger.done(
`Application commands for ${process.env.DEV_GUILD} have been updated.`
);
logger.done(`Application commands for ${process.env.DEV_GUILD} have been updated.`);
} else {
await rest.put(
Routes.applicationCommands(client.user!.id),
{ body: COMMANDS },
);
await rest.put(Routes.applicationCommands(client.user!.id), {
body: COMMANDS,
});
logger.done(`Global application commands have been updated.`);
}
} catch (e) {
@ -90,35 +122,59 @@ client.once('ready', async () => {
}
});
client.on('interactionCreate', async interaction => {
client.on("interactionCreate", async (interaction) => {
try {
if (interaction.isCommand()) {
logger.debug(`Command received: /${interaction.commandName}`);
// The revolutionary Jan command handler
switch (interaction.commandName) {
case 'bridge':
if (!interaction.memberPermissions?.has('MANAGE_GUILD') &&
['confirm', 'unlink'].includes(interaction.options.getSubcommand(true))
case "bridge":
if (
!interaction.memberPermissions?.has("MANAGE_GUILD") &&
["confirm", "unlink"].includes(
interaction.options.getSubcommand(true)
)
) {
return await interaction.reply({ content: `\`MANAGE_GUILD\` permission is required for this.`, ephemeral: true });
return await interaction.reply({
content: `\`MANAGE_GUILD\` permission is required for this.`,
ephemeral: true,
});
}
const ownPerms = (interaction.channel as TextChannel).permissionsFor(client.user!)!;
const ownPerms = (
interaction.channel as TextChannel
).permissionsFor(client.user!)!;
switch (interaction.options.getSubcommand(true)) {
case 'confirm':
if (!ownPerms.has('MANAGE_WEBHOOKS'))
return interaction.reply('Sorry, I lack permission to manage webhooks in this channel.');
case "confirm": {
if (!ownPerms.has("MANAGE_WEBHOOKS"))
return interaction.reply(
"Sorry, I lack permission to manage webhooks in this channel."
);
const id = interaction.options.getString('id', true);
const request = await BRIDGE_REQUESTS.findOne({ id: id });
if (!request || request.expires < Date.now()) return await interaction.reply('Unknown ID.');
const id = interaction.options.getString(
"id",
true
);
const request = await BRIDGE_REQUESTS.findOne({
id: id,
});
if (!request || request.expires < Date.now())
return await interaction.reply("Unknown ID.");
const bridgedCount = await BRIDGE_CONFIG.count({ discord: interaction.channelId });
if (bridgedCount > 0) return await interaction.reply('This channel is already bridged.');
const bridgedCount = await BRIDGE_CONFIG.count({
discord: interaction.channelId,
});
if (bridgedCount > 0)
return await interaction.reply(
"This channel is already bridged."
);
const webhook = await (interaction.channel as TextChannel)
.createWebhook('AutoMod Bridge', { avatar: client.user?.avatarURL() });
const webhook = await(
interaction.channel as TextChannel
).createWebhook("AutoMod Bridge", {
avatar: client.user?.avatarURL(),
});
await BRIDGE_REQUESTS.remove({ id: id });
await BRIDGE_CONFIG.insert({
@ -126,58 +182,133 @@ client.on('interactionCreate', async interaction => {
revolt: request.revolt,
discordWebhook: {
id: webhook.id,
token: webhook.token || '',
}
token: webhook.token || "",
},
});
return await interaction.reply(`✅ Channel bridged!`);
case 'unlink':
const res = await BRIDGE_CONFIG.findOneAndDelete({ discord: interaction.channelId });
return await interaction.reply(
`✅ Channel bridged!`
);
}
case "unlink": {
const res = await BRIDGE_CONFIG.findOneAndDelete({
discord: interaction.channelId,
});
if (res?._id) {
await interaction.reply('Channel unbridged.');
if (ownPerms.has('MANAGE_WEBHOOKS') && res.discordWebhook) {
await interaction.reply("Channel unbridged.");
if (
ownPerms.has("MANAGE_WEBHOOKS") &&
res.discordWebhook
) {
try {
const hooks = await (interaction.channel as TextChannel).fetchWebhooks();
if (hooks.get(res?.discordWebhook?.id)) await hooks.get(res?.discordWebhook?.id)
?.delete('Channel has been unbridged');
const hooks = await(
interaction.channel as TextChannel
).fetchWebhooks();
if (hooks.get(res?.discordWebhook?.id))
await hooks
.get(res?.discordWebhook?.id)
?.delete(
"Channel has been unbridged"
);
} catch (_) {}
}
}
else await interaction.reply('This channel is not bridged.');
} else
await interaction.reply(
"This channel is not bridged."
);
break;
}
case 'help':
const isPrivileged = !!interaction.memberPermissions?.has('MANAGE_GUILD');
case "config": {
const configKey = interaction.options.getString(
"key",
true
) as keyof typeof CONFIG_KEYS;
const newValue = interaction.options.getBoolean(
"value",
false
);
if (newValue == null) {
const currentState =
(
await BRIDGE_CONFIG.findOne({
discord: interaction.channelId,
})
)?.config?.[configKey] ?? false;
return await interaction.reply({
ephemeral: true,
embeds: [
new MessageEmbed()
.setAuthor({
name: "Bridge Configuration",
})
.setTitle(configKey)
.setDescription(
`${CONFIG_KEYS[configKey].description}\n\nCurrent state: \`${currentState}\``
)
.toJSON(),
],
});
}
await BRIDGE_CONFIG.update(
{ discord: interaction.channelId },
{
$set: { [`config.${configKey}`]: newValue },
$setOnInsert: {
discord: interaction.channelId,
},
},
{ upsert: true }
);
return await interaction.reply({
ephemeral: true,
content: `Option \`${configKey}\` has been updated to \`${newValue}\`.`,
});
}
case "help": {
const isPrivileged =
!!interaction.memberPermissions?.has(
"MANAGE_GUILD"
);
const INVITE_URL = `https://discord.com/api/oauth2/authorize?client_id=${client.user?.id}&permissions=536996864&scope=bot%20applications.commands`;
const embed = new MessageEmbed()
.setColor('#ff6e6d')
.setAuthor({ name: 'AutoMod Revolt Bridge', iconURL: client.user?.displayAvatarURL() });
.setColor("#ff6e6d")
.setAuthor({
name: "AutoMod Revolt Bridge",
iconURL: client.user?.displayAvatarURL(),
});
embed.setDescription(
'[AutoMod](https://automod.me) is a utility and moderation bot for [Revolt](https://revolt.chat). ' +
'This Discord bot allows you to link your Discord servers to your Revolt servers ' +
'by mirroring messages between text channels.'
"[AutoMod](https://automod.me) is a utility and moderation bot for [Revolt](https://revolt.chat). " +
"This Discord bot allows you to link your Discord servers to your Revolt servers " +
"by mirroring messages between text channels."
);
embed.addField(
'Setting up a bridge',
"Setting up a bridge",
isPrivileged
? 'The bridge process is initialized by running the `/bridge link` command in the Revolt ' +
'channel you wish to bridge.\n' +
'Afterwards you can run the `/bridge confirm` command in the correct Discord channel to finish the link.'
: 'You don\'t have `Manage Messages` permission - Please ask a moderator to configure the bridge.'
? "The bridge process is initialized by running the `/bridge link` command in the Revolt " +
"channel you wish to bridge.\n" +
"Afterwards you can run the `/bridge confirm` command in the correct Discord channel to finish the link."
: "You don't have `Manage Messages` permission - Please ask a moderator to configure the bridge."
);
embed.addField(
'Adding AutoMod to your server',
"Adding AutoMod to your server",
`You can add the Revolt bot to your server ` +
`[here](https://app.revolt.chat/bot/${revoltClient.user?._id} "Open Revolt"). To add the Discord counterpart, ` +
`click [here](${INVITE_URL} "Add Discord bot").`
);
embed.addField(
'Contact',
"Contact",
`If you have any questions regarding this bot or the Revolt counterpart, feel free to join ` +
`[this](https://discord.gg/4pZgvqgYJ8) Discord server or [this](https://rvlt.gg/jan) Revolt server.\n` +
`If you want to report a bug, suggest a feature or browse the source code, ` +
@ -186,31 +317,45 @@ client.on('interactionCreate', async interaction => {
`Before using this bot, please read the [Privacy Policy](${PRIVACY_POLICY_URL})!`
);
await interaction.reply({ embeds: [ embed ], ephemeral: true });
await interaction.reply({
embeds: [embed],
ephemeral: true,
});
break;
}
case 'opt_out':
const optOut = interaction.options.getBoolean('opt_out', false);
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 });
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.'
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.'
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 },
$setOnInsert: {
id: interaction.user.id,
},
$set: { optOut },
},
{ upsert: true }
@ -218,104 +363,135 @@ client.on('interactionCreate', async interaction => {
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.'
),
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;
}
case 'status':
const bridgeConfig = await BRIDGE_CONFIG.findOne({ discord: interaction.channelId });
case "status": {
const bridgeConfig = await BRIDGE_CONFIG.findOne({
discord: interaction.channelId,
});
if (!bridgeConfig?.revolt) {
return await interaction.reply({
ephemeral: true,
content: 'This channel is **not** bridged. No message content data will be processed.',
content:
"This channel is **not** bridged. No message content data will be processed.",
});
}
else {
} else {
return await interaction.reply({
ephemeral: true,
content: 'This channel is **bridged to Revolt**. Your messages will ' +
'be processed and sent to [Revolt](<https://revolt.chat>) according to AutoMod\'s ' +
content:
"This channel is **bridged to Revolt**. Your messages will " +
"be processed and sent to [Revolt](<https://revolt.chat>) according to AutoMod's " +
`[Privacy Policy](<${PRIVACY_POLICY_URL}>).`,
});
}
break;
}
default: await interaction.reply('Unknown subcommand');
default:
await interaction.reply("Unknown subcommand");
}
break;
}
}
else if (interaction.isMessageContextMenu()) {
logger.debug(`Received context menu: ${interaction.targetMessage.id}`);
} else if (interaction.isMessageContextMenu()) {
logger.debug(
`Received context menu: ${interaction.targetMessage.id}`
);
switch (interaction.commandName) {
case 'Message Info':
case "Message Info":
const message = interaction.targetMessage;
const bridgeInfo = await BRIDGED_MESSAGES.findOne({ "discord.messageId": message.id });
const bridgeInfo = await BRIDGED_MESSAGES.findOne({
"discord.messageId": message.id,
});
const messageUrl = `https://discord.com/channels/${interaction.guildId}/${interaction.channelId}/${message.id}`;
if (!bridgeInfo) return await interaction.reply({
if (!bridgeInfo)
return await interaction.reply({
ephemeral: true,
embeds: [
new MessageEmbed()
.setAuthor({ name: 'Message info', url: messageUrl })
.setDescription('This message has not been bridged.')
.setColor('#7e96ff'),
.setAuthor({
name: "Message info",
url: messageUrl,
})
.setDescription(
"This message has not been bridged."
)
.setColor("#7e96ff"),
],
});
else {
const embed = new MessageEmbed();
embed.setColor('#7e96ff');
embed.setAuthor({ name: 'Message info', url: messageUrl });
embed.setColor("#7e96ff");
embed.setAuthor({
name: "Message info",
url: messageUrl,
});
embed.addField('Origin', bridgeInfo.origin == 'discord' ? 'Discord' : 'Revolt', true);
if (bridgeInfo.origin == 'discord') {
embed.addField(
'Bridge Status',
"Origin",
bridgeInfo.origin == "discord"
? "Discord"
: "Revolt",
true
);
if (bridgeInfo.origin == "discord") {
embed.addField(
"Bridge Status",
bridgeInfo.revolt.messageId
? 'Bridged'
? "Bridged"
: bridgeInfo.revolt.nonce
? 'ID unknown'
: 'Unbridged',
? "ID unknown"
: "Unbridged",
true
);
} else {
embed.addField(
'Bridge Status',
"Bridge Status",
bridgeInfo.discord.messageId
? 'Bridged'
: 'Unbridged',
? "Bridged"
: "Unbridged",
true
);
if (bridgeInfo.channels?.revolt) {
const channel = await revoltClient.channels.get(bridgeInfo.channels.revolt);
const revoltMsg = await revoltFetchMessage(bridgeInfo.revolt.messageId, channel);
const channel = await revoltClient.channels.get(
bridgeInfo.channels.revolt
);
const revoltMsg = await revoltFetchMessage(
bridgeInfo.revolt.messageId,
channel
);
if (revoltMsg) {
const author = await revoltFetchUser(revoltMsg.author_id);
const author = await revoltFetchUser(
revoltMsg.author_id
);
embed.addField(
'Message Author',
`**@${author?.username}** (${revoltMsg.author_id})`,
"Message Author",
`**@${author?.username}** (${revoltMsg.author_id})`
);
}
}
}
embed.addField(
'Bridge Data',
"Bridge Data",
`Origin: \`${bridgeInfo.origin}\`\n` +
`Discord ID: \`${bridgeInfo.discord.messageId}\`\n` +
`Revolt ID: \`${bridgeInfo.revolt.messageId}\`\n` +
@ -333,6 +509,7 @@ client.on('interactionCreate', async interaction => {
}
} catch (e) {
console.error(e);
if (interaction.isCommand()) interaction.reply('An error has occurred: ' + e).catch(() => {});
if (interaction.isCommand())
interaction.reply("An error has occurred: " + e).catch(() => {});
}
});

View file

@ -192,18 +192,24 @@ client.on('messageCreate', async message => {
//attachments: [],
//embeds: [],
nonce: nonce,
replies: reply ? [ { id: reply, mention: !!message.mentions.repliedUser } ] : undefined,
replies: reply
? [{ id: reply, mention: !!message.mentions.repliedUser }]
: undefined,
masquerade: {
name: message.author.username,
avatar: message.author.displayAvatarURL({ size: 128 }),
colour: channel.server?.havePermission('ManageRole')
name: bridgeCfg.config?.bridge_nicknames
? message.member?.nickname ?? message.author.username
: message.author.username,
avatar: bridgeCfg.config?.bridge_nicknames
? message.member?.displayAvatarURL({ size: 128 })
: message.author.displayAvatarURL({ size: 128 }),
colour: channel.server?.havePermission("ManageRole")
? message.member?.displayColor // Discord.js returns black or 0 instead of undefined when no role color is set
? message.member?.displayHexColor
: 'var(--foreground)'
: "var(--foreground)"
: undefined,
},
embeds: message.embeds.length
? message.embeds.map(e => new GenericEmbed(e).toRevolt())
? message.embeds.map((e) => new GenericEmbed(e).toRevolt())
: undefined,
attachments: autumnUrls.length ? autumnUrls : undefined,
};

View file

@ -151,13 +151,26 @@ client.on('message', async message => {
});
const payload: MessagePayload | WebhookMessageOptions = {
content: message.content ? await renderMessageBody(message.content) : undefined,
username: message.author?.username ?? 'Unknown user',
avatarURL: message.author?.generateAvatarURL({ max_side: 128 }),
content: message.content
? await renderMessageBody(message.content)
: undefined,
username:
(bridgeCfg.config?.bridge_nicknames
? message.masquerade?.name ??
message.member?.nickname ??
message.author?.username
: message.author?.username) ?? "Unknown user",
avatarURL: bridgeCfg.config?.bridge_nicknames
? message.masquerade?.avatar ??
message.member?.generateAvatarURL({ max_side: 128 }) ??
message.author?.generateAvatarURL({ max_side: 128 })
: message.author?.generateAvatarURL({ max_side: 128 }),
embeds: message.embeds?.length
? message.embeds
.filter(e => e.type == "Text")
.map(e => new GenericEmbed(e as SendableEmbed).toDiscord())
.filter((e) => e.type == "Text")
.map((e) =>
new GenericEmbed(e as SendableEmbed).toDiscord()
)
: undefined,
allowedMentions: { parse: [] },
};

View file

@ -1,3 +1,5 @@
import { CONFIG_KEYS } from "./ConfigKeys";
export default class {
// Revolt channel ID
revolt?: string;
@ -9,7 +11,9 @@ 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;

View file

@ -0,0 +1,13 @@
export const CONFIG_KEYS = {
bridge_nicknames: {
friendlyName: "Bridge Nicknames",
description:
"If enabled, nicknames and avatar overrides will be bridged.",
},
// disallow_opt_out: {
// friendlyName: "Disallow users who opted out of message bridging",
// description:
// "If enabled, all messages by users who opted out of their messages being bridged (`/bridge opt_out`) will be deleted. " +
// "You should enable this if your Revolt server is bridged to a mostly unmoderated Discord server.",
// },
};