Red-DiscordBot/redbot/core/dev_commands.py
Will d69fd63da7 [V3 Everything] Package bot and write setup scripts (#964)
Ya'll are gonna hate me.

* Initial modifications

* Add initial setup.py

* working setup py help

* Modify setup file to package stuff

* Move a bunch of shit and fix imports

* Fix or skip tests

* Must add init files for find_packages to work

* Move main to scripts folder and rename

* Add shebangs

* Copy over translation files

* WORKING PIP INSTALL

* add dependency information

* Hardcoded version for now, will need to figure out a better way to do this

* OKAY ITS FINALLY FUCKING WORKING

* Add this guy

* Fix stuff

* Change readme to rst

* Remove double sentry opt in

* Oopsie

* Fix this thing

* Aaaand fix test

* Aaaand fix test

* Fix core cog importing and default cog install path

* Adjust readme

* change instance name from optional to required

* Ayyy let's do more dependency injection
2017-09-08 23:14:32 -04:00

272 lines
8.4 KiB
Python

import asyncio
import inspect
import io
import textwrap
import traceback
from contextlib import redirect_stdout
import discord
from . import checks
from .i18n import CogI18n
from discord.ext import commands
from .utils.chat_formatting import box, pagify
"""
Notice:
95% of the below code came from R.Danny which can be found here:
https://github.com/Rapptz/RoboDanny/blob/master/cogs/repl.py
"""
_ = CogI18n("Dev", __file__)
class Dev:
"""Various development focused utilities"""
def __init__(self):
self._last_result = None
self.sessions = set()
@staticmethod
def cleanup_code(content):
"""Automatically removes code blocks from the code."""
# remove ```py\n```
if content.startswith('```') and content.endswith('```'):
return '\n'.join(content.split('\n')[1:-1])
# remove `foo`
return content.strip('` \n')
@staticmethod
def get_syntax_error(e):
if e.text is None:
return '```py\n{0.__class__.__name__}: {0}\n```'.format(e)
return '```py\n{0.text}{1:>{0.offset}}\n{2}: {0}```'.format(e, '^', type(e).__name__)
@staticmethod
def sanitize_output(ctx: commands.Context, input: str) -> str:
token = ctx.bot.http.token
r = "[EXPUNGED]"
result = input.replace(token, r)
result = result.replace(token.lower(), r)
result = result.replace(token.upper(), r)
return result
@commands.command()
@checks.is_owner()
async def debug(self, ctx, *, code):
"""
Executes code and prints the result to discord.
"""
env = {
'bot': ctx.bot,
'ctx': ctx,
'channel': ctx.channel,
'author': ctx.author,
'guild': ctx.guild,
'message': ctx.message
}
code = self.cleanup_code(code)
try:
result = eval(code, env, locals())
except SyntaxError as e:
await ctx.send(self.get_syntax_error(e))
return
except Exception as e:
await ctx.send('```py\n{}: {}```'.format(type(e).__name__, str(e)), )
return
if asyncio.iscoroutine(result):
result = await result
result = str(result)
result = self.sanitize_output(ctx, result)
await ctx.send(box(result, lang="py"))
@commands.command(name='eval')
@checks.is_owner()
async def _eval(self, ctx, *, body: str):
"""
Executes code as if it was the body of an async function
code MUST be in a code block using three ticks and
there MUST be a newline after the first set and
before the last set. This function will ONLY output
the return value of the function code AND anything
that is output to stdout (e.g. using a print()
statement).
"""
env = {
'bot': ctx.bot,
'ctx': ctx,
'channel': ctx.channel,
'author': ctx.author,
'guild': ctx.guild,
'message': ctx.message,
'_': self._last_result
}
env.update(globals())
body = self.cleanup_code(body)
stdout = io.StringIO()
to_compile = 'async def func():\n%s' % textwrap.indent(body, ' ')
try:
exec(to_compile, env)
except SyntaxError as e:
return await ctx.send(self.get_syntax_error(e))
func = env['func']
try:
with redirect_stdout(stdout):
ret = await func()
except:
value = stdout.getvalue()
await ctx.send(box('\n{}{}'.format(value, traceback.format_exc()), lang="py"))
else:
value = stdout.getvalue()
try:
await ctx.bot.add_reaction(ctx.message, '\u2705')
except:
pass
if ret is None:
if value:
value = self.sanitize_output(ctx, value)
await ctx.send(box(value, lang="py"))
else:
ret = self.sanitize_output(ctx, ret)
self._last_result = ret
await ctx.send(box("{}{}".format(value, ret), lang="py"))
@commands.command()
@checks.is_owner()
async def repl(self, ctx):
"""
Opens an interactive REPL.
"""
variables = {
'ctx': ctx,
'bot': ctx.bot,
'message': ctx.message,
'guild': ctx.guild,
'channel': ctx.channel,
'author': ctx.author,
'_': None,
}
if ctx.channel.id in self.sessions:
await ctx.send(_('Already running a REPL session in this channel. Exit it with `quit`.'))
return
self.sessions.add(ctx.channel.id)
await ctx.send(_('Enter code to execute or evaluate. `exit()` or `quit` to exit.'))
def msg_check(m):
return m.author == ctx.author and m.channel == ctx.channel and \
m.content.startswith('`')
while True:
response = await ctx.bot.wait_for(
"message",
check=msg_check)
cleaned = self.cleanup_code(response.content)
if cleaned in ('quit', 'exit', 'exit()'):
await ctx.send('Exiting.')
self.sessions.remove(ctx.channel.id)
return
executor = exec
if cleaned.count('\n') == 0:
# single statement, potentially 'eval'
try:
code = compile(cleaned, '<repl session>', 'eval')
except SyntaxError:
pass
else:
executor = eval
if executor is exec:
try:
code = compile(cleaned, '<repl session>', 'exec')
except SyntaxError as e:
await ctx.send(self.get_syntax_error(e))
continue
variables['message'] = response
stdout = io.StringIO()
msg = None
try:
with redirect_stdout(stdout):
result = executor(code, variables)
if inspect.isawaitable(result):
result = await result
except:
value = stdout.getvalue()
value = self.sanitize_output(ctx, value)
msg = "{}{}".format(value, traceback.format_exc())
else:
value = stdout.getvalue()
if result is not None:
msg = "{}{}".format(value, result)
variables['_'] = result
elif value:
msg = "{}".format(value)
try:
for page in pagify(str(msg), shorten_by=12):
page = self.sanitize_output(ctx, page)
await ctx.send(box(page, "py"))
except discord.Forbidden:
pass
except discord.HTTPException as e:
await ctx.send(_('Unexpected error: `{}`').format(e))
@commands.command()
@checks.is_owner()
async def mock(self, ctx, user: discord.Member, *, command):
"""Runs a command as if it was issued by a different user
The prefix must not be entered"""
# Since we have stateful objects now this might be pretty bad
# Sorry Danny
old_author = ctx.author
old_content = ctx.message.content
ctx.message.author = user
ctx.message.content = ctx.prefix + command
await ctx.bot.process_commands(ctx.message)
ctx.message.author = old_author
ctx.message.content = old_content
@commands.command(name="mockmsg")
@checks.is_owner()
async def mock_msg(self, ctx, user: discord.Member, *, content: str):
"""Bot receives a message is if it were sent by a different user.
Only reads the raw content of the message. Attachments, embeds etc. are ignored."""
old_author = ctx.author
old_content = ctx.message.content
ctx.message.author = user
ctx.message.content = content
ctx.bot.dispatch("message", ctx.message)
await asyncio.sleep(2) # If we change the author and content back too quickly,
# the bot won't process the mocked message in time.
ctx.message.author = old_author
ctx.message.content = old_content