old-automod/bridge/src/discord/commands.ts
2022-05-30 10:11:38 +02:00

339 lines
17 KiB
TypeScript

// fuck slash commands
import { client } from "./client";
import { REST } from '@discordjs/rest';
import { Routes } from 'discord-api-types/v9';
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";
const PRIVACY_POLICY_URL = 'https://github.com/janderedev/automod/wiki/Privacy-Policy';
const COMMANDS: any[] = [
{
name: 'bridge',
description: 'Confirm or delete Revolt bridges',
type: 1, // Slash command
options: [
{
name: 'confirm',
description: 'Confirm a bridge initiated from Revolt',
type: 1, // Subcommand
options: [
{
name: "id",
description: "The bridge request ID",
required: true,
type: 3,
}
],
},
{
name: 'unlink',
description: 'Unbridge the current channel',
type: 1,
},
{
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
},
],
},
{
name: 'status',
description: 'Find out whether this channel is bridged to Revolt',
type: 1
}
],
},
{
name: 'Message Info',
description: '',
type: 3, // Message context menu
}
];
const rest = new REST({ version: '9' }).setToken(process.env['DISCORD_TOKEN']!);
client.once('ready', async () => {
try {
logger.info(`Refreshing application commands.`);
if (process.env.NODE_ENV != 'production' && process.env.DEV_GUILD) {
await rest.put(
Routes.applicationGuildCommands(client.user!.id, process.env.DEV_GUILD),
{ body: COMMANDS },
);
logger.done(`Application commands for ${process.env.DEV_GUILD} have been updated.`);
} else {
await rest.put(
Routes.applicationCommands(client.user!.id),
{ body: COMMANDS },
);
logger.done(`Global application commands have been updated.`);
}
} catch(e) {
console.error(e);
}
});
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))
) {
return await interaction.reply({ content: `\`MANAGE_GUILD\` permission is required for this.`, ephemeral: true });
}
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.');
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 webhook = await (interaction.channel as TextChannel)
.createWebhook('AutoMod Bridge', { avatar: client.user?.avatarURL() });
await BRIDGE_REQUESTS.remove({ id: id });
await BRIDGE_CONFIG.insert({
discord: interaction.channelId,
revolt: request.revolt,
discordWebhook: {
id: webhook.id,
token: webhook.token || '',
}
});
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) {
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');
} catch(_) {}
}
}
else await interaction.reply('This channel is not bridged.');
break;
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() });
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.'
);
embed.addField(
'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.'
);
embed.addField(
'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',
`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, ` +
`feel free to do so [on GitHub](https://github.com/janderedev/automod).\n` +
`For other inquiries, please contact \`contact@automod.me\`.\n\n` +
`Before using this bot, please read the [Privacy Policy](${PRIVACY_POLICY_URL})!`
);
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;
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.',
});
}
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 ' +
`[Privacy Policy](<${PRIVACY_POLICY_URL}>).`,
});
}
break;
default: await interaction.reply('Unknown subcommand');
}
break;
}
}
else if (interaction.isMessageContextMenu()) {
logger.debug(`Received context menu: ${interaction.targetMessage.id}`);
switch(interaction.commandName) {
case 'Message Info':
const message = interaction.targetMessage;
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({
ephemeral: true,
embeds: [
new MessageEmbed()
.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.addField('Origin', bridgeInfo.origin == 'discord' ? 'Discord' : 'Revolt', true);
if (bridgeInfo.origin == 'discord') {
embed.addField(
'Bridge Status',
bridgeInfo.revolt.messageId
? 'Bridged'
: bridgeInfo.revolt.nonce
? 'ID unknown'
: 'Unbridged',
true
);
} else {
embed.addField(
'Bridge Status',
bridgeInfo.discord.messageId
? 'Bridged'
: 'Unbridged',
true
);
if (bridgeInfo.channels?.revolt) {
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);
embed.addField(
'Message Author',
`**@${author?.username}** (${revoltMsg.author_id})`,
);
}
}
}
embed.addField(
'Bridge Data',
`Origin: \`${bridgeInfo.origin}\`\n` +
`Discord ID: \`${bridgeInfo.discord.messageId}\`\n` +
`Revolt ID: \`${bridgeInfo.revolt.messageId}\`\n` +
`Revolt Nonce: \`${bridgeInfo.revolt.nonce}\`\n` +
`Discord Channel: \`${bridgeInfo.channels?.discord}\`\n` +
`Revolt Channel: \`${bridgeInfo.channels?.revolt}\``
);
return await interaction.reply({
ephemeral: true,
embeds: [ embed ],
});
}
}
}
} catch(e) {
console.error(e);
if (interaction.isCommand()) interaction.reply('An error has occurred: ' + e).catch(() => {});
}
});