From 24ec7805160ab3280381ad6b6a8fefda88b33052 Mon Sep 17 00:00:00 2001 From: Masterjoona <69722179+Masterjoona@users.noreply.github.com> Date: Fri, 16 Aug 2024 13:33:34 +0000 Subject: [PATCH 01/17] Support user app installation context --- src/bot.py | 26 ++-- src/cogs/error_handler.py | 24 ++-- src/cogs/management.py | 38 +++--- src/cogs/run.py | 262 +++++------------------------------ src/cogs/user_commands.py | 128 ++++++++++++++++++ src/cogs/utils/codeswap.py | 18 +-- src/cogs/utils/runner.py | 270 +++++++++++++++++++++++++++++++++++++ 7 files changed, 490 insertions(+), 276 deletions(-) create mode 100644 src/cogs/user_commands.py create mode 100644 src/cogs/utils/runner.py diff --git a/src/bot.py b/src/bot.py index e9d6151..97445bf 100644 --- a/src/bot.py +++ b/src/bot.py @@ -1,26 +1,26 @@ -"""PistonBot +"""PistonBot""" -""" -import json import sys import traceback +from discord import AllowedMentions, Intents +from discord.ext.commands.bot import when_mentioned_or +from discord.ext.commands import AutoShardedBot, Context +from discord import Activity, Guild +import json from datetime import datetime, timezone from os import path, listdir -from discord.ext.commands import AutoShardedBot, Context -from discord import Activity, AllowedMentions, Intents +from cogs.utils.runner import Runner from aiohttp import ClientSession, ClientTimeout -from discord.ext.commands.bot import when_mentioned_or class PistonBot(AutoShardedBot): def __init__(self, *args, **options): super().__init__(*args, **options) - self.session = None with open('../state/config.json') as conffile: self.config = json.load(conffile) self.last_errors = [] - self.recent_guilds_joined = [] - self.recent_guilds_left = [] + self.recent_guilds_joined: list[tuple[str, Guild]] = [] + self.recent_guilds_left: list[tuple[str, Guild]] = [] self.default_activity = Activity(name='emkc.org/run | ./run', type=0) self.error_activity = Activity(name='!emkc.org/run | ./run', type=0) self.maintenance_activity = Activity(name='undergoing maintenance', type=0) @@ -29,6 +29,7 @@ def __init__(self, *args, **options): async def start(self, *args, **kwargs): self.session = ClientSession(timeout=ClientTimeout(total=15)) + self.runner = Runner(self.config['emkc_key'], self.session) await super().start(*args, **kwargs) async def close(self): @@ -52,9 +53,8 @@ async def setup_hook(self): exc = f'{type(e).__name__}: {e}' print(f'Failed to load extension {extension}\n{exc}') - - def user_is_admin(self, user): - return user.id in self.config['admins'] + def user_is_admin(self, user_id: int): + return user_id in self.config['admins'] async def log_error(self, error, error_source=None): is_context = isinstance(error_source, Context) @@ -93,7 +93,7 @@ async def on_message(msg): prefixes = await client.get_prefix(msg) for prefix in prefixes: if msg.content.lower().startswith(f'{prefix}run'): - msg.content = msg.content.replace(f'{prefix}run', f'/run', 1) + msg.content = msg.content.replace(f'{prefix}run', '/run', 1) break await client.process_commands(msg) diff --git a/src/cogs/error_handler.py b/src/cogs/error_handler.py index d629c12..cf26a95 100644 --- a/src/cogs/error_handler.py +++ b/src/cogs/error_handler.py @@ -6,6 +6,7 @@ - traceback print traceback of stored error """ + # pylint: disable=E0402 import traceback import typing @@ -24,12 +25,15 @@ def __init__(self, client): # Error handler # ---------------------------------------------- @commands.Cog.listener() - async def on_command_error(self, ctx, error): + async def on_command_error(self, ctx: commands.Context, error): if isinstance(error, commands.CommandNotFound): return if not isinstance(ctx.channel, DMChannel): - perms = ctx.channel.permissions_for(ctx.guild.get_member(self.client.user.id)) + if ctx.guild is None: + return + self_bot = ctx.guild.get_member(self.client.user.id) + perms = ctx.channel.permissions_for(self_bot) try: if not perms.send_messages: await ctx.author.send("I don't have permission to write in this channel.") @@ -125,7 +129,7 @@ async def on_command_error(self, ctx, error): hidden=True, aliases=['errors'] ) - async def error(self, ctx, n: typing.Optional[int] = None): + async def error(self, ctx: commands.Context, n: typing.Optional[int] = None): """Show a concise list of stored errors""" if n is not None: @@ -151,10 +155,10 @@ async def error(self, ctx, n: typing.Optional[int] = None): + call_info + f']\nException: {str(exc)[:200]}' ) - if i % NUM_ERRORS_PER_PAGE == NUM_ERRORS_PER_PAGE-1: + if i % NUM_ERRORS_PER_PAGE == NUM_ERRORS_PER_PAGE - 1: response.append('```') await ctx.send('\n'.join(response)) - response = [f'```css'] + response = ['```css'] if len(response) > 1: response.append('```') await ctx.send('\n'.join(response)) @@ -163,9 +167,9 @@ async def error(self, ctx, n: typing.Optional[int] = None): name='clear', aliases=['delete'], ) - async def error_clear(self, ctx, n: int = None): + async def error_clear(self, ctx, n: int = 0): """Clear error with index [n]""" - if n is None: + if n == 0: self.client.last_errors = [] await ctx.send('Error log cleared') else: @@ -179,18 +183,18 @@ async def error_clear(self, ctx, n: int = None): name='traceback', aliases=['tb'], ) - async def error_traceback(self, ctx, n: int = None): + async def error_traceback(self, ctx: commands.Context, n: int = 0): """Print the traceback of error [n] from the error log""" await self.print_traceback(ctx, n) - async def print_traceback(self, ctx, n): + async def print_traceback(self, ctx: commands.Context, n: int): error_log = self.client.last_errors if not error_log: await ctx.send('Error log is empty') return - if n is None: + if n == 0: await ctx.send('Please specify an error index') await self.client.get_command('error').invoke(ctx) return diff --git a/src/cogs/management.py b/src/cogs/management.py index da9def0..cb15038 100644 --- a/src/cogs/management.py +++ b/src/cogs/management.py @@ -8,6 +8,7 @@ cogs show currently active extensions / cogs error print the traceback of the last unhandled error to chat """ + import json import typing import subprocess @@ -15,7 +16,7 @@ from io import BytesIO from datetime import datetime, timezone from os import path, listdir -from discord import File, errors as discord_errors +from discord import File, Guild, errors as discord_errors from discord.ext import commands @@ -26,7 +27,7 @@ def __init__(self, client): self.cog_re = re.compile(r'\s*src\/cogs\/(.+)\.py\s*\|\s*\d+\s*[+-]+') async def cog_check(self, ctx): - return self.client.user_is_admin(ctx.author) + return self.client.user_is_admin(ctx.author.id) @commands.Cog.listener() async def on_ready(self): @@ -36,21 +37,21 @@ async def on_ready(self): await self.client.change_presence(activity=activity) @commands.Cog.listener() - async def on_guild_join(self, guild): + async def on_guild_join(self, guild: Guild): self.client.recent_guilds_joined.append( (datetime.now(tz=timezone.utc).isoformat()[:19], guild) ) self.client.recent_guilds_joined = self.client.recent_guilds_joined[-10:] @commands.Cog.listener() - async def on_guild_remove(self, guild): + async def on_guild_remove(self, guild: Guild): self.client.recent_guilds_left.append( (datetime.now(tz=timezone.utc).isoformat()[:19], guild) ) self.client.recent_guilds_left = self.client.recent_guilds_left[-10:] def reload_config(self): - with open("../state/config.json") as conffile: + with open('../state/config.json') as conffile: self.client.config = json.load(conffile) def crawl_cogs(self, directory='cogs'): @@ -78,7 +79,7 @@ def crawl_cogs(self, directory='cogs'): description='Load bot extension', hidden=True, ) - async def load_extension(self, ctx, extension_name): + async def load_extension(self, ctx: commands.Context, extension_name: str): for cog_name in self.crawl_cogs(): if extension_name in cog_name: target_extension = cog_name @@ -100,16 +101,13 @@ async def load_extension(self, ctx, extension_name): description='Unload bot extension', hidden=True, ) - async def unload_extension(self, ctx, extension_name): + async def unload_extension(self, ctx: commands.Context, extension_name: str): for cog_name in self.client.extensions: if extension_name in cog_name: target_extension = cog_name break if target_extension.lower() in 'cogs.management': - await ctx.send( - f"```diff\n- {target_extension} can't be unloaded" + - f"\n+ try reload instead```" - ) + await ctx.send(f"```diff\n- {target_extension} can't be unloaded\n+ try reload instead```") return if self.client.extensions.get(target_extension) is None: return @@ -126,7 +124,7 @@ async def unload_extension(self, ctx, extension_name): hidden=True, aliases=['re'] ) - async def reload_extension(self, ctx, extension_name): + async def reload_extension(self, ctx: commands.Context, extension_name: str): target_extensions = [] if extension_name == 'all': target_extensions = [__name__] + \ @@ -160,7 +158,7 @@ async def reload_extension(self, ctx, extension_name): aliases=['extensions'], hidden=True, ) - async def print_cogs(self, ctx): + async def print_cogs(self, ctx: commands.Context): loaded = self.client.extensions unloaded = [x for x in self.crawl_cogs() if x not in loaded] response = ['\n[Loaded extensions]'] + ['\n ' + x for x in loaded] @@ -195,14 +193,14 @@ async def show_servers(self, ctx, include_txt: bool = False): name='git', hidden=True, ) - async def git(self, ctx): + async def git(self, _: commands.Context): """Commands to run git commands on the local repo""" pass @git.command( name='pull', ) - async def pull(self, ctx, noreload: typing.Optional[str] = None): + async def pull(self, ctx: commands.Context, noreload: typing.Optional[str] = None): """Pull the latest changes from github""" try: await ctx.typing() @@ -273,6 +271,14 @@ async def maintenance(self, ctx): self.client.maintenance_mode = True await self.client.change_presence(activity=self.client.maintenance_activity) + # ---------------------------------------------- + # Command to sync slash commands + # ---------------------------------------------- + @commands.command(name='synccmds', hidden=True) + async def sync_commands(self, ctx: commands.Context): + await self.client.tree.sync() + await ctx.send('Commands synced.') + -async def setup(client): +async def setup(client: commands.Bot): await client.add_cog(Management(client)) diff --git a/src/cogs/run.py b/src/cogs/run.py index 91ae91b..e41c548 100644 --- a/src/cogs/run.py +++ b/src/cogs/run.py @@ -5,17 +5,13 @@ run Run code using the Piston API """ + # pylint: disable=E0402 -import json -import re, sys +import sys from dataclasses import dataclass from discord import Embed, Message, errors as discord_errors -from discord.ext import commands, tasks -from discord.utils import escape_mentions -from aiohttp import ContentTypeError -from .utils.codeswap import add_boilerplate -from .utils.errors import PistonInvalidContentType, PistonInvalidStatus, PistonNoOutput -#pylint: disable=E1101 +from discord.ext import commands +# pylint: disable=E1101 @dataclass @@ -46,221 +42,33 @@ def get_size(obj, seen=None): class Run(commands.Cog, name='CodeExecution'): def __init__(self, client): self.client = client - self.run_IO_store = dict() # Store the most recent /run message for each user.id - self.languages = dict() # Store the supported languages and aliases - self.versions = dict() # Store version for each language - self.run_regex_code = re.compile( - r'(?s)/(?:edit_last_)?run' - r'(?: +(?P\S*?)\s*|\s*)' - r'(?:-> *(?P\S*)\s*|\s*)' - r'(?:\n(?P(?:[^\n\r\f\v]*\n)*?)\s*|\s*)' - r'```(?:(?P\S+)\n\s*|\s*)(?P.*)```' - r'(?:\n?(?P(?:[^\n\r\f\v]\n?)+)+|)' - ) - self.run_regex_file = re.compile( - r'/run(?: *(?P\S*)\s*?|\s*?)?' - r'(?: *-> *(?P\S*)\s*?|\s*?)?' - r'(?:\n(?P(?:[^\n\r\f\v]+\n?)*)\s*|\s*)?' - r'(?:\n*(?P(?:[^\n\r\f\v]\n*)+)+|)?' - ) - self.get_available_languages.start() - - @tasks.loop(count=1) - async def get_available_languages(self): - async with self.client.session.get( - 'https://emkc.org/api/v2/piston/runtimes' - ) as response: - runtimes = await response.json() - for runtime in runtimes: - language = runtime['language'] - self.languages[language] = language - self.versions[language] = runtime['version'] - for alias in runtime['aliases']: - self.languages[alias] = language - self.versions[alias] = runtime['version'] - - async def send_to_log(self, ctx, language, source): - logging_data = { - 'server': ctx.guild.name if ctx.guild else 'DMChannel', - 'server_id': str(ctx.guild.id) if ctx.guild else '0', - 'user': f'{ctx.author.name}#{ctx.author.discriminator}', - 'user_id': str(ctx.author.id), - 'language': language, - 'source': source - } - headers = {'Authorization': self.client.config["emkc_key"]} - - async with self.client.session.post( - 'https://emkc.org/api/internal/piston/log', - headers=headers, - data=json.dumps(logging_data) - ) as response: - if response.status != 200: - await self.client.log_error( - commands.CommandError(f'Error sending log. Status: {response.status}'), - ctx - ) - return False - - return True - - async def get_api_parameters_with_codeblock(self, ctx): - if ctx.message.content.count('```') != 2: - raise commands.BadArgument('Invalid command format (missing codeblock?)') - - match = self.run_regex_code.search(ctx.message.content) - - if not match: - raise commands.BadArgument('Invalid command format') - - language, output_syntax, args, syntax, source, stdin = match.groups() - - if not language: - language = syntax - - if language: - language = language.lower() - - if language not in self.languages: - raise commands.BadArgument( - f'Unsupported language: **{str(language)[:1000]}**\n' - '[Request a new language](https://github.com/engineer-man/piston/issues)' - ) - - return language, output_syntax, source, args, stdin - - async def get_api_parameters_with_file(self, ctx): - if len(ctx.message.attachments) != 1: - raise commands.BadArgument('Invalid number of attachments') - - file = ctx.message.attachments[0] - - MAX_BYTES = 65535 - if file.size > MAX_BYTES: - raise commands.BadArgument(f'Source file is too big ({file.size}>{MAX_BYTES})') - - filename_split = file.filename.split('.') - - if len(filename_split) < 2: - raise commands.BadArgument('Please provide a source file with a file extension') - - match = self.run_regex_file.search(ctx.message.content) - - if not match: - raise commands.BadArgument('Invalid command format') - - language, output_syntax, args, stdin = match.groups() - - if not language: - language = filename_split[-1] + self.run_IO_store: dict[int, RunIO] = dict() + # Store the most recent /run message for each user.id - if language: - language = language.lower() - - if language not in self.languages: - raise commands.BadArgument( - f'Unsupported file extension: **{language}**\n' - '[Request a new language](https://github.com/engineer-man/piston/issues)' - ) - - source = await file.read() - try: - source = source.decode('utf-8') - except UnicodeDecodeError as e: - raise commands.BadArgument(str(e)) - - return language, output_syntax, source, args, stdin - - async def get_run_output(self, ctx): + async def get_run_output(self, ctx: commands.Context): # Get parameters to call api depending on how the command was called (file <> codeblock) if ctx.message.attachments: - alias, output_syntax, source, args, stdin = await self.get_api_parameters_with_file(ctx) - else: - alias, output_syntax, source, args, stdin = await self.get_api_parameters_with_codeblock(ctx) - - # Resolve aliases for language - language = self.languages[alias] - - version = self.versions[alias] - - # Add boilerplate code to supported languages - source = add_boilerplate(language, source) - - # Split args at newlines - if args: - args = [arg for arg in args.strip().split('\n') if arg] - - if not source: - raise commands.BadArgument(f'No source code found') - - # Call piston API - data = { - 'language': alias, - 'version': version, - 'files': [{'content': source}], - 'args': args, - 'stdin': stdin or "", - 'log': 0 - } - headers = {'Authorization': self.client.config["emkc_key"]} - async with self.client.session.post( - 'https://emkc.org/api/v2/piston/execute', - headers=headers, - json=data - ) as response: - try: - r = await response.json() - except ContentTypeError: - raise PistonInvalidContentType('invalid content type') - if not response.status == 200: - raise PistonInvalidStatus(f'status {response.status}: {r.get("message", "")}') - - comp_stderr = r['compile']['stderr'] if 'compile' in r else '' - run = r['run'] - - if run['output'] is None: - raise PistonNoOutput('no output') - - # Logging - await self.send_to_log(ctx, language, source) - - language_info=f'{alias}({version})' - - # Return early if no output was received - if len(run['output'] + comp_stderr) == 0: - return f'Your {language_info} code ran without output {ctx.author.mention}' - - # Limit output to 30 lines maximum - output = '\n'.join((comp_stderr + run['output']).split('\n')[:30]) - - # Prevent mentions in the code output - output = escape_mentions(output) - - # Prevent code block escaping by adding zero width spaces to backticks - output = output.replace("`", "`\u200b") - - # Truncate output to be below 2000 char discord limit. - if len(comp_stderr) > 0: - introduction = f'{ctx.author.mention} I received {language_info} compile errors\n' - elif len(run['stdout']) == 0 and len(run['stderr']) > 0: - introduction = f'{ctx.author.mention} I only received {language_info} error output\n' - else: - introduction = f'Here is your {language_info} output {ctx.author.mention}\n' - truncate_indicator = '[...]' - len_codeblock = 7 # 3 Backticks + newline + 3 Backticks - available_chars = 2000-len(introduction)-len_codeblock - if len(output) > available_chars: - output = output[:available_chars-len(truncate_indicator)] + truncate_indicator + return await self.client.runner.get_output_with_file( + ctx.guild, + ctx.author, + input_language="", + output_syntax="", + args="", + stdin="", + content=ctx.message.content, + file=ctx.message.attachments[0], + mention_author=True, + ) - # Use an empty string if no output language is selected - return ( - introduction - + f'```{output_syntax or ""}\n' - + output.replace('\0', '') - + '```' + return await self.client.runner.get_output_with_codeblock( + ctx.guild, + ctx.author, + content=ctx.message.content, + mention_author=True, + needs_strict_re=True, ) - async def delete_last_output(self, user_id): + async def delete_last_output(self, user_id: int): try: msg_to_delete = self.run_IO_store[user_id].output del self.run_IO_store[user_id] @@ -272,14 +80,14 @@ async def delete_last_output(self, user_id): # Message no longer exists in discord (deleted by server admin) return - @commands.command(aliases=['del']) - async def delete(self, ctx): + @commands.command(aliases=["del"]) + async def delete(self, ctx: commands.Context): """Delete the most recent output message you caused Type "./run" or "./help" for instructions""" await self.delete_last_output(ctx.author.id) @commands.command() - async def run(self, ctx, *, source=None): + async def run(self, ctx: commands.Context, *, source=None): """Run some code Type "./run" or "./help" for instructions""" if self.client.maintenance_mode: @@ -312,7 +120,7 @@ async def run(self, ctx, *, source=None): self.run_IO_store[ctx.author.id] = RunIO(input=ctx.message, output=msg) @commands.command(hidden=True) - async def edit_last_run(self, ctx, *, content=None): + async def edit_last_run(self, ctx: commands.Context, *, content=None): """Run some edited code and edit previous message""" if self.client.maintenance_mode: return @@ -346,7 +154,7 @@ async def edit_last_run(self, ctx, *, content=None): return @commands.command(hidden=True) - async def size(self, ctx): + async def size(self, ctx: commands.Context): if ctx.author.id != 98488345952256000: return False await ctx.send( @@ -354,7 +162,7 @@ async def size(self, ctx): f'\nMessage Cache {len(self.client.cached_messages)} / {get_size(self.client.cached_messages) // 1000} kb\n```') @commands.Cog.listener() - async def on_message_edit(self, before, after): + async def on_message_edit(self, before: Message, after: Message): if self.client.maintenance_mode: return if after.author.bot: @@ -376,10 +184,8 @@ async def on_message_edit(self, before, after): break @commands.Cog.listener() - async def on_message_delete(self, message): - if self.client.maintenance_mode: - return - if message.author.bot: + async def on_message_delete(self, message: Message): + if message.author.bot or self.client.maintenance_mode: return if message.author.id not in self.run_IO_store: return @@ -388,7 +194,7 @@ async def on_message_delete(self, message): await self.delete_last_output(message.author.id) async def send_howto(self, ctx): - languages = sorted(set(self.languages.values())) + languages = self.client.runner.get_languages() run_instructions = ( '**Update: Discord changed their client to prevent sending messages**\n' diff --git a/src/cogs/user_commands.py b/src/cogs/user_commands.py new file mode 100644 index 0000000..0f07223 --- /dev/null +++ b/src/cogs/user_commands.py @@ -0,0 +1,128 @@ +import discord +from discord import app_commands, Interaction, Attachment +from discord.ext import commands + + +class CodeModal(discord.ui.Modal, title="Run Code"): + def __init__(self, get_run_output, log_error): + super().__init__() + self.get_run_output = get_run_output + self.log_error = log_error + + lang = discord.ui.TextInput( + label="Language", + placeholder="the language of the code", + max_length=50, + ) + + code = discord.ui.TextInput( + label="Code", + style=discord.TextStyle.long, + placeholder="the codes", + ) + + # It gets pretty crowded with all these fields + # So im not sure which to keep if any at all + + # output_syntax = discord.ui.TextInput( + # label="Output Syntax", + # placeholder="the syntax of the output", + # required=False, + # ) + + # args = discord.ui.TextInput( + # label="Arguments", + # placeholder="the arguments - comma separated", + # required=False, + # ) + + # stdin = discord.ui.TextInput( + # label="Standard Input", + # placeholder="the standard input", + # required=False, + # ) + + async def on_submit(self, interaction: discord.Interaction): + run_output = await self.get_run_output( + guild=interaction.guild, + author=interaction.user, + content=self.code.value, + input_lang=self.lang.value, + output_syntax=None, + args=None, + stdin=None, + mention_author=False, + ) + await interaction.response.send_message(run_output) + + async def on_error( + self, interaction: discord.Interaction, error: Exception + ) -> None: + await interaction.response.send_message( + "Oops! Something went wrong.", ephemeral=True + ) + + self.log_error(error, error_source="CodeModal") + + +class UserCommands(commands.Cog, name="UserCommands"): + def __init__(self, client): + self.client = client + self.ctx_menu = app_commands.ContextMenu( + name="Run Code", + callback=self.run_code_ctx_menu, + ) + self.client.tree.add_command(self.ctx_menu) + + @app_commands.command(name="run", description="Open a modal to run code") + @app_commands.user_install() + @app_commands.allowed_contexts(guilds=True, dms=True, private_channels=True) + async def run(self, interaction: Interaction): + await interaction.response.send_modal( + CodeModal(self.client.runner.get_run_output, self.client.log_error) + ) + + @app_commands.command(name="run_file", description="Run a file") + @app_commands.user_install() + @app_commands.allowed_contexts(guilds=True, dms=True, private_channels=True) + async def run_file( + self, + interaction: Interaction, + file: Attachment, + language: str = "", + output_syntax: str = "None", + args: str = "", + stdin: str = "", + ): + run_output = await self.client.runner.get_output_with_file( + guild=interaction.guild, + author=interaction.user, + content="", + file=file, + input_language=language, + output_syntax=output_syntax, + args=args, + stdin=stdin, + mention_author=False, + ) + + await interaction.response.send_message(run_output) + + @app_commands.user_install() + @app_commands.allowed_contexts(guilds=True, dms=True, private_channels=True) + async def run_code_ctx_menu( + self, interaction: Interaction, message: discord.Message + ): + run_output = await self.client.runner.get_output_with_codeblock( + guild=interaction.guild, + author=interaction.user, + content=message.content, + mention_author=False, + needs_strict_re=False, + ) + + await interaction.response.send_message(run_output) + + +async def setup(client): + await client.add_cog(UserCommands(client)) diff --git a/src/cogs/utils/codeswap.py b/src/cogs/utils/codeswap.py index be005a8..953b7b3 100644 --- a/src/cogs/utils/codeswap.py +++ b/src/cogs/utils/codeswap.py @@ -1,4 +1,4 @@ -def add_boilerplate(language, source): +def add_boilerplate(language: str, source: str): if language == 'java': return for_java(source) if language == 'scala': @@ -13,7 +13,7 @@ def add_boilerplate(language, source): return for_csharp(source) return source -def for_go(source): +def for_go(source: str): if 'main' in source: return source @@ -31,7 +31,7 @@ def for_go(source): code.append('}') return '\n'.join(package + imports + code) -def for_c_cpp(source): +def for_c_cpp(source: str): if 'main' in source: return source @@ -48,13 +48,13 @@ def for_c_cpp(source): code.append('}') return '\n'.join(imports + code) -def for_csharp(source): +def for_csharp(source: str): if 'class' in source: return source imports=[] code = ['class Program{'] - if not 'static void Main' in source: + if 'static void Main' not in source: code.append('static void Main(string[] args){') lines = source.replace(';', ';\n').split('\n') @@ -64,13 +64,13 @@ def for_csharp(source): else: code.append(line) - if not 'static void Main' in source: + if 'static void Main' not in source: code.append('}') code.append('}') return '\n'.join(imports + code).replace(';\n', ';') -def for_java(source): +def for_java(source: str): if 'class' in source: return source @@ -88,7 +88,7 @@ def for_java(source): code.append('}}') return '\n'.join(imports + code).replace(';\n',';') -def for_scala(source): +def for_scala(source: str): if any(s in source for s in ('extends App', 'def main', '@main def', '@main() def')): return source @@ -97,7 +97,7 @@ def for_scala(source): return f'@main def run(): Unit = {{\n{indented_source}}}\n' -def for_rust(source): +def for_rust(source: str): if 'fn main' in source: return source imports = [] diff --git a/src/cogs/utils/runner.py b/src/cogs/utils/runner.py new file mode 100644 index 0000000..06cd64c --- /dev/null +++ b/src/cogs/utils/runner.py @@ -0,0 +1,270 @@ +import re +import json +from .errors import PistonInvalidContentType, PistonInvalidStatus, PistonNoOutput +from discord.ext import commands, tasks +from discord import Guild, User, Member, Attachment +from discord.utils import escape_mentions +from aiohttp import ClientSession, ContentTypeError +from .codeswap import add_boilerplate + + +class Runner: + def __init__(self, emkc_key: str, session: ClientSession): + self.languages = dict() # Store the supported languages and aliases + self.versions = dict() # Store version for each language + self.emkc_key = emkc_key + self.base_re = ( + r'(?: +(?P\S*?)\s*|\s*)' + r'(?:-> *(?P\S*)\s*|\s*)' + r'(?:\n(?P(?:[^\n\r\f\v]*\n)*?)\s*|\s*)' + r'```(?:(?P\S+)\n\s*|\s*)(?P.*)```' + r'(?:\n?(?P(?:[^\n\r\f\v]\n?)+)+|)' + ) + self.run_regex_code = re.compile(self.base_re, re.DOTALL) + self.run_regex_code_strict = re.compile( + r'/(?:edit_last_)?run' + self.base_re, re.DOTALL + ) + + self.run_regex_file = re.compile( + r'/run(?: *(?P\S*)\s*?|\s*?)?' + r'(?: *-> *(?P\S*)\s*?|\s*?)?' + r'(?:\n(?P(?:[^\n\r\f\v]+\n?)*)\s*|\s*)?' + r'(?:\n*(?P(?:[^\n\r\f\v]\n*)+)+|)?' + ) + + self.session = session + + self.update_available_languages.start() + + @tasks.loop(count=1) + async def update_available_languages(self): + async with self.session.get( + 'https://emkc.org/api/v2/piston/runtimes' + ) as response: + runtimes = await response.json() + for runtime in runtimes: + language = runtime['language'] + self.languages[language] = language + self.versions[language] = runtime['version'] + for alias in runtime['aliases']: + self.languages[alias] = language + self.versions[alias] = runtime['version'] + + def get_languages(self): + return sorted(set(self.languages.values())) + + async def send_to_log( + self, + guild: Guild | None, + author: User | Member, + language: str, + source: str, + ): + logging_data = { + 'server': guild.name if guild else 'DMChannel', + 'server_id': f'{guild.id}' if guild else '0', + 'user': f'{author.name}', + 'user_id': f'{author.id}', + 'language': language, + 'source': source, + } + headers = {'Authorization': self.emkc_key} + + async with self.session.post( + 'https://emkc.org/api/internal/piston/log', + headers=headers, + data=json.dumps(logging_data), + ) as response: + if response.status != 200: + pass + return True + + async def get_output_with_codeblock( + self, + guild: Guild | None, + author: User | Member, + content: str, + mention_author: bool, + needs_strict_re: bool, + ): + if needs_strict_re: + match = self.run_regex_code_strict.search(content) + else: + match = self.run_regex_code.search(content) + + if not match: + return 'Invalid command format' + + language, output_syntax, args, syntax, source, stdin = match.groups() + + if not language: + language = syntax + + if language: + language = language.lower() + + if language not in self.languages: + return ( + f'Unsupported language: **{str(language)[:1000]}**\n' + '[Request a new language](https://github.com/engineer-man/piston/issues)' + ) + + return await self.get_run_output( + guild, author, source, language, output_syntax, args, stdin, mention_author + ) + + async def get_output_with_file( + self, + guild: Guild | None, + author: User | Member, + file: Attachment, + input_language: str, + output_syntax: str, + args: str, + stdin: str, + mention_author: bool, + content: str, + ) -> str: + MAX_BYTES = 65535 + if file.size > MAX_BYTES: + return f'Source file is too big ({file.size}>{MAX_BYTES})' + + filename_split = file.filename.split('.') + if len(filename_split) < 2: + return 'Please provide a source file with a file extension' + + match = self.run_regex_file.search(content) + if content and not match: + raise commands.BadArgument('Invalid command format') + + language = input_language or filename_split[-1] + if match: + matched_language, output_syntax, args, stdin = match.groups() # type: ignore + if matched_language: + language = matched_language + + language = language.lower() + + if language not in self.languages: + return ( + f'Unsupported file extension: **{language}**\n' + '[Request a new language](https://github.com/engineer-man/piston/issues)' + ) + + source = await file.read() + try: + source = source.decode('utf-8') + except UnicodeDecodeError as e: + return str(e) + return await self.get_run_output( + guild, + author, + source, + language, # type: ignore + output_syntax, + args, + stdin, + mention_author, + ) + + async def get_run_output( + self, + guild: Guild | None, + author: User | Member, + content: str, + input_lang: str, + output_syntax: str | None, + args: str | None, + stdin: str | None, + mention_author: bool, + ): + lang = self.languages.get(input_lang, None) + if not lang: + return ( + f'Unsupported language: **{str(input_lang)}**\n' + '[Request a new language](https://github.com/engineer-man/piston/issues)' + ) + + version = self.versions[lang] + + # Add boilerplate code to supported languages + source = add_boilerplate(lang, content) + + # Split args at newlines + argugments = [] + if args: + argugments = [arg for arg in args.strip().split(',') if arg] + + if not source: + raise commands.BadArgument('No source code found') + + # Call piston API + data = { + 'language': lang, + 'version': version, + 'files': [{'content': source}], + 'args': argugments or '', + 'stdin': stdin or '', + 'log': 0, + } + headers = {'Authorization': self.emkc_key} + async with self.session.post( + 'https://emkc.org/api/v2/piston/execute', headers=headers, json=data + ) as response: + try: + r = await response.json() + except ContentTypeError: + raise PistonInvalidContentType('invalid content type') + if not response.status == 200: + raise PistonInvalidStatus( + f'status {response.status}: {r.get("message", "")}' + ) + + comp_stderr = r['compile']['stderr'] if 'compile' in r else '' + run = r['run'] + + if run['output'] is None: + raise PistonNoOutput('no output') + + # Logging + await self.send_to_log(guild, author, lang, source) + + language_info = f'{lang}({version})' + + mention = author.mention + '' if mention_author else '' + + # Return early if no output was received + if len(run['output'] + comp_stderr) == 0: + return f'Your {language_info} code ran without output {mention}' + + # Limit output to 30 lines maximum + output = '\n'.join((comp_stderr + run['output']).split('\n')[:30]) + + # Prevent mentions in the code output + output = escape_mentions(output) + + # Prevent code block escaping by adding zero width spaces to backticks + output = output.replace('`', '`\u200b') + + # Truncate output to be below 2000 char discord limit. + if len(comp_stderr) > 0: + introduction = f'{mention}I received {language_info} compile errors\n' + elif len(run['stdout']) == 0 and len(run['stderr']) > 0: + introduction = f'{mention}I only received {language_info} error output\n' + else: + introduction = f'Here is your {language_info} output {mention}\n' + truncate_indicator = '[...]' + len_codeblock = 7 # 3 Backticks + newline + 3 Backticks + available_chars = 2000 - len(introduction) - len_codeblock + if len(output) > available_chars: + output = ( + output[: available_chars - len(truncate_indicator)] + truncate_indicator + ) + + # Use an empty string if no output language is selected + return ( + introduction + + f'```{output_syntax or ""}\n' + + output.replace('\0', "") + + '```' + ) From 74222e27e929b2bff87ac714fb5482d8c113e096 Mon Sep 17 00:00:00 2001 From: Masterjoona <69722179+Masterjoona@users.noreply.github.com> Date: Fri, 16 Aug 2024 13:52:01 +0000 Subject: [PATCH 02/17] Better grammar --- src/cogs/user_commands.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cogs/user_commands.py b/src/cogs/user_commands.py index 0f07223..34306a3 100644 --- a/src/cogs/user_commands.py +++ b/src/cogs/user_commands.py @@ -11,14 +11,14 @@ def __init__(self, get_run_output, log_error): lang = discord.ui.TextInput( label="Language", - placeholder="the language of the code", + placeholder="The language", max_length=50, ) code = discord.ui.TextInput( label="Code", style=discord.TextStyle.long, - placeholder="the codes", + placeholder="The source code", ) # It gets pretty crowded with all these fields @@ -74,7 +74,7 @@ def __init__(self, client): ) self.client.tree.add_command(self.ctx_menu) - @app_commands.command(name="run", description="Open a modal to run code") + @app_commands.command(name="run", description="Open a modal to input code") @app_commands.user_install() @app_commands.allowed_contexts(guilds=True, dms=True, private_channels=True) async def run(self, interaction: Interaction): From 9612a95c62a63b3943610f395dc653a04d1466f0 Mon Sep 17 00:00:00 2001 From: Masterjoona <69722179+Masterjoona@users.noreply.github.com> Date: Sat, 17 Aug 2024 07:58:10 +0000 Subject: [PATCH 03/17] Revert formatting changes --- src/bot.py | 25 ++++++++--------- src/cogs/error_handler.py | 24 +++++++--------- src/cogs/management.py | 27 ++++++++++-------- src/cogs/run.py | 21 +++++++------- src/cogs/utils/codeswap.py | 18 ++++++------ src/cogs/utils/runner.py | 57 +++++++++++++++----------------------- 6 files changed, 79 insertions(+), 93 deletions(-) diff --git a/src/bot.py b/src/bot.py index 97445bf..9888e78 100644 --- a/src/bot.py +++ b/src/bot.py @@ -1,17 +1,16 @@ -"""PistonBot""" +"""PistonBot +""" +import json import sys import traceback -from discord import AllowedMentions, Intents -from discord.ext.commands.bot import when_mentioned_or -from discord.ext.commands import AutoShardedBot, Context -from discord import Activity, Guild -import json from datetime import datetime, timezone from os import path, listdir -from cogs.utils.runner import Runner +from discord.ext.commands import AutoShardedBot, Context +from discord import Activity, AllowedMentions, Intents from aiohttp import ClientSession, ClientTimeout - +from discord.ext.commands.bot import when_mentioned_or +from cogs.utils.runner import Runner class PistonBot(AutoShardedBot): def __init__(self, *args, **options): @@ -19,8 +18,8 @@ def __init__(self, *args, **options): with open('../state/config.json') as conffile: self.config = json.load(conffile) self.last_errors = [] - self.recent_guilds_joined: list[tuple[str, Guild]] = [] - self.recent_guilds_left: list[tuple[str, Guild]] = [] + self.recent_guilds_joined = [] + self.recent_guilds_left = [] self.default_activity = Activity(name='emkc.org/run | ./run', type=0) self.error_activity = Activity(name='!emkc.org/run | ./run', type=0) self.maintenance_activity = Activity(name='undergoing maintenance', type=0) @@ -53,8 +52,8 @@ async def setup_hook(self): exc = f'{type(e).__name__}: {e}' print(f'Failed to load extension {extension}\n{exc}') - def user_is_admin(self, user_id: int): - return user_id in self.config['admins'] + def user_is_admin(self, user): + return user.id in self.config['admins'] async def log_error(self, error, error_source=None): is_context = isinstance(error_source, Context) @@ -93,7 +92,7 @@ async def on_message(msg): prefixes = await client.get_prefix(msg) for prefix in prefixes: if msg.content.lower().startswith(f'{prefix}run'): - msg.content = msg.content.replace(f'{prefix}run', '/run', 1) + msg.content = msg.content.replace(f'{prefix}run', f'/run', 1) break await client.process_commands(msg) diff --git a/src/cogs/error_handler.py b/src/cogs/error_handler.py index cf26a95..d629c12 100644 --- a/src/cogs/error_handler.py +++ b/src/cogs/error_handler.py @@ -6,7 +6,6 @@ - traceback print traceback of stored error """ - # pylint: disable=E0402 import traceback import typing @@ -25,15 +24,12 @@ def __init__(self, client): # Error handler # ---------------------------------------------- @commands.Cog.listener() - async def on_command_error(self, ctx: commands.Context, error): + async def on_command_error(self, ctx, error): if isinstance(error, commands.CommandNotFound): return if not isinstance(ctx.channel, DMChannel): - if ctx.guild is None: - return - self_bot = ctx.guild.get_member(self.client.user.id) - perms = ctx.channel.permissions_for(self_bot) + perms = ctx.channel.permissions_for(ctx.guild.get_member(self.client.user.id)) try: if not perms.send_messages: await ctx.author.send("I don't have permission to write in this channel.") @@ -129,7 +125,7 @@ async def on_command_error(self, ctx: commands.Context, error): hidden=True, aliases=['errors'] ) - async def error(self, ctx: commands.Context, n: typing.Optional[int] = None): + async def error(self, ctx, n: typing.Optional[int] = None): """Show a concise list of stored errors""" if n is not None: @@ -155,10 +151,10 @@ async def error(self, ctx: commands.Context, n: typing.Optional[int] = None): + call_info + f']\nException: {str(exc)[:200]}' ) - if i % NUM_ERRORS_PER_PAGE == NUM_ERRORS_PER_PAGE - 1: + if i % NUM_ERRORS_PER_PAGE == NUM_ERRORS_PER_PAGE-1: response.append('```') await ctx.send('\n'.join(response)) - response = ['```css'] + response = [f'```css'] if len(response) > 1: response.append('```') await ctx.send('\n'.join(response)) @@ -167,9 +163,9 @@ async def error(self, ctx: commands.Context, n: typing.Optional[int] = None): name='clear', aliases=['delete'], ) - async def error_clear(self, ctx, n: int = 0): + async def error_clear(self, ctx, n: int = None): """Clear error with index [n]""" - if n == 0: + if n is None: self.client.last_errors = [] await ctx.send('Error log cleared') else: @@ -183,18 +179,18 @@ async def error_clear(self, ctx, n: int = 0): name='traceback', aliases=['tb'], ) - async def error_traceback(self, ctx: commands.Context, n: int = 0): + async def error_traceback(self, ctx, n: int = None): """Print the traceback of error [n] from the error log""" await self.print_traceback(ctx, n) - async def print_traceback(self, ctx: commands.Context, n: int): + async def print_traceback(self, ctx, n): error_log = self.client.last_errors if not error_log: await ctx.send('Error log is empty') return - if n == 0: + if n is None: await ctx.send('Please specify an error index') await self.client.get_command('error').invoke(ctx) return diff --git a/src/cogs/management.py b/src/cogs/management.py index cb15038..4b3306f 100644 --- a/src/cogs/management.py +++ b/src/cogs/management.py @@ -16,7 +16,7 @@ from io import BytesIO from datetime import datetime, timezone from os import path, listdir -from discord import File, Guild, errors as discord_errors +from discord import File, errors as discord_errors from discord.ext import commands @@ -27,7 +27,7 @@ def __init__(self, client): self.cog_re = re.compile(r'\s*src\/cogs\/(.+)\.py\s*\|\s*\d+\s*[+-]+') async def cog_check(self, ctx): - return self.client.user_is_admin(ctx.author.id) + return self.client.user_is_admin(ctx.author) @commands.Cog.listener() async def on_ready(self): @@ -37,21 +37,21 @@ async def on_ready(self): await self.client.change_presence(activity=activity) @commands.Cog.listener() - async def on_guild_join(self, guild: Guild): + async def on_guild_join(self, guild): self.client.recent_guilds_joined.append( (datetime.now(tz=timezone.utc).isoformat()[:19], guild) ) self.client.recent_guilds_joined = self.client.recent_guilds_joined[-10:] @commands.Cog.listener() - async def on_guild_remove(self, guild: Guild): + async def on_guild_remove(self, guild): self.client.recent_guilds_left.append( (datetime.now(tz=timezone.utc).isoformat()[:19], guild) ) self.client.recent_guilds_left = self.client.recent_guilds_left[-10:] def reload_config(self): - with open('../state/config.json') as conffile: + with open("../state/config.json") as conffile: self.client.config = json.load(conffile) def crawl_cogs(self, directory='cogs'): @@ -79,7 +79,7 @@ def crawl_cogs(self, directory='cogs'): description='Load bot extension', hidden=True, ) - async def load_extension(self, ctx: commands.Context, extension_name: str): + async def load_extension(self, ctx, extension_name): for cog_name in self.crawl_cogs(): if extension_name in cog_name: target_extension = cog_name @@ -101,13 +101,16 @@ async def load_extension(self, ctx: commands.Context, extension_name: str): description='Unload bot extension', hidden=True, ) - async def unload_extension(self, ctx: commands.Context, extension_name: str): + async def unload_extension(self, ctx, extension_name): for cog_name in self.client.extensions: if extension_name in cog_name: target_extension = cog_name break if target_extension.lower() in 'cogs.management': - await ctx.send(f"```diff\n- {target_extension} can't be unloaded\n+ try reload instead```") + await ctx.send( + f"```diff\n- {target_extension} can't be unloaded" + + f"\n+ try reload instead```" + ) return if self.client.extensions.get(target_extension) is None: return @@ -124,7 +127,7 @@ async def unload_extension(self, ctx: commands.Context, extension_name: str): hidden=True, aliases=['re'] ) - async def reload_extension(self, ctx: commands.Context, extension_name: str): + async def reload_extension(self, ctx, extension_name): target_extensions = [] if extension_name == 'all': target_extensions = [__name__] + \ @@ -193,14 +196,14 @@ async def show_servers(self, ctx, include_txt: bool = False): name='git', hidden=True, ) - async def git(self, _: commands.Context): + async def git(self, ctx): """Commands to run git commands on the local repo""" pass @git.command( name='pull', ) - async def pull(self, ctx: commands.Context, noreload: typing.Optional[str] = None): + async def pull(self, ctx, noreload: typing.Optional[str] = None): """Pull the latest changes from github""" try: await ctx.typing() @@ -280,5 +283,5 @@ async def sync_commands(self, ctx: commands.Context): await ctx.send('Commands synced.') -async def setup(client: commands.Bot): +async def setup(client): await client.add_cog(Management(client)) diff --git a/src/cogs/run.py b/src/cogs/run.py index e41c548..dcfbdea 100644 --- a/src/cogs/run.py +++ b/src/cogs/run.py @@ -5,7 +5,6 @@ run Run code using the Piston API """ - # pylint: disable=E0402 import sys from dataclasses import dataclass @@ -68,7 +67,7 @@ async def get_run_output(self, ctx: commands.Context): needs_strict_re=True, ) - async def delete_last_output(self, user_id: int): + async def delete_last_output(self, user_id): try: msg_to_delete = self.run_IO_store[user_id].output del self.run_IO_store[user_id] @@ -80,14 +79,14 @@ async def delete_last_output(self, user_id: int): # Message no longer exists in discord (deleted by server admin) return - @commands.command(aliases=["del"]) - async def delete(self, ctx: commands.Context): + @commands.command(aliases=['del']) + async def delete(self, ctx): """Delete the most recent output message you caused Type "./run" or "./help" for instructions""" await self.delete_last_output(ctx.author.id) @commands.command() - async def run(self, ctx: commands.Context, *, source=None): + async def run(self, ctx, *, source=None): """Run some code Type "./run" or "./help" for instructions""" if self.client.maintenance_mode: @@ -120,7 +119,7 @@ async def run(self, ctx: commands.Context, *, source=None): self.run_IO_store[ctx.author.id] = RunIO(input=ctx.message, output=msg) @commands.command(hidden=True) - async def edit_last_run(self, ctx: commands.Context, *, content=None): + async def edit_last_run(self, ctx, *, content=None): """Run some edited code and edit previous message""" if self.client.maintenance_mode: return @@ -154,7 +153,7 @@ async def edit_last_run(self, ctx: commands.Context, *, content=None): return @commands.command(hidden=True) - async def size(self, ctx: commands.Context): + async def size(self, ctx): if ctx.author.id != 98488345952256000: return False await ctx.send( @@ -162,7 +161,7 @@ async def size(self, ctx: commands.Context): f'\nMessage Cache {len(self.client.cached_messages)} / {get_size(self.client.cached_messages) // 1000} kb\n```') @commands.Cog.listener() - async def on_message_edit(self, before: Message, after: Message): + async def on_message_edit(self, before, after): if self.client.maintenance_mode: return if after.author.bot: @@ -184,8 +183,10 @@ async def on_message_edit(self, before: Message, after: Message): break @commands.Cog.listener() - async def on_message_delete(self, message: Message): - if message.author.bot or self.client.maintenance_mode: + async def on_message_delete(self, message): + if self.client.maintenance_mode: + return + if message.author.bot: return if message.author.id not in self.run_IO_store: return diff --git a/src/cogs/utils/codeswap.py b/src/cogs/utils/codeswap.py index 953b7b3..be005a8 100644 --- a/src/cogs/utils/codeswap.py +++ b/src/cogs/utils/codeswap.py @@ -1,4 +1,4 @@ -def add_boilerplate(language: str, source: str): +def add_boilerplate(language, source): if language == 'java': return for_java(source) if language == 'scala': @@ -13,7 +13,7 @@ def add_boilerplate(language: str, source: str): return for_csharp(source) return source -def for_go(source: str): +def for_go(source): if 'main' in source: return source @@ -31,7 +31,7 @@ def for_go(source: str): code.append('}') return '\n'.join(package + imports + code) -def for_c_cpp(source: str): +def for_c_cpp(source): if 'main' in source: return source @@ -48,13 +48,13 @@ def for_c_cpp(source: str): code.append('}') return '\n'.join(imports + code) -def for_csharp(source: str): +def for_csharp(source): if 'class' in source: return source imports=[] code = ['class Program{'] - if 'static void Main' not in source: + if not 'static void Main' in source: code.append('static void Main(string[] args){') lines = source.replace(';', ';\n').split('\n') @@ -64,13 +64,13 @@ def for_csharp(source: str): else: code.append(line) - if 'static void Main' not in source: + if not 'static void Main' in source: code.append('}') code.append('}') return '\n'.join(imports + code).replace(';\n', ';') -def for_java(source: str): +def for_java(source): if 'class' in source: return source @@ -88,7 +88,7 @@ def for_java(source: str): code.append('}}') return '\n'.join(imports + code).replace(';\n',';') -def for_scala(source: str): +def for_scala(source): if any(s in source for s in ('extends App', 'def main', '@main def', '@main() def')): return source @@ -97,7 +97,7 @@ def for_scala(source: str): return f'@main def run(): Unit = {{\n{indented_source}}}\n' -def for_rust(source: str): +def for_rust(source): if 'fn main' in source: return source imports = [] diff --git a/src/cogs/utils/runner.py b/src/cogs/utils/runner.py index 06cd64c..eee786e 100644 --- a/src/cogs/utils/runner.py +++ b/src/cogs/utils/runner.py @@ -4,12 +4,12 @@ from discord.ext import commands, tasks from discord import Guild, User, Member, Attachment from discord.utils import escape_mentions -from aiohttp import ClientSession, ContentTypeError +from aiohttp import ContentTypeError from .codeswap import add_boilerplate class Runner: - def __init__(self, emkc_key: str, session: ClientSession): + def __init__(self, emkc_key, session): self.languages = dict() # Store the supported languages and aliases self.versions = dict() # Store version for each language self.emkc_key = emkc_key @@ -53,13 +53,7 @@ async def update_available_languages(self): def get_languages(self): return sorted(set(self.languages.values())) - async def send_to_log( - self, - guild: Guild | None, - author: User | Member, - language: str, - source: str, - ): + async def send_to_log(self, guild, author, language, source): logging_data = { 'server': guild.name if guild else 'DMChannel', 'server_id': f'{guild.id}' if guild else '0', @@ -79,14 +73,7 @@ async def send_to_log( pass return True - async def get_output_with_codeblock( - self, - guild: Guild | None, - author: User | Member, - content: str, - mention_author: bool, - needs_strict_re: bool, - ): + async def get_output_with_codeblock(self, guild, author, content, mention_author, needs_strict_re): if needs_strict_re: match = self.run_regex_code_strict.search(content) else: @@ -115,16 +102,16 @@ async def get_output_with_codeblock( async def get_output_with_file( self, - guild: Guild | None, - author: User | Member, - file: Attachment, - input_language: str, - output_syntax: str, - args: str, - stdin: str, - mention_author: bool, - content: str, - ) -> str: + guild, + author, + file, + input_language, + output_syntax, + args, + stdin, + mention_author, + content, + ): MAX_BYTES = 65535 if file.size > MAX_BYTES: return f'Source file is too big ({file.size}>{MAX_BYTES})' @@ -169,14 +156,14 @@ async def get_output_with_file( async def get_run_output( self, - guild: Guild | None, - author: User | Member, - content: str, - input_lang: str, - output_syntax: str | None, - args: str | None, - stdin: str | None, - mention_author: bool, + guild, + author, + content, + input_lang, + output_syntax, + args, + stdin, + mention_author, ): lang = self.languages.get(input_lang, None) if not lang: From 9170a4e2a4f8e3bef84ace0de88ae829167416b6 Mon Sep 17 00:00:00 2001 From: Masterjoona <69722179+Masterjoona@users.noreply.github.com> Date: Sat, 17 Aug 2024 08:00:10 +0000 Subject: [PATCH 04/17] Remove more formatting changes --- src/bot.py | 1 + src/cogs/management.py | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bot.py b/src/bot.py index 9888e78..768c4b2 100644 --- a/src/bot.py +++ b/src/bot.py @@ -15,6 +15,7 @@ class PistonBot(AutoShardedBot): def __init__(self, *args, **options): super().__init__(*args, **options) + self.session = None with open('../state/config.json') as conffile: self.config = json.load(conffile) self.last_errors = [] diff --git a/src/cogs/management.py b/src/cogs/management.py index 4b3306f..a5e932d 100644 --- a/src/cogs/management.py +++ b/src/cogs/management.py @@ -8,7 +8,6 @@ cogs show currently active extensions / cogs error print the traceback of the last unhandled error to chat """ - import json import typing import subprocess @@ -161,7 +160,7 @@ async def reload_extension(self, ctx, extension_name): aliases=['extensions'], hidden=True, ) - async def print_cogs(self, ctx: commands.Context): + async def print_cogs(self, ctx): loaded = self.client.extensions unloaded = [x for x in self.crawl_cogs() if x not in loaded] response = ['\n[Loaded extensions]'] + ['\n ' + x for x in loaded] From 7affe25e9adff4466eeaa47240e9d5d4716c3cfd Mon Sep 17 00:00:00 2001 From: Masterjoona <69722179+Masterjoona@users.noreply.github.com> Date: Sat, 17 Aug 2024 11:01:30 +0000 Subject: [PATCH 05/17] Error handling, context menu changes, input showing --- src/cogs/run.py | 6 +- src/cogs/user_commands.py | 141 +++++++++++++++++++++++++++++++------- src/cogs/utils/runner.py | 55 +++++++++++---- 3 files changed, 161 insertions(+), 41 deletions(-) diff --git a/src/cogs/run.py b/src/cogs/run.py index dcfbdea..5450bd2 100644 --- a/src/cogs/run.py +++ b/src/cogs/run.py @@ -47,7 +47,7 @@ def __init__(self, client): async def get_run_output(self, ctx: commands.Context): # Get parameters to call api depending on how the command was called (file <> codeblock) if ctx.message.attachments: - return await self.client.runner.get_output_with_file( + [introduction, _, run_output] = await self.client.runner.get_output_with_file( ctx.guild, ctx.author, input_language="", @@ -58,14 +58,16 @@ async def get_run_output(self, ctx: commands.Context): file=ctx.message.attachments[0], mention_author=True, ) + return introduction + run_output - return await self.client.runner.get_output_with_codeblock( + [introduction, _, run_output] = await self.client.runner.get_output_with_codeblock( ctx.guild, ctx.author, content=ctx.message.content, mention_author=True, needs_strict_re=True, ) + return introduction + run_output async def delete_last_output(self, user_id): try: diff --git a/src/cogs/user_commands.py b/src/cogs/user_commands.py index 34306a3..6720d80 100644 --- a/src/cogs/user_commands.py +++ b/src/cogs/user_commands.py @@ -1,25 +1,33 @@ import discord from discord import app_commands, Interaction, Attachment from discord.ext import commands +from .utils.errors import PistonError +from asyncio import TimeoutError as AsyncTimeoutError -class CodeModal(discord.ui.Modal, title="Run Code"): - def __init__(self, get_run_output, log_error): +class SourceCodeModal(discord.ui.Modal, title="Run Code"): + def __init__(self, get_run_output, log_error, language): super().__init__() self.get_run_output = get_run_output self.log_error = log_error + self.language = language - lang = discord.ui.TextInput( - label="Language", - placeholder="The language", - max_length=50, - ) + self.lang = discord.ui.TextInput( + label="Language", + placeholder="The language", + max_length=50, + default=self.language or "", + ) - code = discord.ui.TextInput( - label="Code", - style=discord.TextStyle.long, - placeholder="The source code", - ) + self.code = discord.ui.TextInput( + label="Code", + style=discord.TextStyle.long, + placeholder="The source code", + max_length=1900, + ) + + self.add_item(self.lang) + self.add_item(self.code) # It gets pretty crowded with all these fields # So im not sure which to keep if any at all @@ -43,7 +51,7 @@ def __init__(self, get_run_output, log_error): # ) async def on_submit(self, interaction: discord.Interaction): - run_output = await self.get_run_output( + [introduction, source, run_output] = await self.get_run_output( guild=interaction.guild, author=interaction.user, content=self.code.value, @@ -53,7 +61,42 @@ async def on_submit(self, interaction: discord.Interaction): stdin=None, mention_author=False, ) - await interaction.response.send_message(run_output) + await interaction.response.send_message("Here is your input:"+source) + await interaction.followup.send(introduction+run_output) + + async def on_error( + self, interaction: discord.Interaction, error: Exception + ) -> None: + await interaction.response.send_message( + "Oops! Something went wrong.", ephemeral=True + ) + + await self.log_error(error, error_source="CodeModal") + +class NoLang(discord.ui.Modal, title="Give lang"): + def __init__(self, get_output_with_codeblock, log_error, message): + super().__init__() + self.get_output_with_codeblock = get_output_with_codeblock + self.log_error = log_error + self.message = message + + lang = discord.ui.TextInput( + label="Language", + placeholder="The language", + max_length=50, + ) + + async def on_submit(self, interaction: discord.Interaction): + [introduction, _, run_output] = await self.get_output_with_codeblock( + guild=interaction.guild, + author=interaction.user, + content=self.message.content, + mention_author=False, + needs_strict_re=False, + input_lang=self.lang.value, + jump_url=self.message.jump_url, + ) + await interaction.response.send_message(introduction+run_output) async def on_error( self, interaction: discord.Interaction, error: Exception @@ -62,7 +105,7 @@ async def on_error( "Oops! Something went wrong.", ephemeral=True ) - self.log_error(error, error_source="CodeModal") + await self.log_error(error, error_source="CodeModal") class UserCommands(commands.Cog, name="UserCommands"): @@ -74,13 +117,39 @@ def __init__(self, client): ) self.client.tree.add_command(self.ctx_menu) - @app_commands.command(name="run", description="Open a modal to input code") + async def cog_app_command_error(self, interaction: Interaction, error: app_commands.AppCommandError): + if isinstance(error.original, PistonError): + error_message = str(error.original) + if error_message: + error_message = f'`{error_message}` ' + await interaction.response.send_message(f'API Error {error_message}- Please try again later') + await self.client.log_error(error, Interaction) + return + + if isinstance(error.original, AsyncTimeoutError): + await interaction.response.send_message(f'API Timeout - Please try again later') + await self.client.log_error(error, Interaction) + return + await self.client.log_error(error, Interaction) + await interaction.response.send_message(f"An error occurred: {error}") + + + + @app_commands.command(name="run_code", description="Open a modal to input code") @app_commands.user_install() @app_commands.allowed_contexts(guilds=True, dms=True, private_channels=True) - async def run(self, interaction: Interaction): - await interaction.response.send_modal( - CodeModal(self.client.runner.get_run_output, self.client.log_error) - ) + async def run_code(self, interaction: Interaction, language: str = None): + if language not in self.client.runner.get_languages(): + await interaction.response.send_modal(SourceCodeModal(self.client.runner.get_run_output, self.client.log_error, "")) + return + await interaction.response.send_modal(SourceCodeModal(self.client.runner.get_run_output, self.client.log_error, language)) + + @run_code.autocomplete('language') + async def autocomplete_callback(self, interaction: discord.Interaction, current: str): + langs = self.client.runner.get_languages() + if current: + langs = [lang for lang in langs if lang.startswith(current)] + return [app_commands.Choice(name=lang, value=lang) for lang in langs[:25]] @app_commands.command(name="run_file", description="Run a file") @app_commands.user_install() @@ -94,7 +163,7 @@ async def run_file( args: str = "", stdin: str = "", ): - run_output = await self.client.runner.get_output_with_file( + [introduction, source, run_output] = await self.client.runner.get_output_with_file( guild=interaction.guild, author=interaction.user, content="", @@ -106,22 +175,46 @@ async def run_file( mention_author=False, ) - await interaction.response.send_message(run_output) + await interaction.response.send_message(introduction+source) + await interaction.followup.send(run_output) @app_commands.user_install() @app_commands.allowed_contexts(guilds=True, dms=True, private_channels=True) async def run_code_ctx_menu( self, interaction: Interaction, message: discord.Message ): - run_output = await self.client.runner.get_output_with_codeblock( + if len(message.attachments) > 0: + [introduction, _, run_output] = await self.client.runner.get_output_with_file( + guild=interaction.guild, + author=interaction.user, + content=message.content, + file=message.attachments[0], + input_language="", + output_syntax="", + args="", + stdin="", + mention_author=False, + jump_url=message.jump_url, + ) + + await interaction.response.send_message(introduction+run_output) + return + [introduction, _, run_output] = await self.client.runner.get_output_with_codeblock( guild=interaction.guild, author=interaction.user, content=message.content, mention_author=False, needs_strict_re=False, + jump_url=message.jump_url, ) - await interaction.response.send_message(run_output) + if "Unsupported language" in run_output: + await interaction.response.send_modal( + NoLang(self.client.runner.get_output_with_codeblock, self.client.log_error, message) + ) + return + + await interaction.response.send_message(introduction+run_output) async def setup(client): diff --git a/src/cogs/utils/runner.py b/src/cogs/utils/runner.py index eee786e..f59954d 100644 --- a/src/cogs/utils/runner.py +++ b/src/cogs/utils/runner.py @@ -2,7 +2,6 @@ import json from .errors import PistonInvalidContentType, PistonInvalidStatus, PistonNoOutput from discord.ext import commands, tasks -from discord import Guild, User, Member, Attachment from discord.utils import escape_mentions from aiohttp import ContentTypeError from .codeswap import add_boilerplate @@ -73,7 +72,16 @@ async def send_to_log(self, guild, author, language, source): pass return True - async def get_output_with_codeblock(self, guild, author, content, mention_author, needs_strict_re): + async def get_output_with_codeblock( + self, + guild, + author, + content, + mention_author, + needs_strict_re, + input_lang=None, + jump_url=None + ): if needs_strict_re: match = self.run_regex_code_strict.search(content) else: @@ -87,6 +95,9 @@ async def get_output_with_codeblock(self, guild, author, content, mention_author if not language: language = syntax + if input_lang: + language = input_lang + if language: language = language.lower() @@ -97,7 +108,15 @@ async def get_output_with_codeblock(self, guild, author, content, mention_author ) return await self.get_run_output( - guild, author, source, language, output_syntax, args, stdin, mention_author + guild, + author, + source, + language, + output_syntax, + args, + stdin, + mention_author, + jump_url, ) async def get_output_with_file( @@ -111,6 +130,7 @@ async def get_output_with_file( stdin, mention_author, content, + jump_url=None, ): MAX_BYTES = 65535 if file.size > MAX_BYTES: @@ -152,6 +172,7 @@ async def get_output_with_file( args, stdin, mention_author, + jump_url, ) async def get_run_output( @@ -164,6 +185,7 @@ async def get_run_output( args, stdin, mention_author, + jump_url=None, ): lang = self.languages.get(input_lang, None) if not lang: @@ -178,9 +200,8 @@ async def get_run_output( source = add_boilerplate(lang, content) # Split args at newlines - argugments = [] if args: - argugments = [arg for arg in args.strip().split(',') if arg] + args = [arg for arg in args.strip().split('\n') if arg] if not source: raise commands.BadArgument('No source code found') @@ -190,7 +211,7 @@ async def get_run_output( 'language': lang, 'version': version, 'files': [{'content': source}], - 'args': argugments or '', + 'args': args or '', 'stdin': stdin or '', 'log': 0, } @@ -235,11 +256,11 @@ async def get_run_output( # Truncate output to be below 2000 char discord limit. if len(comp_stderr) > 0: - introduction = f'{mention}I received {language_info} compile errors\n' + introduction = f'{mention}I received {language_info} compile errors' elif len(run['stdout']) == 0 and len(run['stderr']) > 0: - introduction = f'{mention}I only received {language_info} error output\n' + introduction = f'{mention}I only received {language_info} error output' else: - introduction = f'Here is your {language_info} output {mention}\n' + introduction = f'Here is your {language_info} output {mention}' truncate_indicator = '[...]' len_codeblock = 7 # 3 Backticks + newline + 3 Backticks available_chars = 2000 - len(introduction) - len_codeblock @@ -248,10 +269,14 @@ async def get_run_output( output[: available_chars - len(truncate_indicator)] + truncate_indicator ) + if jump_url: + jump_url = f'from running: {jump_url}' + introduction = f'{introduction}{jump_url or ""}\n' + source = f'```{lang}\n'+ source+ '```\n' # Use an empty string if no output language is selected - return ( - introduction - + f'```{output_syntax or ""}\n' - + output.replace('\0', "") - + '```' - ) + output_content = f'```{output_syntax or ""}\n' + output.replace('\0', "")+ '```' + return [ + introduction, + source, + output_content + ] From a6f853c05af4a71dd80fb0476bdde8fbc42017af Mon Sep 17 00:00:00 2001 From: Masterjoona <69722179+Masterjoona@users.noreply.github.com> Date: Sat, 17 Aug 2024 11:22:43 +0000 Subject: [PATCH 06/17] Fix context menu on invalid --- src/cogs/user_commands.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/cogs/user_commands.py b/src/cogs/user_commands.py index 6720d80..02a9b68 100644 --- a/src/cogs/user_commands.py +++ b/src/cogs/user_commands.py @@ -87,7 +87,7 @@ def __init__(self, get_output_with_codeblock, log_error, message): ) async def on_submit(self, interaction: discord.Interaction): - [introduction, _, run_output] = await self.get_output_with_codeblock( + output = await self.get_output_with_codeblock( guild=interaction.guild, author=interaction.user, content=self.message.content, @@ -96,6 +96,10 @@ async def on_submit(self, interaction: discord.Interaction): input_lang=self.lang.value, jump_url=self.message.jump_url, ) + if isinstance(output, str): + await interaction.response.send_message(output, ephemeral=True) + return + [introduction, _, run_output] = output await interaction.response.send_message(introduction+run_output) async def on_error( @@ -145,7 +149,7 @@ async def run_code(self, interaction: Interaction, language: str = None): await interaction.response.send_modal(SourceCodeModal(self.client.runner.get_run_output, self.client.log_error, language)) @run_code.autocomplete('language') - async def autocomplete_callback(self, interaction: discord.Interaction, current: str): + async def autocomplete_callback(self, _: discord.Interaction, current: str): langs = self.client.runner.get_languages() if current: langs = [lang for lang in langs if lang.startswith(current)] @@ -199,7 +203,7 @@ async def run_code_ctx_menu( await interaction.response.send_message(introduction+run_output) return - [introduction, _, run_output] = await self.client.runner.get_output_with_codeblock( + output = await self.client.runner.get_output_with_codeblock( guild=interaction.guild, author=interaction.user, content=message.content, @@ -208,12 +212,12 @@ async def run_code_ctx_menu( jump_url=message.jump_url, ) - if "Unsupported language" in run_output: + if isinstance(output, str): await interaction.response.send_modal( NoLang(self.client.runner.get_output_with_codeblock, self.client.log_error, message) ) return - + [introduction, _, run_output] = output await interaction.response.send_message(introduction+run_output) From 20fbdafe726d030444690d1435c75bdb05a5c82d Mon Sep 17 00:00:00 2001 From: Masterjoona <69722179+Masterjoona@users.noreply.github.com> Date: Sat, 17 Aug 2024 11:29:27 +0000 Subject: [PATCH 07/17] Fix more possible issues if error is returned --- src/cogs/user_commands.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/cogs/user_commands.py b/src/cogs/user_commands.py index 02a9b68..a677937 100644 --- a/src/cogs/user_commands.py +++ b/src/cogs/user_commands.py @@ -51,7 +51,7 @@ def __init__(self, get_run_output, log_error, language): # ) async def on_submit(self, interaction: discord.Interaction): - [introduction, source, run_output] = await self.get_run_output( + output = await self.get_run_output( guild=interaction.guild, author=interaction.user, content=self.code.value, @@ -61,6 +61,11 @@ async def on_submit(self, interaction: discord.Interaction): stdin=None, mention_author=False, ) + if isinstance(output, str): + await interaction.response.send_message(output, ephemeral=True) + return + + [introduction, source, run_output] = output await interaction.response.send_message("Here is your input:"+source) await interaction.followup.send(introduction+run_output) @@ -167,7 +172,7 @@ async def run_file( args: str = "", stdin: str = "", ): - [introduction, source, run_output] = await self.client.runner.get_output_with_file( + output = await self.client.runner.get_output_with_file( guild=interaction.guild, author=interaction.user, content="", @@ -178,7 +183,10 @@ async def run_file( stdin=stdin, mention_author=False, ) - + if isinstance(output, str): + await interaction.response.send_message(output) + return + [introduction, source, run_output] = output await interaction.response.send_message(introduction+source) await interaction.followup.send(run_output) @@ -188,7 +196,7 @@ async def run_code_ctx_menu( self, interaction: Interaction, message: discord.Message ): if len(message.attachments) > 0: - [introduction, _, run_output] = await self.client.runner.get_output_with_file( + output = await self.client.runner.get_output_with_file( guild=interaction.guild, author=interaction.user, content=message.content, @@ -200,7 +208,11 @@ async def run_code_ctx_menu( mention_author=False, jump_url=message.jump_url, ) + if isinstance(output, str): + await interaction.response.send_message(output) + return + [introduction, _, run_output] = output await interaction.response.send_message(introduction+run_output) return output = await self.client.runner.get_output_with_codeblock( From 423402ed1985d57bff94a5f962c0ae47b4cca8c4 Mon Sep 17 00:00:00 2001 From: brtwrst <6873862+brtwrst@users.noreply.github.com> Date: Sat, 17 Aug 2024 12:27:33 +0000 Subject: [PATCH 08/17] Add include_aliases parameter to get_languages --- src/cogs/user_commands.py | 4 ++-- src/cogs/utils/runner.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/cogs/user_commands.py b/src/cogs/user_commands.py index a677937..1efb254 100644 --- a/src/cogs/user_commands.py +++ b/src/cogs/user_commands.py @@ -148,14 +148,14 @@ async def cog_app_command_error(self, interaction: Interaction, error: app_comma @app_commands.user_install() @app_commands.allowed_contexts(guilds=True, dms=True, private_channels=True) async def run_code(self, interaction: Interaction, language: str = None): - if language not in self.client.runner.get_languages(): + if language not in self.client.runner.get_languages(inlude_aliases=True): await interaction.response.send_modal(SourceCodeModal(self.client.runner.get_run_output, self.client.log_error, "")) return await interaction.response.send_modal(SourceCodeModal(self.client.runner.get_run_output, self.client.log_error, language)) @run_code.autocomplete('language') async def autocomplete_callback(self, _: discord.Interaction, current: str): - langs = self.client.runner.get_languages() + langs = self.client.runner.get_languages(inlude_aliases=True) if current: langs = [lang for lang in langs if lang.startswith(current)] return [app_commands.Choice(name=lang, value=lang) for lang in langs[:25]] diff --git a/src/cogs/utils/runner.py b/src/cogs/utils/runner.py index f59954d..61028f2 100644 --- a/src/cogs/utils/runner.py +++ b/src/cogs/utils/runner.py @@ -49,8 +49,8 @@ async def update_available_languages(self): self.languages[alias] = language self.versions[alias] = runtime['version'] - def get_languages(self): - return sorted(set(self.languages.values())) + def get_languages(self, inlude_aliases=False): + return sorted(set(self.languages.keys() if inlude_aliases else self.languages.values())) async def send_to_log(self, guild, author, language, source): logging_data = { From e947d01e9d90f402d2bf2a1d830f06aa11b63f81 Mon Sep 17 00:00:00 2001 From: Masterjoona <69722179+Masterjoona@users.noreply.github.com> Date: Sat, 17 Aug 2024 14:24:43 +0000 Subject: [PATCH 09/17] Send as file if long input --- src/cogs/user_commands.py | 68 ++++++++++++++++++--------------------- 1 file changed, 32 insertions(+), 36 deletions(-) diff --git a/src/cogs/user_commands.py b/src/cogs/user_commands.py index 1efb254..5ec2772 100644 --- a/src/cogs/user_commands.py +++ b/src/cogs/user_commands.py @@ -3,7 +3,7 @@ from discord.ext import commands from .utils.errors import PistonError from asyncio import TimeoutError as AsyncTimeoutError - +from io import BytesIO class SourceCodeModal(discord.ui.Modal, title="Run Code"): def __init__(self, get_run_output, log_error, language): @@ -29,27 +29,6 @@ def __init__(self, get_run_output, log_error, language): self.add_item(self.lang) self.add_item(self.code) - # It gets pretty crowded with all these fields - # So im not sure which to keep if any at all - - # output_syntax = discord.ui.TextInput( - # label="Output Syntax", - # placeholder="the syntax of the output", - # required=False, - # ) - - # args = discord.ui.TextInput( - # label="Arguments", - # placeholder="the arguments - comma separated", - # required=False, - # ) - - # stdin = discord.ui.TextInput( - # label="Standard Input", - # placeholder="the standard input", - # required=False, - # ) - async def on_submit(self, interaction: discord.Interaction): output = await self.get_run_output( guild=interaction.guild, @@ -66,8 +45,12 @@ async def on_submit(self, interaction: discord.Interaction): return [introduction, source, run_output] = output - await interaction.response.send_message("Here is your input:"+source) - await interaction.followup.send(introduction+run_output) + if len(self.code.value) > 1000: + file = discord.File(filename=f"source_code.{self.lang.value}", fp=BytesIO(self.code.value.encode('utf-8'))) + await interaction.response.send_message(introduction + run_output, file=file) + return + await interaction.response.send_message("Here is your input:" + source) + await interaction.followup.send(introduction + run_output) async def on_error( self, interaction: discord.Interaction, error: Exception @@ -92,7 +75,7 @@ def __init__(self, get_output_with_codeblock, log_error, message): ) async def on_submit(self, interaction: discord.Interaction): - output = await self.get_output_with_codeblock( + output = await self.get_output_with_codeblock( guild=interaction.guild, author=interaction.user, content=self.message.content, @@ -105,7 +88,7 @@ async def on_submit(self, interaction: discord.Interaction): await interaction.response.send_message(output, ephemeral=True) return [introduction, _, run_output] = output - await interaction.response.send_message(introduction+run_output) + await interaction.response.send_message(introduction + run_output) async def on_error( self, interaction: discord.Interaction, error: Exception @@ -142,16 +125,26 @@ async def cog_app_command_error(self, interaction: Interaction, error: app_comma await self.client.log_error(error, Interaction) await interaction.response.send_message(f"An error occurred: {error}") - - @app_commands.command(name="run_code", description="Open a modal to input code") @app_commands.user_install() @app_commands.allowed_contexts(guilds=True, dms=True, private_channels=True) async def run_code(self, interaction: Interaction, language: str = None): if language not in self.client.runner.get_languages(inlude_aliases=True): - await interaction.response.send_modal(SourceCodeModal(self.client.runner.get_run_output, self.client.log_error, "")) + await interaction.response.send_modal( + SourceCodeModal( + self.client.runner.get_run_output, + self.client.log_error, + "", + ) + ) return - await interaction.response.send_modal(SourceCodeModal(self.client.runner.get_run_output, self.client.log_error, language)) + await interaction.response.send_modal( + SourceCodeModal( + self.client.runner.get_run_output, + self.client.log_error, + language, + ) + ) @run_code.autocomplete('language') async def autocomplete_callback(self, _: discord.Interaction, current: str): @@ -172,7 +165,7 @@ async def run_file( args: str = "", stdin: str = "", ): - output = await self.client.runner.get_output_with_file( + output = await self.client.runner.get_output_with_file( guild=interaction.guild, author=interaction.user, content="", @@ -187,8 +180,11 @@ async def run_file( await interaction.response.send_message(output) return [introduction, source, run_output] = output - await interaction.response.send_message(introduction+source) - await interaction.followup.send(run_output) + if len(source) > 1000: + await interaction.response.send_message(introduction + run_output, file=file) + return + await interaction.response.send_message("Here is your input:" + source) + await interaction.followup.send(introduction + run_output) @app_commands.user_install() @app_commands.allowed_contexts(guilds=True, dms=True, private_channels=True) @@ -196,7 +192,7 @@ async def run_code_ctx_menu( self, interaction: Interaction, message: discord.Message ): if len(message.attachments) > 0: - output = await self.client.runner.get_output_with_file( + output = await self.client.runner.get_output_with_file( guild=interaction.guild, author=interaction.user, content=message.content, @@ -213,7 +209,7 @@ async def run_code_ctx_menu( return [introduction, _, run_output] = output - await interaction.response.send_message(introduction+run_output) + await interaction.response.send_message(introduction + run_output) return output = await self.client.runner.get_output_with_codeblock( guild=interaction.guild, @@ -230,7 +226,7 @@ async def run_code_ctx_menu( ) return [introduction, _, run_output] = output - await interaction.response.send_message(introduction+run_output) + await interaction.response.send_message(introduction + run_output) async def setup(client): From 09078265a23a963609f050fbd2414553607f359c Mon Sep 17 00:00:00 2001 From: Masterjoona <69722179+Masterjoona@users.noreply.github.com> Date: Sat, 17 Aug 2024 15:07:59 +0000 Subject: [PATCH 10/17] Ephermeralize and fix files file --- src/cogs/user_commands.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/cogs/user_commands.py b/src/cogs/user_commands.py index 5ec2772..e9a927e 100644 --- a/src/cogs/user_commands.py +++ b/src/cogs/user_commands.py @@ -114,16 +114,16 @@ async def cog_app_command_error(self, interaction: Interaction, error: app_comma error_message = str(error.original) if error_message: error_message = f'`{error_message}` ' - await interaction.response.send_message(f'API Error {error_message}- Please try again later') + await interaction.response.send_message(f'API Error {error_message}- Please try again later', ephemeral=True) await self.client.log_error(error, Interaction) return if isinstance(error.original, AsyncTimeoutError): - await interaction.response.send_message(f'API Timeout - Please try again later') + await interaction.response.send_message(f'API Timeout - Please try again later', ephemeral=True) await self.client.log_error(error, Interaction) return await self.client.log_error(error, Interaction) - await interaction.response.send_message(f"An error occurred: {error}") + await interaction.response.send_message(f"An error occurred: {error}", ephemeral=True) @app_commands.command(name="run_code", description="Open a modal to input code") @app_commands.user_install() @@ -177,11 +177,12 @@ async def run_file( mention_author=False, ) if isinstance(output, str): - await interaction.response.send_message(output) + await interaction.response.send_message(output, ephemeral=True) return [introduction, source, run_output] = output if len(source) > 1000: - await interaction.response.send_message(introduction + run_output, file=file) + output_file = discord.File(filename=file.filename, fp=BytesIO((await file.read()))) + await interaction.response.send_message(introduction + run_output, file=output_file) return await interaction.response.send_message("Here is your input:" + source) await interaction.followup.send(introduction + run_output) @@ -205,7 +206,7 @@ async def run_code_ctx_menu( jump_url=message.jump_url, ) if isinstance(output, str): - await interaction.response.send_message(output) + await interaction.response.send_message(output, ephemeral=True) return [introduction, _, run_output] = output @@ -221,10 +222,12 @@ async def run_code_ctx_menu( ) if isinstance(output, str): - await interaction.response.send_modal( - NoLang(self.client.runner.get_output_with_codeblock, self.client.log_error, message) - ) - return + if "Unsupported language" in output: + await interaction.response.send_modal( + NoLang(self.client.runner.get_output_with_codeblock, self.client.log_error, message) + ) + return + await interaction.response.send_message(output, ephemeral=True) [introduction, _, run_output] = output await interaction.response.send_message(introduction + run_output) From 843fc3ebc6067114b6a1b2d9bb9a0e47de9b27f4 Mon Sep 17 00:00:00 2001 From: Masterjoona <69722179+Masterjoona@users.noreply.github.com> Date: Sat, 17 Aug 2024 15:13:08 +0000 Subject: [PATCH 11/17] Send file source file first --- src/cogs/user_commands.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/cogs/user_commands.py b/src/cogs/user_commands.py index e9a927e..69c2036 100644 --- a/src/cogs/user_commands.py +++ b/src/cogs/user_commands.py @@ -47,7 +47,8 @@ async def on_submit(self, interaction: discord.Interaction): [introduction, source, run_output] = output if len(self.code.value) > 1000: file = discord.File(filename=f"source_code.{self.lang.value}", fp=BytesIO(self.code.value.encode('utf-8'))) - await interaction.response.send_message(introduction + run_output, file=file) + await interaction.response.send_message("Here is your input:", file=file) + await interaction.followup.send(introduction + run_output) return await interaction.response.send_message("Here is your input:" + source) await interaction.followup.send(introduction + run_output) @@ -182,7 +183,8 @@ async def run_file( [introduction, source, run_output] = output if len(source) > 1000: output_file = discord.File(filename=file.filename, fp=BytesIO((await file.read()))) - await interaction.response.send_message(introduction + run_output, file=output_file) + await interaction.response.send_message("Here is your input:", file=output_file) + await interaction.followup.send(introduction + run_output) return await interaction.response.send_message("Here is your input:" + source) await interaction.followup.send(introduction + run_output) From 3ea1dfb88316eaeb39a8dd7e0dbfe44deac1cb8e Mon Sep 17 00:00:00 2001 From: Masterjoona <69722179+Masterjoona@users.noreply.github.com> Date: Sat, 17 Aug 2024 15:23:10 +0000 Subject: [PATCH 12/17] Ask for lang in context menu file - doesnt work? --- src/cogs/user_commands.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/cogs/user_commands.py b/src/cogs/user_commands.py index 69c2036..13e634d 100644 --- a/src/cogs/user_commands.py +++ b/src/cogs/user_commands.py @@ -208,6 +208,11 @@ async def run_code_ctx_menu( jump_url=message.jump_url, ) if isinstance(output, str): + if "Unsupported language" in output: + await interaction.response.send_modal( + NoLang(self.client.runner.get_output_with_codeblock, self.client.log_error, message) + ) + return await interaction.response.send_message(output, ephemeral=True) return @@ -230,6 +235,7 @@ async def run_code_ctx_menu( ) return await interaction.response.send_message(output, ephemeral=True) + return [introduction, _, run_output] = output await interaction.response.send_message(introduction + run_output) From 1391b9b40e1ec7d823d31c781204fcb4c8d218dd Mon Sep 17 00:00:00 2001 From: Masterjoona <69722179+Masterjoona@users.noreply.github.com> Date: Sun, 18 Aug 2024 13:48:07 +0000 Subject: [PATCH 13/17] Fix insane isistance usage --- src/bot.py | 4 +-- src/cogs/run.py | 10 +++---- src/cogs/user_commands.py | 60 ++++++++++++++++++++------------------- src/cogs/utils/runner.py | 20 ++++++------- 4 files changed, 45 insertions(+), 49 deletions(-) diff --git a/src/bot.py b/src/bot.py index 768c4b2..0a5d999 100644 --- a/src/bot.py +++ b/src/bot.py @@ -7,7 +7,7 @@ from datetime import datetime, timezone from os import path, listdir from discord.ext.commands import AutoShardedBot, Context -from discord import Activity, AllowedMentions, Intents +from discord import Activity, AllowedMentions, Intents, Interaction from aiohttp import ClientSession, ClientTimeout from discord.ext.commands.bot import when_mentioned_or from cogs.utils.runner import Runner @@ -57,7 +57,7 @@ def user_is_admin(self, user): return user.id in self.config['admins'] async def log_error(self, error, error_source=None): - is_context = isinstance(error_source, Context) + is_context = isinstance(error_source, Context) or isinstance(error_source, Interaction) has_attachment = bool(error_source.message.attachments) if is_context else False self.last_errors.append(( error, diff --git a/src/cogs/run.py b/src/cogs/run.py index 5450bd2..b0158d8 100644 --- a/src/cogs/run.py +++ b/src/cogs/run.py @@ -47,7 +47,7 @@ def __init__(self, client): async def get_run_output(self, ctx: commands.Context): # Get parameters to call api depending on how the command was called (file <> codeblock) if ctx.message.attachments: - [introduction, _, run_output] = await self.client.runner.get_output_with_file( + return await self.client.runner.get_output_with_file( ctx.guild, ctx.author, input_language="", @@ -58,16 +58,14 @@ async def get_run_output(self, ctx: commands.Context): file=ctx.message.attachments[0], mention_author=True, ) - return introduction + run_output - [introduction, _, run_output] = await self.client.runner.get_output_with_codeblock( + return await self.client.runner.get_output_with_codeblock( ctx.guild, ctx.author, content=ctx.message.content, mention_author=True, needs_strict_re=True, ) - return introduction + run_output async def delete_last_output(self, user_id): try: @@ -109,7 +107,7 @@ async def run(self, ctx, *, source=None): await self.send_howto(ctx) return try: - run_output = await self.get_run_output(ctx) + run_output, _ = await self.get_run_output(ctx) msg = await ctx.send(run_output) except commands.BadArgument as error: embed = Embed( @@ -129,7 +127,7 @@ async def edit_last_run(self, ctx, *, content=None): return try: msg_to_edit = self.run_IO_store[ctx.author.id].output - run_output = await self.get_run_output(ctx) + run_output, _ = await self.get_run_output(ctx) await msg_to_edit.edit(content=run_output, embed=None) except KeyError: # Message no longer exists in output store diff --git a/src/cogs/user_commands.py b/src/cogs/user_commands.py index 13e634d..b9cc0a7 100644 --- a/src/cogs/user_commands.py +++ b/src/cogs/user_commands.py @@ -30,7 +30,7 @@ def __init__(self, get_run_output, log_error, language): self.add_item(self.code) async def on_submit(self, interaction: discord.Interaction): - output = await self.get_run_output( + output, errored = await self.get_run_output( guild=interaction.guild, author=interaction.user, content=self.code.value, @@ -40,18 +40,18 @@ async def on_submit(self, interaction: discord.Interaction): stdin=None, mention_author=False, ) - if isinstance(output, str): + if errored: await interaction.response.send_message(output, ephemeral=True) return - [introduction, source, run_output] = output if len(self.code.value) > 1000: file = discord.File(filename=f"source_code.{self.lang.value}", fp=BytesIO(self.code.value.encode('utf-8'))) await interaction.response.send_message("Here is your input:", file=file) - await interaction.followup.send(introduction + run_output) + await interaction.followup.send(output) return - await interaction.response.send_message("Here is your input:" + source) - await interaction.followup.send(introduction + run_output) + formatted_src = f"```{self.lang.value}\n{self.code.value}\n```" + await interaction.response.send_message("Here is your input:" + formatted_src) + await interaction.followup.send(output) async def on_error( self, interaction: discord.Interaction, error: Exception @@ -62,7 +62,7 @@ async def on_error( await self.log_error(error, error_source="CodeModal") -class NoLang(discord.ui.Modal, title="Give lang"): +class NoLang(discord.ui.Modal, title="Give language"): def __init__(self, get_output_with_codeblock, log_error, message): super().__init__() self.get_output_with_codeblock = get_output_with_codeblock @@ -76,7 +76,7 @@ def __init__(self, get_output_with_codeblock, log_error, message): ) async def on_submit(self, interaction: discord.Interaction): - output = await self.get_output_with_codeblock( + output, errored = await self.get_output_with_codeblock( guild=interaction.guild, author=interaction.user, content=self.message.content, @@ -85,11 +85,10 @@ async def on_submit(self, interaction: discord.Interaction): input_lang=self.lang.value, jump_url=self.message.jump_url, ) - if isinstance(output, str): + if errored: await interaction.response.send_message(output, ephemeral=True) return - [introduction, _, run_output] = output - await interaction.response.send_message(introduction + run_output) + await interaction.response.send_message(output) async def on_error( self, interaction: discord.Interaction, error: Exception @@ -166,7 +165,7 @@ async def run_file( args: str = "", stdin: str = "", ): - output = await self.client.runner.get_output_with_file( + output, errored = await self.client.runner.get_output_with_file( guild=interaction.guild, author=interaction.user, content="", @@ -177,17 +176,24 @@ async def run_file( stdin=stdin, mention_author=False, ) - if isinstance(output, str): + if errored: + if "Unsupported language" in output: + await interaction.response.send_modal( + NoLang(self.client.runner.get_output_with_file, self.client.log_error, interaction.message) + ) + return await interaction.response.send_message(output, ephemeral=True) return - [introduction, source, run_output] = output - if len(source) > 1000: - output_file = discord.File(filename=file.filename, fp=BytesIO((await file.read()))) + file_contents = await file.read() + if len(file_contents) > 1000: + output_file = discord.File(filename=file.filename, fp=BytesIO(file_contents)) await interaction.response.send_message("Here is your input:", file=output_file) - await interaction.followup.send(introduction + run_output) + await interaction.followup.send(output) return - await interaction.response.send_message("Here is your input:" + source) - await interaction.followup.send(introduction + run_output) + + formatted_src = f"```{language}\n{file_contents.decode()}\n```" + await interaction.response.send_message("Here is your input:" + formatted_src) + await interaction.followup.send(output) @app_commands.user_install() @app_commands.allowed_contexts(guilds=True, dms=True, private_channels=True) @@ -195,7 +201,7 @@ async def run_code_ctx_menu( self, interaction: Interaction, message: discord.Message ): if len(message.attachments) > 0: - output = await self.client.runner.get_output_with_file( + output, errored = await self.client.runner.get_output_with_file( guild=interaction.guild, author=interaction.user, content=message.content, @@ -207,7 +213,7 @@ async def run_code_ctx_menu( mention_author=False, jump_url=message.jump_url, ) - if isinstance(output, str): + if errored: if "Unsupported language" in output: await interaction.response.send_modal( NoLang(self.client.runner.get_output_with_codeblock, self.client.log_error, message) @@ -215,11 +221,9 @@ async def run_code_ctx_menu( return await interaction.response.send_message(output, ephemeral=True) return - - [introduction, _, run_output] = output - await interaction.response.send_message(introduction + run_output) + await interaction.response.send_message(output) return - output = await self.client.runner.get_output_with_codeblock( + output, errored = await self.client.runner.get_output_with_codeblock( guild=interaction.guild, author=interaction.user, content=message.content, @@ -227,8 +231,7 @@ async def run_code_ctx_menu( needs_strict_re=False, jump_url=message.jump_url, ) - - if isinstance(output, str): + if errored: if "Unsupported language" in output: await interaction.response.send_modal( NoLang(self.client.runner.get_output_with_codeblock, self.client.log_error, message) @@ -236,8 +239,7 @@ async def run_code_ctx_menu( return await interaction.response.send_message(output, ephemeral=True) return - [introduction, _, run_output] = output - await interaction.response.send_message(introduction + run_output) + await interaction.response.send_message(output) async def setup(client): diff --git a/src/cogs/utils/runner.py b/src/cogs/utils/runner.py index 61028f2..3275802 100644 --- a/src/cogs/utils/runner.py +++ b/src/cogs/utils/runner.py @@ -88,7 +88,7 @@ async def get_output_with_codeblock( match = self.run_regex_code.search(content) if not match: - return 'Invalid command format' + return 'Invalid command format', True language, output_syntax, args, syntax, source, stdin = match.groups() @@ -105,7 +105,7 @@ async def get_output_with_codeblock( return ( f'Unsupported language: **{str(language)[:1000]}**\n' '[Request a new language](https://github.com/engineer-man/piston/issues)' - ) + ), True return await self.get_run_output( guild, @@ -134,11 +134,11 @@ async def get_output_with_file( ): MAX_BYTES = 65535 if file.size > MAX_BYTES: - return f'Source file is too big ({file.size}>{MAX_BYTES})' + return f'Source file is too big ({file.size}>{MAX_BYTES})', True filename_split = file.filename.split('.') if len(filename_split) < 2: - return 'Please provide a source file with a file extension' + return 'Please provide a source file with a file extension', True match = self.run_regex_file.search(content) if content and not match: @@ -156,7 +156,7 @@ async def get_output_with_file( return ( f'Unsupported file extension: **{language}**\n' '[Request a new language](https://github.com/engineer-man/piston/issues)' - ) + ), True source = await file.read() try: @@ -243,7 +243,7 @@ async def get_run_output( # Return early if no output was received if len(run['output'] + comp_stderr) == 0: - return f'Your {language_info} code ran without output {mention}' + return f'Your {language_info} code ran without output {mention}', False # Limit output to 30 lines maximum output = '\n'.join((comp_stderr + run['output']).split('\n')[:30]) @@ -272,11 +272,7 @@ async def get_run_output( if jump_url: jump_url = f'from running: {jump_url}' introduction = f'{introduction}{jump_url or ""}\n' - source = f'```{lang}\n'+ source+ '```\n' + #source = f'```{lang}\n'+ source+ '```\n' # Use an empty string if no output language is selected output_content = f'```{output_syntax or ""}\n' + output.replace('\0', "")+ '```' - return [ - introduction, - source, - output_content - ] + return introduction + output_content, False From 443031c3bb99b39055870434d7e131d66c8a6ff7 Mon Sep 17 00:00:00 2001 From: Masterjoona <69722179+Masterjoona@users.noreply.github.com> Date: Sun, 18 Aug 2024 14:00:38 +0000 Subject: [PATCH 14/17] Add interaction defers - Incase request takes longer --- src/cogs/user_commands.py | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/src/cogs/user_commands.py b/src/cogs/user_commands.py index b9cc0a7..6eae1c3 100644 --- a/src/cogs/user_commands.py +++ b/src/cogs/user_commands.py @@ -30,6 +30,7 @@ def __init__(self, get_run_output, log_error, language): self.add_item(self.code) async def on_submit(self, interaction: discord.Interaction): + await interaction.response.defer() output, errored = await self.get_run_output( guild=interaction.guild, author=interaction.user, @@ -41,22 +42,22 @@ async def on_submit(self, interaction: discord.Interaction): mention_author=False, ) if errored: - await interaction.response.send_message(output, ephemeral=True) + await interaction.followup.send(output, ephemeral=True) return if len(self.code.value) > 1000: file = discord.File(filename=f"source_code.{self.lang.value}", fp=BytesIO(self.code.value.encode('utf-8'))) - await interaction.response.send_message("Here is your input:", file=file) + await interaction.followup.send("Here is your input:", file=file) await interaction.followup.send(output) return formatted_src = f"```{self.lang.value}\n{self.code.value}\n```" - await interaction.response.send_message("Here is your input:" + formatted_src) + await interaction.followup.send("Here is your input:" + formatted_src) await interaction.followup.send(output) async def on_error( self, interaction: discord.Interaction, error: Exception ) -> None: - await interaction.response.send_message( + await interaction.followup.send( "Oops! Something went wrong.", ephemeral=True ) @@ -76,6 +77,7 @@ def __init__(self, get_output_with_codeblock, log_error, message): ) async def on_submit(self, interaction: discord.Interaction): + await interaction.response.defer() output, errored = await self.get_output_with_codeblock( guild=interaction.guild, author=interaction.user, @@ -86,14 +88,14 @@ async def on_submit(self, interaction: discord.Interaction): jump_url=self.message.jump_url, ) if errored: - await interaction.response.send_message(output, ephemeral=True) + await interaction.followup.send(output, ephemeral=True) return - await interaction.response.send_message(output) + await interaction.followup.send(output) async def on_error( self, interaction: discord.Interaction, error: Exception ) -> None: - await interaction.response.send_message( + await interaction.followup.send( "Oops! Something went wrong.", ephemeral=True ) @@ -114,16 +116,16 @@ async def cog_app_command_error(self, interaction: Interaction, error: app_comma error_message = str(error.original) if error_message: error_message = f'`{error_message}` ' - await interaction.response.send_message(f'API Error {error_message}- Please try again later', ephemeral=True) + await interaction.followup.send(f'API Error {error_message}- Please try again later', ephemeral=True) await self.client.log_error(error, Interaction) return if isinstance(error.original, AsyncTimeoutError): - await interaction.response.send_message(f'API Timeout - Please try again later', ephemeral=True) + await interaction.followup.send(f'API Timeout - Please try again later', ephemeral=True) await self.client.log_error(error, Interaction) return await self.client.log_error(error, Interaction) - await interaction.response.send_message(f"An error occurred: {error}", ephemeral=True) + await interaction.followup.send(f"An error occurred: {error}", ephemeral=True) @app_commands.command(name="run_code", description="Open a modal to input code") @app_commands.user_install() @@ -165,6 +167,7 @@ async def run_file( args: str = "", stdin: str = "", ): + await interaction.response.defer() output, errored = await self.client.runner.get_output_with_file( guild=interaction.guild, author=interaction.user, @@ -182,17 +185,17 @@ async def run_file( NoLang(self.client.runner.get_output_with_file, self.client.log_error, interaction.message) ) return - await interaction.response.send_message(output, ephemeral=True) + await interaction.followup.send(output, ephemeral=True) return file_contents = await file.read() if len(file_contents) > 1000: output_file = discord.File(filename=file.filename, fp=BytesIO(file_contents)) - await interaction.response.send_message("Here is your input:", file=output_file) + await interaction.followup.send("Here is your input:", file=output_file) await interaction.followup.send(output) return formatted_src = f"```{language}\n{file_contents.decode()}\n```" - await interaction.response.send_message("Here is your input:" + formatted_src) + await interaction.followup.send("Here is your input:" + formatted_src) await interaction.followup.send(output) @app_commands.user_install() @@ -200,6 +203,7 @@ async def run_file( async def run_code_ctx_menu( self, interaction: Interaction, message: discord.Message ): + await interaction.response.defer() if len(message.attachments) > 0: output, errored = await self.client.runner.get_output_with_file( guild=interaction.guild, @@ -219,9 +223,9 @@ async def run_code_ctx_menu( NoLang(self.client.runner.get_output_with_codeblock, self.client.log_error, message) ) return - await interaction.response.send_message(output, ephemeral=True) + await interaction.followup.send(output, ephemeral=True) return - await interaction.response.send_message(output) + await interaction.followup.send(output) return output, errored = await self.client.runner.get_output_with_codeblock( guild=interaction.guild, @@ -237,9 +241,9 @@ async def run_code_ctx_menu( NoLang(self.client.runner.get_output_with_codeblock, self.client.log_error, message) ) return - await interaction.response.send_message(output, ephemeral=True) + await interaction.followup.send(output, ephemeral=True) return - await interaction.response.send_message(output) + await interaction.followup.send(output) async def setup(client): From b72bd685d1703be8e24c0210f1ac68263671e4d0 Mon Sep 17 00:00:00 2001 From: Masterjoona <69722179+Masterjoona@users.noreply.github.com> Date: Sun, 18 Aug 2024 14:51:23 +0000 Subject: [PATCH 15/17] Ctx menu error --- src/bot.py | 2 +- src/cogs/user_commands.py | 17 +++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/bot.py b/src/bot.py index 0a5d999..ac77088 100644 --- a/src/bot.py +++ b/src/bot.py @@ -57,7 +57,7 @@ def user_is_admin(self, user): return user.id in self.config['admins'] async def log_error(self, error, error_source=None): - is_context = isinstance(error_source, Context) or isinstance(error_source, Interaction) + is_context = isinstance(error_source, Context) has_attachment = bool(error_source.message.attachments) if is_context else False self.last_errors.append(( error, diff --git a/src/cogs/user_commands.py b/src/cogs/user_commands.py index 6eae1c3..2b503b3 100644 --- a/src/cogs/user_commands.py +++ b/src/cogs/user_commands.py @@ -99,7 +99,7 @@ async def on_error( "Oops! Something went wrong.", ephemeral=True ) - await self.log_error(error, error_source="CodeModal") + await self.log_error(error, error_source="NoLangModal") class UserCommands(commands.Cog, name="UserCommands"): @@ -109,6 +109,7 @@ def __init__(self, client): name="Run Code", callback=self.run_code_ctx_menu, ) + #self.ctx_menu.error(self.run_code_ctx_menu_error) self.client.tree.add_command(self.ctx_menu) async def cog_app_command_error(self, interaction: Interaction, error: app_commands.AppCommandError): @@ -167,7 +168,6 @@ async def run_file( args: str = "", stdin: str = "", ): - await interaction.response.defer() output, errored = await self.client.runner.get_output_with_file( guild=interaction.guild, author=interaction.user, @@ -200,10 +200,7 @@ async def run_file( @app_commands.user_install() @app_commands.allowed_contexts(guilds=True, dms=True, private_channels=True) - async def run_code_ctx_menu( - self, interaction: Interaction, message: discord.Message - ): - await interaction.response.defer() + async def run_code_ctx_menu(self, interaction: Interaction, message: discord.Message): if len(message.attachments) > 0: output, errored = await self.client.runner.get_output_with_file( guild=interaction.guild, @@ -245,6 +242,14 @@ async def run_code_ctx_menu( return await interaction.followup.send(output) + async def run_code_ctx_menu_error(self, interaction: discord.Interaction, error: Exception): + await self.client.log_error(error, interaction) + print(error) + if isinstance(error, commands.BadArgument): + await interaction.followup.send(str(error), ephemeral=True) + return + await interaction.followup.send("Oops! Something went wrong.", ephemeral=True) + async def setup(client): await client.add_cog(UserCommands(client)) From 79ca3142b40e7302c5a70920a39faf5d1ad1aadb Mon Sep 17 00:00:00 2001 From: Masterjoona <69722179+Masterjoona@users.noreply.github.com> Date: Wed, 21 Aug 2024 19:13:32 +0000 Subject: [PATCH 16/17] Fix/revert some stuff - mainly defer stuff --- src/cogs/run.py | 27 +++++++-- src/cogs/user_commands.py | 122 +++++++++++++++++++++----------------- src/cogs/utils/runner.py | 69 +++++++-------------- 3 files changed, 107 insertions(+), 111 deletions(-) diff --git a/src/cogs/run.py b/src/cogs/run.py index b0158d8..5022624 100644 --- a/src/cogs/run.py +++ b/src/cogs/run.py @@ -47,25 +47,40 @@ def __init__(self, client): async def get_run_output(self, ctx: commands.Context): # Get parameters to call api depending on how the command was called (file <> codeblock) if ctx.message.attachments: - return await self.client.runner.get_output_with_file( - ctx.guild, - ctx.author, + source, language, output_syntax, args, stdin = self.client.runner.get_api_params_with_file( input_language="", output_syntax="", args="", stdin="", content=ctx.message.content, file=ctx.message.attachments[0], + ) + return await self.client.runner.get_run_output( + ctx.guild, + ctx.author, + source=source, + language=language, + output_syntax=output_syntax, + args=args, + stdin=stdin, mention_author=True, ) - return await self.client.runner.get_output_with_codeblock( - ctx.guild, - ctx.author, + source, language, output_syntax, args, stdin = self.client.runner.get_api_params_with_codeblock( content=ctx.message.content, mention_author=True, needs_strict_re=True, ) + return await self.client.runner.get_run_output( + ctx.guild, + ctx.author, + source=source, + language=language, + output_syntax=output_syntax, + args=args, + stdin=stdin, + mention_author=True, + ) async def delete_last_output(self, user_id): try: diff --git a/src/cogs/user_commands.py b/src/cogs/user_commands.py index 2b503b3..ed31222 100644 --- a/src/cogs/user_commands.py +++ b/src/cogs/user_commands.py @@ -31,7 +31,7 @@ def __init__(self, get_run_output, log_error, language): async def on_submit(self, interaction: discord.Interaction): await interaction.response.defer() - output, errored = await self.get_run_output( + output = await self.get_run_output( guild=interaction.guild, author=interaction.user, content=self.code.value, @@ -41,9 +41,6 @@ async def on_submit(self, interaction: discord.Interaction): stdin=None, mention_author=False, ) - if errored: - await interaction.followup.send(output, ephemeral=True) - return if len(self.code.value) > 1000: file = discord.File(filename=f"source_code.{self.lang.value}", fp=BytesIO(self.code.value.encode('utf-8'))) @@ -64,9 +61,10 @@ async def on_error( await self.log_error(error, error_source="CodeModal") class NoLang(discord.ui.Modal, title="Give language"): - def __init__(self, get_output_with_codeblock, log_error, message): + def __init__(self, get_api_params_with_codeblock, get_run_output, log_error, message): super().__init__() - self.get_output_with_codeblock = get_output_with_codeblock + self.get_api_params_with_codeblock = get_api_params_with_codeblock + self.get_run_output = get_run_output self.log_error = log_error self.message = message @@ -77,8 +75,7 @@ def __init__(self, get_output_with_codeblock, log_error, message): ) async def on_submit(self, interaction: discord.Interaction): - await interaction.response.defer() - output, errored = await self.get_output_with_codeblock( + source, language, output_syntax, args, stdin = await self.get_api_params_with_codeblock( guild=interaction.guild, author=interaction.user, content=self.message.content, @@ -87,9 +84,18 @@ async def on_submit(self, interaction: discord.Interaction): input_lang=self.lang.value, jump_url=self.message.jump_url, ) - if errored: - await interaction.followup.send(output, ephemeral=True) - return + + await interaction.response.defer() + output = await self.get_run_output( + guild=interaction.guild, + author=interaction.user, + content=source, + lang=language, + output_syntax=output_syntax, + args=args, + stdin=stdin, + mention_author=False, + ) await interaction.followup.send(output) async def on_error( @@ -109,7 +115,7 @@ def __init__(self, client): name="Run Code", callback=self.run_code_ctx_menu, ) - #self.ctx_menu.error(self.run_code_ctx_menu_error) + self.ctx_menu.error(self.run_code_ctx_menu_error) self.client.tree.add_command(self.ctx_menu) async def cog_app_command_error(self, interaction: Interaction, error: app_commands.AppCommandError): @@ -168,7 +174,7 @@ async def run_file( args: str = "", stdin: str = "", ): - output, errored = await self.client.runner.get_output_with_file( + source, language, output_syntax, args, stdin = await self.client.runner.get_api_params_with_file( guild=interaction.guild, author=interaction.user, content="", @@ -179,22 +185,27 @@ async def run_file( stdin=stdin, mention_author=False, ) - if errored: - if "Unsupported language" in output: - await interaction.response.send_modal( - NoLang(self.client.runner.get_output_with_file, self.client.log_error, interaction.message) - ) - return - await interaction.followup.send(output, ephemeral=True) - return - file_contents = await file.read() - if len(file_contents) > 1000: - output_file = discord.File(filename=file.filename, fp=BytesIO(file_contents)) + await interaction.response.defer() + output = await self.client.runner.get_run_output( + guild=interaction.guild, + author=interaction.user, + content=source, + lang=language, + output_syntax=output_syntax, + args=args, + stdin=stdin, + mention_author=False, + jump_url=None, + ) + + + if len(source) > 1000: + output_file = discord.File(filename=file.filename, fp=BytesIO(source)) await interaction.followup.send("Here is your input:", file=output_file) await interaction.followup.send(output) return - formatted_src = f"```{language}\n{file_contents.decode()}\n```" + formatted_src = f"```{language}\n{source}\n```" await interaction.followup.send("Here is your input:" + formatted_src) await interaction.followup.send(output) @@ -202,53 +213,52 @@ async def run_file( @app_commands.allowed_contexts(guilds=True, dms=True, private_channels=True) async def run_code_ctx_menu(self, interaction: Interaction, message: discord.Message): if len(message.attachments) > 0: - output, errored = await self.client.runner.get_output_with_file( - guild=interaction.guild, - author=interaction.user, + source, language, output_syntax, args, stdin = await self.client.runner.get_api_params_with_file( content=message.content, file=message.attachments[0], input_language="", output_syntax="", args="", - stdin="", - mention_author=False, - jump_url=message.jump_url, + stdin="" ) - if errored: - if "Unsupported language" in output: - await interaction.response.send_modal( - NoLang(self.client.runner.get_output_with_codeblock, self.client.log_error, message) - ) - return - await interaction.followup.send(output, ephemeral=True) - return - await interaction.followup.send(output) - return - output, errored = await self.client.runner.get_output_with_codeblock( + else: + source, language, output_syntax, args, stdin = await self.client.runner.get_api_params_with_codeblock( + content=message.content, + needs_strict_re=False, + input_lang=None, + ) + await interaction.response.defer() + output = await self.client.runner.get_run_output( guild=interaction.guild, author=interaction.user, - content=message.content, + content=source, + lang=language, + output_syntax=output_syntax, + args=args, + stdin=stdin, mention_author=False, - needs_strict_re=False, jump_url=message.jump_url, ) - if errored: - if "Unsupported language" in output: - await interaction.response.send_modal( - NoLang(self.client.runner.get_output_with_codeblock, self.client.log_error, message) - ) - return - await interaction.followup.send(output, ephemeral=True) - return await interaction.followup.send(output) async def run_code_ctx_menu_error(self, interaction: discord.Interaction, error: Exception): await self.client.log_error(error, interaction) - print(error) - if isinstance(error, commands.BadArgument): - await interaction.followup.send(str(error), ephemeral=True) + to_user = ["No source code", "Invalid command"] + string_error = str(error) + if "Unsupported lang" in string_error: + await interaction.response.send_modal( + NoLang( + self.client.runner.get_api_params_with_codeblock, + self.client.runner.get_run_output, + self.client.log_error, + interaction.message + ) + ) + return + if any(error in string_error for error in to_user): + await interaction.response.send_message(string_error.split("BadArgument: ")[1], ephemeral=True) return - await interaction.followup.send("Oops! Something went wrong.", ephemeral=True) + await interaction.response.send_message("Oops! Something went wrong.", ephemeral=True) async def setup(client): diff --git a/src/cogs/utils/runner.py b/src/cogs/utils/runner.py index 3275802..da1845d 100644 --- a/src/cogs/utils/runner.py +++ b/src/cogs/utils/runner.py @@ -52,6 +52,9 @@ async def update_available_languages(self): def get_languages(self, inlude_aliases=False): return sorted(set(self.languages.keys() if inlude_aliases else self.languages.values())) + def get_language(self, language): + return self.languages.get(language, None) + async def send_to_log(self, guild, author, language, source): logging_data = { 'server': guild.name if guild else 'DMChannel', @@ -72,23 +75,21 @@ async def send_to_log(self, guild, author, language, source): pass return True - async def get_output_with_codeblock( + async def get_api_params_with_codeblock( self, - guild, - author, content, - mention_author, needs_strict_re, input_lang=None, - jump_url=None ): + if content.count('```') != 2: + raise commands.BadArgument('Invalid command format (missing codeblock?)') if needs_strict_re: match = self.run_regex_code_strict.search(content) else: match = self.run_regex_code.search(content) if not match: - return 'Invalid command format', True + raise commands.BadArgument('Invalid command format') language, output_syntax, args, syntax, source, stdin = match.groups() @@ -102,35 +103,21 @@ async def get_output_with_codeblock( language = language.lower() if language not in self.languages: - return ( + raise commands.BadArgument( f'Unsupported language: **{str(language)[:1000]}**\n' '[Request a new language](https://github.com/engineer-man/piston/issues)' - ), True - - return await self.get_run_output( - guild, - author, - source, - language, - output_syntax, - args, - stdin, - mention_author, - jump_url, - ) + ) + + return source, language, output_syntax, args, stdin - async def get_output_with_file( + async def get_api_params_with_file( self, - guild, - author, file, input_language, output_syntax, args, stdin, - mention_author, content, - jump_url=None, ): MAX_BYTES = 65535 if file.size > MAX_BYTES: @@ -153,47 +140,31 @@ async def get_output_with_file( language = language.lower() if language not in self.languages: - return ( - f'Unsupported file extension: **{language}**\n' + raise commands.BadArgument( + f'Unsupported language: **{str(language)[:1000]}**\n' '[Request a new language](https://github.com/engineer-man/piston/issues)' - ), True + ) source = await file.read() try: source = source.decode('utf-8') except UnicodeDecodeError as e: return str(e) - return await self.get_run_output( - guild, - author, - source, - language, # type: ignore - output_syntax, - args, - stdin, - mention_author, - jump_url, - ) + return source, language, output_syntax, args, stdin + async def get_run_output( self, guild, author, content, - input_lang, + lang, output_syntax, args, stdin, mention_author, jump_url=None, ): - lang = self.languages.get(input_lang, None) - if not lang: - return ( - f'Unsupported language: **{str(input_lang)}**\n' - '[Request a new language](https://github.com/engineer-man/piston/issues)' - ) - version = self.versions[lang] # Add boilerplate code to supported languages @@ -243,7 +214,7 @@ async def get_run_output( # Return early if no output was received if len(run['output'] + comp_stderr) == 0: - return f'Your {language_info} code ran without output {mention}', False + return f'Your {language_info} code ran without output {mention}' # Limit output to 30 lines maximum output = '\n'.join((comp_stderr + run['output']).split('\n')[:30]) @@ -275,4 +246,4 @@ async def get_run_output( #source = f'```{lang}\n'+ source+ '```\n' # Use an empty string if no output language is selected output_content = f'```{output_syntax or ""}\n' + output.replace('\0', "")+ '```' - return introduction + output_content, False + return introduction + output_content From c6121158eddfe8838e192411db1a66c3a33a6f87 Mon Sep 17 00:00:00 2001 From: Masterjoona <69722179+Masterjoona@users.noreply.github.com> Date: Mon, 26 Aug 2024 13:28:16 +0000 Subject: [PATCH 17/17] try except stuff --- src/cogs/error_handler.py | 4 +- src/cogs/user_commands.py | 81 +++++++++++++++++---------------------- src/cogs/utils/errors.py | 4 ++ src/cogs/utils/runner.py | 6 +-- 4 files changed, 45 insertions(+), 50 deletions(-) diff --git a/src/cogs/error_handler.py b/src/cogs/error_handler.py index d629c12..e6f9b99 100644 --- a/src/cogs/error_handler.py +++ b/src/cogs/error_handler.py @@ -13,7 +13,7 @@ from asyncio import TimeoutError as AsyncTimeoutError from discord import Embed, DMChannel, errors as discord_errors from discord.ext import commands -from .utils.errors import PistonError +from .utils.errors import PistonError, NoLanguageFoundError class ErrorHandler(commands.Cog, name='ErrorHandler'): @@ -63,7 +63,7 @@ async def on_command_error(self, ctx, error): await ctx.send(f'Sorry {usr}, you are not allowed to run this command.') return - if isinstance(error, commands.BadArgument): + if isinstance(error, commands.BadArgument) or isinstance(error, NoLanguageFoundError): # It's in an embed to prevent mentions from working embed = Embed( title='Error', diff --git a/src/cogs/user_commands.py b/src/cogs/user_commands.py index ed31222..01b1ea8 100644 --- a/src/cogs/user_commands.py +++ b/src/cogs/user_commands.py @@ -1,7 +1,7 @@ import discord from discord import app_commands, Interaction, Attachment from discord.ext import commands -from .utils.errors import PistonError +from .utils.errors import PistonError, NoLanguageFoundError from asyncio import TimeoutError as AsyncTimeoutError from io import BytesIO @@ -58,7 +58,7 @@ async def on_error( "Oops! Something went wrong.", ephemeral=True ) - await self.log_error(error, error_source="CodeModal") + await self.log_error(error, error_source="SourceCodeModal") class NoLang(discord.ui.Modal, title="Give language"): def __init__(self, get_api_params_with_codeblock, get_run_output, log_error, message): @@ -115,7 +115,6 @@ def __init__(self, client): name="Run Code", callback=self.run_code_ctx_menu, ) - self.ctx_menu.error(self.run_code_ctx_menu_error) self.client.tree.add_command(self.ctx_menu) async def cog_app_command_error(self, interaction: Interaction, error: app_commands.AppCommandError): @@ -132,7 +131,7 @@ async def cog_app_command_error(self, interaction: Interaction, error: app_comma await self.client.log_error(error, Interaction) return await self.client.log_error(error, Interaction) - await interaction.followup.send(f"An error occurred: {error}", ephemeral=True) + await interaction.followup.send(f'{error.original}', ephemeral=True) @app_commands.command(name="run_code", description="Open a modal to input code") @app_commands.user_install() @@ -212,54 +211,46 @@ async def run_file( @app_commands.user_install() @app_commands.allowed_contexts(guilds=True, dms=True, private_channels=True) async def run_code_ctx_menu(self, interaction: Interaction, message: discord.Message): - if len(message.attachments) > 0: - source, language, output_syntax, args, stdin = await self.client.runner.get_api_params_with_file( - content=message.content, - file=message.attachments[0], - input_language="", - output_syntax="", - args="", - stdin="" - ) - else: - source, language, output_syntax, args, stdin = await self.client.runner.get_api_params_with_codeblock( - content=message.content, - needs_strict_re=False, - input_lang=None, + try: + if len(message.attachments) > 0: + source, language, output_syntax, args, stdin = await self.client.runner.get_api_params_with_file( + content=message.content, + file=message.attachments[0], + input_language="", + output_syntax="", + args="", + stdin="" + ) + else: + source, language, output_syntax, args, stdin = await self.client.runner.get_api_params_with_codeblock( + content=message.content, + needs_strict_re=False, + input_lang=None, + ) + await interaction.response.defer() + output = await self.client.runner.get_run_output( + guild=interaction.guild, + author=interaction.user, + content=source, + lang=language, + output_syntax=output_syntax, + args=args, + stdin=stdin, + mention_author=False, + jump_url=message.jump_url, ) - await interaction.response.defer() - output = await self.client.runner.get_run_output( - guild=interaction.guild, - author=interaction.user, - content=source, - lang=language, - output_syntax=output_syntax, - args=args, - stdin=stdin, - mention_author=False, - jump_url=message.jump_url, - ) - await interaction.followup.send(output) - - async def run_code_ctx_menu_error(self, interaction: discord.Interaction, error: Exception): - await self.client.log_error(error, interaction) - to_user = ["No source code", "Invalid command"] - string_error = str(error) - if "Unsupported lang" in string_error: + await interaction.followup.send(output) + except NoLanguageFoundError: await interaction.response.send_modal( NoLang( self.client.runner.get_api_params_with_codeblock, self.client.runner.get_run_output, self.client.log_error, - interaction.message - ) + message, ) - return - if any(error in string_error for error in to_user): - await interaction.response.send_message(string_error.split("BadArgument: ")[1], ephemeral=True) - return - await interaction.response.send_message("Oops! Something went wrong.", ephemeral=True) - + ) + except commands.BadArgument as e: + await interaction.followup.send(str(e), ephemeral=True) async def setup(client): await client.add_cog(UserCommands(client)) diff --git a/src/cogs/utils/errors.py b/src/cogs/utils/errors.py index ede1dae..7ac06da 100644 --- a/src/cogs/utils/errors.py +++ b/src/cogs/utils/errors.py @@ -13,3 +13,7 @@ class PistonInvalidStatus(PistonError): class PistonInvalidContentType(PistonError): """Exception raised when the API request returns a non JSON content type""" pass + +class NoLanguageFoundError(Exception): + """Exception raised when no language is found""" + pass diff --git a/src/cogs/utils/runner.py b/src/cogs/utils/runner.py index da1845d..0cc740c 100644 --- a/src/cogs/utils/runner.py +++ b/src/cogs/utils/runner.py @@ -1,6 +1,6 @@ import re import json -from .errors import PistonInvalidContentType, PistonInvalidStatus, PistonNoOutput +from .errors import PistonInvalidContentType, PistonInvalidStatus, PistonNoOutput, NoLanguageFoundError from discord.ext import commands, tasks from discord.utils import escape_mentions from aiohttp import ContentTypeError @@ -103,7 +103,7 @@ async def get_api_params_with_codeblock( language = language.lower() if language not in self.languages: - raise commands.BadArgument( + raise NoLanguageFoundError( f'Unsupported language: **{str(language)[:1000]}**\n' '[Request a new language](https://github.com/engineer-man/piston/issues)' ) @@ -140,7 +140,7 @@ async def get_api_params_with_file( language = language.lower() if language not in self.languages: - raise commands.BadArgument( + raise NoLanguageFoundError( f'Unsupported language: **{str(language)[:1000]}**\n' '[Request a new language](https://github.com/engineer-man/piston/issues)' )