aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar Henrik Böving <[email protected]>2018-05-08 09:26:11 +0200
committerGravatar Leon Sandøy <[email protected]>2018-05-08 09:26:11 +0200
commit9f8feeabc920473c5ce47e2d0c9b4542a3f64f6c (patch)
tree2bd8022e9ebe873532f543e0c43530386fe449a5
parentUpdate README.md (diff)
Codeblock 1.0 (#35)
* adding detection for bad ticks * no more codeblock tag * several minor changes and a seperate function for repl stripping * fixing linter issues * improving repl stripper * line limit * better comments * logging * New sings, reformatting strings and removing one fstring lemon missed * improved logging * now exchanging bad ticks and sending backtick + codeblocking hint * removing the need for a codeblock to be a python one * Reworked the which hint to send when logic * damn flake8 * this should fix the issues addressed by lemon * lemons other issues * forgot to save.... * rstrip * now fixing overidented code * merging caused code format issues * ... * adding support for bad indentation fixing * dont requrie py or python to be at the code block, move mention out of the embed * typo * improved bad ticks hint * refactoring the embed yet AGAIN and ignoring code with backticks in it * neeew embeds * perfect formatting * leaving repl code in the embed * flake8......(and auto spaces) * ...... * adding support for not complete python at beginning of the codeblock * issue with bad tick detection fixed * more logging, always sending hint once repl detected * too much logging * fixes to 'when to send the repl hint' * forgot to remove a debug print * better comments * even better comments * more and better comments, fixed one bug with returning in codeblock_stripping * flake8......(and auto spaces) again * addressed all the issues by lemon * oversaw one * DESTROY JUAN * blank line for readability * flake8 * .... * apertures issues
-rw-r--r--bot/cogs/bot.py263
1 files changed, 210 insertions, 53 deletions
diff --git a/bot/cogs/bot.py b/bot/cogs/bot.py
index 3870356ce..e64d928fa 100644
--- a/bot/cogs/bot.py
+++ b/bot/cogs/bot.py
@@ -26,15 +26,16 @@ class Bot:
def __init__(self, bot: AutoShardedBot):
self.bot = bot
- # Stores allowed channels plus unix timestamp from last call
- self.channel_cooldowns = {HELP1_CHANNEL: 0,
- HELP2_CHANNEL: 0,
- HELP3_CHANNEL: 0,
- HELP4_CHANNEL: 0,
- PYTHON_CHANNEL: 0,
- DEVTEST_CHANNEL: 0,
- BOT_CHANNEL: 0
- } # noqa. E124
+ # Stores allowed channels plus unix timestamp from last call.
+ self.channel_cooldowns = {
+ HELP1_CHANNEL: 0,
+ HELP2_CHANNEL: 0,
+ HELP3_CHANNEL: 0,
+ HELP4_CHANNEL: 0,
+ PYTHON_CHANNEL: 0,
+ DEVTEST_CHANNEL: 0,
+ BOT_CHANNEL: 0
+ }
@group(invoke_without_command=True, name="bot", hidden=True)
@with_role(VERIFIED_ROLE)
@@ -100,7 +101,7 @@ class Bot:
embed = Embed(description=text)
await ctx.send(embed=embed)
- def codeblock_stripping(self, msg: str):
+ def codeblock_stripping(self, msg: str, bad_ticks: bool):
"""
Strip msg in order to find Python code.
@@ -108,73 +109,229 @@ class Bot:
None if the block is a valid Python codeblock.
"""
if msg.count("\n") >= 3:
- # Filtering valid Python codeblocks and exiting if a valid Python codeblock is found
- if re.search("```(?:py|python)\n(.*?)```", msg, re.IGNORECASE | re.DOTALL):
- log.trace("Someone wrote a message that was already a "
- "valid Python syntax highlighted code block. No action taken.")
+ # Filtering valid Python codeblocks and exiting if a valid Python codeblock is found.
+ if re.search("```(?:py|python)\n(.*?)```", msg, re.IGNORECASE | re.DOTALL) and not bad_ticks:
+ log.trace(
+ "Someone wrote a message that was already a "
+ "valid Python syntax highlighted code block. No action taken."
+ )
return None
+
else:
# Stripping backticks from every line of the message.
log.trace(f"Stripping backticks from message.\n\n{msg}\n\n")
content = ""
- for line in msg.splitlines():
- content += line.strip("`") + "\n"
+ for line in msg.splitlines(keepends=True):
+ content += line.strip("`")
content = content.strip()
- # Remove "Python" or "Py" from top of the message if exists
+ # Remove "Python" or "Py" from start of the message if it exists.
log.trace(f"Removing 'py' or 'python' from message.\n\n{content}\n\n")
+ pycode = False
if content.lower().startswith("python"):
content = content[6:]
+ pycode = True
elif content.lower().startswith("py"):
content = content[2:]
+ pycode = True
+
+ if pycode:
+ content = content.splitlines(keepends=True)
+
+ # Check if there might be code in the first line, and preserve it.
+ first_line = content[0]
+ if " " in content[0]:
+ first_space = first_line.index(" ")
+ content[0] = first_line[first_space:]
+ content = "".join(content)
+
+ # If there's no code we can just get rid of the first line.
+ else:
+ content = "".join(content[1:])
+
+ # Strip it again to remove any leading whitespace. This is neccessary
+ # if the first line of the message looked like ```python <code>
+ old = content.strip()
+
+ # Strips REPL code out of the message if there is any.
+ content, repl_code = self.repl_stripping(old)
+ if old != content:
+ return (content, old), repl_code
+
+ # Try to apply indentation fixes to the code.
+ content = self.fix_indentation(content)
+
+ # Check if the code contains backticks, if it does ignore the message.
+ if "`" in content:
+ log.trace("Detected ` inside the code, won't reply")
+ return None
+ else:
+ log.trace(f"Returning message.\n\n{content}\n\n")
+ return (content,), repl_code
+
+ def fix_indentation(self, msg: str):
+ """
+ Attempts to fix badly indented code.
+ """
+ def unindent(code, skip_spaces=0):
+ """
+ Unindents all code down to the number of spaces given ins skip_spaces
+ """
+ final = ""
+ current = code[0]
+ leading_spaces = 0
+
+ # Get numbers of spaces before code in the first line.
+ while current == " ":
+ current = code[leading_spaces+1]
+ leading_spaces += 1
+ leading_spaces -= skip_spaces
+
+ # If there are any, remove that number of spaces from every line.
+ if leading_spaces > 0:
+ for line in code.splitlines(keepends=True):
+ line = line[leading_spaces:]
+ final += line
+ return final
+ else:
+ return code
+
+ # Apply fix for "all lines are overindented" case.
+ msg = unindent(msg)
+
+ # If the first line does not end with a colon, we can be
+ # certain the next line will be on the same indentation level.
+ #
+ # If it does end with a colon, we will need to indent all successive
+ # lines one additional level.
+ first_line = msg.splitlines()[0]
+ code = "".join(msg.splitlines(keepends=True)[1:])
+ if not first_line.endswith(":"):
+ msg = f"{first_line}\n{unindent(code)}"
+ else:
+ msg = f"{first_line}\n{unindent(code, 4)}"
+ return msg
+
+ def repl_stripping(self, msg: str):
+ """
+ Strip msg in order to extract Python code out of REPL output.
- # Strip again to remove the whitespace(s) left before the code
- # If the msg looked like "Python <code>" before removing Python
- content = content.strip()
- log.trace(f"Returning message.\n\n{content}\n\n")
- return content
+ Tries to strip out REPL Python code out of msg and returns the stripped msg.
+ """
+ final = ""
+ for line in msg.splitlines(keepends=True):
+ if line.startswith(">>>") or line.startswith("..."):
+ final += line[4:]
+ log.trace(f"Formatted: \n\n{msg}\n\n to \n\n{final}\n\n")
+ if not final:
+ log.debug(f"Found no REPL code in \n\n{msg}\n\n")
+ return msg, False
+ else:
+ log.debug(f"Found REPL code in \n\n{msg}\n\n")
+ return final.rstrip(), True
async def on_message(self, msg: Message):
- if not msg.author.bot:
- if msg.channel.id in self.channel_cooldowns:
- on_cooldown = time.time() - self.channel_cooldowns[msg.channel.id] < 300
- if not on_cooldown or msg.channel.id == DEVTEST_CHANNEL:
- try:
- content = self.codeblock_stripping(msg.content)
- if not content:
+ if msg.channel.id in self.channel_cooldowns and not msg.author.bot and len(msg.content.splitlines()) > 3:
+ on_cooldown = time.time() - self.channel_cooldowns[msg.channel.id] < 300
+ if not on_cooldown or msg.channel.id == DEVTEST_CHANNEL:
+ try:
+ not_backticks = ["'''", '"""', "´´´", "‘‘‘", "’’’", "′′′", "“““", "”””", "″″″", "〃〃〃"]
+ bad_ticks = msg.content[:3] in not_backticks
+ if bad_ticks:
+ ticks = msg.content[:3]
+ content = self.codeblock_stripping(f"```{msg.content[3:-3]}```", True)
+ if content is None:
+ return
+
+ content, repl_code = content
+
+ if len(content) == 2:
+ content = content[1]
+ else:
+ content = content[0]
+
+ space_left = 204
+ if len(content) >= space_left:
+ current_length = 0
+ lines_walked = 0
+ for line in content.splitlines(keepends=True):
+ if current_length+len(line) > space_left or lines_walked == 10:
+ break
+ current_length += len(line)
+ lines_walked += 1
+ content = content[:current_length] + "#..."
+
+ howto = (
+ "It looks like you are trying to paste code into this channel.\n\n"
+ "You seem to be using the wrong symbols to indicate where the codeblock should start. "
+ f"The correct symbols would be \`\`\`, not `{ticks}`.\n\n"
+ "**Here is an example of how it should look:**\n"
+ f"\`\`\`python\n{content}\n\`\`\`\n\n**This will result in the following:**\n"
+ f"```python\n{content}\n```"
+ )
+
+ else:
+ howto = ""
+ content = self.codeblock_stripping(msg.content, False)
+ if content is None:
return
+ content, repl_code = content
# Attempts to parse the message into an AST node.
# Invalid Python code will raise a SyntaxError.
- tree = ast.parse(content)
+ tree = ast.parse(content[0])
# Multiple lines of single words could be interpreted as expressions.
# This check is to avoid all nodes being parsed as expressions.
# (e.g. words over multiple lines)
- if not all(isinstance(node, ast.Expr) for node in tree.body):
- codeblock_tag = await self.bot.get_cog("Tags").get_tag_data("codeblock")
-
- if codeblock_tag == {}:
- log.warning(f"{msg.author} posted something that needed to be put inside Python "
- "code blocks, but the 'codeblock' tag was not in the tags database!")
- return
-
- log.debug(f"{msg.author} posted something that needed to be put inside python code blocks. "
- "Sending the user some instructions.")
- howto = (f"Hey {msg.author.mention}!\n\n"
- "I noticed you were trying to paste code into this channel.\n\n"
- f"{codeblock_tag['tag_content']}")
-
- howto_embed = Embed(description=howto)
- await msg.channel.send(embed=howto_embed)
- self.channel_cooldowns[msg.channel.id] = time.time()
-
- except SyntaxError:
- log.trace(f"{msg.author} posted in a help channel, and when we tried to parse it as Python "
- f"code, ast.parse raised a SyntaxError. This probably just means it wasn't Python "
- f"code. The message that was posted was:\n\n{msg.content}\n\n")
- pass
+ if not all(isinstance(node, ast.Expr) for node in tree.body) or repl_code:
+ # Shorten the code to 10 lines and/or 204 characters.
+ space_left = 204
+ if content and repl_code:
+ content = content[1]
+ else:
+ content = content[0]
+
+ if len(content) >= space_left:
+ current_length = 0
+ lines_walked = 0
+ for line in content.splitlines(keepends=True):
+ if current_length+len(line) > space_left or lines_walked == 10:
+ break
+ current_length += len(line)
+ lines_walked += 1
+ content = content[:current_length]+"#..."
+
+ howto += (
+ "It looks like you're trying to paste code into this channel.\n\n"
+ "Discord has support for Markdown, which allows you to post code with full "
+ "syntax highlighting. Please use these whenever you paste code, as this "
+ "helps improve the legibility and makes it easier for us to help you.\n\n"
+ f"**To do this, use the following method:**\n"
+ f"\`\`\`python\n{content}\n\`\`\`\n\n**This will result in the following:**\n"
+ f"```python\n{content}\n```"
+ )
+
+ log.debug(f"{msg.author} posted something that needed to be put inside python code "
+ "blocks. Sending the user some instructions.")
+ else:
+ log.trace("The code consists only of expressions, not sending instructions")
+
+ if howto != "":
+ howto_embed = Embed(description=howto)
+ await msg.channel.send(f"Hey {msg.author.mention}!", embed=howto_embed)
+ else:
+ return
+
+ self.channel_cooldowns[msg.channel.id] = time.time()
+
+ except SyntaxError:
+ log.trace(
+ f"{msg.author} posted in a help channel, and when we tried to parse it as Python code, "
+ "ast.parse raised a SyntaxError. This probably just means it wasn't Python code. "
+ f"The message that was posted was:\n\n{msg.content}\n\n"
+ )
def setup(bot):