diff --git a/package.json b/package.json index 8b0d6e4..14f5ee3 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,9 @@ "author": "", "license": "ISC", "dependencies": { + "axios": "^1.3.5", "dotenv": "^16.0.3", + "form-data": "^4.0.0", "lowdb": "^3.0.0", "revolt-api": "^0.5.16", "revolt.js": "^6.0.20", diff --git a/src/index.ts b/src/index.ts index 40fe5d3..1ab3658 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,7 @@ import { config } from 'dotenv'; import { SendableEmbed } from 'revolt-api'; import { Low, JSONFile } from 'lowdb'; import { decodeTime } from 'ulid'; +import sherlock from "./sherlock"; config(); @@ -27,7 +28,11 @@ const CommandFlags: CommandFlag[] = [ { name: 'kick', type: 'boolean', - } + }, + { + name: 'nsfw', + type: 'boolean', + }, ]; const PREFIX_WORD = '/kibby'; @@ -38,7 +43,7 @@ const RE_FLAG = /^--\S+(=.*)?$/g; const RE_FLAG_NAME = /^--[^=\s]+(=*?|$)/g; const DB_FILE = process.env.DB_FILE || './db.json'; const RE_USER_MENTION = /^<@[0-9A-HJ-KM-NP-TV-Z]{26}>$/i; -const PUBLIC_COMMANDS = ['suicide', 'status', 'help', 'type']; +const PUBLIC_COMMANDS = ['suicide', 'status', 'help', 'type', 'sherlock']; const COMMANDS = { 'help': 'List available commands', 'status': 'Edit the bot\'s status', @@ -49,6 +54,7 @@ const COMMANDS = { 'unapprove': 'Send users to probation', 'block': 'Troll a user', 'unblock': 'Untroll a user', + 'sherlock': 'Run a sherlock scan on a username', } if (!process.env.TOKEN) throw "$TOKEN not set"; @@ -489,6 +495,11 @@ client.on('message', async (message) => { break; } + case 'sherlock': { + await sherlock(message, args, !!getFlag('nsfw')); + break; + } + case 'help': { const commands = Object.entries(COMMANDS).filter(c => privileged || PUBLIC_COMMANDS.includes(c[0])); @@ -590,3 +601,5 @@ client.on('packet', async (packet) => { console.error(e); } }); + +export { embed } diff --git a/src/sherlock.ts b/src/sherlock.ts new file mode 100644 index 0000000..188d0ca --- /dev/null +++ b/src/sherlock.ts @@ -0,0 +1,176 @@ +import { Message } from "revolt.js"; +import { spawn } from 'child_process'; +import { embed } from "."; +import { SendableEmbed } from "revolt-api"; +import FormData from 'form-data'; +import axios from "axios"; + +let RUNNING: string[] = []; + +export default async function sherlock(message: Message, args: string[], nsfw: boolean) { + const SHERLOCK_COMMAND = (process.env.SHERLOCK_COMMAND || 'python3 sherlock').split(' '); + + if (RUNNING.includes(message.channel_id)) { + return await message.reply({ embeds: [ + embed('Please wait for the previous scan to finish before launching another one!', 'Sherlock', 'ERROR'), + ] }); + } + + return new Promise(async (resolve, reject) => { + try { + if (!args.length) { + await message.reply({ embeds: [ + embed('No username(s) to search for provided.', 'Sherlock', 'ERROR'), + ] }); + return resolve(); + } + + if (args.length > 3) { + await message.reply({ embeds: [ + embed('Please provide no more than 3 usernames.', 'Sherlock', 'ERROR'), + ] }); + return resolve(); + } + + RUNNING.push(message.channel_id); + + const infoSearchingEmbed = embed( + `Looking up \`${args.join('`, `')}\`. This might take a while, please be patient!\n\nIncluding NSFW results: \`${nsfw}\``, + 'Sherlock', + 'INFO', + ); + const msg = await message.reply({ embeds: [ infoSearchingEmbed ] }); + const matches: { user: string, name: string, url: string }[] = []; + const searchedEmbeds: SendableEmbed[] = []; + const attachments: string[] = []; + let currentUser = ''; + + const addEmbed = async (name: string) => { + let text = `Found ${matches.filter(match => match.user == name).length} links!\n`; + for (const match of matches.filter(match => match.user == name)) { + const newLine = args.length > 1 + ? `\n- [${match.name}](${match.url.replaceAll(' ', '%20')})` + : `\n$\\textsf{\\color{#1abc9c}${match.name}}$: ${match.url.replaceAll(' ', '%20')}`; + + if (text.length + newLine.length > 2000) break; + else if (text.length + newLine.length > 1800 / args.length) { + text = 'Message too long - Results will be sent as file'; + attachments.push(await uploadFile( + matches.filter(match => match.user == name) + .map(m => `${m.name}: ${m.url}`) + .join('\n'), + name, + )); + break; + } + else text += newLine; + } + + searchedEmbeds.push(embed( + text, + `Sherlock - ${name}`, + 'SUCCESS' + )); + } + + const previousLength = matches.length; + const interval = setInterval(async () => { + try { + if (matches.length > previousLength) { + await msg?.edit({ embeds: [ + infoSearchingEmbed, + ...searchedEmbeds, + embed( + `Searching: \`${currentUser}\` - ${matches.filter(m => m.user == currentUser).length} found so far`, + `Sherlock - ${currentUser}`, + 'INFO', + ), + ] }); + } + } catch(e) { + // Would be super funny if the errors were actually fucking catched, wouldn't it? + clearInterval(interval); + proc.kill(); + RUNNING = RUNNING.filter(r => r != message.channel_id); + reject(e); + } + }, 3000); + + const cmdArgs = nsfw + ? [ ...SHERLOCK_COMMAND.slice(1), '--nsfw', '--output', '/dev/null', '--', ...args ] + : [ ...SHERLOCK_COMMAND.slice(1), '--output', '/dev/null', '--', ...args ]; + console.log(`Invoking: ${SHERLOCK_COMMAND[0]} ${cmdArgs.join(' ')}`); + + const proc = spawn(`${SHERLOCK_COMMAND[0]}`, cmdArgs, { shell: false }); + + proc.on('exit', async (res) => { + console.log(`[Sherlock] Exited with code ${res}`); + RUNNING = RUNNING.filter(r => r != message.channel_id); + clearInterval(interval); + + if (res != 0) { + await msg?.edit({ embeds: [ + embed(`Sherlock exited with code ${res}.\n\`\`\`\n${stderr}\n\`\`\``, 'Sherlock - Error', 'ERROR'), + ] }); + } + else { + await addEmbed(currentUser); + await msg?.edit({ embeds: searchedEmbeds }); + if (attachments.length) { + await message.channel?.sendMessage({ attachments }); + } + } + return resolve(); + }); + + let stderr = ''; + proc.stderr.on('data', (data) => { + console.log('[Sherlock] [stderr] ' + data.toString('utf8')); + stderr += data.toString('utf8'); + }); + + let stdoutBuf = ''; + proc.stdout.on('data', async (data) => { + stdoutBuf += data.toString('utf8'); + + if (stdoutBuf.includes('\n')) { + const buf = stdoutBuf.split('\n'); + stdoutBuf = buf.pop() ?? ''; + + for (let line of buf) { + console.log('[Sherlock] [stdout] ' + line); + if (line.startsWith('[+]')) { + line = line.substring(3).trimStart(); + const [name, url] = line.split(': '); + matches.push({ user: currentUser, name, url }); + } + else if (line.startsWith('[*] Checking username')) { + line = line.substring('[*] Checking username'.length, line.length - 'on:'.length).trim(); + + if (currentUser) { + await addEmbed(currentUser); + } + + currentUser = line; + console.log('[Sherlock] Now scanning user:', currentUser); + } + } + } + }); + + } catch(e) { + reject(e); + RUNNING = RUNNING.filter(r => r != message.channel_id); + } + }); +} + +async function uploadFile(file: any, filename: string): Promise { + let data = new FormData(); + data.append("file", file, { filename: filename }); + + let req = await axios.post("https://autumn.revolt.chat/attachments", data, { + headers: data.getHeaders(), + }); + return (req.data as any)["id"] as string; +} diff --git a/yarn.lock b/yarn.lock index 8dde6ca..a299407 100644 --- a/yarn.lock +++ b/yarn.lock @@ -32,6 +32,11 @@ argparse@^2.0.1: resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + axios@^0.21.4: version "0.21.4" resolved "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz" @@ -46,6 +51,15 @@ axios@^0.26.1: dependencies: follow-redirects "^1.14.8" +axios@^1.3.5: + version "1.3.5" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.3.5.tgz#e07209b39a0d11848e3e341fa087acd71dadc542" + integrity sha512-glL/PvG/E+xCWwV8S6nCHcrfg1exGx7vxyUIivIA1iL7BIh6bePylCfVHwp6k13ao7SATxB6imau2kqY+I67kw== + dependencies: + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + busboy@^1.6.0: version "1.6.0" resolved "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz" @@ -53,6 +67,18 @@ busboy@^1.6.0: dependencies: streamsearch "^1.1.0" +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + dotenv@^16.0.3: version "16.0.3" resolved "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz" @@ -63,11 +89,20 @@ eventemitter3@^4.0.7: resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== -follow-redirects@^1.14.0, follow-redirects@^1.14.8: +follow-redirects@^1.14.0, follow-redirects@^1.14.8, follow-redirects@^1.15.0: version "1.15.2" resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz" integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + globalyzer@0.1.0: version "0.1.0" resolved "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz" @@ -112,6 +147,18 @@ lowdb@^3.0.0: dependencies: steno "^2.1.0" +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + mime@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz" @@ -139,6 +186,11 @@ prettier@^2.6.2: resolved "https://registry.npmjs.org/prettier/-/prettier-2.8.4.tgz" integrity sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw== +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + revolt-api@0.5.7: version "0.5.7" resolved "https://registry.npmjs.org/revolt-api/-/revolt-api-0.5.7.tgz"