Compare commits

...

2 Commits

Author SHA1 Message Date
Lea 8785588f44
straight to prod you go 2024-05-08 17:48:51 +02:00
Lea 9a69d15a1e
child command 2024-05-08 16:15:11 +02:00
2 changed files with 230 additions and 43 deletions

1
.gitignore vendored
View File

@ -2,3 +2,4 @@ dist/
node_modules
.env
db.json
reactionroles.json

View File

@ -1,16 +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 = {
@ -36,7 +50,7 @@ const CommandFlags: CommandFlag[] = [
},
];
const PREFIX_WORD = '/kibby';
const PREFIX_WORD = process.env.PREFIX_WORD || '/kibby';
const RE_COMMAND_ARGS = /(?<!\\)"[^(?<!\\)"\n]*"|[\S]+/g;
const RE_COMMAND_ARG_QUOTED = /^".*(?<!\\)"$/g;
const RE_QUOTE_BACKSLASH = /\\"/g;
@ -55,6 +69,9 @@ const COMMANDS = {
'unapprove': 'Send users to probation',
'block': 'Troll a user',
'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',
}
@ -68,9 +85,11 @@ const client = new Client({ });
client.loginBot(process.env.TOKEN);
db.read().then(() => {
db.data ||= { probation: [], blocked: {} };
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');
@ -92,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)',
@ -101,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,
}
@ -252,6 +271,21 @@ const setProbation = async (member: Member, probation: boolean) => {
}
}
const setChild = async (member: Member, child: boolean) => {
if (child) {
if (member.roles?.includes(process.env.NSFW_ROLE!)) {
await member?.edit({ roles: member.roles.filter(r => r != process.env.NSFW_ROLE) });
}
if (!db.data?.children.includes(member._id.user)) {
db.data?.children.push(member._id.user);
await db.write();
}
} else {
if (db.data?.children.includes(member._id.user)) db.data.children = db.data.children.filter((id) => id != member._id.user);
await db.write();
}
}
client.on('message', async (message) => {
try {
if (!message.content || typeof message.content != 'string') return;
@ -508,6 +542,95 @@ client.on('message', async (message) => {
break;
}
case 'child': {
const members = getMembers(message.channel!.server_id);
const users = await extractUsers(message, args);
if (!users) return message.reply({ embeds: [
embed("No users provided", undefined, "ERROR"),
] });
for (const user of users) {
const member = members.find(m => m._id.user == user._id);
await setChild(member!, true);
}
await message.reply({ embeds: [
embed(`The selected user${users.length != 1 ? 's' : ''} have been marked as child.`),
] });
break;
}
case 'adult':
case 'unchild':
case 'notchild': {
const members = getMembers(message.channel!.server_id);
const users = await extractUsers(message, args);
if (!users) return message.reply({ embeds: [
embed("No users provided", undefined, "ERROR"),
] });
for (const user of users) {
const member = members.find(m => m._id.user == user._id);
await setChild(member!, false);
}
await message.reply({ embeds: [
embed(`The selected user${users.length != 1 ? 's' : ''} have been removed from the child list.`),
] });
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();
@ -604,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);
}
}
});