diff --git a/.gitignore b/.gitignore index 9fab2e3..7920a86 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules .env -.idea/ \ No newline at end of file +.idea/ +data/ \ No newline at end of file diff --git a/commands/eval.js b/commands/eval.js new file mode 100644 index 0000000..cfe9f78 --- /dev/null +++ b/commands/eval.js @@ -0,0 +1,124 @@ +const Revolt = require('revolt.js'); +const { client, logger, config } = require('..'); +const { exec } = require('child_process'); + +let evalEnabled = []; +let OTP = []; +let OTPTimer = []; + +module.exports = { + OTP, evalEnabled, OTPTimer +} + +module.exports.meta = { + name: 'eval', + aliases: [], + description: 'Evaluate code.', + devLevel: 99 +} + +/** + * + * @param { Revolt.Message } message + * @param { string[] } args + */ +module.exports.run = async (message, args) => new Promise(async (resolve, reject) => { + if (!evalEnabled[message.author] && process.env['DISABLE_EVAL_OTP'] != 'true') { + OTP[message.author] = createOTP(); + client.channels.sendMessage(message.channel, `OTP has been printed to console. Use \`${config.prefix}verify [code]\` to verify.`); + + console.log(`Admin authentication required`); + console.log(`OTP: \u001b[31;1m${OTP[message.author]}\u001b[0m`); + console.log(`Code expires in 120 seconds.`); + + if (OTPTimer[message.author]) { + console.log('Old OTP code has been invalidated.'); + clearTimeout(OTPTimer[message.author]); + } + OTPTimer[message.author] = setTimeout(() => { + console.log(`OTP Code \u001b[31;1m${OTP[message.author]}\u001b[0m has been invalidated.`); + OTP[message.author] = null; + }, 120000); + return; + } + + let command = args.join(' '); + if (command.length == 0) return client.channels.sendMessage(message.channel, `❌ No command provided`); + if (command.startsWith('js:')) { + // Execute javascript + command = command.substring(3); + if (command.length == 0) return client.channels.sendMessage(message.channel, `❌ No command provided`); + let mesg = await client.channels.sendMessage(message.channel, `Evaluating JavaScript...`); + + let parseOutput = (output) => { + if (output instanceof Error) return `${output.name || 'Error'} : ${output.message}`; + if (output instanceof Object) return `${JSON.stringify(output, null, 4)}`; + return `${output}`; + } + + try { + let output = eval(command); + if (output instanceof Promise) { + client.channels.editMessage(message.channel, mesg._id, { content: `\`\`\`js\nPromise : Pending\n\`\`\`` }); + output + .then(val => { + client.channels.editMessage(message.channel, mesg._id, { content: `\`\`\`js\nPromise : Resolved\n${parseOutput(val)}\n\`\`\`` }); + }) + .catch(val => { + client.channels.editMessage(message.channel, mesg._id, { content: `\`\`\`js\nPromise : Rejected\n${parseOutput(val)}\n\`\`\`` }); + }); + } else { + client.channels.editMessage(message.channel, mesg._id, { content: `\`\`\`js\n${parseOutput(output)}\n\`\`\`` }); + } + } catch(e) { + client.channels.editMessage(message.channel, mesg._id, { content: `\`\`\`js\n${parseOutput(e)}\n\`\`\`` }); + } + } else { + let mesg = await client.channels.sendMessage(message.channel, `Executing: \`${command}\``); + + let outTxt = ''; + let cOutTxt = ''; + let prevTxt = ''; + let cutLetters = 0; + let exited = false; + let updateMsg = (msg, exitCode) => { + outTxt += `${msg}` + .replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '') + .replace(/`/g, '\u200b`'); + while (outTxt.length >= 1920) { + outTxt = outTxt.slice(2, outTxt.length); + cutLetters++; + } + + let charCutTxt = ''; + if (cutLetters > 0) charCutTxt = `${cutLetters} character${cutLetters == 1 ? ' has' : 's have'} been cut\n`; + let exitText = ''; + if (exitCode != null) { + exitText = `\nExited with code ${exitCode}`; + exited = true; + } + + cOutTxt = `${charCutTxt}\`\`\`bash\n${outTxt.trimRight() || 'No output'}\n\`\`\`${exitText}`; + } + + let execution = exec(command); + execution.stdout.on('data', updateMsg); + execution.stderr.on('data', updateMsg); + execution.once('exit', (exitCode) => { + updateMsg('', exitCode); + }); + + let interval = setInterval(() => { + if (cOutTxt != prevTxt) { + prevTxt = cOutTxt; + client.channels.editMessage(message.channel, mesg._id, { content: cOutTxt }); + } + }, 200); + } +}); +// $\color{red}\text{h}$ +function createOTP() { + let code = ''; + for (let i = 0; i < 6; i++) code += Math.round(Math.random() * 10); + return code.substr(0, 6); +} \ No newline at end of file diff --git a/commands/evaloff.js b/commands/evaloff.js new file mode 100644 index 0000000..7796c58 --- /dev/null +++ b/commands/evaloff.js @@ -0,0 +1,30 @@ +const Revolt = require('revolt.js'); +const { client, logger, config } = require('..'); + +const { OTP, evalEnabled, OTPTimer } = require('./eval'); + +module.exports.meta = { + name: 'evaloff', + aliases: [], + description: 'Disable eval functionality.', + devLevel: 99 +} + +/** + * + * @param { Revolt.Message } message + * @param { string[] } args + */ +module.exports.run = async (message, args) => new Promise(async (resolve, reject) => { + if (OTPTimer[message.author]) clearTimeout(OTPTimer[message.author]); + if (evalEnabled[message.author]) { + evalEnabled[message.author] = null; + OTP[message.author] = null; + return client.channels.sendMessage(message.channel, 'Access has been revoked.'); + } + if (OTP[message.author]) { + OTP[message.author] = null; + return client.channels.sendMessage(message.channel, 'Existing OTP has been invalidated.'); + } + return client.channels.sendMessage(message.channel, 'You were not authenticated.'); +}); \ No newline at end of file diff --git a/commands/verify.js b/commands/verify.js new file mode 100644 index 0000000..af63b1d --- /dev/null +++ b/commands/verify.js @@ -0,0 +1,51 @@ +const Revolt = require('revolt.js'); +const { client, logger, config } = require('..'); + +const { OTP, evalEnabled, OTPTimer } = require('./eval'); + +const banIntervals = [ 5, 10, 15, 30, 60, 120, 300 ]; +const failedAttempts = []; +const failedTimeouts = []; + +module.exports.meta = { + name: 'verify', + aliases: [ 'v' ], + description: 'Verify dev actions.', + devLevel: 1 +} + +/** + * + * @param { Revolt.Message } message + * @param { string[] } args + */ +module.exports.run = async (message, args) => new Promise(async (resolve, reject) => { + if (failedAttempts[message.author] == null) failedAttempts[message.author] = 0; + + if (failedAttempts[message.author] > 0 && failedTimeouts[message.author] != null) { + let timeout = (banIntervals[failedAttempts[message.author] - 1] ?? Infinity) * 1000; + if (failedTimeouts[message.author] + timeout > Date.now()) { + if (timeout == Infinity) + return client.channels.sendMessage(message.channel, `❌ Blocked`); + else + return client.channels.sendMessage(message.channel, `❌ Blocked for ${timeout / 1000} seconds`); + } + } + + let otp = OTP[message.author]; + let code = args[0]; + if (!otp) return client.channels.sendMessage(message.channel, 'There is nothing to verify right now.'); + if (!code) return client.channels.sendMessage(message.channel, 'You need to specify a code.'); + if (code != otp) { + failedAttempts[message.author]++; + failedTimeouts[message.author] = Date.now(); + return client.channels.sendMessage(message.channel, '❌ Invalid code'); + } + + logger.warn('Enabled eval for ' + message.author); + + clearTimeout(OTPTimer[message.author]); + evalEnabled[message.author] = true; + + client.channels.sendMessage(message.channel, `Authenticated - Use \`${config.prefix}evaloff\` to disable`); +}); \ No newline at end of file