aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorromangraef <roman.graef@gmail.com>2018-07-04 14:08:56 +0200
committerromangraef <roman.graef@gmail.com>2018-07-04 14:08:56 +0200
commit168a8ed88b48b539f09bc77bfad3122e7a986675 (patch)
treeb4e3d2e4cb725fd9aa35ad999fbba7682de12d26
downloadevalbot-168a8ed88b48b539f09bc77bfad3122e7a986675.tar.gz
evalbot-168a8ed88b48b539f09bc77bfad3122e7a986675.tar.bz2
evalbot-168a8ed88b48b539f09bc77bfad3122e7a986675.zip
Initial commit
-rw-r--r--.gitignore2
-rw-r--r--README.md24
-rw-r--r--compile_api.py45
-rw-r--r--config.py14
-rw-r--r--main.py11
-rw-r--r--modules/admin.py119
-rw-r--r--modules/execute.py114
-rw-r--r--requirements.txt3
-rw-r--r--util.py11
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']
diff --git a/main.py b/main.py
new file mode 100644
index 0000000..caf02e6
--- /dev/null
+++ b/main.py
@@ -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
diff --git a/util.py b/util.py
new file mode 100644
index 0000000..cf80109
--- /dev/null
+++ b/util.py
@@ -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])