translate spoiler blocks between discord/revolt
This commit is contained in:
parent
48de9db224
commit
229e2ecb34
3 changed files with 151 additions and 78 deletions
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
|
@ -1,3 +1,5 @@
|
||||||
{
|
{
|
||||||
"editor.formatOnSave": false
|
"editor.formatOnSave": true,
|
||||||
|
"editor.formatOnSaveMode": "modifications",
|
||||||
|
"prettier.tabWidth": 4
|
||||||
}
|
}
|
||||||
|
|
|
@ -242,36 +242,41 @@ client.on('messageCreate', async message => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
client.on('guildCreate', async server => {
|
client.on("guildCreate", async (server) => {
|
||||||
try {
|
try {
|
||||||
const me = server.me || await server.members.fetch({ user: client.user!.id });
|
const me =
|
||||||
const channels = Array.from(server.channels.cache.filter(
|
server.me ||
|
||||||
c => c.permissionsFor(me).has('SEND_MESSAGES') && c.isText()
|
(await server.members.fetch({ user: client.user!.id }));
|
||||||
));
|
const channels = Array.from(
|
||||||
|
server.channels.cache.filter(
|
||||||
|
(c) => c.permissionsFor(me).has("SEND_MESSAGES") && c.isText()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
if (!channels.length) return;
|
if (!channels.length) return;
|
||||||
|
|
||||||
const channel = (channels.find(c => c[0] == server.systemChannel?.id) || channels[0])?.[1] as TextChannel;
|
const channel = (channels.find(
|
||||||
|
(c) => c[0] == server.systemChannel?.id
|
||||||
|
) || channels[0])?.[1] as TextChannel;
|
||||||
|
|
||||||
const message =
|
const message =
|
||||||
':wave: Hi there!\n\n' +
|
":wave: Hi there!\n\n" +
|
||||||
'Thanks for adding AutoMod to this server! Please note that despite it\'s name, this bot only provides ' +
|
"Thanks for adding AutoMod to this server! Please note that despite its name, this bot only provides " +
|
||||||
'bridge integration with the AutoMod bot on Revolt (<https://revolt.chat>) and does not offer any moderation ' +
|
"bridge integration with the AutoMod bot on Revolt (<https://revolt.chat>) and does not offer any moderation " +
|
||||||
'features on Discord. To get started, run the `/bridge help` command!\n\n' +
|
"features on Discord. To get started, run the `/bridge help` command!\n\n" +
|
||||||
'Before using AutoMod, please make sure you have read the privacy policy: <https://github.com/janderedev/automod/wiki/Privacy-Policy>\n\n' +
|
"Before using AutoMod, please make sure you have read the privacy policy: <https://github.com/janderedev/automod/wiki/Privacy-Policy>\n\n" +
|
||||||
'A note to this server\'s administrators: When using the bridge, please make sure to also provide your members ' +
|
"A note to this server's administrators: When using the bridge, please make sure to also provide your members " +
|
||||||
'with a link to AutoMod\'s privacy policy in an accessible place like your rules channel.';
|
"with a link to AutoMod's privacy policy in an accessible place like your rules channel.";
|
||||||
|
|
||||||
if (channel.permissionsFor(me).has('EMBED_LINKS')) {
|
if (channel.permissionsFor(me).has("EMBED_LINKS")) {
|
||||||
await channel.send({
|
await channel.send({
|
||||||
embeds: [
|
embeds: [
|
||||||
new MessageEmbed()
|
new MessageEmbed()
|
||||||
.setDescription(message)
|
.setDescription(message)
|
||||||
.setColor('#ff6e6d')
|
.setColor("#ff6e6d"),
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
await channel.send(message);
|
await channel.send(message);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -279,60 +284,99 @@ client.on('guildCreate', async server => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Replaces @mentions and #channel mentions
|
// Replaces @mentions and #channel mentions and modifies body to make markdown render on Revolt
|
||||||
async function renderMessageBody(message: string): Promise<string> {
|
async function renderMessageBody(message: string): Promise<string> {
|
||||||
|
|
||||||
// Replace Tenor URLs so they render properly.
|
// Replace Tenor URLs so they render properly.
|
||||||
// We have to download the page first, then extract
|
// We have to download the page first, then extract
|
||||||
// the `c.tenor.com` URL from the meta tags.
|
// the `c.tenor.com` URL from the meta tags.
|
||||||
// Could query autumn but that's too much effort and I already wrote this.
|
// Could query autumn but that's too much effort and I already wrote this.
|
||||||
if (RE_TENOR.test(message)) {
|
if (RE_TENOR.test(message)) {
|
||||||
try {
|
try {
|
||||||
logger.debug('Replacing tenor URL');
|
logger.debug("Replacing tenor URL");
|
||||||
|
|
||||||
const res = await axios.get(
|
const res = await axios.get(message, {
|
||||||
message,
|
|
||||||
{
|
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent': 'AutoMod/1.0; https://github.com/janderedev/automod',
|
"User-Agent":
|
||||||
}
|
"AutoMod/1.0; https://github.com/janderedev/automod",
|
||||||
}
|
},
|
||||||
);
|
});
|
||||||
|
|
||||||
const metaTag = RE_TENOR_META.exec(res.data as string)?.[0];
|
const metaTag = RE_TENOR_META.exec(res.data as string)?.[0];
|
||||||
if (metaTag) {
|
if (metaTag) {
|
||||||
return metaTag
|
return metaTag
|
||||||
.replace('<meta class="dynamic" property="og:url" content="', '')
|
.replace(
|
||||||
.replace('">', '');
|
'<meta class="dynamic" property="og:url" content="',
|
||||||
|
""
|
||||||
|
)
|
||||||
|
.replace('">', "");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn(`Replacing tenor URL failed: ${e}`);
|
||||||
}
|
}
|
||||||
} catch(e) { logger.warn(`Replacing tenor URL failed: ${e}`) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// @mentions
|
// @mentions
|
||||||
message = await smartReplace(message, RE_MENTION_USER, async (match: string) => {
|
message = await smartReplace(
|
||||||
const id = match.replace('<@!', '').replace('<@', '').replace('>', '');
|
message,
|
||||||
|
RE_MENTION_USER,
|
||||||
|
async (match: string) => {
|
||||||
|
const id = match
|
||||||
|
.replace("<@!", "")
|
||||||
|
.replace("<@", "")
|
||||||
|
.replace(">", "");
|
||||||
const user = await discordFetchUser(id);
|
const user = await discordFetchUser(id);
|
||||||
return `@${user?.username || id}`;
|
return `@${user?.username || id}`;
|
||||||
}, { cacheMatchResults: true, maxMatches: 10 });
|
},
|
||||||
|
{ cacheMatchResults: true, maxMatches: 10 }
|
||||||
|
);
|
||||||
|
|
||||||
// #channels
|
// #channels
|
||||||
message = await smartReplace(message, RE_MENTION_CHANNEL, async (match: string) => {
|
message = await smartReplace(
|
||||||
const id = match.replace('<#', '').replace('>', '');
|
message,
|
||||||
|
RE_MENTION_CHANNEL,
|
||||||
|
async (match: string) => {
|
||||||
|
const id = match.replace("<#", "").replace(">", "");
|
||||||
const channel = client.channels.cache.get(id);
|
const channel = client.channels.cache.get(id);
|
||||||
const bridgeCfg = channel ? await BRIDGE_CONFIG.findOne({ discord: channel.id }) : undefined;
|
const bridgeCfg = channel
|
||||||
|
? await BRIDGE_CONFIG.findOne({ discord: channel.id })
|
||||||
|
: undefined;
|
||||||
const revoltChannel = bridgeCfg?.revolt
|
const revoltChannel = bridgeCfg?.revolt
|
||||||
? revoltClient.channels.get(bridgeCfg.revolt)
|
? revoltClient.channels.get(bridgeCfg.revolt)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
return revoltChannel ? `<#${revoltChannel._id}>` : `#${(channel as TextChannel)?.name || id}`;
|
return revoltChannel
|
||||||
}, { cacheMatchResults: true, maxMatches: 10 });
|
? `<#${revoltChannel._id}>`
|
||||||
|
: `#${(channel as TextChannel)?.name || id}`;
|
||||||
|
},
|
||||||
|
{ cacheMatchResults: true, maxMatches: 10 }
|
||||||
|
);
|
||||||
|
|
||||||
// :emojis:
|
// :emojis:
|
||||||
message = await smartReplace(message, RE_EMOJI, async (match: string) => {
|
message = await smartReplace(
|
||||||
|
message,
|
||||||
|
RE_EMOJI,
|
||||||
|
async (match: string) => {
|
||||||
return match
|
return match
|
||||||
.replace(/<(a?)?:/, ':\u200b') // We don't want to accidentally send an unrelated emoji, so we add a zero width space here
|
.replace(/<(a?)?:/, ":\u200b") // We don't want to accidentally send an unrelated emoji, so we add a zero width space here
|
||||||
.replace(/(:\d{18}?>)/, ':');
|
.replace(/(:\d{18}?>)/, ":");
|
||||||
}, { cacheMatchResults: true });
|
},
|
||||||
|
{ cacheMatchResults: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
message = message
|
||||||
|
// "Escape" !!Revite style spoilers!!
|
||||||
|
.replace(
|
||||||
|
/!!.+!!/g,
|
||||||
|
(match) => `!\u200b!${match.substring(2, match.length - 2)}!!`
|
||||||
|
)
|
||||||
|
// Translate ||Discord spoilers|| to !!Revite spoilers!!, while making sure multiline spoilers continue working
|
||||||
|
.replace(/\|\|.+\|\|/gs, (match) => {
|
||||||
|
return match
|
||||||
|
.substring(2, match.length - 2)
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => `!!${line.replace(/!!/g, "!\u200b!")}!!`)
|
||||||
|
.join("\n");
|
||||||
|
});
|
||||||
|
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
|
@ -258,41 +258,68 @@ client.on('message', async message => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Replaces @mentions, #channel mentions and :emojis:
|
// Replaces @mentions, #channel mentions, :emojis: and makes markdown features work on Discord
|
||||||
async function renderMessageBody(message: string): Promise<string> {
|
async function renderMessageBody(message: string): Promise<string> {
|
||||||
// We don't want users to generate large amounts of database and API queries
|
|
||||||
let failsafe = 0;
|
|
||||||
|
|
||||||
// @mentions
|
// @mentions
|
||||||
message = await smartReplace(message, RE_MENTION_USER, async (match) => {
|
message = await smartReplace(
|
||||||
const id = match.replace('<@', '').replace('>', '');
|
message,
|
||||||
|
RE_MENTION_USER,
|
||||||
|
async (match) => {
|
||||||
|
const id = match.replace("<@", "").replace(">", "");
|
||||||
const user = await revoltFetchUser(id);
|
const user = await revoltFetchUser(id);
|
||||||
return `@${user?.username || id}`;
|
return `@${user?.username || id}`;
|
||||||
}, { cacheMatchResults: true, maxMatches: 10 });
|
},
|
||||||
|
{ cacheMatchResults: true, maxMatches: 10 }
|
||||||
|
);
|
||||||
|
|
||||||
// #channels
|
// #channels
|
||||||
message = await smartReplace(message, RE_MENTION_CHANNEL, async (match) => {
|
message = await smartReplace(
|
||||||
const id = match.replace('<#', '').replace('>', '');
|
message,
|
||||||
|
RE_MENTION_CHANNEL,
|
||||||
|
async (match) => {
|
||||||
|
const id = match.replace("<#", "").replace(">", "");
|
||||||
const channel = client.channels.get(id);
|
const channel = client.channels.get(id);
|
||||||
|
|
||||||
const bridgeCfg = channel ? await BRIDGE_CONFIG.findOne({ revolt: channel._id }) : undefined;
|
const bridgeCfg = channel
|
||||||
|
? await BRIDGE_CONFIG.findOne({ revolt: channel._id })
|
||||||
|
: undefined;
|
||||||
const discordChannel = bridgeCfg?.discord
|
const discordChannel = bridgeCfg?.discord
|
||||||
? discordClient.channels.cache.get(bridgeCfg.discord)
|
? discordClient.channels.cache.get(bridgeCfg.discord)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
return discordChannel ? `<#${discordChannel.id}>` : `#${channel?.name || id}`;
|
return discordChannel
|
||||||
}, { cacheMatchResults: true, maxMatches: 10 });
|
? `<#${discordChannel.id}>`
|
||||||
|
: `#${channel?.name || id}`;
|
||||||
|
},
|
||||||
|
{ cacheMatchResults: true, maxMatches: 10 }
|
||||||
|
);
|
||||||
|
|
||||||
message = await smartReplace(message, RE_EMOJI, async (match) => {
|
message = await smartReplace(
|
||||||
const emojiName = match.replace(/(^:)|(:$)/g, '');
|
message,
|
||||||
|
RE_EMOJI,
|
||||||
|
async (match) => {
|
||||||
|
const emojiName = match.replace(/(^:)|(:$)/g, "");
|
||||||
|
|
||||||
if (!KNOWN_EMOJI_NAMES.includes(emojiName)) return match;
|
if (!KNOWN_EMOJI_NAMES.includes(emojiName)) return match;
|
||||||
|
|
||||||
const dbEmoji = await BRIDGED_EMOJIS.findOne({ name: emojiName });
|
const dbEmoji = await BRIDGED_EMOJIS.findOne({ name: emojiName });
|
||||||
if (!dbEmoji) return match;
|
if (!dbEmoji) return match;
|
||||||
|
|
||||||
return `<${dbEmoji.animated ? 'a' : ''}:${emojiName}:${dbEmoji.emojiid}>`;
|
return `<${dbEmoji.animated ? "a" : ""}:${emojiName}:${
|
||||||
}, { cacheMatchResults: true, maxMatches: 40 });
|
dbEmoji.emojiid
|
||||||
|
}>`;
|
||||||
|
},
|
||||||
|
{ cacheMatchResults: true, maxMatches: 40 }
|
||||||
|
);
|
||||||
|
|
||||||
|
message = message
|
||||||
|
// Escape ||Discord style spoilers|| since Revite doesn't support them
|
||||||
|
.replace(/\|\|.+\|\|/gs, (match) => "\\" + match)
|
||||||
|
// Translate !!Revite spoilers!! to ||Discord spoilers||
|
||||||
|
.replace(
|
||||||
|
/!!.+!!/g,
|
||||||
|
(match) => `||${match.substring(2, match.length - 2)}||`
|
||||||
|
);
|
||||||
|
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue