From 09820f5b4a55d6240a05f848ea446bd46062f444 Mon Sep 17 00:00:00 2001 From: Andi Qu Date: Sun, 5 Jul 2020 20:33:03 +0200 Subject: Added better support for GitHub/GitLab --- bot/__main__.py | 2 + bot/cogs/print_snippets.py | 200 +++++++++++++++++++++++++++++++++++++++++++++ bot/cogs/repo_widgets.py | 123 ++++++++++++++++++++++++++++ 3 files changed, 325 insertions(+) create mode 100644 bot/cogs/print_snippets.py create mode 100644 bot/cogs/repo_widgets.py diff --git a/bot/__main__.py b/bot/__main__.py index 4e0d4a111..1d415eb20 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -71,6 +71,8 @@ bot.load_extension("bot.cogs.utils") bot.load_extension("bot.cogs.watchchannels") bot.load_extension("bot.cogs.webhook_remover") bot.load_extension("bot.cogs.wolfram") +bot.load_extension("bot.cogs.print_snippets") +bot.load_extension("bot.cogs.repo_widgets") if constants.HelpChannels.enable: bot.load_extension("bot.cogs.help_channels") diff --git a/bot/cogs/print_snippets.py b/bot/cogs/print_snippets.py new file mode 100644 index 000000000..06c9d6cc1 --- /dev/null +++ b/bot/cogs/print_snippets.py @@ -0,0 +1,200 @@ +""" +Cog that prints out snippets to Discord + +Matches each message against a regex and prints the contents +of the first matched snippet url +""" + +import os +import re +import textwrap + +from discord import Message +from discord.ext.commands import Cog +import aiohttp + +from bot.bot import Bot + + +async def fetch_http(session: aiohttp.ClientSession, url: str, response_format='text', **kwargs) -> str: + """Uses aiohttp to make http GET requests""" + + async with session.get(url, **kwargs) as response: + if response_format == 'text': + return await response.text() + elif response_format == 'json': + return await response.json() + + +async def revert_to_orig(d: dict) -> dict: + """Replace URL Encoded values back to their original""" + + for obj in d: + if d[obj] is not None: + d[obj] = d[obj].replace('%2F', '/').replace('%2E', '.') + + +async def orig_to_encode(d: dict) -> dict: + """Encode URL Parameters""" + + for obj in d: + if d[obj] is not None: + d[obj] = d[obj].replace('/', '%2F').replace('.', '%2E') + + +async def snippet_to_embed(d: dict, file_contents: str) -> str: + """ + Given a regex groupdict and file contents, creates a code block + """ + + if d['end_line']: + start_line = int(d['start_line']) + end_line = int(d['end_line']) + else: + start_line = end_line = int(d['start_line']) + + split_file_contents = file_contents.split('\n') + + if start_line > end_line: + start_line, end_line = end_line, start_line + if start_line > len(split_file_contents) or end_line < 1: + return '' + start_line = max(1, start_line) + end_line = min(len(split_file_contents), end_line) + + required = '\n'.join(split_file_contents[start_line - 1:end_line]) + required = textwrap.dedent(required).rstrip().replace('`', '`\u200b') + + language = d['file_path'].split('/')[-1].split('.')[-1] + if not language.replace('-', '').replace('+', '').replace('_', '').isalnum(): + language = '' + + if len(required) != 0: + return f'```{language}\n{required}```\n' + return '``` ```\n' + + +GITHUB_RE = re.compile( + r'https://github\.com/(?P.+?)/blob/(?P.+?)/' + + r'(?P.+?)#L(?P\d+)([-~]L(?P\d+))?\b' +) + +GITHUB_GIST_RE = re.compile( + r'https://gist\.github\.com/([^/]*)/(?P[0-9a-zA-Z]+)/*' + + r'(?P[0-9a-zA-Z]*)/*#file-(?P.+?)' + + r'-L(?P\d+)([-~]L(?P\d+))?\b' +) + +GITLAB_RE = re.compile( + r'https://gitlab\.com/(?P.+?)/\-/blob/(?P.+?)/' + + r'(?P.+?)#L(?P\d+)([-~](?P\d+))?\b' +) + +BITBUCKET_RE = re.compile( + r'https://bitbucket\.org/(?P.+?)/src/(?P.+?)/' + + r'(?P.+?)#lines-(?P\d+)(:(?P\d+))?\b' +) + + +class PrintSnippets(Cog): + def __init__(self, bot): + """Initializes the cog's bot""" + + self.bot = bot + self.session = aiohttp.ClientSession() + + @Cog.listener() + async def on_message(self, message: Message) -> None: + """ + Checks if the message starts is a GitHub snippet, then removes the embed, + then sends the snippet in Discord + """ + + gh_match = GITHUB_RE.search(message.content) + gh_gist_match = GITHUB_GIST_RE.search(message.content) + gl_match = GITLAB_RE.search(message.content) + bb_match = BITBUCKET_RE.search(message.content) + + if (gh_match or gh_gist_match or gl_match or bb_match) and not message.author.bot: + message_to_send = '' + + for gh in GITHUB_RE.finditer(message.content): + d = gh.groupdict() + headers = {'Accept': 'application/vnd.github.v3.raw'} + if 'GITHUB_TOKEN' in os.environ: + headers['Authorization'] = f'token {os.environ["GITHUB_TOKEN"]}' + file_contents = await fetch_http( + self.session, + f'https://api.github.com/repos/{d["repo"]}/contents/{d["file_path"]}?ref={d["branch"]}', + 'text', + headers=headers, + ) + message_to_send += await snippet_to_embed(d, file_contents) + + for gh_gist in GITHUB_GIST_RE.finditer(message.content): + d = gh_gist.groupdict() + gist_json = await fetch_http( + self.session, + f'https://api.github.com/gists/{d["gist_id"]}{"/" + d["revision"] if len(d["revision"]) > 0 else ""}', + 'json', + ) + for f in gist_json['files']: + if d['file_path'] == f.lower().replace('.', '-'): + d['file_path'] = f + file_contents = await fetch_http( + self.session, + gist_json['files'][f]['raw_url'], + 'text', + ) + message_to_send += await snippet_to_embed(d, file_contents) + break + + for gl in GITLAB_RE.finditer(message.content): + d = gl.groupdict() + await orig_to_encode(d) + headers = {} + if 'GITLAB_TOKEN' in os.environ: + headers['PRIVATE-TOKEN'] = os.environ["GITLAB_TOKEN"] + file_contents = await fetch_http( + self.session, + f'https://gitlab.com/api/v4/projects/{d["repo"]}/repository/files/{d["file_path"]}/raw?ref={d["branch"]}', + 'text', + headers=headers, + ) + await revert_to_orig(d) + message_to_send += await snippet_to_embed(d, file_contents) + + for bb in BITBUCKET_RE.finditer(message.content): + d = bb.groupdict() + await orig_to_encode(d) + file_contents = await fetch_http( + self.session, + f'https://bitbucket.org/{d["repo"]}/raw/{d["branch"]}/{d["file_path"]}', + 'text', + ) + await revert_to_orig(d) + message_to_send += await snippet_to_embed(d, file_contents) + + message_to_send = message_to_send[:-1] + + if len(message_to_send) > 2000: + await message.channel.send( + 'Sorry, Discord has a 2000 character limit. Please send a shorter ' + + 'snippet or split the big snippet up into several smaller ones :slight_smile:' + ) + elif len(message_to_send) == 0: + await message.channel.send( + 'Please send valid snippet links to prevent spam :slight_smile:' + ) + elif message_to_send.count('\n') > 50: + await message.channel.send( + 'Please limit the total number of lines to at most 50 to prevent spam :slight_smile:' + ) + else: + await message.channel.send(message_to_send) + await message.edit(suppress=True) + + +def setup(bot: Bot) -> None: + """Load the Utils cog.""" + bot.add_cog(PrintSnippets(bot)) diff --git a/bot/cogs/repo_widgets.py b/bot/cogs/repo_widgets.py new file mode 100644 index 000000000..70ca387ec --- /dev/null +++ b/bot/cogs/repo_widgets.py @@ -0,0 +1,123 @@ +""" +Cog that sends pretty embeds of repos + +Matches each message against a regex and prints the contents +of the first matched snippet url +""" + +import os +import re + +from discord import Embed, Message +from discord.ext.commands import Cog +import aiohttp + +from bot.bot import Bot + + +async def fetch_http(session: aiohttp.ClientSession, url: str, response_format='text', **kwargs) -> str: + """Uses aiohttp to make http GET requests""" + + async with session.get(url, **kwargs) as response: + if response_format == 'text': + return await response.text() + elif response_format == 'json': + return await response.json() + + +async def orig_to_encode(d: dict) -> dict: + """Encode URL Parameters""" + + for obj in d: + if d[obj] is not None: + d[obj] = d[obj].replace('/', '%2F').replace('.', '%2E') + + +GITHUB_RE = re.compile( + r'https://github\.com/(?P[^/]+?)/(?P[^/]+?)(?:\s|$)') + +GITLAB_RE = re.compile( + r'https://gitlab\.com/(?P[^/]+?)/(?P[^/]+?)(?:\s|$)') + + +class RepoWidgets(Cog): + def __init__(self, bot: Bot): + """Initializes the cog's bot""" + + self.bot = bot + self.session = aiohttp.ClientSession() + + @Cog.listener() + async def on_message(self, message: Message) -> None: + """ + Checks if the message starts is a GitHub repo link, then removes the embed, + then sends a rich embed to Discord + """ + + gh_match = GITHUB_RE.search(message.content) + gl_match = GITLAB_RE.search(message.content) + + if (gh_match or gl_match) and not message.author.bot: + for gh in GITHUB_RE.finditer(message.content): + d = gh.groupdict() + headers = {} + if 'GITHUB_TOKEN' in os.environ: + headers['Authorization'] = f'token {os.environ["GITHUB_TOKEN"]}' + repo = await fetch_http( + self.session, + f'https://api.github.com/repos/{d["owner"]}/{d["repo"]}', + 'json', + headers=headers, + ) + + embed = Embed( + title=repo['full_name'], + description='No description provided' if repo[ + 'description'] is None else repo['description'], + url=repo['html_url'], + color=0x111111 + ).set_footer( + text=f'Language: {repo["language"]} | ' + + f'Stars: {repo["stargazers_count"]} | ' + + f'Forks: {repo["forks_count"]} | ' + + f'Size: {repo["size"]}kb' + ).set_thumbnail(url=repo['owner']['avatar_url']) + if repo['homepage']: + embed.add_field(name='Website', value=repo['homepage']) + await message.channel.send(embed=embed) + + for gl in GITLAB_RE.finditer(message.content): + d = gl.groupdict() + await orig_to_encode(d) + headers = {} + if 'GITLAB_TOKEN' in os.environ: + headers['PRIVATE-TOKEN'] = os.environ["GITLAB_TOKEN"] + repo = await fetch_http( + self.session, + f'https://gitlab.com/api/v4/projects/{d["owner"]}%2F{d["repo"]}', + 'json', + headers=headers, + ) + + embed = Embed( + title=repo['path_with_namespace'], + description='No description provided' if repo[ + 'description'] == "" else repo['description'], + url=repo['web_url'], + color=0x111111 + ).set_footer( + text=f'Stars: {repo["star_count"]} | ' + + f'Forks: {repo["forks_count"]}' + ) + + if repo['avatar_url'] is not None: + embed.set_thumbnail(url=repo['avatar_url']) + + await message.channel.send(embed=embed) + + await message.edit(suppress=True) + + +def setup(bot: Bot) -> None: + """Load the Utils cog.""" + bot.add_cog(RepoWidgets(bot)) -- cgit v1.2.3 From 668d96e12acd76c5021ede07401cdb6062b89add Mon Sep 17 00:00:00 2001 From: Andi Qu Date: Sun, 5 Jul 2020 20:49:46 +0200 Subject: Tried to fix some of the flake8 style errors --- bot/cogs/print_snippets.py | 43 +++++++++++++++++-------------------------- bot/cogs/repo_widgets.py | 26 +++++++++----------------- 2 files changed, 26 insertions(+), 43 deletions(-) diff --git a/bot/cogs/print_snippets.py b/bot/cogs/print_snippets.py index 06c9d6cc1..4be3653d5 100644 --- a/bot/cogs/print_snippets.py +++ b/bot/cogs/print_snippets.py @@ -1,24 +1,16 @@ -""" -Cog that prints out snippets to Discord - -Matches each message against a regex and prints the contents -of the first matched snippet url -""" - import os import re import textwrap +import aiohttp from discord import Message from discord.ext.commands import Cog -import aiohttp from bot.bot import Bot -async def fetch_http(session: aiohttp.ClientSession, url: str, response_format='text', **kwargs) -> str: +async def fetch_http(session: aiohttp.ClientSession, url: str, response_format: str, **kwargs) -> str: """Uses aiohttp to make http GET requests""" - async with session.get(url, **kwargs) as response: if response_format == 'text': return await response.text() @@ -28,7 +20,6 @@ async def fetch_http(session: aiohttp.ClientSession, url: str, response_format=' async def revert_to_orig(d: dict) -> dict: """Replace URL Encoded values back to their original""" - for obj in d: if d[obj] is not None: d[obj] = d[obj].replace('%2F', '/').replace('%2E', '.') @@ -36,17 +27,13 @@ async def revert_to_orig(d: dict) -> dict: async def orig_to_encode(d: dict) -> dict: """Encode URL Parameters""" - for obj in d: if d[obj] is not None: d[obj] = d[obj].replace('/', '%2F').replace('.', '%2E') async def snippet_to_embed(d: dict, file_contents: str) -> str: - """ - Given a regex groupdict and file contents, creates a code block - """ - + """Given a regex groupdict and file contents, creates a code block""" if d['end_line']: start_line = int(d['start_line']) end_line = int(d['end_line']) @@ -97,19 +84,20 @@ BITBUCKET_RE = re.compile( class PrintSnippets(Cog): - def __init__(self, bot): - """Initializes the cog's bot""" + """ + Cog that prints out snippets to Discord + Matches each message against a regex and prints the contents of all matched snippets + """ + + def __init__(self, bot: Bot): + """Initializes the cog's bot""" self.bot = bot self.session = aiohttp.ClientSession() @Cog.listener() async def on_message(self, message: Message) -> None: - """ - Checks if the message starts is a GitHub snippet, then removes the embed, - then sends the snippet in Discord - """ - + """Checks if the message starts is a GitHub snippet, then removes the embed, then sends the snippet in Discord""" gh_match = GITHUB_RE.search(message.content) gh_gist_match = GITHUB_GIST_RE.search(message.content) gl_match = GITLAB_RE.search(message.content) @@ -125,7 +113,8 @@ class PrintSnippets(Cog): headers['Authorization'] = f'token {os.environ["GITHUB_TOKEN"]}' file_contents = await fetch_http( self.session, - f'https://api.github.com/repos/{d["repo"]}/contents/{d["file_path"]}?ref={d["branch"]}', + f'https://api.github.com/repos/{d["repo"]}\ + /contents/{d["file_path"]}?ref={d["branch"]}', 'text', headers=headers, ) @@ -135,7 +124,8 @@ class PrintSnippets(Cog): d = gh_gist.groupdict() gist_json = await fetch_http( self.session, - f'https://api.github.com/gists/{d["gist_id"]}{"/" + d["revision"] if len(d["revision"]) > 0 else ""}', + f'https://api.github.com/gists/{d["gist_id"]}\ + {"/" + d["revision"] if len(d["revision"]) > 0 else ""}', 'json', ) for f in gist_json['files']: @@ -157,7 +147,8 @@ class PrintSnippets(Cog): headers['PRIVATE-TOKEN'] = os.environ["GITLAB_TOKEN"] file_contents = await fetch_http( self.session, - f'https://gitlab.com/api/v4/projects/{d["repo"]}/repository/files/{d["file_path"]}/raw?ref={d["branch"]}', + f'https://gitlab.com/api/v4/projects/{d["repo"]}/\ + repository/files/{d["file_path"]}/raw?ref={d["branch"]}', 'text', headers=headers, ) diff --git a/bot/cogs/repo_widgets.py b/bot/cogs/repo_widgets.py index 70ca387ec..feb931e72 100644 --- a/bot/cogs/repo_widgets.py +++ b/bot/cogs/repo_widgets.py @@ -1,23 +1,15 @@ -""" -Cog that sends pretty embeds of repos - -Matches each message against a regex and prints the contents -of the first matched snippet url -""" - import os import re +import aiohttp from discord import Embed, Message from discord.ext.commands import Cog -import aiohttp from bot.bot import Bot -async def fetch_http(session: aiohttp.ClientSession, url: str, response_format='text', **kwargs) -> str: +async def fetch_http(session: aiohttp.ClientSession, url: str, response_format: str, **kwargs) -> str: """Uses aiohttp to make http GET requests""" - async with session.get(url, **kwargs) as response: if response_format == 'text': return await response.text() @@ -27,7 +19,6 @@ async def fetch_http(session: aiohttp.ClientSession, url: str, response_format=' async def orig_to_encode(d: dict) -> dict: """Encode URL Parameters""" - for obj in d: if d[obj] is not None: d[obj] = d[obj].replace('/', '%2F').replace('.', '%2E') @@ -41,19 +32,20 @@ GITLAB_RE = re.compile( class RepoWidgets(Cog): + """ + Cog that sends pretty embeds of repos + + Matches each message against a regex and sends an embed with the details of all referenced repos + """ + def __init__(self, bot: Bot): """Initializes the cog's bot""" - self.bot = bot self.session = aiohttp.ClientSession() @Cog.listener() async def on_message(self, message: Message) -> None: - """ - Checks if the message starts is a GitHub repo link, then removes the embed, - then sends a rich embed to Discord - """ - + """Checks if the message starts is a GitHub repo link, then removes the embed, then sends a rich embed to Discord""" gh_match = GITHUB_RE.search(message.content) gl_match = GITLAB_RE.search(message.content) -- cgit v1.2.3 From 2fe46fd372a5c8a69437e3f29c0137cb11d156d9 Mon Sep 17 00:00:00 2001 From: Andi Qu Date: Sun, 5 Jul 2020 20:54:55 +0200 Subject: Fixed all docstrings --- bot/cogs/print_snippets.py | 14 +++++++------- bot/cogs/repo_widgets.py | 20 ++++++++++---------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/bot/cogs/print_snippets.py b/bot/cogs/print_snippets.py index 4be3653d5..5c83cd62b 100644 --- a/bot/cogs/print_snippets.py +++ b/bot/cogs/print_snippets.py @@ -10,7 +10,7 @@ from bot.bot import Bot async def fetch_http(session: aiohttp.ClientSession, url: str, response_format: str, **kwargs) -> str: - """Uses aiohttp to make http GET requests""" + """Uses aiohttp to make http GET requests.""" async with session.get(url, **kwargs) as response: if response_format == 'text': return await response.text() @@ -19,21 +19,21 @@ async def fetch_http(session: aiohttp.ClientSession, url: str, response_format: async def revert_to_orig(d: dict) -> dict: - """Replace URL Encoded values back to their original""" + """Replace URL Encoded values back to their original.""" for obj in d: if d[obj] is not None: d[obj] = d[obj].replace('%2F', '/').replace('%2E', '.') async def orig_to_encode(d: dict) -> dict: - """Encode URL Parameters""" + """Encode URL Parameters.""" for obj in d: if d[obj] is not None: d[obj] = d[obj].replace('/', '%2F').replace('.', '%2E') async def snippet_to_embed(d: dict, file_contents: str) -> str: - """Given a regex groupdict and file contents, creates a code block""" + """Given a regex groupdict and file contents, creates a code block.""" if d['end_line']: start_line = int(d['start_line']) end_line = int(d['end_line']) @@ -85,9 +85,9 @@ BITBUCKET_RE = re.compile( class PrintSnippets(Cog): """ - Cog that prints out snippets to Discord + Cog that prints out snippets to Discord. - Matches each message against a regex and prints the contents of all matched snippets + Matches each message against a regex and prints the contents of all matched snippets. """ def __init__(self, bot: Bot): @@ -97,7 +97,7 @@ class PrintSnippets(Cog): @Cog.listener() async def on_message(self, message: Message) -> None: - """Checks if the message starts is a GitHub snippet, then removes the embed, then sends the snippet in Discord""" + """Checks if the message has a snippet link, removes the embed, then sends the snippet contents.""" gh_match = GITHUB_RE.search(message.content) gh_gist_match = GITHUB_GIST_RE.search(message.content) gl_match = GITLAB_RE.search(message.content) diff --git a/bot/cogs/repo_widgets.py b/bot/cogs/repo_widgets.py index feb931e72..c8fde7c8e 100644 --- a/bot/cogs/repo_widgets.py +++ b/bot/cogs/repo_widgets.py @@ -9,7 +9,7 @@ from bot.bot import Bot async def fetch_http(session: aiohttp.ClientSession, url: str, response_format: str, **kwargs) -> str: - """Uses aiohttp to make http GET requests""" + """Uses aiohttp to make http GET requests.""" async with session.get(url, **kwargs) as response: if response_format == 'text': return await response.text() @@ -18,7 +18,7 @@ async def fetch_http(session: aiohttp.ClientSession, url: str, response_format: async def orig_to_encode(d: dict) -> dict: - """Encode URL Parameters""" + """Encode URL Parameters.""" for obj in d: if d[obj] is not None: d[obj] = d[obj].replace('/', '%2F').replace('.', '%2E') @@ -33,19 +33,19 @@ GITLAB_RE = re.compile( class RepoWidgets(Cog): """ - Cog that sends pretty embeds of repos + Cog that sends pretty embeds of repos. - Matches each message against a regex and sends an embed with the details of all referenced repos + Matches each message against a regex and sends an embed with the details of all referenced repos. """ def __init__(self, bot: Bot): - """Initializes the cog's bot""" + """Initializes the cog's bot.""" self.bot = bot self.session = aiohttp.ClientSession() @Cog.listener() async def on_message(self, message: Message) -> None: - """Checks if the message starts is a GitHub repo link, then removes the embed, then sends a rich embed to Discord""" + """Checks if the message has a repo link, removes the embed, then sends a rich embed.""" gh_match = GITHUB_RE.search(message.content) gl_match = GITLAB_RE.search(message.content) @@ -69,10 +69,10 @@ class RepoWidgets(Cog): url=repo['html_url'], color=0x111111 ).set_footer( - text=f'Language: {repo["language"]} | ' + - f'Stars: {repo["stargazers_count"]} | ' + - f'Forks: {repo["forks_count"]} | ' + - f'Size: {repo["size"]}kb' + text=f'Language: {repo["language"]} | ' + + f'Stars: {repo["stargazers_count"]} | ' + + f'Forks: {repo["forks_count"]} | ' + + f'Size: {repo["size"]}kb' ).set_thumbnail(url=repo['owner']['avatar_url']) if repo['homepage']: embed.add_field(name='Website', value=repo['homepage']) -- cgit v1.2.3 From ec3cc1704c7678f6389ac5c0688be90697410bed Mon Sep 17 00:00:00 2001 From: Andi Qu Date: Sun, 5 Jul 2020 20:59:18 +0200 Subject: Minor style fixes --- bot/cogs/print_snippets.py | 2 +- bot/cogs/repo_widgets.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bot/cogs/print_snippets.py b/bot/cogs/print_snippets.py index 5c83cd62b..67d411a63 100644 --- a/bot/cogs/print_snippets.py +++ b/bot/cogs/print_snippets.py @@ -91,7 +91,7 @@ class PrintSnippets(Cog): """ def __init__(self, bot: Bot): - """Initializes the cog's bot""" + """Initializes the cog's bot.""" self.bot = bot self.session = aiohttp.ClientSession() diff --git a/bot/cogs/repo_widgets.py b/bot/cogs/repo_widgets.py index c8fde7c8e..32c2451df 100644 --- a/bot/cogs/repo_widgets.py +++ b/bot/cogs/repo_widgets.py @@ -98,8 +98,8 @@ class RepoWidgets(Cog): url=repo['web_url'], color=0x111111 ).set_footer( - text=f'Stars: {repo["star_count"]} | ' + - f'Forks: {repo["forks_count"]}' + text=f'Stars: {repo["star_count"]} | ' + + f'Forks: {repo["forks_count"]}' ) if repo['avatar_url'] is not None: -- cgit v1.2.3 From 5fb1203883a975d752d9c8b803bb8420ef0f7c60 Mon Sep 17 00:00:00 2001 From: Andi Qu Date: Tue, 7 Jul 2020 19:42:53 +0200 Subject: Removed repo widget prettification and added reaction to remove lines --- bot/__main__.py | 1 - bot/cogs/print_snippets.py | 45 +++++++++--------- bot/cogs/repo_widgets.py | 115 --------------------------------------------- 3 files changed, 22 insertions(+), 139 deletions(-) delete mode 100644 bot/cogs/repo_widgets.py diff --git a/bot/__main__.py b/bot/__main__.py index 1d415eb20..3191faf85 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -72,7 +72,6 @@ bot.load_extension("bot.cogs.watchchannels") bot.load_extension("bot.cogs.webhook_remover") bot.load_extension("bot.cogs.wolfram") bot.load_extension("bot.cogs.print_snippets") -bot.load_extension("bot.cogs.repo_widgets") if constants.HelpChannels.enable: bot.load_extension("bot.cogs.help_channels") diff --git a/bot/cogs/print_snippets.py b/bot/cogs/print_snippets.py index 67d411a63..3f784d2c6 100644 --- a/bot/cogs/print_snippets.py +++ b/bot/cogs/print_snippets.py @@ -1,9 +1,10 @@ +import asyncio import os import re import textwrap import aiohttp -from discord import Message +from discord import Message, Reaction, User from discord.ext.commands import Cog from bot.bot import Bot @@ -113,8 +114,8 @@ class PrintSnippets(Cog): headers['Authorization'] = f'token {os.environ["GITHUB_TOKEN"]}' file_contents = await fetch_http( self.session, - f'https://api.github.com/repos/{d["repo"]}\ - /contents/{d["file_path"]}?ref={d["branch"]}', + f'https://api.github.com/repos/{d["repo"]}' + + f'/contents/{d["file_path"]}?ref={d["branch"]}', 'text', headers=headers, ) @@ -124,8 +125,8 @@ class PrintSnippets(Cog): d = gh_gist.groupdict() gist_json = await fetch_http( self.session, - f'https://api.github.com/gists/{d["gist_id"]}\ - {"/" + d["revision"] if len(d["revision"]) > 0 else ""}', + f'https://api.github.com/gists/{d["gist_id"]}' + + f'{"/" + d["revision"] if len(d["revision"]) > 0 else ""}', 'json', ) for f in gist_json['files']: @@ -147,8 +148,8 @@ class PrintSnippets(Cog): headers['PRIVATE-TOKEN'] = os.environ["GITLAB_TOKEN"] file_contents = await fetch_http( self.session, - f'https://gitlab.com/api/v4/projects/{d["repo"]}/\ - repository/files/{d["file_path"]}/raw?ref={d["branch"]}', + f'https://gitlab.com/api/v4/projects/{d["repo"]}/' + + f'repository/files/{d["file_path"]}/raw?ref={d["branch"]}', 'text', headers=headers, ) @@ -168,22 +169,20 @@ class PrintSnippets(Cog): message_to_send = message_to_send[:-1] - if len(message_to_send) > 2000: - await message.channel.send( - 'Sorry, Discord has a 2000 character limit. Please send a shorter ' - + 'snippet or split the big snippet up into several smaller ones :slight_smile:' - ) - elif len(message_to_send) == 0: - await message.channel.send( - 'Please send valid snippet links to prevent spam :slight_smile:' - ) - elif message_to_send.count('\n') > 50: - await message.channel.send( - 'Please limit the total number of lines to at most 50 to prevent spam :slight_smile:' - ) - else: - await message.channel.send(message_to_send) - await message.edit(suppress=True) + if 0 < len(message_to_send) <= 2000 and message_to_send.count('\n') <= 50: + sent_message = await message.channel.send(message_to_send) + await message.edit(suppress=True) + await sent_message.add_reaction('❌') + + def check(reaction: Reaction, user: User) -> bool: + return user == message.author and str(reaction.emoji) == '❌' + + try: + reaction, user = await self.bot.wait_for('reaction_add', timeout=10.0, check=check) + except asyncio.TimeoutError: + await sent_message.remove_reaction('❌', self.bot.user) + else: + await sent_message.delete() def setup(bot: Bot) -> None: diff --git a/bot/cogs/repo_widgets.py b/bot/cogs/repo_widgets.py deleted file mode 100644 index 32c2451df..000000000 --- a/bot/cogs/repo_widgets.py +++ /dev/null @@ -1,115 +0,0 @@ -import os -import re - -import aiohttp -from discord import Embed, Message -from discord.ext.commands import Cog - -from bot.bot import Bot - - -async def fetch_http(session: aiohttp.ClientSession, url: str, response_format: str, **kwargs) -> str: - """Uses aiohttp to make http GET requests.""" - async with session.get(url, **kwargs) as response: - if response_format == 'text': - return await response.text() - elif response_format == 'json': - return await response.json() - - -async def orig_to_encode(d: dict) -> dict: - """Encode URL Parameters.""" - for obj in d: - if d[obj] is not None: - d[obj] = d[obj].replace('/', '%2F').replace('.', '%2E') - - -GITHUB_RE = re.compile( - r'https://github\.com/(?P[^/]+?)/(?P[^/]+?)(?:\s|$)') - -GITLAB_RE = re.compile( - r'https://gitlab\.com/(?P[^/]+?)/(?P[^/]+?)(?:\s|$)') - - -class RepoWidgets(Cog): - """ - Cog that sends pretty embeds of repos. - - Matches each message against a regex and sends an embed with the details of all referenced repos. - """ - - def __init__(self, bot: Bot): - """Initializes the cog's bot.""" - self.bot = bot - self.session = aiohttp.ClientSession() - - @Cog.listener() - async def on_message(self, message: Message) -> None: - """Checks if the message has a repo link, removes the embed, then sends a rich embed.""" - gh_match = GITHUB_RE.search(message.content) - gl_match = GITLAB_RE.search(message.content) - - if (gh_match or gl_match) and not message.author.bot: - for gh in GITHUB_RE.finditer(message.content): - d = gh.groupdict() - headers = {} - if 'GITHUB_TOKEN' in os.environ: - headers['Authorization'] = f'token {os.environ["GITHUB_TOKEN"]}' - repo = await fetch_http( - self.session, - f'https://api.github.com/repos/{d["owner"]}/{d["repo"]}', - 'json', - headers=headers, - ) - - embed = Embed( - title=repo['full_name'], - description='No description provided' if repo[ - 'description'] is None else repo['description'], - url=repo['html_url'], - color=0x111111 - ).set_footer( - text=f'Language: {repo["language"]} | ' - + f'Stars: {repo["stargazers_count"]} | ' - + f'Forks: {repo["forks_count"]} | ' - + f'Size: {repo["size"]}kb' - ).set_thumbnail(url=repo['owner']['avatar_url']) - if repo['homepage']: - embed.add_field(name='Website', value=repo['homepage']) - await message.channel.send(embed=embed) - - for gl in GITLAB_RE.finditer(message.content): - d = gl.groupdict() - await orig_to_encode(d) - headers = {} - if 'GITLAB_TOKEN' in os.environ: - headers['PRIVATE-TOKEN'] = os.environ["GITLAB_TOKEN"] - repo = await fetch_http( - self.session, - f'https://gitlab.com/api/v4/projects/{d["owner"]}%2F{d["repo"]}', - 'json', - headers=headers, - ) - - embed = Embed( - title=repo['path_with_namespace'], - description='No description provided' if repo[ - 'description'] == "" else repo['description'], - url=repo['web_url'], - color=0x111111 - ).set_footer( - text=f'Stars: {repo["star_count"]} | ' - + f'Forks: {repo["forks_count"]}' - ) - - if repo['avatar_url'] is not None: - embed.set_thumbnail(url=repo['avatar_url']) - - await message.channel.send(embed=embed) - - await message.edit(suppress=True) - - -def setup(bot: Bot) -> None: - """Load the Utils cog.""" - bot.add_cog(RepoWidgets(bot)) -- cgit v1.2.3 From b759a940a097effd16b761e0c62231ae0ca9562b Mon Sep 17 00:00:00 2001 From: dolphingarlic Date: Thu, 30 Jul 2020 20:13:15 +0200 Subject: Cleaned the code for CodeSnippets --- bot/__main__.py | 2 +- bot/cogs/code_snippets.py | 216 +++++++++++++++++++++++++++++++++++++++++++++ bot/cogs/print_snippets.py | 190 --------------------------------------- 3 files changed, 217 insertions(+), 191 deletions(-) create mode 100644 bot/cogs/code_snippets.py delete mode 100644 bot/cogs/print_snippets.py diff --git a/bot/__main__.py b/bot/__main__.py index 3191faf85..3d414c4b8 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -71,7 +71,7 @@ bot.load_extension("bot.cogs.utils") bot.load_extension("bot.cogs.watchchannels") bot.load_extension("bot.cogs.webhook_remover") bot.load_extension("bot.cogs.wolfram") -bot.load_extension("bot.cogs.print_snippets") +bot.load_extension("bot.cogs.code_snippets") if constants.HelpChannels.enable: bot.load_extension("bot.cogs.help_channels") diff --git a/bot/cogs/code_snippets.py b/bot/cogs/code_snippets.py new file mode 100644 index 000000000..9bd06f6ff --- /dev/null +++ b/bot/cogs/code_snippets.py @@ -0,0 +1,216 @@ +import re +import textwrap +from urllib.parse import quote_plus + +from aiohttp import ClientSession +from discord import Message +from discord.ext.commands import Cog + +from bot.bot import Bot +from bot.utils.messages import wait_for_deletion + + +async def fetch_http(session: ClientSession, url: str, response_format: str, **kwargs) -> str: + """Uses aiohttp to make http GET requests.""" + async with session.get(url, **kwargs) as response: + if response_format == 'text': + return await response.text() + elif response_format == 'json': + return await response.json() + + +async def fetch_github_snippet(session: ClientSession, repo: str, + path: str, start_line: str, end_line: str) -> str: + """Fetches a snippet from a GitHub repo.""" + headers = {'Accept': 'application/vnd.github.v3.raw'} + + # Search the GitHub API for the specified branch + refs = (await fetch_http(session, f'https://api.github.com/repos/{repo}/branches', 'json', headers=headers) + + await fetch_http(session, f'https://api.github.com/repos/{repo}/tags', 'json', headers=headers)) + + ref = path.split('/')[0] + file_path = '/'.join(path.split('/')[1:]) + for possible_ref in refs: + if path.startswith(possible_ref['name'] + '/'): + ref = possible_ref['name'] + file_path = path[len(ref) + 1:] + break + + file_contents = await fetch_http( + session, + f'https://api.github.com/repos/{repo}/contents/{file_path}?ref={ref}', + 'text', + headers=headers, + ) + + return await snippet_to_md(file_contents, file_path, start_line, end_line) + + +async def fetch_github_gist_snippet(session: ClientSession, gist_id: str, revision: str, + file_path: str, start_line: str, end_line: str) -> str: + """Fetches a snippet from a GitHub gist.""" + headers = {'Accept': 'application/vnd.github.v3.raw'} + + gist_json = await fetch_http( + session, + f'https://api.github.com/gists/{gist_id}{f"/{revision}" if len(revision) > 0 else ""}', + 'json', + headers=headers, + ) + + # Check each file in the gist for the specified file + for gist_file in gist_json['files']: + if file_path == gist_file.lower().replace('.', '-'): + file_contents = await fetch_http( + session, + gist_json['files'][gist_file]['raw_url'], + 'text', + ) + + return await snippet_to_md(file_contents, gist_file, start_line, end_line) + + return '' + + +async def fetch_gitlab_snippet(session: ClientSession, repo: str, + path: str, start_line: str, end_line: str) -> str: + """Fetches a snippet from a GitLab repo.""" + enc_repo = quote_plus(repo) + + # Searches the GitLab API for the specified branch + refs = (await fetch_http(session, f'https://gitlab.com/api/v4/projects/{enc_repo}/repository/branches', 'json') + + await fetch_http(session, f'https://gitlab.com/api/v4/projects/{enc_repo}/repository/tags', 'json')) + + ref = path.split('/')[0] + file_path = '/'.join(path.split('/')[1:]) + for possible_ref in refs: + if path.startswith(possible_ref['name'] + '/'): + ref = possible_ref['name'] + file_path = path[len(ref) + 1:] + break + + enc_ref = quote_plus(ref) + enc_file_path = quote_plus(file_path) + + file_contents = await fetch_http( + session, + f'https://gitlab.com/api/v4/projects/{enc_repo}/repository/files/{enc_file_path}/raw?ref={enc_ref}', + 'text', + ) + + return await snippet_to_md(file_contents, file_path, start_line, end_line) + + +async def fetch_bitbucket_snippet(session: ClientSession, repo: str, ref: str, + file_path: str, start_line: int, end_line: int) -> str: + """Fetches a snippet from a BitBucket repo.""" + file_contents = await fetch_http( + session, + f'https://bitbucket.org/{quote_plus(repo)}/raw/{quote_plus(ref)}/{quote_plus(file_path)}', + 'text', + ) + + return await snippet_to_md(file_contents, file_path, start_line, end_line) + + +async def snippet_to_md(file_contents: str, file_path: str, start_line: str, end_line: str) -> str: + """Given file contents, file path, start line and end line creates a code block.""" + # Parse start_line and end_line into integers + if end_line is None: + start_line = end_line = int(start_line) + else: + start_line = int(start_line) + end_line = int(end_line) + + split_file_contents = file_contents.splitlines() + + # Make sure that the specified lines are in range + if start_line > end_line: + start_line, end_line = end_line, start_line + if start_line > len(split_file_contents) or end_line < 1: + return '' + start_line = max(1, start_line) + end_line = min(len(split_file_contents), end_line) + + # Gets the code lines, dedents them, and inserts zero-width spaces to prevent Markdown injection + required = '\n'.join(split_file_contents[start_line - 1:end_line]) + required = textwrap.dedent(required).rstrip().replace('`', '`\u200b') + + # Extracts the code language and checks whether it's a "valid" language + language = file_path.split('/')[-1].split('.')[-1] + if not language.replace('-', '').replace('+', '').replace('_', '').isalnum(): + language = '' + + if len(required) != 0: + return f'```{language}\n{required}```\n' + return '' + + +GITHUB_RE = re.compile( + r'https://github\.com/(?P.+?)/blob/(?P.+/.+)' + r'#L(?P\d+)([-~]L(?P\d+))?\b' +) + +GITHUB_GIST_RE = re.compile( + r'https://gist\.github\.com/([^/]+)/(?P[^\W_]+)/*' + r'(?P[^\W_]*)/*#file-(?P.+?)' + r'-L(?P\d+)([-~]L(?P\d+))?\b' +) + +GITLAB_RE = re.compile( + r'https://gitlab\.com/(?P.+?)/\-/blob/(?P.+/.+)' + r'#L(?P\d+)([-](?P\d+))?\b' +) + +BITBUCKET_RE = re.compile( + r'https://bitbucket\.org/(?P.+?)/src/(?P.+?)/' + r'(?P.+?)#lines-(?P\d+)(:(?P\d+))?\b' +) + + +class CodeSnippets(Cog): + """ + Cog that prints out snippets to Discord. + + Matches each message against a regex and prints the contents of all matched snippets. + """ + + def __init__(self, bot: Bot): + """Initializes the cog's bot.""" + self.bot = bot + + @Cog.listener() + async def on_message(self, message: Message) -> None: + """Checks if the message has a snippet link, removes the embed, then sends the snippet contents.""" + gh_match = GITHUB_RE.search(message.content) + gh_gist_match = GITHUB_GIST_RE.search(message.content) + gl_match = GITLAB_RE.search(message.content) + bb_match = BITBUCKET_RE.search(message.content) + + if (gh_match or gh_gist_match or gl_match or bb_match) and not message.author.bot: + message_to_send = '' + + for gh in GITHUB_RE.finditer(message.content): + message_to_send += await fetch_github_snippet(self.bot.http_session, **gh.groupdict()) + + for gh_gist in GITHUB_GIST_RE.finditer(message.content): + message_to_send += await fetch_github_gist_snippet(self.bot.http_session, **gh_gist.groupdict()) + + for gl in GITLAB_RE.finditer(message.content): + message_to_send += await fetch_gitlab_snippet(self.bot.http_session, **gl.groupdict()) + + for bb in BITBUCKET_RE.finditer(message.content): + message_to_send += await fetch_bitbucket_snippet(self.bot.http_session, **bb.groupdict()) + + if 0 < len(message_to_send) <= 2000 and message_to_send.count('\n') <= 15: + await message.edit(suppress=True) + await wait_for_deletion( + await message.channel.send(message_to_send), + (message.author.id,), + client=self.bot + ) + + +def setup(bot: Bot) -> None: + """Load the CodeSnippets cog.""" + bot.add_cog(CodeSnippets(bot)) diff --git a/bot/cogs/print_snippets.py b/bot/cogs/print_snippets.py deleted file mode 100644 index 3f784d2c6..000000000 --- a/bot/cogs/print_snippets.py +++ /dev/null @@ -1,190 +0,0 @@ -import asyncio -import os -import re -import textwrap - -import aiohttp -from discord import Message, Reaction, User -from discord.ext.commands import Cog - -from bot.bot import Bot - - -async def fetch_http(session: aiohttp.ClientSession, url: str, response_format: str, **kwargs) -> str: - """Uses aiohttp to make http GET requests.""" - async with session.get(url, **kwargs) as response: - if response_format == 'text': - return await response.text() - elif response_format == 'json': - return await response.json() - - -async def revert_to_orig(d: dict) -> dict: - """Replace URL Encoded values back to their original.""" - for obj in d: - if d[obj] is not None: - d[obj] = d[obj].replace('%2F', '/').replace('%2E', '.') - - -async def orig_to_encode(d: dict) -> dict: - """Encode URL Parameters.""" - for obj in d: - if d[obj] is not None: - d[obj] = d[obj].replace('/', '%2F').replace('.', '%2E') - - -async def snippet_to_embed(d: dict, file_contents: str) -> str: - """Given a regex groupdict and file contents, creates a code block.""" - if d['end_line']: - start_line = int(d['start_line']) - end_line = int(d['end_line']) - else: - start_line = end_line = int(d['start_line']) - - split_file_contents = file_contents.split('\n') - - if start_line > end_line: - start_line, end_line = end_line, start_line - if start_line > len(split_file_contents) or end_line < 1: - return '' - start_line = max(1, start_line) - end_line = min(len(split_file_contents), end_line) - - required = '\n'.join(split_file_contents[start_line - 1:end_line]) - required = textwrap.dedent(required).rstrip().replace('`', '`\u200b') - - language = d['file_path'].split('/')[-1].split('.')[-1] - if not language.replace('-', '').replace('+', '').replace('_', '').isalnum(): - language = '' - - if len(required) != 0: - return f'```{language}\n{required}```\n' - return '``` ```\n' - - -GITHUB_RE = re.compile( - r'https://github\.com/(?P.+?)/blob/(?P.+?)/' - + r'(?P.+?)#L(?P\d+)([-~]L(?P\d+))?\b' -) - -GITHUB_GIST_RE = re.compile( - r'https://gist\.github\.com/([^/]*)/(?P[0-9a-zA-Z]+)/*' - + r'(?P[0-9a-zA-Z]*)/*#file-(?P.+?)' - + r'-L(?P\d+)([-~]L(?P\d+))?\b' -) - -GITLAB_RE = re.compile( - r'https://gitlab\.com/(?P.+?)/\-/blob/(?P.+?)/' - + r'(?P.+?)#L(?P\d+)([-~](?P\d+))?\b' -) - -BITBUCKET_RE = re.compile( - r'https://bitbucket\.org/(?P.+?)/src/(?P.+?)/' - + r'(?P.+?)#lines-(?P\d+)(:(?P\d+))?\b' -) - - -class PrintSnippets(Cog): - """ - Cog that prints out snippets to Discord. - - Matches each message against a regex and prints the contents of all matched snippets. - """ - - def __init__(self, bot: Bot): - """Initializes the cog's bot.""" - self.bot = bot - self.session = aiohttp.ClientSession() - - @Cog.listener() - async def on_message(self, message: Message) -> None: - """Checks if the message has a snippet link, removes the embed, then sends the snippet contents.""" - gh_match = GITHUB_RE.search(message.content) - gh_gist_match = GITHUB_GIST_RE.search(message.content) - gl_match = GITLAB_RE.search(message.content) - bb_match = BITBUCKET_RE.search(message.content) - - if (gh_match or gh_gist_match or gl_match or bb_match) and not message.author.bot: - message_to_send = '' - - for gh in GITHUB_RE.finditer(message.content): - d = gh.groupdict() - headers = {'Accept': 'application/vnd.github.v3.raw'} - if 'GITHUB_TOKEN' in os.environ: - headers['Authorization'] = f'token {os.environ["GITHUB_TOKEN"]}' - file_contents = await fetch_http( - self.session, - f'https://api.github.com/repos/{d["repo"]}' - + f'/contents/{d["file_path"]}?ref={d["branch"]}', - 'text', - headers=headers, - ) - message_to_send += await snippet_to_embed(d, file_contents) - - for gh_gist in GITHUB_GIST_RE.finditer(message.content): - d = gh_gist.groupdict() - gist_json = await fetch_http( - self.session, - f'https://api.github.com/gists/{d["gist_id"]}' - + f'{"/" + d["revision"] if len(d["revision"]) > 0 else ""}', - 'json', - ) - for f in gist_json['files']: - if d['file_path'] == f.lower().replace('.', '-'): - d['file_path'] = f - file_contents = await fetch_http( - self.session, - gist_json['files'][f]['raw_url'], - 'text', - ) - message_to_send += await snippet_to_embed(d, file_contents) - break - - for gl in GITLAB_RE.finditer(message.content): - d = gl.groupdict() - await orig_to_encode(d) - headers = {} - if 'GITLAB_TOKEN' in os.environ: - headers['PRIVATE-TOKEN'] = os.environ["GITLAB_TOKEN"] - file_contents = await fetch_http( - self.session, - f'https://gitlab.com/api/v4/projects/{d["repo"]}/' - + f'repository/files/{d["file_path"]}/raw?ref={d["branch"]}', - 'text', - headers=headers, - ) - await revert_to_orig(d) - message_to_send += await snippet_to_embed(d, file_contents) - - for bb in BITBUCKET_RE.finditer(message.content): - d = bb.groupdict() - await orig_to_encode(d) - file_contents = await fetch_http( - self.session, - f'https://bitbucket.org/{d["repo"]}/raw/{d["branch"]}/{d["file_path"]}', - 'text', - ) - await revert_to_orig(d) - message_to_send += await snippet_to_embed(d, file_contents) - - message_to_send = message_to_send[:-1] - - if 0 < len(message_to_send) <= 2000 and message_to_send.count('\n') <= 50: - sent_message = await message.channel.send(message_to_send) - await message.edit(suppress=True) - await sent_message.add_reaction('❌') - - def check(reaction: Reaction, user: User) -> bool: - return user == message.author and str(reaction.emoji) == '❌' - - try: - reaction, user = await self.bot.wait_for('reaction_add', timeout=10.0, check=check) - except asyncio.TimeoutError: - await sent_message.remove_reaction('❌', self.bot.user) - else: - await sent_message.delete() - - -def setup(bot: Bot) -> None: - """Load the Utils cog.""" - bot.add_cog(PrintSnippets(bot)) -- cgit v1.2.3 From c966853e92b696b9132c6f5316e6920e3cb70733 Mon Sep 17 00:00:00 2001 From: Andi Qu Date: Tue, 27 Oct 2020 10:58:49 +0200 Subject: Moved code for finding the right ref to a function --- bot/cogs/code_snippets.py | 34 ++++++++++++++-------------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/bot/cogs/code_snippets.py b/bot/cogs/code_snippets.py index 9bd06f6ff..b10c68789 100644 --- a/bot/cogs/code_snippets.py +++ b/bot/cogs/code_snippets.py @@ -19,6 +19,18 @@ async def fetch_http(session: ClientSession, url: str, response_format: str, **k return await response.json() +def find_ref(path: str, refs: tuple) -> tuple: + """Loops through all branches and tags to find the required ref.""" + ref = path.split('/')[0] + file_path = '/'.join(path.split('/')[1:]) + for possible_ref in refs: + if path.startswith(possible_ref['name'] + '/'): + ref = possible_ref['name'] + file_path = path[len(ref) + 1:] + break + return (ref, file_path) + + async def fetch_github_snippet(session: ClientSession, repo: str, path: str, start_line: str, end_line: str) -> str: """Fetches a snippet from a GitHub repo.""" @@ -28,13 +40,7 @@ async def fetch_github_snippet(session: ClientSession, repo: str, refs = (await fetch_http(session, f'https://api.github.com/repos/{repo}/branches', 'json', headers=headers) + await fetch_http(session, f'https://api.github.com/repos/{repo}/tags', 'json', headers=headers)) - ref = path.split('/')[0] - file_path = '/'.join(path.split('/')[1:]) - for possible_ref in refs: - if path.startswith(possible_ref['name'] + '/'): - ref = possible_ref['name'] - file_path = path[len(ref) + 1:] - break + ref, file_path = find_ref(path, refs) file_contents = await fetch_http( session, @@ -42,7 +48,6 @@ async def fetch_github_snippet(session: ClientSession, repo: str, 'text', headers=headers, ) - return await snippet_to_md(file_contents, file_path, start_line, end_line) @@ -66,9 +71,7 @@ async def fetch_github_gist_snippet(session: ClientSession, gist_id: str, revisi gist_json['files'][gist_file]['raw_url'], 'text', ) - return await snippet_to_md(file_contents, gist_file, start_line, end_line) - return '' @@ -81,14 +84,7 @@ async def fetch_gitlab_snippet(session: ClientSession, repo: str, refs = (await fetch_http(session, f'https://gitlab.com/api/v4/projects/{enc_repo}/repository/branches', 'json') + await fetch_http(session, f'https://gitlab.com/api/v4/projects/{enc_repo}/repository/tags', 'json')) - ref = path.split('/')[0] - file_path = '/'.join(path.split('/')[1:]) - for possible_ref in refs: - if path.startswith(possible_ref['name'] + '/'): - ref = possible_ref['name'] - file_path = path[len(ref) + 1:] - break - + ref, file_path = find_ref(path, refs) enc_ref = quote_plus(ref) enc_file_path = quote_plus(file_path) @@ -97,7 +93,6 @@ async def fetch_gitlab_snippet(session: ClientSession, repo: str, f'https://gitlab.com/api/v4/projects/{enc_repo}/repository/files/{enc_file_path}/raw?ref={enc_ref}', 'text', ) - return await snippet_to_md(file_contents, file_path, start_line, end_line) @@ -109,7 +104,6 @@ async def fetch_bitbucket_snippet(session: ClientSession, repo: str, ref: str, f'https://bitbucket.org/{quote_plus(repo)}/raw/{quote_plus(ref)}/{quote_plus(file_path)}', 'text', ) - return await snippet_to_md(file_contents, file_path, start_line, end_line) -- cgit v1.2.3 From 372cfb9c1dcfb761ad468ac38955473db57f18b6 Mon Sep 17 00:00:00 2001 From: Andi Qu Date: Tue, 27 Oct 2020 11:02:03 +0200 Subject: Renamed fetch_http to fetch_response --- bot/cogs/code_snippets.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/bot/cogs/code_snippets.py b/bot/cogs/code_snippets.py index b10c68789..27faf70ec 100644 --- a/bot/cogs/code_snippets.py +++ b/bot/cogs/code_snippets.py @@ -10,8 +10,8 @@ from bot.bot import Bot from bot.utils.messages import wait_for_deletion -async def fetch_http(session: ClientSession, url: str, response_format: str, **kwargs) -> str: - """Uses aiohttp to make http GET requests.""" +async def fetch_response(session: ClientSession, url: str, response_format: str, **kwargs) -> str: + """Makes http requests using aiohttp.""" async with session.get(url, **kwargs) as response: if response_format == 'text': return await response.text() @@ -37,12 +37,12 @@ async def fetch_github_snippet(session: ClientSession, repo: str, headers = {'Accept': 'application/vnd.github.v3.raw'} # Search the GitHub API for the specified branch - refs = (await fetch_http(session, f'https://api.github.com/repos/{repo}/branches', 'json', headers=headers) - + await fetch_http(session, f'https://api.github.com/repos/{repo}/tags', 'json', headers=headers)) + refs = (await fetch_response(session, f'https://api.github.com/repos/{repo}/branches', 'json', headers=headers) + + await fetch_response(session, f'https://api.github.com/repos/{repo}/tags', 'json', headers=headers)) ref, file_path = find_ref(path, refs) - file_contents = await fetch_http( + file_contents = await fetch_response( session, f'https://api.github.com/repos/{repo}/contents/{file_path}?ref={ref}', 'text', @@ -56,7 +56,7 @@ async def fetch_github_gist_snippet(session: ClientSession, gist_id: str, revisi """Fetches a snippet from a GitHub gist.""" headers = {'Accept': 'application/vnd.github.v3.raw'} - gist_json = await fetch_http( + gist_json = await fetch_response( session, f'https://api.github.com/gists/{gist_id}{f"/{revision}" if len(revision) > 0 else ""}', 'json', @@ -66,7 +66,7 @@ async def fetch_github_gist_snippet(session: ClientSession, gist_id: str, revisi # Check each file in the gist for the specified file for gist_file in gist_json['files']: if file_path == gist_file.lower().replace('.', '-'): - file_contents = await fetch_http( + file_contents = await fetch_response( session, gist_json['files'][gist_file]['raw_url'], 'text', @@ -81,14 +81,14 @@ async def fetch_gitlab_snippet(session: ClientSession, repo: str, enc_repo = quote_plus(repo) # Searches the GitLab API for the specified branch - refs = (await fetch_http(session, f'https://gitlab.com/api/v4/projects/{enc_repo}/repository/branches', 'json') - + await fetch_http(session, f'https://gitlab.com/api/v4/projects/{enc_repo}/repository/tags', 'json')) + refs = (await fetch_response(session, f'https://gitlab.com/api/v4/projects/{enc_repo}/repository/branches', 'json') + + await fetch_response(session, f'https://gitlab.com/api/v4/projects/{enc_repo}/repository/tags', 'json')) ref, file_path = find_ref(path, refs) enc_ref = quote_plus(ref) enc_file_path = quote_plus(file_path) - file_contents = await fetch_http( + file_contents = await fetch_response( session, f'https://gitlab.com/api/v4/projects/{enc_repo}/repository/files/{enc_file_path}/raw?ref={enc_ref}', 'text', @@ -99,7 +99,7 @@ async def fetch_gitlab_snippet(session: ClientSession, repo: str, async def fetch_bitbucket_snippet(session: ClientSession, repo: str, ref: str, file_path: str, start_line: int, end_line: int) -> str: """Fetches a snippet from a BitBucket repo.""" - file_contents = await fetch_http( + file_contents = await fetch_response( session, f'https://bitbucket.org/{quote_plus(repo)}/raw/{quote_plus(ref)}/{quote_plus(file_path)}', 'text', -- cgit v1.2.3 From c3ce61937211cbd8c7e3df1c501cda70d97623cb Mon Sep 17 00:00:00 2001 From: Andi Qu Date: Tue, 27 Oct 2020 11:16:14 +0200 Subject: Renamed snippet_to_md and wrote a better docstring --- bot/cogs/code_snippets.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/bot/cogs/code_snippets.py b/bot/cogs/code_snippets.py index 27faf70ec..dda4d185f 100644 --- a/bot/cogs/code_snippets.py +++ b/bot/cogs/code_snippets.py @@ -21,8 +21,10 @@ async def fetch_response(session: ClientSession, url: str, response_format: str, def find_ref(path: str, refs: tuple) -> tuple: """Loops through all branches and tags to find the required ref.""" + # Base case: there is no slash in the branch name ref = path.split('/')[0] file_path = '/'.join(path.split('/')[1:]) + # In case there are slashes in the branch name, we loop through all branches and tags for possible_ref in refs: if path.startswith(possible_ref['name'] + '/'): ref = possible_ref['name'] @@ -48,7 +50,7 @@ async def fetch_github_snippet(session: ClientSession, repo: str, 'text', headers=headers, ) - return await snippet_to_md(file_contents, file_path, start_line, end_line) + return snippet_to_codeblock(file_contents, file_path, start_line, end_line) async def fetch_github_gist_snippet(session: ClientSession, gist_id: str, revision: str, @@ -71,7 +73,7 @@ async def fetch_github_gist_snippet(session: ClientSession, gist_id: str, revisi gist_json['files'][gist_file]['raw_url'], 'text', ) - return await snippet_to_md(file_contents, gist_file, start_line, end_line) + return snippet_to_codeblock(file_contents, gist_file, start_line, end_line) return '' @@ -93,7 +95,7 @@ async def fetch_gitlab_snippet(session: ClientSession, repo: str, f'https://gitlab.com/api/v4/projects/{enc_repo}/repository/files/{enc_file_path}/raw?ref={enc_ref}', 'text', ) - return await snippet_to_md(file_contents, file_path, start_line, end_line) + return snippet_to_codeblock(file_contents, file_path, start_line, end_line) async def fetch_bitbucket_snippet(session: ClientSession, repo: str, ref: str, @@ -104,11 +106,21 @@ async def fetch_bitbucket_snippet(session: ClientSession, repo: str, ref: str, f'https://bitbucket.org/{quote_plus(repo)}/raw/{quote_plus(ref)}/{quote_plus(file_path)}', 'text', ) - return await snippet_to_md(file_contents, file_path, start_line, end_line) + return snippet_to_codeblock(file_contents, file_path, start_line, end_line) -async def snippet_to_md(file_contents: str, file_path: str, start_line: str, end_line: str) -> str: - """Given file contents, file path, start line and end line creates a code block.""" +def snippet_to_codeblock(file_contents: str, file_path: str, start_line: str, end_line: str) -> str: + """ + Given the entire file contents and target lines, creates a code block. + + First, we split the file contents into a list of lines and then keep and join only the required + ones together. + + We then dedent the lines to look nice, and replace all ` characters with `\u200b to prevent + markdown injection. + + Finally, we surround the code with ``` characters. + """ # Parse start_line and end_line into integers if end_line is None: start_line = end_line = int(start_line) -- cgit v1.2.3 From 28dfd8278a8ee24fb26bc5359729ca0ed0307632 Mon Sep 17 00:00:00 2001 From: Andi Qu <31325319+dolphingarlic@users.noreply.github.com> Date: Tue, 27 Oct 2020 11:17:26 +0200 Subject: Update bot/cogs/code_snippets.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Leon Sandøy --- bot/cogs/code_snippets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/cogs/code_snippets.py b/bot/cogs/code_snippets.py index dda4d185f..d5424ea15 100644 --- a/bot/cogs/code_snippets.py +++ b/bot/cogs/code_snippets.py @@ -176,7 +176,7 @@ BITBUCKET_RE = re.compile( class CodeSnippets(Cog): """ - Cog that prints out snippets to Discord. + Cog that parses and sends code snippets to Discord. Matches each message against a regex and prints the contents of all matched snippets. """ -- cgit v1.2.3 From fd0bbdcd80156a443e5b91ad4b7f74e2c0285242 Mon Sep 17 00:00:00 2001 From: Andi Qu Date: Tue, 27 Oct 2020 11:19:56 +0200 Subject: Split up refs into branches and tags --- bot/cogs/code_snippets.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/bot/cogs/code_snippets.py b/bot/cogs/code_snippets.py index dda4d185f..77c0ede42 100644 --- a/bot/cogs/code_snippets.py +++ b/bot/cogs/code_snippets.py @@ -39,9 +39,9 @@ async def fetch_github_snippet(session: ClientSession, repo: str, headers = {'Accept': 'application/vnd.github.v3.raw'} # Search the GitHub API for the specified branch - refs = (await fetch_response(session, f'https://api.github.com/repos/{repo}/branches', 'json', headers=headers) - + await fetch_response(session, f'https://api.github.com/repos/{repo}/tags', 'json', headers=headers)) - + branches = await fetch_response(session, f'https://api.github.com/repos/{repo}/branches', 'json', headers=headers) + tags = await fetch_response(session, f'https://api.github.com/repos/{repo}/tags', 'json', headers=headers) + refs = branches + tags ref, file_path = find_ref(path, refs) file_contents = await fetch_response( @@ -83,9 +83,9 @@ async def fetch_gitlab_snippet(session: ClientSession, repo: str, enc_repo = quote_plus(repo) # Searches the GitLab API for the specified branch - refs = (await fetch_response(session, f'https://gitlab.com/api/v4/projects/{enc_repo}/repository/branches', 'json') - + await fetch_response(session, f'https://gitlab.com/api/v4/projects/{enc_repo}/repository/tags', 'json')) - + branches = await fetch_response(session, f'https://api.github.com/repos/{repo}/branches', 'json') + tags = await fetch_response(session, f'https://api.github.com/repos/{repo}/tags', 'json') + refs = branches + tags ref, file_path = find_ref(path, refs) enc_ref = quote_plus(ref) enc_file_path = quote_plus(file_path) -- cgit v1.2.3 From 7807939084f01fed327ff2d1772fb81efc0edbba Mon Sep 17 00:00:00 2001 From: Andi Qu Date: Tue, 27 Oct 2020 15:34:52 +0200 Subject: Made check for valid language easier to read --- bot/exts/info/code_snippets.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bot/exts/info/code_snippets.py b/bot/exts/info/code_snippets.py index 3d38ef1c3..c53c28e8b 100644 --- a/bot/exts/info/code_snippets.py +++ b/bot/exts/info/code_snippets.py @@ -144,7 +144,9 @@ def snippet_to_codeblock(file_contents: str, file_path: str, start_line: str, en # Extracts the code language and checks whether it's a "valid" language language = file_path.split('/')[-1].split('.')[-1] - if not language.replace('-', '').replace('+', '').replace('_', '').isalnum(): + trimmed_language = language.replace('-', '').replace('+', '').replace('_', '') + is_valid_language = trimmed_language.isalnum() + if not is_valid_language: language = '' if len(required) != 0: -- cgit v1.2.3 From 76afc563ac73f6b8d40194c15e28f42a9fe6be0f Mon Sep 17 00:00:00 2001 From: Andi Qu Date: Tue, 27 Oct 2020 15:45:09 +0200 Subject: Moved global functions into the cog and got rid of unnecessary aiohttp sessions --- bot/exts/info/code_snippets.py | 307 +++++++++++++++++++++-------------------- 1 file changed, 158 insertions(+), 149 deletions(-) diff --git a/bot/exts/info/code_snippets.py b/bot/exts/info/code_snippets.py index c53c28e8b..12eb692d4 100644 --- a/bot/exts/info/code_snippets.py +++ b/bot/exts/info/code_snippets.py @@ -2,7 +2,6 @@ import re import textwrap from urllib.parse import quote_plus -from aiohttp import ClientSession from discord import Message from discord.ext.commands import Cog @@ -10,150 +9,6 @@ from bot.bot import Bot from bot.utils.messages import wait_for_deletion -async def fetch_response(session: ClientSession, url: str, response_format: str, **kwargs) -> str: - """Makes http requests using aiohttp.""" - async with session.get(url, **kwargs) as response: - if response_format == 'text': - return await response.text() - elif response_format == 'json': - return await response.json() - - -def find_ref(path: str, refs: tuple) -> tuple: - """Loops through all branches and tags to find the required ref.""" - # Base case: there is no slash in the branch name - ref = path.split('/')[0] - file_path = '/'.join(path.split('/')[1:]) - # In case there are slashes in the branch name, we loop through all branches and tags - for possible_ref in refs: - if path.startswith(possible_ref['name'] + '/'): - ref = possible_ref['name'] - file_path = path[len(ref) + 1:] - break - return (ref, file_path) - - -async def fetch_github_snippet(session: ClientSession, repo: str, - path: str, start_line: str, end_line: str) -> str: - """Fetches a snippet from a GitHub repo.""" - headers = {'Accept': 'application/vnd.github.v3.raw'} - - # Search the GitHub API for the specified branch - branches = await fetch_response(session, f'https://api.github.com/repos/{repo}/branches', 'json', headers=headers) - tags = await fetch_response(session, f'https://api.github.com/repos/{repo}/tags', 'json', headers=headers) - refs = branches + tags - ref, file_path = find_ref(path, refs) - - file_contents = await fetch_response( - session, - f'https://api.github.com/repos/{repo}/contents/{file_path}?ref={ref}', - 'text', - headers=headers, - ) - return snippet_to_codeblock(file_contents, file_path, start_line, end_line) - - -async def fetch_github_gist_snippet(session: ClientSession, gist_id: str, revision: str, - file_path: str, start_line: str, end_line: str) -> str: - """Fetches a snippet from a GitHub gist.""" - headers = {'Accept': 'application/vnd.github.v3.raw'} - - gist_json = await fetch_response( - session, - f'https://api.github.com/gists/{gist_id}{f"/{revision}" if len(revision) > 0 else ""}', - 'json', - headers=headers, - ) - - # Check each file in the gist for the specified file - for gist_file in gist_json['files']: - if file_path == gist_file.lower().replace('.', '-'): - file_contents = await fetch_response( - session, - gist_json['files'][gist_file]['raw_url'], - 'text', - ) - return snippet_to_codeblock(file_contents, gist_file, start_line, end_line) - return '' - - -async def fetch_gitlab_snippet(session: ClientSession, repo: str, - path: str, start_line: str, end_line: str) -> str: - """Fetches a snippet from a GitLab repo.""" - enc_repo = quote_plus(repo) - - # Searches the GitLab API for the specified branch - branches = await fetch_response(session, f'https://api.github.com/repos/{repo}/branches', 'json') - tags = await fetch_response(session, f'https://api.github.com/repos/{repo}/tags', 'json') - refs = branches + tags - ref, file_path = find_ref(path, refs) - enc_ref = quote_plus(ref) - enc_file_path = quote_plus(file_path) - - file_contents = await fetch_response( - session, - f'https://gitlab.com/api/v4/projects/{enc_repo}/repository/files/{enc_file_path}/raw?ref={enc_ref}', - 'text', - ) - return snippet_to_codeblock(file_contents, file_path, start_line, end_line) - - -async def fetch_bitbucket_snippet(session: ClientSession, repo: str, ref: str, - file_path: str, start_line: int, end_line: int) -> str: - """Fetches a snippet from a BitBucket repo.""" - file_contents = await fetch_response( - session, - f'https://bitbucket.org/{quote_plus(repo)}/raw/{quote_plus(ref)}/{quote_plus(file_path)}', - 'text', - ) - return snippet_to_codeblock(file_contents, file_path, start_line, end_line) - - -def snippet_to_codeblock(file_contents: str, file_path: str, start_line: str, end_line: str) -> str: - """ - Given the entire file contents and target lines, creates a code block. - - First, we split the file contents into a list of lines and then keep and join only the required - ones together. - - We then dedent the lines to look nice, and replace all ` characters with `\u200b to prevent - markdown injection. - - Finally, we surround the code with ``` characters. - """ - # Parse start_line and end_line into integers - if end_line is None: - start_line = end_line = int(start_line) - else: - start_line = int(start_line) - end_line = int(end_line) - - split_file_contents = file_contents.splitlines() - - # Make sure that the specified lines are in range - if start_line > end_line: - start_line, end_line = end_line, start_line - if start_line > len(split_file_contents) or end_line < 1: - return '' - start_line = max(1, start_line) - end_line = min(len(split_file_contents), end_line) - - # Gets the code lines, dedents them, and inserts zero-width spaces to prevent Markdown injection - required = '\n'.join(split_file_contents[start_line - 1:end_line]) - required = textwrap.dedent(required).rstrip().replace('`', '`\u200b') - - # Extracts the code language and checks whether it's a "valid" language - language = file_path.split('/')[-1].split('.')[-1] - trimmed_language = language.replace('-', '').replace('+', '').replace('_', '') - is_valid_language = trimmed_language.isalnum() - if not is_valid_language: - language = '' - - if len(required) != 0: - return f'```{language}\n{required}```\n' - return '' - - GITHUB_RE = re.compile( r'https://github\.com/(?P.+?)/blob/(?P.+/.+)' r'#L(?P\d+)([-~]L(?P\d+))?\b' @@ -183,6 +38,160 @@ class CodeSnippets(Cog): Matches each message against a regex and prints the contents of all matched snippets. """ + async def _fetch_response(self, url: str, response_format: str, **kwargs) -> str: + """Makes http requests using aiohttp.""" + async with self.bot.http_session.get(url, **kwargs) as response: + if response_format == 'text': + return await response.text() + elif response_format == 'json': + return await response.json() + + def _find_ref(self, path: str, refs: tuple) -> tuple: + """Loops through all branches and tags to find the required ref.""" + # Base case: there is no slash in the branch name + ref = path.split('/')[0] + file_path = '/'.join(path.split('/')[1:]) + # In case there are slashes in the branch name, we loop through all branches and tags + for possible_ref in refs: + if path.startswith(possible_ref['name'] + '/'): + ref = possible_ref['name'] + file_path = path[len(ref) + 1:] + break + return (ref, file_path) + + async def _fetch_github_snippet( + self, + repo: str, + path: str, + start_line: str, + end_line: str + ) -> str: + """Fetches a snippet from a GitHub repo.""" + headers = {'Accept': 'application/vnd.github.v3.raw'} + + # Search the GitHub API for the specified branch + branches = await self._fetch_response(f'https://api.github.com/repos/{repo}/branches', 'json', headers=headers) + tags = await self._fetch_response(f'https://api.github.com/repos/{repo}/tags', 'json', headers=headers) + refs = branches + tags + ref, file_path = self._find_ref(path, refs) + + file_contents = await self._fetch_response( + f'https://api.github.com/repos/{repo}/contents/{file_path}?ref={ref}', + 'text', + headers=headers, + ) + return self._snippet_to_codeblock(file_contents, file_path, start_line, end_line) + + async def _fetch_github_gist_snippet( + self, + gist_id: str, + revision: str, + file_path: str, + start_line: str, + end_line: str + ) -> str: + """Fetches a snippet from a GitHub gist.""" + headers = {'Accept': 'application/vnd.github.v3.raw'} + + gist_json = await self._fetch_response( + f'https://api.github.com/gists/{gist_id}{f"/{revision}" if len(revision) > 0 else ""}', + 'json', + headers=headers, + ) + + # Check each file in the gist for the specified file + for gist_file in gist_json['files']: + if file_path == gist_file.lower().replace('.', '-'): + file_contents = await self._fetch_response( + gist_json['files'][gist_file]['raw_url'], + 'text', + ) + return self._snippet_to_codeblock(file_contents, gist_file, start_line, end_line) + return '' + + async def _fetch_gitlab_snippet( + self, + repo: str, + path: str, + start_line: str, + end_line: str + ) -> str: + """Fetches a snippet from a GitLab repo.""" + enc_repo = quote_plus(repo) + + # Searches the GitLab API for the specified branch + branches = await self._fetch_response(f'https://api.github.com/repos/{repo}/branches', 'json') + tags = await self._fetch_response(f'https://api.github.com/repos/{repo}/tags', 'json') + refs = branches + tags + ref, file_path = self._find_ref(path, refs) + enc_ref = quote_plus(ref) + enc_file_path = quote_plus(file_path) + + file_contents = await self._fetch_response( + f'https://gitlab.com/api/v4/projects/{enc_repo}/repository/files/{enc_file_path}/raw?ref={enc_ref}', + 'text', + ) + return self._snippet_to_codeblock(file_contents, file_path, start_line, end_line) + + async def _fetch_bitbucket_snippet( + self, + repo: str, + ref: str, + file_path: str, + start_line: int, + end_line: int + ) -> str: + """Fetches a snippet from a BitBucket repo.""" + file_contents = await self._fetch_response( + f'https://bitbucket.org/{quote_plus(repo)}/raw/{quote_plus(ref)}/{quote_plus(file_path)}', + 'text', + ) + return self._snippet_to_codeblock(file_contents, file_path, start_line, end_line) + + def _snippet_to_codeblock(self, file_contents: str, file_path: str, start_line: str, end_line: str) -> str: + """ + Given the entire file contents and target lines, creates a code block. + + First, we split the file contents into a list of lines and then keep and join only the required + ones together. + + We then dedent the lines to look nice, and replace all ` characters with `\u200b to prevent + markdown injection. + + Finally, we surround the code with ``` characters. + """ + # Parse start_line and end_line into integers + if end_line is None: + start_line = end_line = int(start_line) + else: + start_line = int(start_line) + end_line = int(end_line) + + split_file_contents = file_contents.splitlines() + + # Make sure that the specified lines are in range + if start_line > end_line: + start_line, end_line = end_line, start_line + if start_line > len(split_file_contents) or end_line < 1: + return '' + start_line = max(1, start_line) + end_line = min(len(split_file_contents), end_line) + + # Gets the code lines, dedents them, and inserts zero-width spaces to prevent Markdown injection + required = '\n'.join(split_file_contents[start_line - 1:end_line]) + required = textwrap.dedent(required).rstrip().replace('`', '`\u200b') + + # Extracts the code language and checks whether it's a "valid" language + language = file_path.split('/')[-1].split('.')[-1] + trimmed_language = language.replace('-', '').replace('+', '').replace('_', '') + is_valid_language = trimmed_language.isalnum() + if not is_valid_language: + language = '' + + if len(required) != 0: + return f'```{language}\n{required}```\n' + return '' + def __init__(self, bot: Bot): """Initializes the cog's bot.""" self.bot = bot @@ -199,16 +208,16 @@ class CodeSnippets(Cog): message_to_send = '' for gh in GITHUB_RE.finditer(message.content): - message_to_send += await fetch_github_snippet(self.bot.http_session, **gh.groupdict()) + message_to_send += await self._fetch_github_snippet(**gh.groupdict()) for gh_gist in GITHUB_GIST_RE.finditer(message.content): - message_to_send += await fetch_github_gist_snippet(self.bot.http_session, **gh_gist.groupdict()) + message_to_send += await self._fetch_github_gist_snippet(**gh_gist.groupdict()) for gl in GITLAB_RE.finditer(message.content): - message_to_send += await fetch_gitlab_snippet(self.bot.http_session, **gl.groupdict()) + message_to_send += await self._fetch_gitlab_snippet(**gl.groupdict()) for bb in BITBUCKET_RE.finditer(message.content): - message_to_send += await fetch_bitbucket_snippet(self.bot.http_session, **bb.groupdict()) + message_to_send += await self._fetch_bitbucket_snippet(**bb.groupdict()) if 0 < len(message_to_send) <= 2000 and message_to_send.count('\n') <= 15: await message.edit(suppress=True) -- cgit v1.2.3 From 3102c698e8892d5a3b1b0fcc2183bf2c480d60fd Mon Sep 17 00:00:00 2001 From: Andi Qu Date: Tue, 27 Oct 2020 15:55:34 +0200 Subject: Used a list of tuples for on_message instead --- bot/exts/info/code_snippets.py | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/bot/exts/info/code_snippets.py b/bot/exts/info/code_snippets.py index 12eb692d4..1bb00b677 100644 --- a/bot/exts/info/code_snippets.py +++ b/bot/exts/info/code_snippets.py @@ -199,25 +199,18 @@ class CodeSnippets(Cog): @Cog.listener() async def on_message(self, message: Message) -> None: """Checks if the message has a snippet link, removes the embed, then sends the snippet contents.""" - gh_match = GITHUB_RE.search(message.content) - gh_gist_match = GITHUB_GIST_RE.search(message.content) - gl_match = GITLAB_RE.search(message.content) - bb_match = BITBUCKET_RE.search(message.content) - - if (gh_match or gh_gist_match or gl_match or bb_match) and not message.author.bot: + if not message.author.bot: message_to_send = '' - - for gh in GITHUB_RE.finditer(message.content): - message_to_send += await self._fetch_github_snippet(**gh.groupdict()) - - for gh_gist in GITHUB_GIST_RE.finditer(message.content): - message_to_send += await self._fetch_github_gist_snippet(**gh_gist.groupdict()) - - for gl in GITLAB_RE.finditer(message.content): - message_to_send += await self._fetch_gitlab_snippet(**gl.groupdict()) - - for bb in BITBUCKET_RE.finditer(message.content): - message_to_send += await self._fetch_bitbucket_snippet(**bb.groupdict()) + pattern_handlers = [ + (GITHUB_RE, self._fetch_github_snippet), + (GITHUB_GIST_RE, self._fetch_github_gist_snippet), + (GITLAB_RE, self._fetch_gitlab_snippet), + (BITBUCKET_RE, self._fetch_bitbucket_snippet) + ] + + for pattern, handler in pattern_handlers: + for match in pattern.finditer(message.content): + message_to_send += await handler(**match.groupdict()) if 0 < len(message_to_send) <= 2000 and message_to_send.count('\n') <= 15: await message.edit(suppress=True) -- cgit v1.2.3 From bbf7a600ca4b657258b46074c00cab1982791613 Mon Sep 17 00:00:00 2001 From: Andi Qu <31325319+dolphingarlic@users.noreply.github.com> Date: Wed, 28 Oct 2020 09:26:09 +0200 Subject: Update bot/exts/info/code_snippets.py Co-authored-by: Mark --- bot/exts/info/code_snippets.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bot/exts/info/code_snippets.py b/bot/exts/info/code_snippets.py index 1bb00b677..4594c36f2 100644 --- a/bot/exts/info/code_snippets.py +++ b/bot/exts/info/code_snippets.py @@ -49,8 +49,7 @@ class CodeSnippets(Cog): def _find_ref(self, path: str, refs: tuple) -> tuple: """Loops through all branches and tags to find the required ref.""" # Base case: there is no slash in the branch name - ref = path.split('/')[0] - file_path = '/'.join(path.split('/')[1:]) + ref, file_path = path.split('/', 1) # In case there are slashes in the branch name, we loop through all branches and tags for possible_ref in refs: if path.startswith(possible_ref['name'] + '/'): -- cgit v1.2.3 From 1b8610c83dacfe1b19f3efa5d3a2b66c4c6e1e5d Mon Sep 17 00:00:00 2001 From: Andi Qu <31325319+dolphingarlic@users.noreply.github.com> Date: Wed, 28 Oct 2020 09:31:01 +0200 Subject: Removed unnecessary space before equals sign --- bot/exts/info/code_snippets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/info/code_snippets.py b/bot/exts/info/code_snippets.py index 4594c36f2..d854ebb4c 100644 --- a/bot/exts/info/code_snippets.py +++ b/bot/exts/info/code_snippets.py @@ -49,7 +49,7 @@ class CodeSnippets(Cog): def _find_ref(self, path: str, refs: tuple) -> tuple: """Loops through all branches and tags to find the required ref.""" # Base case: there is no slash in the branch name - ref, file_path = path.split('/', 1) + ref, file_path = path.split('/', 1) # In case there are slashes in the branch name, we loop through all branches and tags for possible_ref in refs: if path.startswith(possible_ref['name'] + '/'): -- cgit v1.2.3 From 8b41a7678d175de69ae6bf72e6a9f6e7036e1968 Mon Sep 17 00:00:00 2001 From: Andi Qu Date: Tue, 8 Dec 2020 10:21:41 +0200 Subject: Add file path to codeblock --- bot/exts/info/code_snippets.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/bot/exts/info/code_snippets.py b/bot/exts/info/code_snippets.py index 1bb00b677..f807fa9a7 100644 --- a/bot/exts/info/code_snippets.py +++ b/bot/exts/info/code_snippets.py @@ -188,9 +188,16 @@ class CodeSnippets(Cog): if not is_valid_language: language = '' + # Adds a label showing the file path to the snippet + if start_line == end_line: + ret = f'`{file_path}` line {start_line}\n' + else: + ret = f'`{file_path}` lines {start_line} to {end_line}\n' + if len(required) != 0: - return f'```{language}\n{required}```\n' - return '' + return f'{ret}```{language}\n{required}```\n' + # Returns an empty codeblock if the snippet is empty + return f'{ret}``` ```\n' def __init__(self, bot: Bot): """Initializes the cog's bot.""" -- cgit v1.2.3 From e8d2448c771aef262b294a583661092c9e90baef Mon Sep 17 00:00:00 2001 From: Andi Qu Date: Tue, 8 Dec 2020 10:36:56 +0200 Subject: Add logging for HTTP requests --- bot/exts/info/code_snippets.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/bot/exts/info/code_snippets.py b/bot/exts/info/code_snippets.py index f807fa9a7..e1025e568 100644 --- a/bot/exts/info/code_snippets.py +++ b/bot/exts/info/code_snippets.py @@ -1,3 +1,4 @@ +import logging import re import textwrap from urllib.parse import quote_plus @@ -8,6 +9,7 @@ from discord.ext.commands import Cog from bot.bot import Bot from bot.utils.messages import wait_for_deletion +log = logging.getLogger(__name__) GITHUB_RE = re.compile( r'https://github\.com/(?P.+?)/blob/(?P.+/.+)' @@ -40,11 +42,14 @@ class CodeSnippets(Cog): async def _fetch_response(self, url: str, response_format: str, **kwargs) -> str: """Makes http requests using aiohttp.""" - async with self.bot.http_session.get(url, **kwargs) as response: - if response_format == 'text': - return await response.text() - elif response_format == 'json': - return await response.json() + try: + async with self.bot.http_session.get(url, **kwargs) as response: + if response_format == 'text': + return await response.text() + elif response_format == 'json': + return await response.json() + except Exception: + log.exception(f'Failed to fetch code snippet from {url}.') def _find_ref(self, path: str, refs: tuple) -> tuple: """Loops through all branches and tags to find the required ref.""" -- cgit v1.2.3 From d32e8f1029be8deb76e8c0d9bb457c9768ca878e Mon Sep 17 00:00:00 2001 From: Andi Qu Date: Wed, 13 Jan 2021 19:08:32 +0200 Subject: Better regex, moved pattern handlers to __init__, and constant header --- bot/exts/info/code_snippets.py | 52 +++++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/bot/exts/info/code_snippets.py b/bot/exts/info/code_snippets.py index 669a21c7d..1899b139b 100644 --- a/bot/exts/info/code_snippets.py +++ b/bot/exts/info/code_snippets.py @@ -12,24 +12,27 @@ from bot.utils.messages import wait_for_deletion log = logging.getLogger(__name__) GITHUB_RE = re.compile( - r'https://github\.com/(?P.+?)/blob/(?P.+/.+)' - r'#L(?P\d+)([-~]L(?P\d+))?\b' + r'https://github\.com/(?P\S+?)/blob/(?P\S+/[^\s#]+)' + r'(#L(?P\d+)([-~:]L(?P\d+))?)?($|\s)' ) GITHUB_GIST_RE = re.compile( r'https://gist\.github\.com/([^/]+)/(?P[^\W_]+)/*' - r'(?P[^\W_]*)/*#file-(?P.+?)' - r'-L(?P\d+)([-~]L(?P\d+))?\b' + r'(?P[^\W_]*)/*#file-(?P\S+?)' + r'(-L(?P\d+)([-~:]L(?P\d+))?)?($|\s)' ) +GITHUB_HEADERS = {'Accept': 'application/vnd.github.v3.raw'} + GITLAB_RE = re.compile( - r'https://gitlab\.com/(?P.+?)/\-/blob/(?P.+/.+)' - r'#L(?P\d+)([-](?P\d+))?\b' + r'https://gitlab\.com/(?P\S+?)/\-/blob/(?P\S+/[^\s#]+)' + r'(#L(?P\d+)([-](?P\d+))?)?($|\s)' ) BITBUCKET_RE = re.compile( - r'https://bitbucket\.org/(?P.+?)/src/(?P.+?)/' - r'(?P.+?)#lines-(?P\d+)(:(?P\d+))?\b' + r'https://bitbucket\.org/(?P\S+?)/src/' + r'(?P\S+?)/(?P[^\s#]+)' + r'(#lines-(?P\d+)(:(?P\d+))?)?($|\s)' ) @@ -71,18 +74,20 @@ class CodeSnippets(Cog): end_line: str ) -> str: """Fetches a snippet from a GitHub repo.""" - headers = {'Accept': 'application/vnd.github.v3.raw'} - # Search the GitHub API for the specified branch - branches = await self._fetch_response(f'https://api.github.com/repos/{repo}/branches', 'json', headers=headers) - tags = await self._fetch_response(f'https://api.github.com/repos/{repo}/tags', 'json', headers=headers) + branches = await self._fetch_response( + f'https://api.github.com/repos/{repo}/branches', + 'json', + headers=GITHUB_HEADERS + ) + tags = await self._fetch_response(f'https://api.github.com/repos/{repo}/tags', 'json', headers=GITHUB_HEADERS) refs = branches + tags ref, file_path = self._find_ref(path, refs) file_contents = await self._fetch_response( f'https://api.github.com/repos/{repo}/contents/{file_path}?ref={ref}', 'text', - headers=headers, + headers=GITHUB_HEADERS, ) return self._snippet_to_codeblock(file_contents, file_path, start_line, end_line) @@ -95,12 +100,10 @@ class CodeSnippets(Cog): end_line: str ) -> str: """Fetches a snippet from a GitHub gist.""" - headers = {'Accept': 'application/vnd.github.v3.raw'} - gist_json = await self._fetch_response( f'https://api.github.com/gists/{gist_id}{f"/{revision}" if len(revision) > 0 else ""}', 'json', - headers=headers, + headers=GITHUB_HEADERS, ) # Check each file in the gist for the specified file @@ -207,19 +210,20 @@ class CodeSnippets(Cog): """Initializes the cog's bot.""" self.bot = bot + self.pattern_handlers = [ + (GITHUB_RE, self._fetch_github_snippet), + (GITHUB_GIST_RE, self._fetch_github_gist_snippet), + (GITLAB_RE, self._fetch_gitlab_snippet), + (BITBUCKET_RE, self._fetch_bitbucket_snippet) + ] + @Cog.listener() async def on_message(self, message: Message) -> None: """Checks if the message has a snippet link, removes the embed, then sends the snippet contents.""" if not message.author.bot: message_to_send = '' - pattern_handlers = [ - (GITHUB_RE, self._fetch_github_snippet), - (GITHUB_GIST_RE, self._fetch_github_gist_snippet), - (GITLAB_RE, self._fetch_gitlab_snippet), - (BITBUCKET_RE, self._fetch_bitbucket_snippet) - ] - - for pattern, handler in pattern_handlers: + + for pattern, handler in self.pattern_handlers: for match in pattern.finditer(message.content): message_to_send += await handler(**match.groupdict()) -- cgit v1.2.3 From 1856ed852515c17c2095c10b93d4d418787ec178 Mon Sep 17 00:00:00 2001 From: Andi Qu Date: Wed, 13 Jan 2021 19:10:03 +0200 Subject: Better regex now works for --- bot/exts/info/code_snippets.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/bot/exts/info/code_snippets.py b/bot/exts/info/code_snippets.py index 1899b139b..1d1bc2850 100644 --- a/bot/exts/info/code_snippets.py +++ b/bot/exts/info/code_snippets.py @@ -12,27 +12,27 @@ from bot.utils.messages import wait_for_deletion log = logging.getLogger(__name__) GITHUB_RE = re.compile( - r'https://github\.com/(?P\S+?)/blob/(?P\S+/[^\s#]+)' - r'(#L(?P\d+)([-~:]L(?P\d+))?)?($|\s)' + r'https://github\.com/(?P\S+?)/blob/(?P\S+/[^\s#,>]+)' + r'(#L(?P\d+)([-~:]L(?P\d+))?)?($|\s|,|>)' ) GITHUB_GIST_RE = re.compile( r'https://gist\.github\.com/([^/]+)/(?P[^\W_]+)/*' r'(?P[^\W_]*)/*#file-(?P\S+?)' - r'(-L(?P\d+)([-~:]L(?P\d+))?)?($|\s)' + r'(-L(?P\d+)([-~:]L(?P\d+))?)?($|\s|,|>)' ) GITHUB_HEADERS = {'Accept': 'application/vnd.github.v3.raw'} GITLAB_RE = re.compile( - r'https://gitlab\.com/(?P\S+?)/\-/blob/(?P\S+/[^\s#]+)' - r'(#L(?P\d+)([-](?P\d+))?)?($|\s)' + r'https://gitlab\.com/(?P\S+?)/\-/blob/(?P\S+/[^\s#,>]+)' + r'(#L(?P\d+)([-](?P\d+))?)?($|\s|,|>)' ) BITBUCKET_RE = re.compile( r'https://bitbucket\.org/(?P\S+?)/src/' - r'(?P\S+?)/(?P[^\s#]+)' - r'(#lines-(?P\d+)(:(?P\d+))?)?($|\s)' + r'(?P\S+?)/(?P[^\s#,>]+)' + r'(#lines-(?P\d+)(:(?P\d+))?)?($|\s|,|>)' ) -- cgit v1.2.3 From 08b793024f271de009aab2391cd85576af5313cf Mon Sep 17 00:00:00 2001 From: Andi Qu Date: Wed, 13 Jan 2021 19:19:49 +0200 Subject: Better error reporting in _fetch_response(?) --- bot/exts/info/code_snippets.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/bot/exts/info/code_snippets.py b/bot/exts/info/code_snippets.py index 1d1bc2850..3469b88f4 100644 --- a/bot/exts/info/code_snippets.py +++ b/bot/exts/info/code_snippets.py @@ -3,6 +3,7 @@ import re import textwrap from urllib.parse import quote_plus +from aiohttp import ClientResponseError from discord import Message from discord.ext.commands import Cog @@ -46,13 +47,13 @@ class CodeSnippets(Cog): async def _fetch_response(self, url: str, response_format: str, **kwargs) -> str: """Makes http requests using aiohttp.""" try: - async with self.bot.http_session.get(url, **kwargs) as response: + async with self.bot.http_session.get(url, raise_for_status=True, **kwargs) as response: if response_format == 'text': return await response.text() elif response_format == 'json': return await response.json() - except Exception: - log.exception(f'Failed to fetch code snippet from {url}.') + except ClientResponseError as error: + log.error(f'Failed to fetch code snippet from {url}. HTTP Status: {error.status}. Message: {str(error)}.') def _find_ref(self, path: str, refs: tuple) -> tuple: """Loops through all branches and tags to find the required ref.""" -- cgit v1.2.3 From 318a0f6c5e597c61833984cd608359c8b4e5ddf0 Mon Sep 17 00:00:00 2001 From: Andi Qu Date: Tue, 19 Jan 2021 21:00:34 +0200 Subject: Better GitHub regex --- bot/exts/info/code_snippets.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bot/exts/info/code_snippets.py b/bot/exts/info/code_snippets.py index 3469b88f4..84f606036 100644 --- a/bot/exts/info/code_snippets.py +++ b/bot/exts/info/code_snippets.py @@ -13,27 +13,27 @@ from bot.utils.messages import wait_for_deletion log = logging.getLogger(__name__) GITHUB_RE = re.compile( - r'https://github\.com/(?P\S+?)/blob/(?P\S+/[^\s#,>]+)' - r'(#L(?P\d+)([-~:]L(?P\d+))?)?($|\s|,|>)' + r'https://github\.com/(?P[a-zA-Z0-9-]+/[\w.-]+)/blob/' + r'(?P[^#>]+/{0,1})(#L(?P\d+)([-~:]L(?P\d+))?)' ) GITHUB_GIST_RE = re.compile( r'https://gist\.github\.com/([^/]+)/(?P[^\W_]+)/*' r'(?P[^\W_]*)/*#file-(?P\S+?)' - r'(-L(?P\d+)([-~:]L(?P\d+))?)?($|\s|,|>)' + r'(-L(?P\d+)([-~:]L(?P\d+))?)' ) GITHUB_HEADERS = {'Accept': 'application/vnd.github.v3.raw'} GITLAB_RE = re.compile( r'https://gitlab\.com/(?P\S+?)/\-/blob/(?P\S+/[^\s#,>]+)' - r'(#L(?P\d+)([-](?P\d+))?)?($|\s|,|>)' + r'(#L(?P\d+)([-](?P\d+))?)' ) BITBUCKET_RE = re.compile( r'https://bitbucket\.org/(?P\S+?)/src/' r'(?P\S+?)/(?P[^\s#,>]+)' - r'(#lines-(?P\d+)(:(?P\d+))?)?($|\s|,|>)' + r'(#lines-(?P\d+)(:(?P\d+))?)' ) -- cgit v1.2.3 From e9f48d83d482502a846dd8d37cee6ab4c01fdf7e Mon Sep 17 00:00:00 2001 From: Andi Qu Date: Tue, 19 Jan 2021 21:14:19 +0200 Subject: Account for query params in bitbucket --- bot/exts/info/code_snippets.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/bot/exts/info/code_snippets.py b/bot/exts/info/code_snippets.py index 84f606036..75d8ac290 100644 --- a/bot/exts/info/code_snippets.py +++ b/bot/exts/info/code_snippets.py @@ -18,22 +18,21 @@ GITHUB_RE = re.compile( ) GITHUB_GIST_RE = re.compile( - r'https://gist\.github\.com/([^/]+)/(?P[^\W_]+)/*' - r'(?P[^\W_]*)/*#file-(?P\S+?)' + r'https://gist\.github\.com/([^/]+)/(?P[a-zA-Z0-9]+)/*' + r'(?P[a-zA-Z0-9-]*)/*#file-(?P[^#>]+?)' r'(-L(?P\d+)([-~:]L(?P\d+))?)' ) GITHUB_HEADERS = {'Accept': 'application/vnd.github.v3.raw'} GITLAB_RE = re.compile( - r'https://gitlab\.com/(?P\S+?)/\-/blob/(?P\S+/[^\s#,>]+)' - r'(#L(?P\d+)([-](?P\d+))?)' + r'https://gitlab\.com/(?P[a-zA-Z0-9-]+?)/\-/blob/(?P[^#>]+/{0,1})' + r'(#L(?P\d+)(-(?P\d+))?)' ) BITBUCKET_RE = re.compile( - r'https://bitbucket\.org/(?P\S+?)/src/' - r'(?P\S+?)/(?P[^\s#,>]+)' - r'(#lines-(?P\d+)(:(?P\d+))?)' + r'https://bitbucket\.org/(?P[a-zA-Z0-9-]+/[\w.-]+?)/src/(?P[0-9a-zA-Z]+?)' + r'/(?P[^#>]+?)(\?[^#>]+)?(#lines-(?P\d+)(:(?P\d+))?)' ) -- cgit v1.2.3 From 87facef69acfaa1d8b69b5a03bfabc9582aa1ace Mon Sep 17 00:00:00 2001 From: Andi Qu <31325319+dolphingarlic@users.noreply.github.com> Date: Sun, 24 Jan 2021 19:57:26 +0200 Subject: More restrictive GitHub gist regex for usernames --- bot/exts/info/code_snippets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/info/code_snippets.py b/bot/exts/info/code_snippets.py index 75d8ac290..e1b2079d0 100644 --- a/bot/exts/info/code_snippets.py +++ b/bot/exts/info/code_snippets.py @@ -18,7 +18,7 @@ GITHUB_RE = re.compile( ) GITHUB_GIST_RE = re.compile( - r'https://gist\.github\.com/([^/]+)/(?P[a-zA-Z0-9]+)/*' + r'https://gist\.github\.com/([a-zA-Z0-9-]+)/(?P[a-zA-Z0-9]+)/*' r'(?P[a-zA-Z0-9-]*)/*#file-(?P[^#>]+?)' r'(-L(?P\d+)([-~:]L(?P\d+))?)' ) -- cgit v1.2.3 From 69a87371aeaf815cea71d5b44a7b6a824f7fa5ed Mon Sep 17 00:00:00 2001 From: Andi Qu <31325319+dolphingarlic@users.noreply.github.com> Date: Sun, 24 Jan 2021 19:58:36 +0200 Subject: Don't match dashes in GitHub gist revisions Gist revisions don't allow dashes oops --- bot/exts/info/code_snippets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/info/code_snippets.py b/bot/exts/info/code_snippets.py index e1b2079d0..44f11cdbd 100644 --- a/bot/exts/info/code_snippets.py +++ b/bot/exts/info/code_snippets.py @@ -19,7 +19,7 @@ GITHUB_RE = re.compile( GITHUB_GIST_RE = re.compile( r'https://gist\.github\.com/([a-zA-Z0-9-]+)/(?P[a-zA-Z0-9]+)/*' - r'(?P[a-zA-Z0-9-]*)/*#file-(?P[^#>]+?)' + r'(?P[a-zA-Z0-9]*)/*#file-(?P[^#>]+?)' r'(-L(?P\d+)([-~:]L(?P\d+))?)' ) -- cgit v1.2.3 From ae5e1c64983431e1bcac1fc9a50255fdc32777ee Mon Sep 17 00:00:00 2001 From: Andi Qu <31325319+dolphingarlic@users.noreply.github.com> Date: Sun, 24 Jan 2021 19:59:49 +0200 Subject: Add matching for query params to all the regexes --- bot/exts/info/code_snippets.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bot/exts/info/code_snippets.py b/bot/exts/info/code_snippets.py index 44f11cdbd..3f943aea8 100644 --- a/bot/exts/info/code_snippets.py +++ b/bot/exts/info/code_snippets.py @@ -14,12 +14,12 @@ log = logging.getLogger(__name__) GITHUB_RE = re.compile( r'https://github\.com/(?P[a-zA-Z0-9-]+/[\w.-]+)/blob/' - r'(?P[^#>]+/{0,1})(#L(?P\d+)([-~:]L(?P\d+))?)' + r'(?P[^#>]+/{0,1})(\?[^#>]+)?(#L(?P\d+)([-~:]L(?P\d+))?)' ) GITHUB_GIST_RE = re.compile( r'https://gist\.github\.com/([a-zA-Z0-9-]+)/(?P[a-zA-Z0-9]+)/*' - r'(?P[a-zA-Z0-9]*)/*#file-(?P[^#>]+?)' + r'(?P[a-zA-Z0-9]*)/*#file-(?P[^#>]+?)(\?[^#>]+)?' r'(-L(?P\d+)([-~:]L(?P\d+))?)' ) @@ -27,7 +27,7 @@ GITHUB_HEADERS = {'Accept': 'application/vnd.github.v3.raw'} GITLAB_RE = re.compile( r'https://gitlab\.com/(?P[a-zA-Z0-9-]+?)/\-/blob/(?P[^#>]+/{0,1})' - r'(#L(?P\d+)(-(?P\d+))?)' + r'(\?[^#>]+)?(#L(?P\d+)(-(?P\d+))?)' ) BITBUCKET_RE = re.compile( -- cgit v1.2.3 From 64596679aeed67a0bfdb645ade5065af129c8c56 Mon Sep 17 00:00:00 2001 From: Andi Qu <31325319+dolphingarlic@users.noreply.github.com> Date: Sun, 24 Jan 2021 20:06:20 +0200 Subject: Match both username *and* repo in the GitLab regex --- bot/exts/info/code_snippets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/info/code_snippets.py b/bot/exts/info/code_snippets.py index 3f943aea8..e825ec513 100644 --- a/bot/exts/info/code_snippets.py +++ b/bot/exts/info/code_snippets.py @@ -26,7 +26,7 @@ GITHUB_GIST_RE = re.compile( GITHUB_HEADERS = {'Accept': 'application/vnd.github.v3.raw'} GITLAB_RE = re.compile( - r'https://gitlab\.com/(?P[a-zA-Z0-9-]+?)/\-/blob/(?P[^#>]+/{0,1})' + r'https://gitlab\.com/(?P[\w.-]+/[\w.-]+)/\-/blob/(?P[^#>]+/{0,1})' r'(\?[^#>]+)?(#L(?P\d+)(-(?P\d+))?)' ) -- cgit v1.2.3 From 4dee6d3c4e18144b35011fc4441738a82fcb522b Mon Sep 17 00:00:00 2001 From: Andi Qu <31325319+dolphingarlic@users.noreply.github.com> Date: Sat, 30 Jan 2021 11:43:14 +0200 Subject: Got rid of unnecessary regex matching things Stuff like `/{0,1}` and `?` at the ends of groups --- bot/exts/info/code_snippets.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bot/exts/info/code_snippets.py b/bot/exts/info/code_snippets.py index e825ec513..4c8de05fc 100644 --- a/bot/exts/info/code_snippets.py +++ b/bot/exts/info/code_snippets.py @@ -14,7 +14,7 @@ log = logging.getLogger(__name__) GITHUB_RE = re.compile( r'https://github\.com/(?P[a-zA-Z0-9-]+/[\w.-]+)/blob/' - r'(?P[^#>]+/{0,1})(\?[^#>]+)?(#L(?P\d+)([-~:]L(?P\d+))?)' + r'(?P[^#>]+)(\?[^#>]+)?(#L(?P\d+)([-~:]L(?P\d+))?)' ) GITHUB_GIST_RE = re.compile( @@ -26,13 +26,13 @@ GITHUB_GIST_RE = re.compile( GITHUB_HEADERS = {'Accept': 'application/vnd.github.v3.raw'} GITLAB_RE = re.compile( - r'https://gitlab\.com/(?P[\w.-]+/[\w.-]+)/\-/blob/(?P[^#>]+/{0,1})' + r'https://gitlab\.com/(?P[\w.-]+/[\w.-]+)/\-/blob/(?P[^#>]+)' r'(\?[^#>]+)?(#L(?P\d+)(-(?P\d+))?)' ) BITBUCKET_RE = re.compile( - r'https://bitbucket\.org/(?P[a-zA-Z0-9-]+/[\w.-]+?)/src/(?P[0-9a-zA-Z]+?)' - r'/(?P[^#>]+?)(\?[^#>]+)?(#lines-(?P\d+)(:(?P\d+))?)' + r'https://bitbucket\.org/(?P[a-zA-Z0-9-]+/[\w.-]+)/src/(?P[0-9a-zA-Z]+)' + r'/(?P[^#>]+)(\?[^#>]+)?(#lines-(?P\d+)(:(?P\d+))?)' ) -- cgit v1.2.3 From 25702f7d44eefbdb3d727b39bc0752e042320d8d Mon Sep 17 00:00:00 2001 From: Andi Qu Date: Sat, 30 Jan 2021 22:53:52 +0200 Subject: Use the GitLab API for GitLab snippets --- bot/exts/info/code_snippets.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/bot/exts/info/code_snippets.py b/bot/exts/info/code_snippets.py index 4c8de05fc..e149b5637 100644 --- a/bot/exts/info/code_snippets.py +++ b/bot/exts/info/code_snippets.py @@ -127,8 +127,11 @@ class CodeSnippets(Cog): enc_repo = quote_plus(repo) # Searches the GitLab API for the specified branch - branches = await self._fetch_response(f'https://api.github.com/repos/{repo}/branches', 'json') - tags = await self._fetch_response(f'https://api.github.com/repos/{repo}/tags', 'json') + branches = await self._fetch_response( + f'https://gitlab.com/api/v4/projects/{enc_repo}/repository/branches', + 'json' + ) + tags = await self._fetch_response(f'https://gitlab.com/api/v4/projects/{enc_repo}/repository/tags', 'json') refs = branches + tags ref, file_path = self._find_ref(path, refs) enc_ref = quote_plus(ref) -- cgit v1.2.3 From 7b27971c7d2cda0ebea091af76314f11bd6d0ba7 Mon Sep 17 00:00:00 2001 From: Andi Qu Date: Sat, 30 Jan 2021 22:56:25 +0200 Subject: Fixed syntax error with wait_for_deletion --- bot/exts/info/code_snippets.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bot/exts/info/code_snippets.py b/bot/exts/info/code_snippets.py index e149b5637..f0cd54c0c 100644 --- a/bot/exts/info/code_snippets.py +++ b/bot/exts/info/code_snippets.py @@ -234,8 +234,7 @@ class CodeSnippets(Cog): await message.edit(suppress=True) await wait_for_deletion( await message.channel.send(message_to_send), - (message.author.id,), - client=self.bot + (message.author.id,) ) -- cgit v1.2.3 From 0dd2e75be2368a4197e9370cf982dc8be8fa862b Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Sat, 20 Feb 2021 17:01:19 +0100 Subject: Add bot and verified bot badges to the user embed. --- bot/constants.py | 2 ++ bot/exts/info/information.py | 3 +++ config-default.yml | 2 ++ 3 files changed, 7 insertions(+) diff --git a/bot/constants.py b/bot/constants.py index 8a93ff9cf..91d425b1d 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -279,6 +279,8 @@ class Emojis(metaclass=YAMLGetter): badge_partner: str badge_staff: str badge_verified_bot_developer: str + badge_verified_bot: str + bot: str defcon_disabled: str # noqa: E704 defcon_enabled: str # noqa: E704 diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index 4499e4c25..256be2161 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -228,6 +228,9 @@ class Information(Cog): if on_server and user.nick: name = f"{user.nick} ({name})" + if user.bot: + name += f" {constants.Emojis.bot}" + badges = [] for badge, is_set in user.public_flags: diff --git a/config-default.yml b/config-default.yml index 25bbcc3c5..822b37daf 100644 --- a/config-default.yml +++ b/config-default.yml @@ -46,6 +46,8 @@ style: badge_partner: "<:partner:748666453242413136>" badge_staff: "<:discord_staff:743882896498098226>" badge_verified_bot_developer: "<:verified_bot_dev:743882897299210310>" + badge_verified_bot: "<:verified_bot:811645219220750347>" + bot: "<:bot:812712599464443914>" defcon_disabled: "<:defcondisabled:470326273952972810>" defcon_enabled: "<:defconenabled:470326274213150730>" -- cgit v1.2.3 From e06f496a6e3f9a9d6cfaeb3902547aa9da1dd7c1 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Wed, 14 Apr 2021 20:05:04 +0300 Subject: Add Duty cog and new Moderators role Added a cog to allow moderators to go off and on duty. The off-duty state is cached via a redis cache, and its expiry is scheduled via the Scheduler. Additionally changes which roles are pinged on mod alerts. --- bot/constants.py | 1 + bot/exts/moderation/duty.py | 135 ++++++++++++++++++++++++++++++++++++++++++ bot/exts/moderation/modlog.py | 6 +- config-default.yml | 5 +- 4 files changed, 143 insertions(+), 4 deletions(-) create mode 100644 bot/exts/moderation/duty.py diff --git a/bot/constants.py b/bot/constants.py index 6d14bbb3a..cc3aa41a5 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -491,6 +491,7 @@ class Roles(metaclass=YAMLGetter): domain_leads: int helpers: int moderators: int + mod_team: int owners: int project_leads: int diff --git a/bot/exts/moderation/duty.py b/bot/exts/moderation/duty.py new file mode 100644 index 000000000..13be016f2 --- /dev/null +++ b/bot/exts/moderation/duty.py @@ -0,0 +1,135 @@ +import datetime +import logging + +from async_rediscache import RedisCache +from dateutil.parser import isoparse +from discord import Member +from discord.ext.commands import Cog, Context, group, has_any_role + +from bot.bot import Bot +from bot.constants import Emojis, Guild, MODERATION_ROLES, Roles +from bot.converters import Expiry +from bot.utils.scheduling import Scheduler + + +log = logging.getLogger(__name__) + + +class Duty(Cog): + """Commands for a moderator to go on and off duty.""" + + # RedisCache[str, str] + # The cache's keys are mods who are off-duty. + # The cache's values are the times when the role should be re-applied to them, stored in ISO format. + off_duty_mods = RedisCache() + + def __init__(self, bot: Bot): + self.bot = bot + self._role_scheduler = Scheduler(self.__class__.__name__) + + self.guild = None + self.moderators_role = None + + self.bot.loop.create_task(self.reschedule_roles()) + + async def reschedule_roles(self) -> None: + """Reschedule moderators role re-apply times.""" + await self.bot.wait_until_guild_available() + self.guild = self.bot.get_guild(Guild.id) + self.moderators_role = self.guild.get_role(Roles.moderators) + + mod_team = self.guild.get_role(Roles.mod_team) + on_duty = self.moderators_role.members + off_duty = await self.off_duty_mods.to_dict() + + log.trace("Applying the moderators role to the mod team where necessary.") + for mod in mod_team.members: + if mod in on_duty: # Make sure that on-duty mods aren't in the cache. + if mod in off_duty: + await self.off_duty_mods.delete(mod.id) + continue + + # Keep the role off only for those in the cache. + if mod.id not in off_duty: + await self.reapply_role(mod) + else: + expiry = isoparse(off_duty[mod.id]).replace(tzinfo=None) + self._role_scheduler.schedule_at(expiry, mod.id, self.reapply_role(mod)) + + async def reapply_role(self, mod: Member) -> None: + """Reapply the moderator's role to the given moderator.""" + log.trace(f"Re-applying role to mod with ID {mod.id}.") + await mod.add_roles(self.moderators_role, reason="Off-duty period expired.") + + @group(name='duty', invoke_without_command=True) + @has_any_role(*MODERATION_ROLES) + async def duty_group(self, ctx: Context) -> None: + """Allow the removal and re-addition of the pingable moderators role.""" + await ctx.send_help(ctx.command) + + @duty_group.command(name='off') + @has_any_role(*MODERATION_ROLES) + async def off_command(self, ctx: Context, duration: Expiry) -> None: + """ + Temporarily removes the pingable moderators role for a set amount of time. + + A unit of time should be appended to the duration. + Units (∗case-sensitive): + \u2003`y` - years + \u2003`m` - months∗ + \u2003`w` - weeks + \u2003`d` - days + \u2003`h` - hours + \u2003`M` - minutes∗ + \u2003`s` - seconds + + Alternatively, an ISO 8601 timestamp can be provided for the duration. + + The duration cannot be longer than 30 days. + """ + duration: datetime.datetime + delta = duration - datetime.datetime.utcnow() + if delta > datetime.timedelta(days=30): + await ctx.send(":x: Cannot remove the role for longer than 30 days.") + return + + mod = ctx.author + + await mod.remove_roles(self.moderators_role, reason="Entered off-duty period.") + + await self.off_duty_mods.update({mod.id: duration.isoformat()}) + + if mod.id in self._role_scheduler: + self._role_scheduler.cancel(mod.id) + self._role_scheduler.schedule_at(duration, mod.id, self.reapply_role(mod)) + + until_date = duration.replace(microsecond=0).isoformat() + await ctx.send(f"{Emojis.check_mark} Moderators role has been removed until {until_date}.") + + @duty_group.command(name='on') + @has_any_role(*MODERATION_ROLES) + async def on_command(self, ctx: Context) -> None: + """Re-apply the pingable moderators role.""" + mod = ctx.author + if mod in self.moderators_role.members: + await ctx.send(":question: You already have the role.") + return + + await mod.add_roles(self.moderators_role, reason="Off-duty period canceled.") + + await self.off_duty_mods.delete(mod.id) + + if mod.id in self._role_scheduler: + self._role_scheduler.cancel(mod.id) + + await ctx.send(f"{Emojis.check_mark} Moderators role has been re-applied.") + + def cog_unload(self) -> None: + """Cancel role tasks when the cog unloads.""" + log.trace("Cog unload: canceling role tasks.") + self._role_scheduler.cancel_all() + + +def setup(bot: Bot) -> None: + """Load the Slowmode cog.""" + bot.add_cog(Duty(bot)) diff --git a/bot/exts/moderation/modlog.py b/bot/exts/moderation/modlog.py index 2dae9d268..f68a1880e 100644 --- a/bot/exts/moderation/modlog.py +++ b/bot/exts/moderation/modlog.py @@ -14,7 +14,7 @@ from discord.abc import GuildChannel from discord.ext.commands import Cog, Context from bot.bot import Bot -from bot.constants import Categories, Channels, Colours, Emojis, Event, Guild as GuildConstant, Icons, URLs +from bot.constants import Categories, Channels, Colours, Emojis, Event, Guild as GuildConstant, Icons, Roles, URLs from bot.utils.messages import format_user from bot.utils.time import humanize_delta @@ -115,9 +115,9 @@ class ModLog(Cog, name="ModLog"): if ping_everyone: if content: - content = f"@everyone\n{content}" + content = f"<@&{Roles.moderators}> @here\n{content}" else: - content = "@everyone" + content = f"<@&{Roles.moderators}> @here" # Truncate content to 2000 characters and append an ellipsis. if content and len(content) > 2000: diff --git a/config-default.yml b/config-default.yml index 8c6e18470..6eb954cd5 100644 --- a/config-default.yml +++ b/config-default.yml @@ -260,7 +260,8 @@ guild: devops: 409416496733880320 domain_leads: 807415650778742785 helpers: &HELPERS_ROLE 267630620367257601 - moderators: &MODS_ROLE 267629731250176001 + moderators: &MODS_ROLE 831776746206265384 + mod_team: &MOD_TEAM_ROLE 267629731250176001 owners: &OWNERS_ROLE 267627879762755584 project_leads: 815701647526330398 @@ -274,12 +275,14 @@ guild: moderation_roles: - *ADMINS_ROLE - *MODS_ROLE + - *MOD_TEAM_ROLE - *OWNERS_ROLE staff_roles: - *ADMINS_ROLE - *HELPERS_ROLE - *MODS_ROLE + - *MOD_TEAM_ROLE - *OWNERS_ROLE webhooks: -- cgit v1.2.3 From 65df8e24874cda7b9525acde346199f66e59650f Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Thu, 15 Apr 2021 00:55:29 +0300 Subject: Remove extra newline Co-authored-by: ks129 <45097959+ks129@users.noreply.github.com> --- bot/exts/moderation/duty.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bot/exts/moderation/duty.py b/bot/exts/moderation/duty.py index 13be016f2..94eed9331 100644 --- a/bot/exts/moderation/duty.py +++ b/bot/exts/moderation/duty.py @@ -11,7 +11,6 @@ from bot.constants import Emojis, Guild, MODERATION_ROLES, Roles from bot.converters import Expiry from bot.utils.scheduling import Scheduler - log = logging.getLogger(__name__) -- cgit v1.2.3 From 38714aef8c5b71c5e8313a82bef18947f1f1395a Mon Sep 17 00:00:00 2001 From: mbaruh Date: Thu, 15 Apr 2021 01:00:31 +0300 Subject: Fix setup docstring to specify correct cog --- bot/exts/moderation/duty.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/moderation/duty.py b/bot/exts/moderation/duty.py index 94eed9331..3f34e366c 100644 --- a/bot/exts/moderation/duty.py +++ b/bot/exts/moderation/duty.py @@ -130,5 +130,5 @@ class Duty(Cog): def setup(bot: Bot) -> None: - """Load the Slowmode cog.""" + """Load the Duty cog.""" bot.add_cog(Duty(bot)) -- cgit v1.2.3 From 6c00f74c8dcd2f3f1aaa4eff89e72cc135b75357 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Thu, 15 Apr 2021 01:04:16 +0300 Subject: Add off-duty expiration date to audit log --- bot/exts/moderation/duty.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/moderation/duty.py b/bot/exts/moderation/duty.py index 3f34e366c..265261be8 100644 --- a/bot/exts/moderation/duty.py +++ b/bot/exts/moderation/duty.py @@ -94,7 +94,8 @@ class Duty(Cog): mod = ctx.author - await mod.remove_roles(self.moderators_role, reason="Entered off-duty period.") + until_date = duration.replace(microsecond=0).isoformat() + await mod.remove_roles(self.moderators_role, reason=f"Entered off-duty period until {until_date}.") await self.off_duty_mods.update({mod.id: duration.isoformat()}) @@ -102,7 +103,6 @@ class Duty(Cog): self._role_scheduler.cancel(mod.id) self._role_scheduler.schedule_at(duration, mod.id, self.reapply_role(mod)) - until_date = duration.replace(microsecond=0).isoformat() await ctx.send(f"{Emojis.check_mark} Moderators role has been removed until {until_date}.") @duty_group.command(name='on') -- cgit v1.2.3 From b5fbca6f32c437aa45e28916451de39fb1485a75 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Thu, 15 Apr 2021 01:10:37 +0300 Subject: Use set instead of update in duty off --- bot/exts/moderation/duty.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/moderation/duty.py b/bot/exts/moderation/duty.py index 265261be8..0b07510db 100644 --- a/bot/exts/moderation/duty.py +++ b/bot/exts/moderation/duty.py @@ -97,7 +97,7 @@ class Duty(Cog): until_date = duration.replace(microsecond=0).isoformat() await mod.remove_roles(self.moderators_role, reason=f"Entered off-duty period until {until_date}.") - await self.off_duty_mods.update({mod.id: duration.isoformat()}) + await self.off_duty_mods.set(mod.id, duration.isoformat()) if mod.id in self._role_scheduler: self._role_scheduler.cancel(mod.id) -- cgit v1.2.3 From f80303718eed9bc676fe2e3e3fc06cffffbf1a92 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Thu, 15 Apr 2021 22:10:26 +0200 Subject: Make trace logging optional and allow selective enabling Because coloredlogs' install changes the level of the root handler, the setLevel call had to be moved to after the install. --- bot/constants.py | 1 + bot/log.py | 20 ++++++++++++++------ config-default.yml | 7 ++++--- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/bot/constants.py b/bot/constants.py index 6d14bbb3a..14400700f 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -199,6 +199,7 @@ class Bot(metaclass=YAMLGetter): prefix: str sentry_dsn: Optional[str] token: str + trace_loggers: Optional[str] class Redis(metaclass=YAMLGetter): diff --git a/bot/log.py b/bot/log.py index e92233a33..339ed63a7 100644 --- a/bot/log.py +++ b/bot/log.py @@ -20,7 +20,6 @@ def setup() -> None: logging.addLevelName(TRACE_LEVEL, "TRACE") Logger.trace = _monkeypatch_trace - log_level = TRACE_LEVEL if constants.DEBUG_MODE else logging.INFO format_string = "%(asctime)s | %(name)s | %(levelname)s | %(message)s" log_format = logging.Formatter(format_string) @@ -30,7 +29,6 @@ def setup() -> None: file_handler.setFormatter(log_format) root_log = logging.getLogger() - root_log.setLevel(log_level) root_log.addHandler(file_handler) if "COLOREDLOGS_LEVEL_STYLES" not in os.environ: @@ -44,11 +42,9 @@ def setup() -> None: if "COLOREDLOGS_LOG_FORMAT" not in os.environ: coloredlogs.DEFAULT_LOG_FORMAT = format_string - if "COLOREDLOGS_LOG_LEVEL" not in os.environ: - coloredlogs.DEFAULT_LOG_LEVEL = log_level - - coloredlogs.install(logger=root_log, stream=sys.stdout) + coloredlogs.install(level=logging.TRACE, logger=root_log, stream=sys.stdout) + root_log.setLevel(logging.DEBUG if constants.DEBUG_MODE else logging.INFO) logging.getLogger("discord").setLevel(logging.WARNING) logging.getLogger("websockets").setLevel(logging.WARNING) logging.getLogger("chardet").setLevel(logging.WARNING) @@ -57,6 +53,8 @@ def setup() -> None: # Set back to the default of INFO even if asyncio's debug mode is enabled. logging.getLogger("asyncio").setLevel(logging.INFO) + _set_trace_loggers() + def setup_sentry() -> None: """Set up the Sentry logging integrations.""" @@ -86,3 +84,13 @@ def _monkeypatch_trace(self: logging.Logger, msg: str, *args, **kwargs) -> None: """ if self.isEnabledFor(TRACE_LEVEL): self._log(TRACE_LEVEL, msg, args, **kwargs) + + +def _set_trace_loggers() -> None: + """Set loggers to the trace level according to the value from the BOT_TRACE_LOGGERS env var.""" + if constants.Bot.trace_loggers: + if constants.Bot.trace_loggers in {"*", "ROOT"}: + logging.getLogger().setLevel(logging.TRACE) + else: + for logger_name in constants.Bot.trace_loggers.split(","): + logging.getLogger(logger_name).setLevel(logging.TRACE) diff --git a/config-default.yml b/config-default.yml index 8c6e18470..b9786925d 100644 --- a/config-default.yml +++ b/config-default.yml @@ -1,7 +1,8 @@ bot: - prefix: "!" - sentry_dsn: !ENV "BOT_SENTRY_DSN" - token: !ENV "BOT_TOKEN" + prefix: "!" + sentry_dsn: !ENV "BOT_SENTRY_DSN" + token: !ENV "BOT_TOKEN" + trace_loggers: !ENV "BOT_TRACE_LOGGERS" clean: # Maximum number of messages to traverse for clean commands -- cgit v1.2.3 From f11ebfde17634eed7fa242f72b309c4a75c885cd Mon Sep 17 00:00:00 2001 From: mbaruh Date: Sat, 17 Apr 2021 01:15:38 +0300 Subject: Keep config succint A moderator is expected to have the mod-team role and therefore it's enough to specify the latter in the mod and staff roles. --- config-default.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/config-default.yml b/config-default.yml index 6eb954cd5..b19164d3f 100644 --- a/config-default.yml +++ b/config-default.yml @@ -274,14 +274,12 @@ guild: moderation_roles: - *ADMINS_ROLE - - *MODS_ROLE - *MOD_TEAM_ROLE - *OWNERS_ROLE staff_roles: - *ADMINS_ROLE - *HELPERS_ROLE - - *MODS_ROLE - *MOD_TEAM_ROLE - *OWNERS_ROLE -- cgit v1.2.3 From 2053b2e36ece02680ed85b970c4fbf687fe07e0f Mon Sep 17 00:00:00 2001 From: mbaruh Date: Sat, 17 Apr 2021 01:19:54 +0300 Subject: Assume a scheduled task exists for `duty on` The lack of such a task may be indicative of a bug. --- bot/exts/moderation/duty.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bot/exts/moderation/duty.py b/bot/exts/moderation/duty.py index 0b07510db..8d0c96363 100644 --- a/bot/exts/moderation/duty.py +++ b/bot/exts/moderation/duty.py @@ -118,8 +118,7 @@ class Duty(Cog): await self.off_duty_mods.delete(mod.id) - if mod.id in self._role_scheduler: - self._role_scheduler.cancel(mod.id) + self._role_scheduler.cancel(mod.id) await ctx.send(f"{Emojis.check_mark} Moderators role has been re-applied.") -- cgit v1.2.3 From 5506fb74f90831e686f4636595f62e4bcc72a703 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Sat, 17 Apr 2021 01:24:17 +0300 Subject: Improve documentation --- bot/exts/moderation/duty.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bot/exts/moderation/duty.py b/bot/exts/moderation/duty.py index 8d0c96363..eab0fd99f 100644 --- a/bot/exts/moderation/duty.py +++ b/bot/exts/moderation/duty.py @@ -94,11 +94,12 @@ class Duty(Cog): mod = ctx.author - until_date = duration.replace(microsecond=0).isoformat() + until_date = duration.replace(microsecond=0).isoformat() # Looks noisy with microseconds. await mod.remove_roles(self.moderators_role, reason=f"Entered off-duty period until {until_date}.") await self.off_duty_mods.set(mod.id, duration.isoformat()) + # Allow rescheduling the task without cancelling it separately via the `on` command. if mod.id in self._role_scheduler: self._role_scheduler.cancel(mod.id) self._role_scheduler.schedule_at(duration, mod.id, self.reapply_role(mod)) @@ -118,6 +119,7 @@ class Duty(Cog): await self.off_duty_mods.delete(mod.id) + # We assume the task exists. Lack of it may indicate a bug. self._role_scheduler.cancel(mod.id) await ctx.send(f"{Emojis.check_mark} Moderators role has been re-applied.") -- cgit v1.2.3 From 4a051cdb016748daca724e95957bd011cc3f6c3f Mon Sep 17 00:00:00 2001 From: mbaruh Date: Sat, 17 Apr 2021 01:43:17 +0300 Subject: Name the rescheduling task, and cancel it on cog unload --- bot/exts/moderation/duty.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bot/exts/moderation/duty.py b/bot/exts/moderation/duty.py index eab0fd99f..e05472448 100644 --- a/bot/exts/moderation/duty.py +++ b/bot/exts/moderation/duty.py @@ -29,7 +29,7 @@ class Duty(Cog): self.guild = None self.moderators_role = None - self.bot.loop.create_task(self.reschedule_roles()) + self.reschedule_task = self.bot.loop.create_task(self.reschedule_roles(), name="duty-reschedule") async def reschedule_roles(self) -> None: """Reschedule moderators role re-apply times.""" @@ -127,6 +127,7 @@ class Duty(Cog): def cog_unload(self) -> None: """Cancel role tasks when the cog unloads.""" log.trace("Cog unload: canceling role tasks.") + self.reschedule_task.cancel() self._role_scheduler.cancel_all() -- cgit v1.2.3 From d2d939c96de22ae174072dd8cc2bad2fe4f2174a Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Sat, 17 Apr 2021 13:19:08 +0300 Subject: Remove here ping Kinda defeats the purpose of being off-duty. --- bot/exts/moderation/modlog.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/moderation/modlog.py b/bot/exts/moderation/modlog.py index f68a1880e..5e8ea595b 100644 --- a/bot/exts/moderation/modlog.py +++ b/bot/exts/moderation/modlog.py @@ -115,9 +115,9 @@ class ModLog(Cog, name="ModLog"): if ping_everyone: if content: - content = f"<@&{Roles.moderators}> @here\n{content}" + content = f"<@&{Roles.moderators}>\n{content}" else: - content = f"<@&{Roles.moderators}> @here" + content = f"<@&{Roles.moderators}>" # Truncate content to 2000 characters and append an ellipsis. if content and len(content) > 2000: -- cgit v1.2.3 From 0e4fd3d2d0ae4b0f403cc8f163c783284aefae56 Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Sat, 17 Apr 2021 18:37:44 +0200 Subject: Make YAMLGetter raise AttributeError instead of KeyError Utility functions such as hasattr or getattr except __getattribute__ to raise AttributeError not KeyError. This commit also lowers the logging level of the error message to info since it is up to the caller to decide if this is an expected failure or not. --- bot/constants.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/bot/constants.py b/bot/constants.py index dc9cd4dfb..3254c2761 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -175,13 +175,14 @@ class YAMLGetter(type): if cls.subsection is not None: return _CONFIG_YAML[cls.section][cls.subsection][name] return _CONFIG_YAML[cls.section][name] - except KeyError: + except KeyError as e: dotted_path = '.'.join( (cls.section, cls.subsection, name) if cls.subsection is not None else (cls.section, name) ) - log.critical(f"Tried accessing configuration variable at `{dotted_path}`, but it could not be found.") - raise + # Only an INFO log since this can be caught through `hasattr` or `getattr`. + log.info(f"Tried accessing configuration variable at `{dotted_path}`, but it could not be found.") + raise AttributeError(repr(name)) from e def __getitem__(cls, name): return cls.__getattr__(name) -- cgit v1.2.3 From c910427937760f50fe7df3851989170c3494cde2 Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Sat, 17 Apr 2021 18:38:09 +0200 Subject: Move the verified developer badge to the embed title --- bot/constants.py | 2 +- bot/exts/info/information.py | 4 +++- config-default.yml | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/bot/constants.py b/bot/constants.py index 3254c2761..813f970cd 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -280,7 +280,7 @@ class Emojis(metaclass=YAMLGetter): badge_partner: str badge_staff: str badge_verified_bot_developer: str - badge_verified_bot: str + verified_bot: str bot: str defcon_shutdown: str # noqa: E704 diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index 226e4992e..834fee1b4 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -230,7 +230,9 @@ class Information(Cog): if on_server and user.nick: name = f"{user.nick} ({name})" - if user.bot: + if user.public_flags.verified_bot: + name += f" {constants.Emojis.verified_bot}" + elif user.bot: name += f" {constants.Emojis.bot}" badges = [] diff --git a/config-default.yml b/config-default.yml index dba354117..b6955c63c 100644 --- a/config-default.yml +++ b/config-default.yml @@ -46,8 +46,8 @@ style: badge_partner: "<:partner:748666453242413136>" badge_staff: "<:discord_staff:743882896498098226>" badge_verified_bot_developer: "<:verified_bot_dev:743882897299210310>" - badge_verified_bot: "<:verified_bot:811645219220750347>" bot: "<:bot:812712599464443914>" + verified_bot: "<:verified_bot:811645219220750347>" defcon_shutdown: "<:defcondisabled:470326273952972810>" defcon_unshutdown: "<:defconenabled:470326274213150730>" -- cgit v1.2.3 From 93c9e536a3e771db2ac03054a5c2470883d59f1f Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Sat, 17 Apr 2021 18:52:19 +0200 Subject: Tests: members shouldn't have any public flags --- tests/bot/exts/info/test_information.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/bot/exts/info/test_information.py b/tests/bot/exts/info/test_information.py index a996ce477..d2ecee033 100644 --- a/tests/bot/exts/info/test_information.py +++ b/tests/bot/exts/info/test_information.py @@ -281,9 +281,13 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase): """The embed should use the string representation of the user if they don't have a nick.""" ctx = helpers.MockContext(channel=helpers.MockTextChannel(id=1)) user = helpers.MockMember() + public_flags = unittest.mock.MagicMock() + public_flags.__iter__.return_value = iter(()) + public_flags.verified_bot = False user.nick = None user.__str__ = unittest.mock.Mock(return_value="Mr. Hemlock") user.colour = 0 + user.public_flags = public_flags embed = await self.cog.create_user_embed(ctx, user) @@ -297,9 +301,13 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase): """The embed should use the nick if it's available.""" ctx = helpers.MockContext(channel=helpers.MockTextChannel(id=1)) user = helpers.MockMember() + public_flags = unittest.mock.MagicMock() + public_flags.__iter__.return_value = iter(()) + public_flags.verified_bot = False user.nick = "Cat lover" user.__str__ = unittest.mock.Mock(return_value="Mr. Hemlock") user.colour = 0 + user.public_flags = public_flags embed = await self.cog.create_user_embed(ctx, user) -- cgit v1.2.3 From 17770021be89e82c0e3edf1d01a6e10775fd871a Mon Sep 17 00:00:00 2001 From: Andi Qu <31325319+dolphingarlic@users.noreply.github.com> Date: Sat, 17 Apr 2021 19:02:20 +0200 Subject: Sort snippet matches by their start position --- bot/exts/info/code_snippets.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/bot/exts/info/code_snippets.py b/bot/exts/info/code_snippets.py index f0cd54c0c..b9e7cc3d0 100644 --- a/bot/exts/info/code_snippets.py +++ b/bot/exts/info/code_snippets.py @@ -205,9 +205,9 @@ class CodeSnippets(Cog): ret = f'`{file_path}` lines {start_line} to {end_line}\n' if len(required) != 0: - return f'{ret}```{language}\n{required}```\n' + return f'{ret}```{language}\n{required}```' # Returns an empty codeblock if the snippet is empty - return f'{ret}``` ```\n' + return f'{ret}``` ```' def __init__(self, bot: Bot): """Initializes the cog's bot.""" @@ -224,13 +224,18 @@ class CodeSnippets(Cog): async def on_message(self, message: Message) -> None: """Checks if the message has a snippet link, removes the embed, then sends the snippet contents.""" if not message.author.bot: - message_to_send = '' + all_snippets = [] for pattern, handler in self.pattern_handlers: for match in pattern.finditer(message.content): - message_to_send += await handler(**match.groupdict()) + snippet = await handler(**match.groupdict()) + all_snippets.append((match.start(), snippet)) - if 0 < len(message_to_send) <= 2000 and message_to_send.count('\n') <= 15: + # Sorts the list of snippets by their match index and joins them into + # a single message + message_to_send = '\n'.join(map(lambda x: x[1], sorted(all_snippets))) + + if 0 < len(message_to_send) <= 2000 and len(all_snippets) <= 15: await message.edit(suppress=True) await wait_for_deletion( await message.channel.send(message_to_send), -- cgit v1.2.3 From 94af3c07678f1f2dee722f4780a816426efd0851 Mon Sep 17 00:00:00 2001 From: Vivaan Verma Date: Sun, 18 Apr 2021 21:12:08 +0100 Subject: Added default duration of 1h to superstarify --- bot/exts/moderation/infraction/superstarify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/moderation/infraction/superstarify.py b/bot/exts/moderation/infraction/superstarify.py index 704dddf9c..245f14905 100644 --- a/bot/exts/moderation/infraction/superstarify.py +++ b/bot/exts/moderation/infraction/superstarify.py @@ -109,7 +109,7 @@ class Superstarify(InfractionScheduler, Cog): self, ctx: Context, member: Member, - duration: Expiry, + duration: Expiry = "1h", *, reason: str = '', ) -> None: -- cgit v1.2.3 From 3126e00a28e498afc8ecef1ed87b356f0e4a38c4 Mon Sep 17 00:00:00 2001 From: Vivaan Verma Date: Sun, 18 Apr 2021 22:11:46 +0100 Subject: Make duration an optional arg and default it to 1 hour --- bot/exts/moderation/infraction/superstarify.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bot/exts/moderation/infraction/superstarify.py b/bot/exts/moderation/infraction/superstarify.py index 245f14905..8a6d14d41 100644 --- a/bot/exts/moderation/infraction/superstarify.py +++ b/bot/exts/moderation/infraction/superstarify.py @@ -1,3 +1,4 @@ +import datetime import json import logging import random @@ -109,7 +110,7 @@ class Superstarify(InfractionScheduler, Cog): self, ctx: Context, member: Member, - duration: Expiry = "1h", + duration: t.Optional[Expiry], *, reason: str = '', ) -> None: @@ -134,6 +135,9 @@ class Superstarify(InfractionScheduler, Cog): if await _utils.get_active_infraction(ctx, member, "superstar"): return + # Set the duration to 1 hour if none was provided + duration = datetime.datetime.now() + datetime.timedelta(hours=1) + # Post the infraction to the API old_nick = member.display_name infraction_reason = f'Old nickname: {old_nick}. {reason}' -- cgit v1.2.3 From 7fc5e37ecd2e1589b77b7fa16af26ee42e72dcdc Mon Sep 17 00:00:00 2001 From: Vivaan Verma Date: Sun, 18 Apr 2021 22:17:27 +0100 Subject: Check if a duration was provided --- bot/exts/moderation/infraction/superstarify.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bot/exts/moderation/infraction/superstarify.py b/bot/exts/moderation/infraction/superstarify.py index 8a6d14d41..f5d6259cd 100644 --- a/bot/exts/moderation/infraction/superstarify.py +++ b/bot/exts/moderation/infraction/superstarify.py @@ -136,8 +136,9 @@ class Superstarify(InfractionScheduler, Cog): return # Set the duration to 1 hour if none was provided - duration = datetime.datetime.now() + datetime.timedelta(hours=1) - + if not duration: + duration = datetime.datetime.now() + datetime.timedelta(hours=1) + # Post the infraction to the API old_nick = member.display_name infraction_reason = f'Old nickname: {old_nick}. {reason}' -- cgit v1.2.3 From 6169ed2b73a5f2d763a2758e69ba4983127a1373 Mon Sep 17 00:00:00 2001 From: Vivaan Verma <54081925+doublevcodes@users.noreply.github.com> Date: Sun, 18 Apr 2021 22:31:40 +0100 Subject: Fix linting errors --- bot/exts/moderation/infraction/superstarify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/moderation/infraction/superstarify.py b/bot/exts/moderation/infraction/superstarify.py index f5d6259cd..6fa0d550f 100644 --- a/bot/exts/moderation/infraction/superstarify.py +++ b/bot/exts/moderation/infraction/superstarify.py @@ -138,7 +138,7 @@ class Superstarify(InfractionScheduler, Cog): # Set the duration to 1 hour if none was provided if not duration: duration = datetime.datetime.now() + datetime.timedelta(hours=1) - + # Post the infraction to the API old_nick = member.display_name infraction_reason = f'Old nickname: {old_nick}. {reason}' -- cgit v1.2.3 From bd54449e8994c38b2fd073056f82e6c52785d4c6 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Mon, 19 Apr 2021 15:43:33 +0300 Subject: Renamed Duty cog to Modpings The renaming includes the commands inside it. --- bot/exts/moderation/duty.py | 46 ++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/bot/exts/moderation/duty.py b/bot/exts/moderation/duty.py index e05472448..c351db615 100644 --- a/bot/exts/moderation/duty.py +++ b/bot/exts/moderation/duty.py @@ -14,13 +14,13 @@ from bot.utils.scheduling import Scheduler log = logging.getLogger(__name__) -class Duty(Cog): - """Commands for a moderator to go on and off duty.""" +class Modpings(Cog): + """Commands for a moderator to turn moderator pings on and off.""" # RedisCache[str, str] - # The cache's keys are mods who are off-duty. + # The cache's keys are mods who have pings off. # The cache's values are the times when the role should be re-applied to them, stored in ISO format. - off_duty_mods = RedisCache() + pings_off_mods = RedisCache() def __init__(self, bot: Bot): self.bot = bot @@ -29,7 +29,7 @@ class Duty(Cog): self.guild = None self.moderators_role = None - self.reschedule_task = self.bot.loop.create_task(self.reschedule_roles(), name="duty-reschedule") + self.reschedule_task = self.bot.loop.create_task(self.reschedule_roles(), name="mod-pings-reschedule") async def reschedule_roles(self) -> None: """Reschedule moderators role re-apply times.""" @@ -38,35 +38,35 @@ class Duty(Cog): self.moderators_role = self.guild.get_role(Roles.moderators) mod_team = self.guild.get_role(Roles.mod_team) - on_duty = self.moderators_role.members - off_duty = await self.off_duty_mods.to_dict() + pings_on = self.moderators_role.members + pings_off = await self.pings_off_mods.to_dict() log.trace("Applying the moderators role to the mod team where necessary.") for mod in mod_team.members: - if mod in on_duty: # Make sure that on-duty mods aren't in the cache. - if mod in off_duty: - await self.off_duty_mods.delete(mod.id) + if mod in pings_on: # Make sure that on-duty mods aren't in the cache. + if mod in pings_off: + await self.pings_off_mods.delete(mod.id) continue # Keep the role off only for those in the cache. - if mod.id not in off_duty: + if mod.id not in pings_off: await self.reapply_role(mod) else: - expiry = isoparse(off_duty[mod.id]).replace(tzinfo=None) + expiry = isoparse(pings_off[mod.id]).replace(tzinfo=None) self._role_scheduler.schedule_at(expiry, mod.id, self.reapply_role(mod)) async def reapply_role(self, mod: Member) -> None: """Reapply the moderator's role to the given moderator.""" log.trace(f"Re-applying role to mod with ID {mod.id}.") - await mod.add_roles(self.moderators_role, reason="Off-duty period expired.") + await mod.add_roles(self.moderators_role, reason="Pings off period expired.") - @group(name='duty', invoke_without_command=True) + @group(name='modpings', aliases=('modping',), invoke_without_command=True) @has_any_role(*MODERATION_ROLES) - async def duty_group(self, ctx: Context) -> None: + async def modpings_group(self, ctx: Context) -> None: """Allow the removal and re-addition of the pingable moderators role.""" await ctx.send_help(ctx.command) - @duty_group.command(name='off') + @modpings_group.command(name='off') @has_any_role(*MODERATION_ROLES) async def off_command(self, ctx: Context, duration: Expiry) -> None: """ @@ -95,9 +95,9 @@ class Duty(Cog): mod = ctx.author until_date = duration.replace(microsecond=0).isoformat() # Looks noisy with microseconds. - await mod.remove_roles(self.moderators_role, reason=f"Entered off-duty period until {until_date}.") + await mod.remove_roles(self.moderators_role, reason=f"Turned pings off until {until_date}.") - await self.off_duty_mods.set(mod.id, duration.isoformat()) + await self.pings_off_mods.set(mod.id, duration.isoformat()) # Allow rescheduling the task without cancelling it separately via the `on` command. if mod.id in self._role_scheduler: @@ -106,7 +106,7 @@ class Duty(Cog): await ctx.send(f"{Emojis.check_mark} Moderators role has been removed until {until_date}.") - @duty_group.command(name='on') + @modpings_group.command(name='on') @has_any_role(*MODERATION_ROLES) async def on_command(self, ctx: Context) -> None: """Re-apply the pingable moderators role.""" @@ -115,9 +115,9 @@ class Duty(Cog): await ctx.send(":question: You already have the role.") return - await mod.add_roles(self.moderators_role, reason="Off-duty period canceled.") + await mod.add_roles(self.moderators_role, reason="Pings off period canceled.") - await self.off_duty_mods.delete(mod.id) + await self.pings_off_mods.delete(mod.id) # We assume the task exists. Lack of it may indicate a bug. self._role_scheduler.cancel(mod.id) @@ -132,5 +132,5 @@ class Duty(Cog): def setup(bot: Bot) -> None: - """Load the Duty cog.""" - bot.add_cog(Duty(bot)) + """Load the Modpings cog.""" + bot.add_cog(Modpings(bot)) -- cgit v1.2.3 From e30667fb4e23648c3f308bfc06cf643852d0c29c Mon Sep 17 00:00:00 2001 From: mbaruh Date: Mon, 19 Apr 2021 15:44:58 +0300 Subject: Renamed duty.py to modpings.py --- bot/exts/moderation/duty.py | 136 ---------------------------------------- bot/exts/moderation/modpings.py | 136 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+), 136 deletions(-) delete mode 100644 bot/exts/moderation/duty.py create mode 100644 bot/exts/moderation/modpings.py diff --git a/bot/exts/moderation/duty.py b/bot/exts/moderation/duty.py deleted file mode 100644 index c351db615..000000000 --- a/bot/exts/moderation/duty.py +++ /dev/null @@ -1,136 +0,0 @@ -import datetime -import logging - -from async_rediscache import RedisCache -from dateutil.parser import isoparse -from discord import Member -from discord.ext.commands import Cog, Context, group, has_any_role - -from bot.bot import Bot -from bot.constants import Emojis, Guild, MODERATION_ROLES, Roles -from bot.converters import Expiry -from bot.utils.scheduling import Scheduler - -log = logging.getLogger(__name__) - - -class Modpings(Cog): - """Commands for a moderator to turn moderator pings on and off.""" - - # RedisCache[str, str] - # The cache's keys are mods who have pings off. - # The cache's values are the times when the role should be re-applied to them, stored in ISO format. - pings_off_mods = RedisCache() - - def __init__(self, bot: Bot): - self.bot = bot - self._role_scheduler = Scheduler(self.__class__.__name__) - - self.guild = None - self.moderators_role = None - - self.reschedule_task = self.bot.loop.create_task(self.reschedule_roles(), name="mod-pings-reschedule") - - async def reschedule_roles(self) -> None: - """Reschedule moderators role re-apply times.""" - await self.bot.wait_until_guild_available() - self.guild = self.bot.get_guild(Guild.id) - self.moderators_role = self.guild.get_role(Roles.moderators) - - mod_team = self.guild.get_role(Roles.mod_team) - pings_on = self.moderators_role.members - pings_off = await self.pings_off_mods.to_dict() - - log.trace("Applying the moderators role to the mod team where necessary.") - for mod in mod_team.members: - if mod in pings_on: # Make sure that on-duty mods aren't in the cache. - if mod in pings_off: - await self.pings_off_mods.delete(mod.id) - continue - - # Keep the role off only for those in the cache. - if mod.id not in pings_off: - await self.reapply_role(mod) - else: - expiry = isoparse(pings_off[mod.id]).replace(tzinfo=None) - self._role_scheduler.schedule_at(expiry, mod.id, self.reapply_role(mod)) - - async def reapply_role(self, mod: Member) -> None: - """Reapply the moderator's role to the given moderator.""" - log.trace(f"Re-applying role to mod with ID {mod.id}.") - await mod.add_roles(self.moderators_role, reason="Pings off period expired.") - - @group(name='modpings', aliases=('modping',), invoke_without_command=True) - @has_any_role(*MODERATION_ROLES) - async def modpings_group(self, ctx: Context) -> None: - """Allow the removal and re-addition of the pingable moderators role.""" - await ctx.send_help(ctx.command) - - @modpings_group.command(name='off') - @has_any_role(*MODERATION_ROLES) - async def off_command(self, ctx: Context, duration: Expiry) -> None: - """ - Temporarily removes the pingable moderators role for a set amount of time. - - A unit of time should be appended to the duration. - Units (∗case-sensitive): - \u2003`y` - years - \u2003`m` - months∗ - \u2003`w` - weeks - \u2003`d` - days - \u2003`h` - hours - \u2003`M` - minutes∗ - \u2003`s` - seconds - - Alternatively, an ISO 8601 timestamp can be provided for the duration. - - The duration cannot be longer than 30 days. - """ - duration: datetime.datetime - delta = duration - datetime.datetime.utcnow() - if delta > datetime.timedelta(days=30): - await ctx.send(":x: Cannot remove the role for longer than 30 days.") - return - - mod = ctx.author - - until_date = duration.replace(microsecond=0).isoformat() # Looks noisy with microseconds. - await mod.remove_roles(self.moderators_role, reason=f"Turned pings off until {until_date}.") - - await self.pings_off_mods.set(mod.id, duration.isoformat()) - - # Allow rescheduling the task without cancelling it separately via the `on` command. - if mod.id in self._role_scheduler: - self._role_scheduler.cancel(mod.id) - self._role_scheduler.schedule_at(duration, mod.id, self.reapply_role(mod)) - - await ctx.send(f"{Emojis.check_mark} Moderators role has been removed until {until_date}.") - - @modpings_group.command(name='on') - @has_any_role(*MODERATION_ROLES) - async def on_command(self, ctx: Context) -> None: - """Re-apply the pingable moderators role.""" - mod = ctx.author - if mod in self.moderators_role.members: - await ctx.send(":question: You already have the role.") - return - - await mod.add_roles(self.moderators_role, reason="Pings off period canceled.") - - await self.pings_off_mods.delete(mod.id) - - # We assume the task exists. Lack of it may indicate a bug. - self._role_scheduler.cancel(mod.id) - - await ctx.send(f"{Emojis.check_mark} Moderators role has been re-applied.") - - def cog_unload(self) -> None: - """Cancel role tasks when the cog unloads.""" - log.trace("Cog unload: canceling role tasks.") - self.reschedule_task.cancel() - self._role_scheduler.cancel_all() - - -def setup(bot: Bot) -> None: - """Load the Modpings cog.""" - bot.add_cog(Modpings(bot)) diff --git a/bot/exts/moderation/modpings.py b/bot/exts/moderation/modpings.py new file mode 100644 index 000000000..c351db615 --- /dev/null +++ b/bot/exts/moderation/modpings.py @@ -0,0 +1,136 @@ +import datetime +import logging + +from async_rediscache import RedisCache +from dateutil.parser import isoparse +from discord import Member +from discord.ext.commands import Cog, Context, group, has_any_role + +from bot.bot import Bot +from bot.constants import Emojis, Guild, MODERATION_ROLES, Roles +from bot.converters import Expiry +from bot.utils.scheduling import Scheduler + +log = logging.getLogger(__name__) + + +class Modpings(Cog): + """Commands for a moderator to turn moderator pings on and off.""" + + # RedisCache[str, str] + # The cache's keys are mods who have pings off. + # The cache's values are the times when the role should be re-applied to them, stored in ISO format. + pings_off_mods = RedisCache() + + def __init__(self, bot: Bot): + self.bot = bot + self._role_scheduler = Scheduler(self.__class__.__name__) + + self.guild = None + self.moderators_role = None + + self.reschedule_task = self.bot.loop.create_task(self.reschedule_roles(), name="mod-pings-reschedule") + + async def reschedule_roles(self) -> None: + """Reschedule moderators role re-apply times.""" + await self.bot.wait_until_guild_available() + self.guild = self.bot.get_guild(Guild.id) + self.moderators_role = self.guild.get_role(Roles.moderators) + + mod_team = self.guild.get_role(Roles.mod_team) + pings_on = self.moderators_role.members + pings_off = await self.pings_off_mods.to_dict() + + log.trace("Applying the moderators role to the mod team where necessary.") + for mod in mod_team.members: + if mod in pings_on: # Make sure that on-duty mods aren't in the cache. + if mod in pings_off: + await self.pings_off_mods.delete(mod.id) + continue + + # Keep the role off only for those in the cache. + if mod.id not in pings_off: + await self.reapply_role(mod) + else: + expiry = isoparse(pings_off[mod.id]).replace(tzinfo=None) + self._role_scheduler.schedule_at(expiry, mod.id, self.reapply_role(mod)) + + async def reapply_role(self, mod: Member) -> None: + """Reapply the moderator's role to the given moderator.""" + log.trace(f"Re-applying role to mod with ID {mod.id}.") + await mod.add_roles(self.moderators_role, reason="Pings off period expired.") + + @group(name='modpings', aliases=('modping',), invoke_without_command=True) + @has_any_role(*MODERATION_ROLES) + async def modpings_group(self, ctx: Context) -> None: + """Allow the removal and re-addition of the pingable moderators role.""" + await ctx.send_help(ctx.command) + + @modpings_group.command(name='off') + @has_any_role(*MODERATION_ROLES) + async def off_command(self, ctx: Context, duration: Expiry) -> None: + """ + Temporarily removes the pingable moderators role for a set amount of time. + + A unit of time should be appended to the duration. + Units (∗case-sensitive): + \u2003`y` - years + \u2003`m` - months∗ + \u2003`w` - weeks + \u2003`d` - days + \u2003`h` - hours + \u2003`M` - minutes∗ + \u2003`s` - seconds + + Alternatively, an ISO 8601 timestamp can be provided for the duration. + + The duration cannot be longer than 30 days. + """ + duration: datetime.datetime + delta = duration - datetime.datetime.utcnow() + if delta > datetime.timedelta(days=30): + await ctx.send(":x: Cannot remove the role for longer than 30 days.") + return + + mod = ctx.author + + until_date = duration.replace(microsecond=0).isoformat() # Looks noisy with microseconds. + await mod.remove_roles(self.moderators_role, reason=f"Turned pings off until {until_date}.") + + await self.pings_off_mods.set(mod.id, duration.isoformat()) + + # Allow rescheduling the task without cancelling it separately via the `on` command. + if mod.id in self._role_scheduler: + self._role_scheduler.cancel(mod.id) + self._role_scheduler.schedule_at(duration, mod.id, self.reapply_role(mod)) + + await ctx.send(f"{Emojis.check_mark} Moderators role has been removed until {until_date}.") + + @modpings_group.command(name='on') + @has_any_role(*MODERATION_ROLES) + async def on_command(self, ctx: Context) -> None: + """Re-apply the pingable moderators role.""" + mod = ctx.author + if mod in self.moderators_role.members: + await ctx.send(":question: You already have the role.") + return + + await mod.add_roles(self.moderators_role, reason="Pings off period canceled.") + + await self.pings_off_mods.delete(mod.id) + + # We assume the task exists. Lack of it may indicate a bug. + self._role_scheduler.cancel(mod.id) + + await ctx.send(f"{Emojis.check_mark} Moderators role has been re-applied.") + + def cog_unload(self) -> None: + """Cancel role tasks when the cog unloads.""" + log.trace("Cog unload: canceling role tasks.") + self.reschedule_task.cancel() + self._role_scheduler.cancel_all() + + +def setup(bot: Bot) -> None: + """Load the Modpings cog.""" + bot.add_cog(Modpings(bot)) -- cgit v1.2.3 From 0204f7cc73bcf803fe86ca45cbdca19432b83cb6 Mon Sep 17 00:00:00 2001 From: francisdbillones <57383750+francisdbillones@users.noreply.github.com> Date: Mon, 19 Apr 2021 21:42:40 +0800 Subject: Fix zen's negative indexing Negative indexing starts at -1, not 0, meaning lower bound should be -1 * len(zen_lines), not -1 * upper_bound. --- bot/exts/utils/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/utils/utils.py b/bot/exts/utils/utils.py index 8d9d27c64..4c39a7c2a 100644 --- a/bot/exts/utils/utils.py +++ b/bot/exts/utils/utils.py @@ -109,7 +109,7 @@ class Utils(Cog): # handle if it's an index int if isinstance(search_value, int): upper_bound = len(zen_lines) - 1 - lower_bound = -1 * upper_bound + lower_bound = -1 * len(zen_lines) if not (lower_bound <= search_value <= upper_bound): raise BadArgument(f"Please provide an index between {lower_bound} and {upper_bound}.") -- cgit v1.2.3 From 2ede01f32a49c3c1d4376b542789e770106711bc Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Mon, 19 Apr 2021 15:46:14 +0200 Subject: Add blacklist format to the BOT_TRACE_LOGGERS env var To mimic the same behaviour, setting all of the loggers to the trace level was changed to a "*" prefix without looking at other contents instead of setting it exactly to "ROOT" or "*" --- bot/log.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/bot/log.py b/bot/log.py index 339ed63a7..4e20c005e 100644 --- a/bot/log.py +++ b/bot/log.py @@ -87,10 +87,27 @@ def _monkeypatch_trace(self: logging.Logger, msg: str, *args, **kwargs) -> None: def _set_trace_loggers() -> None: - """Set loggers to the trace level according to the value from the BOT_TRACE_LOGGERS env var.""" - if constants.Bot.trace_loggers: - if constants.Bot.trace_loggers in {"*", "ROOT"}: + """ + Set loggers to the trace level according to the value from the BOT_TRACE_LOGGERS env var. + + When the env var is a list of logger names delimited by a comma, + each of the listed loggers will be set to the trace level. + + If this list is prefixed with a "!", all of the loggers except the listed ones will be set to the trace level. + + Otherwise if the env var begins with a "*", + the root logger is set to the trace level and other contents are ignored. + """ + level_filter = constants.Bot.trace_loggers + if level_filter: + if level_filter.startswith("*"): + logging.getLogger().setLevel(logging.TRACE) + + elif level_filter.startswith("!"): logging.getLogger().setLevel(logging.TRACE) + for logger_name in level_filter.strip("!,").split(","): + logging.getLogger(logger_name).setLevel(logging.DEBUG) + else: - for logger_name in constants.Bot.trace_loggers.split(","): + for logger_name in level_filter.strip(",").split(","): logging.getLogger(logger_name).setLevel(logging.TRACE) -- cgit v1.2.3 From a7581a4f9f2724672eebfdf541a922973c018c23 Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Mon, 19 Apr 2021 20:48:26 +0300 Subject: CamelCase the cog name --- bot/exts/moderation/modpings.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bot/exts/moderation/modpings.py b/bot/exts/moderation/modpings.py index c351db615..690aa7c68 100644 --- a/bot/exts/moderation/modpings.py +++ b/bot/exts/moderation/modpings.py @@ -14,7 +14,7 @@ from bot.utils.scheduling import Scheduler log = logging.getLogger(__name__) -class Modpings(Cog): +class ModPings(Cog): """Commands for a moderator to turn moderator pings on and off.""" # RedisCache[str, str] @@ -132,5 +132,5 @@ class Modpings(Cog): def setup(bot: Bot) -> None: - """Load the Modpings cog.""" - bot.add_cog(Modpings(bot)) + """Load the ModPings cog.""" + bot.add_cog(ModPings(bot)) -- cgit v1.2.3 From b8b920bfa5c4d918d41bfe06d85b1e85f4bec0da Mon Sep 17 00:00:00 2001 From: Vivaan Verma <54081925+doublevcodes@users.noreply.github.com> Date: Mon, 19 Apr 2021 20:01:41 +0100 Subject: Inline duration assignment Co-authored-by: Rohan Reddy Alleti --- bot/exts/moderation/infraction/superstarify.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bot/exts/moderation/infraction/superstarify.py b/bot/exts/moderation/infraction/superstarify.py index 6fa0d550f..3d880dec3 100644 --- a/bot/exts/moderation/infraction/superstarify.py +++ b/bot/exts/moderation/infraction/superstarify.py @@ -136,8 +136,7 @@ class Superstarify(InfractionScheduler, Cog): return # Set the duration to 1 hour if none was provided - if not duration: - duration = datetime.datetime.now() + datetime.timedelta(hours=1) + duration = duration or datetime.datetime.utcnow() + datetime.timedelta(hours=1) # Post the infraction to the API old_nick = member.display_name -- cgit v1.2.3 From ae5d1cb65ddec0e70df00a4051a5bf813d4e6e20 Mon Sep 17 00:00:00 2001 From: Vivaan Verma Date: Mon, 19 Apr 2021 21:06:15 +0100 Subject: Add default duration as constant and use Duration converter --- bot/exts/moderation/infraction/superstarify.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bot/exts/moderation/infraction/superstarify.py b/bot/exts/moderation/infraction/superstarify.py index 3d880dec3..0bc2198c3 100644 --- a/bot/exts/moderation/infraction/superstarify.py +++ b/bot/exts/moderation/infraction/superstarify.py @@ -1,4 +1,3 @@ -import datetime import json import logging import random @@ -12,7 +11,7 @@ from discord.utils import escape_markdown from bot import constants from bot.bot import Bot -from bot.converters import Expiry +from bot.converters import Duration from bot.exts.moderation.infraction import _utils from bot.exts.moderation.infraction._scheduler import InfractionScheduler from bot.utils.messages import format_user @@ -20,6 +19,7 @@ from bot.utils.time import format_infraction log = logging.getLogger(__name__) NICKNAME_POLICY_URL = "https://pythondiscord.com/pages/rules/#nickname-policy" +SUPERSTARIFY_DEFAULT_DURATION = "1h" with Path("bot/resources/stars.json").open(encoding="utf-8") as stars_file: STAR_NAMES = json.load(stars_file) @@ -110,7 +110,7 @@ class Superstarify(InfractionScheduler, Cog): self, ctx: Context, member: Member, - duration: t.Optional[Expiry], + duration: t.Optional[Duration], *, reason: str = '', ) -> None: @@ -136,7 +136,7 @@ class Superstarify(InfractionScheduler, Cog): return # Set the duration to 1 hour if none was provided - duration = duration or datetime.datetime.utcnow() + datetime.timedelta(hours=1) + duration = duration or await Duration().convert(ctx, SUPERSTARIFY_DEFAULT_DURATION) # Post the infraction to the API old_nick = member.display_name -- cgit v1.2.3 From 03f909df6758a10c95f0b63df487f1acd97ec36d Mon Sep 17 00:00:00 2001 From: Vivaan Verma Date: Mon, 19 Apr 2021 21:15:11 +0100 Subject: Change type hint from duration to expiry --- bot/exts/moderation/infraction/superstarify.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/moderation/infraction/superstarify.py b/bot/exts/moderation/infraction/superstarify.py index 0bc2198c3..ef88fb43f 100644 --- a/bot/exts/moderation/infraction/superstarify.py +++ b/bot/exts/moderation/infraction/superstarify.py @@ -11,7 +11,7 @@ from discord.utils import escape_markdown from bot import constants from bot.bot import Bot -from bot.converters import Duration +from bot.converters import Duration, Expiry from bot.exts.moderation.infraction import _utils from bot.exts.moderation.infraction._scheduler import InfractionScheduler from bot.utils.messages import format_user @@ -110,7 +110,7 @@ class Superstarify(InfractionScheduler, Cog): self, ctx: Context, member: Member, - duration: t.Optional[Duration], + duration: t.Optional[Expiry], *, reason: str = '', ) -> None: -- cgit v1.2.3 From 91bdf9415ec88715fadf2e0a56b900b376b638db Mon Sep 17 00:00:00 2001 From: Vivaan Verma <54081925+doublevcodes@users.noreply.github.com> Date: Mon, 19 Apr 2021 22:02:45 +0100 Subject: Update bot/exts/moderation/infraction/superstarify.py Co-authored-by: Boris Muratov <8bee278@gmail.com> --- bot/exts/moderation/infraction/superstarify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/moderation/infraction/superstarify.py b/bot/exts/moderation/infraction/superstarify.py index ef88fb43f..07e79b9fe 100644 --- a/bot/exts/moderation/infraction/superstarify.py +++ b/bot/exts/moderation/infraction/superstarify.py @@ -135,7 +135,7 @@ class Superstarify(InfractionScheduler, Cog): if await _utils.get_active_infraction(ctx, member, "superstar"): return - # Set the duration to 1 hour if none was provided + # Set to default duration if none was provided. duration = duration or await Duration().convert(ctx, SUPERSTARIFY_DEFAULT_DURATION) # Post the infraction to the API -- cgit v1.2.3 From 9aa2b42aa04724a4ebc74d3ff6c339c33547dce3 Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Tue, 20 Apr 2021 17:20:44 +0200 Subject: Tests: AsyncMock is now in the standard library! The `tests/README.md` file still referenced our old custom `AsyncMock` that has been removed in favour of the standard library one that has been introduced in 3.8. This commit fixes this by updating the section. --- tests/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/README.md b/tests/README.md index 4f62edd68..092324123 100644 --- a/tests/README.md +++ b/tests/README.md @@ -114,7 +114,7 @@ class BotCogTests(unittest.TestCase): ### Mocking coroutines -By default, the `unittest.mock.Mock` and `unittest.mock.MagicMock` classes cannot mock coroutines, since the `__call__` method they provide is synchronous. In anticipation of the `AsyncMock` that will be [introduced in Python 3.8](https://docs.python.org/3.9/whatsnew/3.8.html#unittest), we have added an `AsyncMock` helper to [`helpers.py`](/tests/helpers.py). Do note that this drop-in replacement only implements an asynchronous `__call__` method, not the additional assertions that will come with the new `AsyncMock` type in Python 3.8. +By default, the `unittest.mock.Mock` and `unittest.mock.MagicMock` classes cannot mock coroutines, since the `__call__` method they provide is synchronous. The [`AsyncMock`](https://docs.python.org/3/library/unittest.mock.html#unittest.mock.AsyncMock) that has been [introduced in Python 3.8](https://docs.python.org/3.9/whatsnew/3.8.html#unittest) is an asynchronous version of `MagicMock` that can be used anywhere a coroutine is expected. ### Special mocks for some `discord.py` types -- cgit v1.2.3 From b12666dc4b75146b150c0812c5cb56f4317773ae Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Tue, 20 Apr 2021 18:48:12 +0300 Subject: Improve rediscache doc Co-authored-by: ChrisJL --- bot/exts/moderation/modpings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/moderation/modpings.py b/bot/exts/moderation/modpings.py index 690aa7c68..2f180e594 100644 --- a/bot/exts/moderation/modpings.py +++ b/bot/exts/moderation/modpings.py @@ -17,7 +17,7 @@ log = logging.getLogger(__name__) class ModPings(Cog): """Commands for a moderator to turn moderator pings on and off.""" - # RedisCache[str, str] + # RedisCache[discord.Member.id, 'Naïve ISO 8601 string'] # The cache's keys are mods who have pings off. # The cache's values are the times when the role should be re-applied to them, stored in ISO format. pings_off_mods = RedisCache() -- cgit v1.2.3 From 8a73d2b5e71444595b72155d7106c0fc48eeb027 Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Tue, 20 Apr 2021 19:14:10 +0300 Subject: Remove allowed mentions in modlog alert The modlog alert embed no longer pings everyone. --- bot/exts/moderation/modlog.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bot/exts/moderation/modlog.py b/bot/exts/moderation/modlog.py index 5e8ea595b..e92f76c9a 100644 --- a/bot/exts/moderation/modlog.py +++ b/bot/exts/moderation/modlog.py @@ -127,8 +127,7 @@ class ModLog(Cog, name="ModLog"): log_message = await channel.send( content=content, embed=embed, - files=files, - allowed_mentions=discord.AllowedMentions(everyone=True) + files=files ) if additional_embeds: -- cgit v1.2.3 From c20f84ff95671527e6fbacb04f07bcee3baaafcd Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Tue, 20 Apr 2021 17:54:44 +0100 Subject: Add the Moderators role to moderation_roles in config This allows mod alert pings to go through in #mod-alerts, the allowed mentions only included the mods team role which is not pinged on mod alerts. --- config-default.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/config-default.yml b/config-default.yml index b19164d3f..b7c446889 100644 --- a/config-default.yml +++ b/config-default.yml @@ -275,6 +275,7 @@ guild: moderation_roles: - *ADMINS_ROLE - *MOD_TEAM_ROLE + - *MODS_ROLE - *OWNERS_ROLE staff_roles: -- cgit v1.2.3 From 1a65e2a0505c719a77ccf9b0832f44ac035c4f1c Mon Sep 17 00:00:00 2001 From: ToxicKidz <78174417+ToxicKidz@users.noreply.github.com> Date: Tue, 20 Apr 2021 18:07:16 -0400 Subject: chore: Use Embed.timestamp for showing when the reminder will be sent --- bot/exts/utils/reminders.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/bot/exts/utils/reminders.py b/bot/exts/utils/reminders.py index 3113a1149..1d0832d9a 100644 --- a/bot/exts/utils/reminders.py +++ b/bot/exts/utils/reminders.py @@ -90,15 +90,18 @@ class Reminders(Cog): delivery_dt: t.Optional[datetime], ) -> None: """Send an embed confirming the reminder change was made successfully.""" - embed = discord.Embed() - embed.colour = discord.Colour.green() - embed.title = random.choice(POSITIVE_REPLIES) - embed.description = on_success + embed = discord.Embed( + description=on_success, + colour=discord.Colour.green(), + title=random.choice(POSITIVE_REPLIES) + ) footer_str = f"ID: {reminder_id}" + if delivery_dt: # Reminder deletion will have a `None` `delivery_dt` - footer_str = f"{footer_str}, Due: {delivery_dt.strftime('%Y-%m-%dT%H:%M:%S')}" + footer_str += ', Done at' + embed.timestamp = delivery_dt embed.set_footer(text=footer_str) -- cgit v1.2.3 From 3188d61f9f6ef871864aed273844ff6a57eb36a0 Mon Sep 17 00:00:00 2001 From: ToxicKidz <78174417+ToxicKidz@users.noreply.github.com> Date: Wed, 21 Apr 2021 10:42:31 -0400 Subject: chore: Revert back to 'Due' --- bot/exts/utils/reminders.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/utils/reminders.py b/bot/exts/utils/reminders.py index 1d0832d9a..6c21920a1 100644 --- a/bot/exts/utils/reminders.py +++ b/bot/exts/utils/reminders.py @@ -100,7 +100,7 @@ class Reminders(Cog): if delivery_dt: # Reminder deletion will have a `None` `delivery_dt` - footer_str += ', Done at' + footer_str += ', Due' embed.timestamp = delivery_dt embed.set_footer(text=footer_str) -- cgit v1.2.3 From 1fdd5aabd4ef5e356f358fdb6e9b26a5b5da99ce Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Sat, 24 Apr 2021 17:04:48 +0200 Subject: Tests: simplify public flags handling Co_authored-by: Numerlor <25886452+Numerlor@users.noreply.github.com> --- tests/bot/exts/info/test_information.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/tests/bot/exts/info/test_information.py b/tests/bot/exts/info/test_information.py index d2ecee033..770660fe3 100644 --- a/tests/bot/exts/info/test_information.py +++ b/tests/bot/exts/info/test_information.py @@ -281,13 +281,10 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase): """The embed should use the string representation of the user if they don't have a nick.""" ctx = helpers.MockContext(channel=helpers.MockTextChannel(id=1)) user = helpers.MockMember() - public_flags = unittest.mock.MagicMock() - public_flags.__iter__.return_value = iter(()) - public_flags.verified_bot = False + user.public_flags = unittest.mock.MagicMock(verified_bot=False) user.nick = None user.__str__ = unittest.mock.Mock(return_value="Mr. Hemlock") user.colour = 0 - user.public_flags = public_flags embed = await self.cog.create_user_embed(ctx, user) @@ -301,13 +298,10 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase): """The embed should use the nick if it's available.""" ctx = helpers.MockContext(channel=helpers.MockTextChannel(id=1)) user = helpers.MockMember() - public_flags = unittest.mock.MagicMock() - public_flags.__iter__.return_value = iter(()) - public_flags.verified_bot = False + user.public_flags = unittest.mock.MagicMock(verified_bot=False) user.nick = "Cat lover" user.__str__ = unittest.mock.Mock(return_value="Mr. Hemlock") user.colour = 0 - user.public_flags = public_flags embed = await self.cog.create_user_embed(ctx, user) -- cgit v1.2.3 From 3fa889aaee4a4d901ce17a24dd6760a4fea88fd7 Mon Sep 17 00:00:00 2001 From: Andi Qu <31325319+dolphingarlic@users.noreply.github.com> Date: Tue, 27 Apr 2021 08:39:51 +0200 Subject: Merge two comments into one Co-authored-by: Xithrius <15021300+Xithrius@users.noreply.github.com> --- bot/exts/info/code_snippets.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bot/exts/info/code_snippets.py b/bot/exts/info/code_snippets.py index b9e7cc3d0..c20115830 100644 --- a/bot/exts/info/code_snippets.py +++ b/bot/exts/info/code_snippets.py @@ -231,8 +231,7 @@ class CodeSnippets(Cog): snippet = await handler(**match.groupdict()) all_snippets.append((match.start(), snippet)) - # Sorts the list of snippets by their match index and joins them into - # a single message + # Sorts the list of snippets by their match index and joins them into a single message message_to_send = '\n'.join(map(lambda x: x[1], sorted(all_snippets))) if 0 < len(message_to_send) <= 2000 and len(all_snippets) <= 15: -- cgit v1.2.3 From 99549d7e76556c09d27148ee43fa61a38bc9a0b4 Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Tue, 27 Apr 2021 16:58:41 +0200 Subject: Use a specific error message when a warned user isn't in the guild This commit changes sighly how the warn, kick and mute commands to take a fetched member as their argument and to return a little error message if the user isn't in the guild rather than showing the whole help page. --- bot/exts/moderation/infraction/infractions.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/bot/exts/moderation/infraction/infractions.py b/bot/exts/moderation/infraction/infractions.py index d89e80acc..38d1ffc0e 100644 --- a/bot/exts/moderation/infraction/infractions.py +++ b/bot/exts/moderation/infraction/infractions.py @@ -54,8 +54,12 @@ class Infractions(InfractionScheduler, commands.Cog): # region: Permanent infractions @command() - async def warn(self, ctx: Context, user: Member, *, reason: t.Optional[str] = None) -> None: + async def warn(self, ctx: Context, user: FetchedMember, *, reason: t.Optional[str] = None) -> None: """Warn a user for the given reason.""" + if not isinstance(user, Member): + await ctx.send(":x: The user doesn't appear to be on the server.") + return + infraction = await _utils.post_infraction(ctx, user, "warning", reason, active=False) if infraction is None: return @@ -63,8 +67,12 @@ class Infractions(InfractionScheduler, commands.Cog): await self.apply_infraction(ctx, infraction, user) @command() - async def kick(self, ctx: Context, user: Member, *, reason: t.Optional[str] = None) -> None: + async def kick(self, ctx: Context, user: FetchedMember, *, reason: t.Optional[str] = None) -> None: """Kick a user for the given reason.""" + if not isinstance(user, Member): + await ctx.send(":x: The user doesn't appear to be on the server.") + return + await self.apply_kick(ctx, user, reason) @command() @@ -100,7 +108,7 @@ class Infractions(InfractionScheduler, commands.Cog): @command(aliases=["mute"]) async def tempmute( self, ctx: Context, - user: Member, + user: FetchedMember, duration: t.Optional[Expiry] = None, *, reason: t.Optional[str] = None @@ -122,6 +130,10 @@ class Infractions(InfractionScheduler, commands.Cog): If no duration is given, a one hour duration is used by default. """ + if not isinstance(user, Member): + await ctx.send(":x: The user doesn't appear to be on the server.") + return + if duration is None: duration = await Duration().convert(ctx, "1h") await self.apply_mute(ctx, user, reason, expires_at=duration) -- cgit v1.2.3