diff options
author | romangraef <roman.graef@gmail.com> | 2018-07-04 14:08:56 +0200 |
---|---|---|
committer | romangraef <roman.graef@gmail.com> | 2018-07-04 14:08:56 +0200 |
commit | 168a8ed88b48b539f09bc77bfad3122e7a986675 (patch) | |
tree | b4e3d2e4cb725fd9aa35ad999fbba7682de12d26 | |
download | evalbot-168a8ed88b48b539f09bc77bfad3122e7a986675.tar.gz evalbot-168a8ed88b48b539f09bc77bfad3122e7a986675.tar.bz2 evalbot-168a8ed88b48b539f09bc77bfad3122e7a986675.zip |
Initial commit
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | README.md | 24 | ||||
-rw-r--r-- | compile_api.py | 45 | ||||
-rw-r--r-- | config.py | 14 | ||||
-rw-r--r-- | main.py | 11 | ||||
-rw-r--r-- | modules/admin.py | 119 | ||||
-rw-r--r-- | modules/execute.py | 114 | ||||
-rw-r--r-- | requirements.txt | 3 | ||||
-rw-r--r-- | util.py | 11 |
9 files changed, 343 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..60a913f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +venv/ +config.ini diff --git a/README.md b/README.md new file mode 100644 index 0000000..4a2034d --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +EVALBOT +------ + +Discord Code eval bot using the [Compilebot][compilebot] API. + +### SETUP + - Create a discord bot account at [the discord developers page][discord-devs]. **IMPORTANT** Create a bot user as well. + - Go to the [Compilebot][compilebot] website and subscribe to a plan. + - clone the repository and create a [Virtual Environment][venv] + - Install all the requirements via `pip install -r requirements.txt` + - Create a `config.ini` in the following format: +```ini +[discord] +token = YOURDISCORDTOKEN. DISCORD! TOKEN! NOT ID OR SECRET + +[jdoodle] +id = yourcompilebotid +secret = yourcompilebotsecret +``` + - Launch the bot via `python main.py`. If you want to run the bot permanently i recommend using `tmux` or `screen`. + +[compilebot]: https://www.jdoodle.com/compiler-api/ +[discord-devs]: https://discordapp.com/developers/applications/me +[venv]: https://docs.python.org/3/library/venv.html diff --git a/compile_api.py b/compile_api.py new file mode 100644 index 0000000..5da9f02 --- /dev/null +++ b/compile_api.py @@ -0,0 +1,45 @@ +import asyncio +from dataclasses import dataclass + +import aiohttp + +from config import jdoodle_id, jdoodle_secret + +EXECUTE_ENDPOINT = "https://api.jdoodle.com/v1/execute" + + +def get_parameters(**kwargs): + return dict( + clientId=jdoodle_id, + clientSecret=jdoodle_secret, + **kwargs, + ) + + +def post(url, data=None, json=None, **kwargs): + return asyncio.get_event_loop().run_in_executor(None, lambda *_: requests.post(url, data, json, **kwargs)) + + +@dataclass +class ExecuteResponse(object): + output: str + cpu_time: float + memory: int + status_code: int + + +def parse_execute_response(response: dict) -> ExecuteResponse: + memory = response['memory'] + output = response['output'] + cpu_time = response['cpuTime'] + status_code = response['statusCode'] + return ExecuteResponse(output, cpu_time, memory, status_code) + + +async def execute(code: str, language: str, version: str) -> ExecuteResponse: + async with aiohttp.ClientSession() as session: + response = await session.post(EXECUTE_ENDPOINT, json=get_parameters( + script=code, + language=language, + versionIndex=version)) + return parse_execute_response(await response.json()) diff --git a/config.py b/config.py new file mode 100644 index 0000000..6595e98 --- /dev/null +++ b/config.py @@ -0,0 +1,14 @@ +from configparser import ConfigParser + +config = ConfigParser() +config.read('config.ini') + +discord = config['discord'] + +token = discord['token'] + + +jdoodle = config['jdoodle'] + +jdoodle_secret = jdoodle['secret'] +jdoodle_id = jdoodle['id'] @@ -0,0 +1,11 @@ +from discord.ext.commands import Bot, when_mentioned_or + +from config import token +from util import load_all_modules + +bot = Bot(command_prefix=when_mentioned_or('!')) + +load_all_modules(bot) + +if __name__ == '__main__': + bot.run(token) diff --git a/modules/admin.py b/modules/admin.py new file mode 100644 index 0000000..e1d2921 --- /dev/null +++ b/modules/admin.py @@ -0,0 +1,119 @@ +import re +from typing import List, Dict, Pattern + +import discord +from discord import Embed, Color +from discord.ext import commands +from discord.ext.commands import Context as CommandContext, Bot, is_owner, command + +from util import load_all_modules + +REPLACEMENTS: Dict[Pattern, str] = { + re.compile(r'<@!?(?P<id>[0-9]+)>'): '(guild.get_member({id}) if guild is not None else client.get_user({id}))', + re.compile(r'<#(?P<id>[0-9]+)>'): '(discord.utils.get(all_channels, id={id}))', + re.compile(r'<@&(?P<id>[0-9]+)>'): '(discord.utils.get(all_roles, id={id}))', + # Maybe later emoji support +} + + +async def handle_eval(message: discord.Message, client: discord.Client, to_eval: str): + channel: discord.TextChannel = message.channel + author: discord.Member = message.author + + all_channels: List[discord.Guild] = [] + all_roles: List[discord.Role] = [] + for guild in client.guilds: + guild: discord.Guild = guild # for type hints + all_channels += guild.channels + all_roles += guild.roles + + variables = { + 'message': message, + 'author': author, + 'channel': channel, + 'all_channels': all_channels, + 'all_roles': all_roles, + 'client': client, + 'discord': discord, + 'print': (lambda *text: client.loop.create_task(channel.send(' '.join(text)))) + } + if channel.guild is not None: + variables['guild'] = channel.guild + lines: List[str] = to_eval.strip().split('\n') + lines[-1] = 'return ' + lines[-1] + block: str = '\n'.join(' ' + line for line in lines) + code = f"async def code({', '.join(variables.keys())}):\n" \ + f"{block}" + + for regex, replacement in REPLACEMENTS.items(): + code = re.sub(regex, lambda match: replacement.format(**match.groupdict()), code) + + _globals, _locals = {}, {} + try: + exec(code, _globals, _locals) + except Exception as e: + await message.channel.send( + embed=discord.Embed(color=discord.Color.red(), description="Compiler Error: `%s`" % (str(e)))) + return + result = {**_globals, **_locals} + try: + result = await result["code"](**variables) + except Exception as e: + await message.channel.send( + embed=discord.Embed(color=discord.Color.red(), description="Runtime Error: `%s`" % (str(e)))) + return + + return await channel.send( + embed=Embed( + color=Color.red(), + description="📥 Evaluation success: ```py\n%r\n```" % result)) + + +class AdminCog(object): + def __init__(self, bot: commands.Bot): + self.bot: commands.Bot = bot + + @command() + @is_owner() + async def eval(self, ctx: CommandContext, *, to_eval: str = None): + if to_eval is None: + return await ctx.send( + embed=Embed( + description="<Insert generic insult about your stupidity here>", + color=Color.red())) + await handle_eval(ctx.message, self.bot, to_eval) + + async def on_ready(self): + print('-' * 50) + print('Name: ' + self.bot.user.name) + print('Guilds: ' + ', '.join(guild.name for guild in self.bot.guilds)) + print('-' * 50) + + @command() + @is_owner() + async def reload(self, ctx: CommandContext, *extensions): + for extension in (extensions or self.bot.extensions.copy().keys()): + self.bot.unload_extension(extension) + await ctx.send( + embed=Embed( + color=Color.red(), + description='Unloaded extensions', )) + if len(extensions) == 0: + load_all_modules(self.bot) + else: + for extension in extensions: + try: + self.bot.load_extension(extension) + except: + await ctx.send( + embed=Embed( + title=f"Failed to load module `{extension}`", + color=Color.red())) + await ctx.send( + embed=Embed( + title=f"Reloaded {len(extensions) or len(self.bot.extensions)} extension(s)", + color=Color.green())) + + +def setup(bot: Bot): + bot.add_cog(AdminCog(bot)) diff --git a/modules/execute.py b/modules/execute.py new file mode 100644 index 0000000..6127647 --- /dev/null +++ b/modules/execute.py @@ -0,0 +1,114 @@ +import re +from collections import defaultdict +from datetime import datetime, timedelta +from typing import Pattern + +from discord import Embed, Color, Message, Guild, TextChannel, Member +from discord.ext.commands import Bot + +from compile_api import execute + +CODE_BLOCK_REGEX: Pattern = re.compile("```(?P<lang>.*)\n(?P<code>[\\s\\S]*?)```") +INPUT_BLOCK_REGEX: Pattern = re.compile("input[: \t\n]*```(?P<lang>.*)?\n(?P<text>[\\s\\S]*?)```", re.IGNORECASE) + +PYTHON_3 = ('python3', 2) +NODEJS = ('nodejs', 2) +C_LANG = ('c', 3) +CPP = ('cpp14', 2) +PHP = ('php', 2) +PYTHON_2 = ('python2', 1) +RUBY = ('ruby', 2) +GO_LANG = ('go', 2) +SCALA = ('scala', 2) +BASH = ('bash', 2) +CSHARP = ('csharp', 2) +HASKELL = ('haskell', 2) +BRAINFUCK = ('brainfuck', 0) +LUA = ('lua', 1) +DART = ('dart', 2) +KOTLIN = ('kotlin', 1) + +languages = { + 'kt': KOTLIN, + 'kotlin': KOTLIN, + 'dart': DART, + 'dt': DART, + 'lua': LUA, + 'py': PYTHON_3, + 'python': PYTHON_3, + 'js': NODEJS, + 'javascript': NODEJS, + 'c': C_LANG, + 'c++': CPP, + 'cpp': CPP, + 'py2': PYTHON_2, + 'go': GO_LANG, + 'scala': SCALA, + 'sc': SCALA, + 'bash': BASH, + 'hs': HASKELL, + 'haskell': HASKELL, + 'brainfuck': BRAINFUCK, + 'bf': BRAINFUCK, +} + + +class ExecuteCog(object): + def __init__(self, bot: Bot): + self.bot: Bot = bot + self.last_messaged = defaultdict(lambda: datetime.fromtimestamp(0)) + + # noinspection PyMethodMayBeStatic + async def on_message(self, message: Message): + if message.guild is None: + return + content: str = message.content + guild: Guild = message.guild + author: Member = message.author + channel: TextChannel = message.channel + if guild.me not in message.mentions: + return + code = "" + lang = "" + for match in CODE_BLOCK_REGEX.finditer(content): + code = match.group('code') + lang = match.group('lang') + break + if lang is "" or code is "": + return + inp = "" + for match in INPUT_BLOCK_REGEX.finditer(content): + inp += match.group("text") + '\n' + last = self.last_messaged[author.id] + delta = datetime.now() - last + if delta < timedelta(seconds=30): + return await channel.send( + embed=Embed( + description=f"You are not allowed to eval code again. Check again in " + f"{(timedelta(seconds=30)-delta).seconds}secs")) + if not author.guild_permissions.manage_messages: + self.last_messaged[author.id] = datetime.now() + response = await execute(code, *languages[lang]) + if response.status_code == 429: + return await channel.send( + embed=Embed( + color=Color.blurple(), + description="The daily ratelimit for our API is reached. A great alternative is [Ideone](" + "https://ideone.com/) or [Repl.it](https://repl.it/)")) + if response.status_code == 401: + return await channel.send( + embed=Embed( + color=Color.red(), + description="Our API credentials are invalid. Please contact the bot owner")) + memory = response.memory + output = response.output + cpu_time = response.cpu_time + await channel.send( + embed=Embed( + title="Executed your code", + description=f"```\n{output}```" + ).set_footer(text=f"Executed in {cpu_time}s. Memory: {memory}")) + + +def setup(bot: Bot): + bot.add_cog(ExecuteCog(bot)) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..64e0f99 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +aiohttp +git+https://github.com/Rapptz/discord.py@rewrite +git+https://github.com/aaugustin/websockets
\ No newline at end of file @@ -0,0 +1,11 @@ +import os + +from discord.ext.commands import Bot + + +def load_all_modules(bot: Bot, module_folder='modules', module_package=None): + if module_package is None: + module_package = module_folder.replace('/', '.') + for module in os.listdir(module_folder): + if module.endswith('.py') and not module.startswith('_'): + bot.load_extension(module_package + '.' + module[:-3]) |