From 8785588f44c44cbf087989bd3c0f2e2853af6db0 Mon Sep 17 00:00:00 2001 From: Lea Date: Wed, 8 May 2024 17:48:51 +0200 Subject: [PATCH] straight to prod you go --- .gitignore | 1 + src/index.ts | 217 +++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 176 insertions(+), 42 deletions(-) diff --git a/.gitignore b/.gitignore index d3a8807..065e6fb 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ dist/ node_modules .env db.json +reactionroles.json diff --git a/src/index.ts b/src/index.ts index 9addb49..fd2ade2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,17 +1,30 @@ -import { Channel, Client, Member, Message, User } from "revolt.js"; +import { Channel, Client, ClientboundNotification, Member, Message, User } from "revolt.js"; import { config } from 'dotenv'; import { SendableEmbed } from 'revolt-api'; import { Low, JSONFile } from 'lowdb'; import { decodeTime } from 'ulid'; import sherlock from "./sherlock.js"; import axios from "axios"; +import { readFileSync } from "fs"; +import path from "path"; +import { AnyMxRecord } from "dns"; +import { fileURLToPath } from "url"; config(); +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +type ReactionId = { message: string, channel: string, emoji: string }; type Db = { probation: string[]; blocked: { [key: string]: { blocked: boolean, kick: boolean } }; children: string[]; + reaction_ids: { + nsfw_role: ReactionId, + cosmetic_roles: { + [key: string]: ReactionId, + } + } } type CommandFlag = { @@ -58,6 +71,7 @@ const COMMANDS = { 'unblock': 'Untroll a user', 'child': 'Mark a user as child', 'notchild': 'Remove user\'s child status', + 'setup_reactions': 'Set up reaction role messages', 'sherlock': 'Run a sherlock scan on a username', } @@ -71,10 +85,11 @@ const client = new Client({ }); client.loginBot(process.env.TOKEN); db.read().then(() => { - db.data ||= { probation: [], blocked: {}, children: [] }; + db.data ||= { probation: [], blocked: {}, children: [], reaction_ids: { cosmetic_roles: {}, nsfw_role: { emoji: 'a', channel: '', message: '1' } } }; db.data.probation ||= []; db.data.blocked ||= {}; db.data.children ||= []; + db.data.reaction_ids ||= { cosmetic_roles: {}, nsfw_role: { emoji: 'a', channel: '', message: '1' } }; if (Array.isArray(db.data.blocked)) { console.log('Running migration: db.blocked'); @@ -96,7 +111,7 @@ client.once('ready', async () => { console.log('Got server members'); }); -function embed(content: string, title?: string, type?: 'INFO'|'SUCCESS'|'WARN'|'ERROR'): SendableEmbed { +function embed(content: string, title?: string, type?: 'INFO'|'SUCCESS'|'WARN'|'ERROR'|`#${string}`): SendableEmbed { const colors = { 'SUCCESS': 'var(--success)', 'INFO': 'var(--status-streaming)', @@ -105,7 +120,7 @@ function embed(content: string, title?: string, type?: 'INFO'|'SUCCESS'|'WARN'|' } return { - colour: colors[type || 'INFO'], + colour: type ? (colors as any)[type] || type : undefined, description: content, title: title, } @@ -561,6 +576,61 @@ client.on('message', async (message) => { break; } + case 'setup_reactions': { + db.data!.reaction_ids.cosmetic_roles = {}; + const file = Object.entries(JSON.parse(readFileSync(path.join(__dirname, "..", "reactionroles.json"), "utf-8")) as { [key: string]: string }); + + const nsfwReactionId = '01GE2KP575W886T71MZK9Z17D6'; + const nsfwMsg = await message.channel!.sendMessage({ + embeds: [embed( + "React to this message to get access to the NSFW channel.\n> This channel is marked 18+, and as such we ask minors not to interact with it :01H9HFRZTGPK9PA3WBZV07BQ9Q:\n> Please also read the channel description before participating!", + "Reaction role", + "#aa557f", + )], + interactions: { + reactions: ['01GE2KP575W886T71MZK9Z17D6'], + restrict_reactions: true, + }, + masquerade: { + name: message.channel?.server?.name, + avatar: message.channel?.server?.generateIconURL({ size: 256 }), + }, + }); + + db.data!.reaction_ids.nsfw_role.channel = nsfwMsg.channel_id; + db.data!.reaction_ids.nsfw_role.message = nsfwMsg._id; + db.data!.reaction_ids.nsfw_role.emoji = nsfwReactionId; + + while (file.length > 0) { + const items = file.splice(0, 15); + const msg = await message.channel!.sendMessage({ + embeds: [embed( + `React to receive one of the following roles:\n\n` + + items.map((item) => `${item[1]} ${message.channel?.server?.roles?.[item[0]].name}`).join("\n"), + "Reaction role", + "#aa557f", + )], + interactions: { + reactions: items.map((item) => encodeURIComponent(item[1].replaceAll(":", ""))), + restrict_reactions: true, + }, + masquerade: { + name: message.channel?.server?.name, + avatar: message.channel?.server?.generateIconURL({ size: 256 }), + }, + }); + for (const item of items) { + db.data!.reaction_ids.cosmetic_roles[item[0]] = { + channel: msg.channel_id, + message: msg._id, + emoji: item[1].replaceAll(":", ""), + }; + } + } + await db.write(); + break; + } + case 'type': { if (message.channel?.typing_ids.has(client.user!._id)) { message.channel.stopTyping(); @@ -657,52 +727,115 @@ client.on('member/join', async (member) => { } }); -client.on('packet', async (packet) => { - if (packet.type != 'MessageReact') return; - if (packet.emoji_id != '↩️') return; - try { - const channel = client.channels.get(packet.channel_id); - const message = client.messages.get(packet.id) || await channel!.fetchMessage(packet.id); - if (!message || message.author_id != client.user?._id) return console.log('Ignoring react: Author mismatch'); +async function processActionButtonReactions(packet: ClientboundNotification & { type: "MessageReact" }) { + const channel = client.channels.get(packet.channel_id); + const message = client.messages.get(packet.id) || await channel!.fetchMessage(packet.id); + if (!message || message.author_id != client.user?._id) return console.log('Ignoring react: Author mismatch'); - const members = getMembers(channel!.server_id!); - const member = members.find(m => m._id.user == packet.user_id); - if (!member) return console.log('Ignoring react: Could not find reacting user'); - const privileged = member.hasPermission(channel!.server!, 'ManageMessages'); - if (!privileged) return console.log('Ignoring react: User is unprivileged'); + const members = getMembers(channel!.server_id!); + const member = members.find(m => m._id.user == packet.user_id); + if (!member) return console.log('Ignoring react: Could not find reacting user'); + const privileged = member.hasPermission(channel!.server!, 'ManageMessages'); + if (!privileged) return console.log('Ignoring react: User is unprivileged'); - let info = message.content?.match(/^\$%log action=\S+ user=[A-Z0-9]+%\$$/)?.[0]; - if (!info) return console.log('Ignoring react: Could not extract message metadata'); + let info = message.content?.match(/^\$%log action=\S+ user=[A-Z0-9]+%\$$/)?.[0]; + if (!info) return console.log('Ignoring react: Could not extract message metadata'); - let action = info.match(/action=\S+/)?.[0].substring(7), - user = info.match(/user=[A-Z0-9]+/)?.[0].substring(5); + let action = info.match(/action=\S+/)?.[0].substring(7), + user = info.match(/user=[A-Z0-9]+/)?.[0].substring(5); - const target = members.find(m => m._id.user == user); - if (!target) return await channel?.sendMessage({ embeds: [ - embed(`Failed to find user \`${user}\`.`, 'Message interaction failed', 'ERROR'), - ] }); + const target = members.find(m => m._id.user == user); + if (!target) return await channel?.sendMessage({ embeds: [ + embed(`Failed to find user \`${user}\`.`, 'Message interaction failed', 'ERROR'), + ] }); - switch(action) { - case 'probation_add': {// Log message was for added probation, clicking button will remove it - await setProbation(target, false); - const embed = message.embeds?.[0] as SendableEmbed; - embed.description = embed.description?.replace(' Press ↩️ to undo.', ''); - embed.colour = 'var(--status-invisible)'; - await message.edit({ content: '#', embeds: [ embed ] }); - break; + switch(action) { + case 'probation_add': {// Log message was for added probation, clicking button will remove it + await setProbation(target, false); + const embed = message.embeds?.[0] as SendableEmbed; + embed.description = embed.description?.replace(' Press ↩️ to undo.', ''); + embed.colour = 'var(--status-invisible)'; + await message.edit({ content: '#', embeds: [ embed ] }); + break; + } + case 'probation_remove': { + await setProbation(target, true); + const embed = message.embeds?.[0] as SendableEmbed; + embed.description = embed.description?.replace(' Press ↩️ to undo.', ''); + embed.colour = 'var(--status-invisible)'; + await message.edit({ content: '#', embeds: [ embed ] }); + break; + } + } + if (!target) return; +} + +async function processReactionRoles(messageId: string, emoji: string, userId: string, added: boolean) { + + async function assignRole(member: Member, role: string, assign: boolean) { + if (assign) { + if (!member.roles?.includes(role)) { + await member.edit({ roles: [...(member.roles ?? []), role] }); } - case 'probation_remove': { - await setProbation(target, true); - const embed = message.embeds?.[0] as SendableEmbed; - embed.description = embed.description?.replace(' Press ↩️ to undo.', ''); - embed.colour = 'var(--status-invisible)'; - await message.edit({ content: '#', embeds: [ embed ] }); - break; + } else { + if (member.roles?.includes(role)) { + await member.edit({ roles: member.roles.filter((r) => r != role) }); } } - if (!target) return; - } catch(e) { - console.error(e); + } + + // nsfw role needs extra processing + if (messageId == db.data?.reaction_ids.nsfw_role.message && emoji == db.data.reaction_ids.nsfw_role.emoji) { + const channel = await client.channels.fetch(db.data.reaction_ids.nsfw_role.channel); + const members = getMembers(channel.server_id!); + const member = members.find((member) => member._id.user == userId); + if (!member) return; + + if (added && db.data.children.includes(userId)) { + try { + await client.api.delete( + `/channels/${channel._id}/messages/${messageId}/reactions/${emoji}` as '-/channels/{target}/messages/{msg}/reactions/{emoji}', + { user_id: userId }, + ); + } catch(e) { + console.error(e); + } + await assignRole(member, process.env.NSFW_ROLE!, false).catch(); // Remove the role if it's still there for some reason + return; + } + + await assignRole(member, process.env.NSFW_ROLE!, added); + return; + } + + const roleConfig = Object.entries(db.data?.reaction_ids.cosmetic_roles ?? []).find( + (r) => r[1].emoji == emoji && r[1].message == messageId + ); + + if (!roleConfig) return; + + const channel = await client.channels.fetch(roleConfig[1].channel); + const members = getMembers(channel.server_id!); + const member = members.find((member) => member._id.user == userId); + if (!member) return; + + await assignRole(member, roleConfig[0], added); +} + +client.on('packet', async (packet) => { + if (packet.type == 'MessageReact') { + try { + if (packet.emoji_id == '↩️') await processActionButtonReactions(packet); + await processReactionRoles(packet.id, packet.emoji_id, packet.user_id, true); + } catch(e) { + console.error(e); + } + } else if (packet.type == 'MessageUnreact') { + try { + await processReactionRoles(packet.id, packet.emoji_id, packet.user_id, false); + } catch(e) { + console.error(e); + } } });