From dfe0e6001d4c1886e29b20eafacf4de1cc3a938b Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Sat, 27 Mar 2021 18:17:26 +0100 Subject: Allow automatic linking of issues everywhere We have been limiting the channels where automatic linking issues can be used in (See #566). Since we haven't seen any potential issue with it, we can allow it everywhere to avoid updating it every two days or because of missing channels. --- bot/exts/evergreen/issues.py | 6 ------ 1 file changed, 6 deletions(-) (limited to 'bot/exts/evergreen/issues.py') diff --git a/bot/exts/evergreen/issues.py b/bot/exts/evergreen/issues.py index bbcbf611..6ca0c3c9 100644 --- a/bot/exts/evergreen/issues.py +++ b/bot/exts/evergreen/issues.py @@ -180,12 +180,6 @@ class Issues(commands.Cog): @commands.Cog.listener() async def on_message(self, message: discord.Message) -> None: """Command to retrieve issue(s) from a GitHub repository using automatic linking if matching #.""" - if not( - message.channel.category.id in WHITELISTED_CATEGORIES - or message.channel.id in WHITELISTED_CHANNELS_ON_MESSAGE - ): - return - message_repo_issue_map = re.findall(fr"({self.repo_regex})#(\d+)", message.content) links = [] -- cgit v1.2.3 From d03a41dfbeb3653fbc96ae4c6e3160fe581b1d16 Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Sat, 27 Mar 2021 18:33:36 +0100 Subject: Remove unused WHITELISTED_CHANNELS_ON_MESSAGE constant --- bot/exts/evergreen/issues.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) (limited to 'bot/exts/evergreen/issues.py') diff --git a/bot/exts/evergreen/issues.py b/bot/exts/evergreen/issues.py index 6ca0c3c9..3d23b869 100644 --- a/bot/exts/evergreen/issues.py +++ b/bot/exts/evergreen/issues.py @@ -7,7 +7,7 @@ from enum import Enum import discord from discord.ext import commands, tasks -from bot.constants import Categories, Channels, Colours, ERROR_REPLIES, Emojis, Tokens, WHITELISTED_CHANNELS +from bot.constants import Categories, Colours, ERROR_REPLIES, Emojis, Tokens, WHITELISTED_CHANNELS log = logging.getLogger(__name__) @@ -26,9 +26,6 @@ if GITHUB_TOKEN := Tokens.github: WHITELISTED_CATEGORIES = ( Categories.development, Categories.devprojects, Categories.media, Categories.staff ) -WHITELISTED_CHANNELS_ON_MESSAGE = ( - Channels.organisation, Channels.mod_meta, Channels.mod_tools, Channels.staff_voice -) CODE_BLOCK_RE = re.compile( r"^`([^`\n]+)`" # Inline codeblock -- cgit v1.2.3 From 9edeff2eea5df363446e98d2905283562849619b Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Mon, 5 Apr 2021 16:08:24 +0200 Subject: Issues: remove duplicates --- bot/exts/evergreen/issues.py | 3 +++ 1 file changed, 3 insertions(+) (limited to 'bot/exts/evergreen/issues.py') diff --git a/bot/exts/evergreen/issues.py b/bot/exts/evergreen/issues.py index 3d23b869..d7e29a90 100644 --- a/bot/exts/evergreen/issues.py +++ b/bot/exts/evergreen/issues.py @@ -181,6 +181,9 @@ class Issues(commands.Cog): links = [] if message_repo_issue_map: + # Remove duplicates + message_repo_issue_map = set(message_repo_issue_map) + for repo_issue in message_repo_issue_map: if not self.check_in_block(message, " ".join(repo_issue)): result = await self.fetch_issues({repo_issue[1]}, repo_issue[0], "python-discord") -- cgit v1.2.3 From 0e2422f7caaec73fbee9708c507d9be23635dd78 Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Mon, 5 Apr 2021 16:43:28 +0200 Subject: Issues: limit results to 5 --- bot/exts/evergreen/issues.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) (limited to 'bot/exts/evergreen/issues.py') diff --git a/bot/exts/evergreen/issues.py b/bot/exts/evergreen/issues.py index d7e29a90..afaa7012 100644 --- a/bot/exts/evergreen/issues.py +++ b/bot/exts/evergreen/issues.py @@ -15,8 +15,6 @@ BAD_RESPONSE = { 404: "Issue/pull request not located! Please enter a valid number!", 403: "Rate limit has been hit! Please try again later!" } - -MAX_REQUESTS = 10 REQUEST_HEADERS = dict() REPOS_API = "https://api.github.com/orgs/{org}/repos" @@ -33,6 +31,9 @@ CODE_BLOCK_RE = re.compile( re.DOTALL | re.MULTILINE ) +# Maximum number of issues in one message +MAXIMUM_ISSUES = 5 + class FetchIssueErrors(Enum): """Errors returned in fetch issues.""" @@ -83,7 +84,7 @@ class Issues(commands.Cog): if not numbers: return FetchIssueErrors.value_error - if len(numbers) > MAX_REQUESTS: + if len(numbers) > MAXIMUM_ISSUES: return FetchIssueErrors.max_requests for number in numbers: @@ -162,7 +163,7 @@ class Issues(commands.Cog): embed = discord.Embed( title=random.choice(ERROR_REPLIES), color=Colours.soft_red, - description=f"Too many issues/PRs! (maximum of {MAX_REQUESTS})" + description=f"Too many issues/PRs! (maximum of {MAXIMUM_ISSUES})" ) await ctx.send(embed=embed) @@ -184,6 +185,15 @@ class Issues(commands.Cog): # Remove duplicates message_repo_issue_map = set(message_repo_issue_map) + if len(message_repo_issue_map) > MAXIMUM_ISSUES: + embed = discord.Embed( + title=random.choice(ERROR_REPLIES), + color=Colours.soft_red, + description=f"Too many issues/PRs! (maximum of {MAXIMUM_ISSUES})" + ) + await message.channel.send(embed=embed) + return + for repo_issue in message_repo_issue_map: if not self.check_in_block(message, " ".join(repo_issue)): result = await self.fetch_issues({repo_issue[1]}, repo_issue[0], "python-discord") -- cgit v1.2.3 From 08b1348b33d2d865987f2677a68ba62645db2abe Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Tue, 6 Apr 2021 10:05:40 +0200 Subject: Rewrite issue cog - Switch to a dataclass based communication - Use GitHub headers when querying the repo_regex - Properly handle non-200 return codes - Use @whitelist_override --- bot/exts/evergreen/issues.py | 205 +++++++++++++++++++++++++------------------ 1 file changed, 119 insertions(+), 86 deletions(-) (limited to 'bot/exts/evergreen/issues.py') diff --git a/bot/exts/evergreen/issues.py b/bot/exts/evergreen/issues.py index afaa7012..dce11678 100644 --- a/bot/exts/evergreen/issues.py +++ b/bot/exts/evergreen/issues.py @@ -2,12 +2,13 @@ import logging import random import re import typing as t -from enum import Enum +from dataclasses import dataclass import discord from discord.ext import commands, tasks from bot.constants import Categories, Colours, ERROR_REPLIES, Emojis, Tokens, WHITELISTED_CHANNELS +from bot.utils.decorators import whitelist_override log = logging.getLogger(__name__) @@ -17,7 +18,10 @@ BAD_RESPONSE = { } REQUEST_HEADERS = dict() -REPOS_API = "https://api.github.com/orgs/{org}/repos" +REPOSITORY_ENDPOINT = "https://api.github.com/orgs/{org}/repos" +ISSUE_ENDPOINT = "https://api.github.com/repos/{user}/{repository}/issues/{number}" +PR_MERGE_ENDPOINT = "https://api.github.com/repos/{user}/{repository}/pulls/{number}/merge" + if GITHUB_TOKEN := Tokens.github: REQUEST_HEADERS["Authorization"] = f"token {GITHUB_TOKEN}" @@ -35,11 +39,23 @@ CODE_BLOCK_RE = re.compile( MAXIMUM_ISSUES = 5 -class FetchIssueErrors(Enum): - """Errors returned in fetch issues.""" +@dataclass +class FetchError: + """Dataclass representing an error while fetching an issue.""" + + return_code: int + message: str + - value_error = "Numbers not found." - max_requests = "Max requests hit." +@dataclass +class IssueState: + """Dataclass representing the state of an issue.""" + + repository: str + number: int + url: str + title: str + icon_url: str class Issues(commands.Cog): @@ -48,19 +64,27 @@ class Issues(commands.Cog): def __init__(self, bot: commands.Bot): self.bot = bot self.repos = [] + self.repo_regex = None self.get_pydis_repos.start() @tasks.loop(minutes=30) async def get_pydis_repos(self) -> None: - """Get all python-discord repositories on github.""" - async with self.bot.http_session.get(REPOS_API.format(org="python-discord")) as resp: + """ + Get all python-discord repositories on github. + + This task will update a pipe-separated list of repositories in self.repo_regex. + """ + async with self.bot.http_session.get( + REPOSITORY_ENDPOINT.format(org="python-discord"), + headers=REQUEST_HEADERS + ) as resp: if resp.status == 200: data = await resp.json() for repo in data: self.repos.append(repo["full_name"].split("/")[1]) self.repo_regex = "|".join(self.repos) else: - log.debug(f"Failed to get latest Pydis repositories. Status code {resp.status}") + log.warning(f"Failed to get latest Pydis repositories. Status code {resp.status}") @staticmethod def check_in_block(message: discord.Message, repo_issue: str) -> bool: @@ -75,70 +99,85 @@ class Issues(commands.Cog): async def fetch_issues( self, - numbers: set, + number: int, repository: str, user: str - ) -> t.Union[FetchIssueErrors, str, list]: - """Retrieve issue(s) from a GitHub repository.""" - links = [] - if not numbers: - return FetchIssueErrors.value_error - - if len(numbers) > MAXIMUM_ISSUES: - return FetchIssueErrors.max_requests - - for number in numbers: - url = f"https://api.github.com/repos/{user}/{repository}/issues/{number}" - merge_url = f"https://api.github.com/repos/{user}/{repository}/pulls/{number}/merge" - log.trace(f"Querying GH issues API: {url}") - async with self.bot.http_session.get(url, headers=REQUEST_HEADERS) as r: - json_data = await r.json() - - if r.status in BAD_RESPONSE: - log.warning(f"Received response {r.status} from: {url}") - return f"[{str(r.status)}] #{number} {BAD_RESPONSE.get(r.status)}" - - # The initial API request is made to the issues API endpoint, which will return information - # if the issue or PR is present. However, the scope of information returned for PRs differs - # from issues: if the 'issues' key is present in the response then we can pull the data we - # need from the initial API call. - if "issues" in json_data.get("html_url"): + ) -> t.Union[IssueState, FetchError]: + """ + Retrieve an issue from a GitHub repository. + + returns IssueState on success, FetchError on failure. + """ + url = ISSUE_ENDPOINT.format(user=user, repository=repository, number=number) + merge_url = PR_MERGE_ENDPOINT.format(user=user, repository=repository, number=number) + log.trace(f"Querying GH issues API: {url}") + + async with self.bot.http_session.get(url, headers=REQUEST_HEADERS) as r: + json_data = await r.json() + + if r.status == 403: + if "X-RateLimit-Remaining" in r.headers and r.headers["X-RateLimit-Remaining"] == "0": + log.info(f"Ratelimit reached while fetching {url}") + return FetchError(403, "Ratelimit reached, please retry in a few minutes.") + return FetchError(403, "Cannot access issue.") + elif r.status in (404, 410): + return FetchError(r.status, "Issue not found.") + elif r.status != 200: + return FetchError(r.status, "Error while fetching issue.") + + # The initial API request is made to the issues API endpoint, which will return information + # if the issue or PR is present. However, the scope of information returned for PRs differs + # from issues: if the 'issues' key is present in the response then we can pull the data we + # need from the initial API call. + if "issues" in json_data.get("html_url"): + if json_data.get("state") == "open": + icon_url = Emojis.issue + else: + icon_url = Emojis.issue_closed + + # If the 'issues' key is not contained in the API response and there is no error code, then + # we know that a PR has been requested and a call to the pulls API endpoint is necessary + # to get the desired information for the PR. + else: + log.trace(f"PR provided, querying GH pulls API for additional information: {merge_url}") + async with self.bot.http_session.get(merge_url) as m: if json_data.get("state") == "open": - icon_url = Emojis.issue + icon_url = Emojis.pull_request + # When the status is 204 this means that the state of the PR is merged + elif m.status == 204: + icon_url = Emojis.merge else: - icon_url = Emojis.issue_closed - - # If the 'issues' key is not contained in the API response and there is no error code, then - # we know that a PR has been requested and a call to the pulls API endpoint is necessary - # to get the desired information for the PR. - else: - log.trace(f"PR provided, querying GH pulls API for additional information: {merge_url}") - async with self.bot.http_session.get(merge_url) as m: - if json_data.get("state") == "open": - icon_url = Emojis.pull_request - # When the status is 204 this means that the state of the PR is merged - elif m.status == 204: - icon_url = Emojis.merge - else: - icon_url = Emojis.pull_request_closed + icon_url = Emojis.pull_request_closed - issue_url = json_data.get("html_url") - links.append([icon_url, f"[{repository}] #{number} {json_data.get('title')}", issue_url]) + issue_url = json_data.get("html_url") - return links + return IssueState(repository, number, issue_url, json_data.get('title', ''), icon_url) @staticmethod - def get_embed(result: list, user: str = "python-discord", repository: str = "") -> discord.Embed: - """Get Response Embed.""" - description_list = ["{0} [{1}]({2})".format(*link) for link in result] + def format_embed( + results: t.List[t.Union[IssueState, FetchError]], + user: str, + repository: t.Optional[str] = None + ) -> discord.Embed: + """Take a list of IssueState or FetchError and format a Discord embed for them.""" + description_list = [] + + for result in results: + if isinstance(result, IssueState): + description_list.append(f"{result.icon_url} [{result.title}]({result.url})") + elif isinstance(result, FetchError): + description_list.append(f"[{result.return_code}] {result.message}") + resp = discord.Embed( colour=Colours.bright_green, description='\n'.join(description_list) ) - resp.set_author(name="GitHub", url=f"https://github.com/{user}/{repository}") + embed_url = f"https://github.com/{user}/{repository}" if repository else f"https://github.com/{user}" + resp.set_author(name="GitHub", url=embed_url) return resp + @whitelist_override(channels=WHITELISTED_CHANNELS, categories=WHITELISTED_CATEGORIES) @commands.command(aliases=("pr",)) async def issue( self, @@ -148,62 +187,56 @@ class Issues(commands.Cog): user: str = "python-discord" ) -> None: """Command to retrieve issue(s) from a GitHub repository.""" - if not( - ctx.channel.category.id in WHITELISTED_CATEGORIES - or ctx.channel.id in WHITELISTED_CHANNELS - ): - return - - result = await self.fetch_issues(set(numbers), repository, user) - - if result == FetchIssueErrors.value_error: - await ctx.invoke(self.bot.get_command('help'), 'issue') + # Remove duplicates + numbers = set(numbers) - elif result == FetchIssueErrors.max_requests: + if len(numbers) > MAXIMUM_ISSUES: embed = discord.Embed( title=random.choice(ERROR_REPLIES), color=Colours.soft_red, description=f"Too many issues/PRs! (maximum of {MAXIMUM_ISSUES})" ) await ctx.send(embed=embed) + await ctx.invoke(self.bot.get_command('help'), 'issue') - elif isinstance(result, list): - # Issue/PR format: emoji to show if open/closed/merged, number and the title as a singular link. - resp = self.get_embed(result, user, repository) - await ctx.send(embed=resp) - - elif isinstance(result, str): - await ctx.send(result) + results = [await self.fetch_issues(number, repository, user) for number in numbers] + await ctx.send(embed=self.format_embed(results)) @commands.Cog.listener() async def on_message(self, message: discord.Message) -> None: """Command to retrieve issue(s) from a GitHub repository using automatic linking if matching #.""" - message_repo_issue_map = re.findall(fr"({self.repo_regex})#(\d+)", message.content) + if not self.repo_regex: + log.warning("repo_regex isn't ready, cannot look for issues.") + return + + # `issues` will hold a list of two element tuples `(repository, issue_number)` + issues = re.findall(fr"({self.repo_regex})#(\d+)", message.content) links = [] - if message_repo_issue_map: + if issues: + log.trace(f"Found {issues = }") # Remove duplicates - message_repo_issue_map = set(message_repo_issue_map) + issues = set(issues) - if len(message_repo_issue_map) > MAXIMUM_ISSUES: + if len(issues) > MAXIMUM_ISSUES: embed = discord.Embed( title=random.choice(ERROR_REPLIES), color=Colours.soft_red, description=f"Too many issues/PRs! (maximum of {MAXIMUM_ISSUES})" ) - await message.channel.send(embed=embed) + await message.channel.send(embed=embed).delete(delay=5) return - for repo_issue in message_repo_issue_map: + for repo_issue in issues: if not self.check_in_block(message, " ".join(repo_issue)): - result = await self.fetch_issues({repo_issue[1]}, repo_issue[0], "python-discord") - if isinstance(result, list): - links.extend(result) + result = await self.fetch_issues(repo_issue[1], repo_issue[0], "python-discord") + if isinstance(result, IssueState): + links.append(result) if not links: return - resp = self.get_embed(links, "python-discord") + resp = self.format_embed(links, "python-discord") await message.channel.send(embed=resp) -- cgit v1.2.3 From f8e14d089e87cc2628a9087a60bf0c3cc6eefc39 Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Tue, 6 Apr 2021 10:13:31 +0200 Subject: Issues: ignore bots --- bot/exts/evergreen/issues.py | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'bot/exts/evergreen/issues.py') diff --git a/bot/exts/evergreen/issues.py b/bot/exts/evergreen/issues.py index dce11678..7e9defbe 100644 --- a/bot/exts/evergreen/issues.py +++ b/bot/exts/evergreen/issues.py @@ -209,6 +209,10 @@ class Issues(commands.Cog): log.warning("repo_regex isn't ready, cannot look for issues.") return + # Ignore bots + if message.author.bot: + return + # `issues` will hold a list of two element tuples `(repository, issue_number)` issues = re.findall(fr"({self.repo_regex})#(\d+)", message.content) links = [] -- cgit v1.2.3 From fd2ff1d06e31a9b0f412c2a81e373bf46fe192f7 Mon Sep 17 00:00:00 2001 From: vcokltfre Date: Thu, 8 Apr 2021 03:15:47 +0100 Subject: increase the number of repos per page we fetch from github --- bot/exts/evergreen/issues.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bot/exts/evergreen/issues.py') diff --git a/bot/exts/evergreen/issues.py b/bot/exts/evergreen/issues.py index 4a73d20b..58f9d7aa 100644 --- a/bot/exts/evergreen/issues.py +++ b/bot/exts/evergreen/issues.py @@ -28,7 +28,7 @@ BAD_RESPONSE = { MAX_REQUESTS = 10 REQUEST_HEADERS = dict() -REPOS_API = "https://api.github.com/orgs/{org}/repos" +REPOS_API = "https://api.github.com/orgs/{org}/repos?per_page=100" if GITHUB_TOKEN := Tokens.github: REQUEST_HEADERS["Authorization"] = f"token {GITHUB_TOKEN}" -- cgit v1.2.3 From 291997462779b79ff088432d7333bee9b2ff2f2c Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Thu, 8 Apr 2021 09:07:01 +0200 Subject: Refactor issue cog Co-authored-by: ToxicKidz <78174417+ToxicKidz@users.noreply.github.com> --- bot/exts/evergreen/issues.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'bot/exts/evergreen/issues.py') diff --git a/bot/exts/evergreen/issues.py b/bot/exts/evergreen/issues.py index 7255d450..bcaeee1f 100644 --- a/bot/exts/evergreen/issues.py +++ b/bot/exts/evergreen/issues.py @@ -115,7 +115,7 @@ class Issues(commands.Cog): """ Retrieve an issue from a GitHub repository. - returns IssueState on success, FetchError on failure. + Returns IssueState on success, FetchError on failure. """ url = ISSUE_ENDPOINT.format(user=user, repository=repository, number=number) merge_url = PR_MERGE_ENDPOINT.format(user=user, repository=repository, number=number) @@ -125,7 +125,7 @@ class Issues(commands.Cog): json_data = await r.json() if r.status == 403: - if "X-RateLimit-Remaining" in r.headers and r.headers["X-RateLimit-Remaining"] == "0": + if r.headers.get("X-RateLimit-Remaining") == "0": log.info(f"Ratelimit reached while fetching {url}") return FetchError(403, "Ratelimit reached, please retry in a few minutes.") return FetchError(403, "Cannot access issue.") @@ -138,7 +138,7 @@ class Issues(commands.Cog): # if the issue or PR is present. However, the scope of information returned for PRs differs # from issues: if the 'issues' key is present in the response then we can pull the data we # need from the initial API call. - if "issues" in json_data.get("html_url"): + if "issues" in json_data["html_url"]: if json_data.get("state") == "open": icon_url = Emojis.issue else: @@ -251,7 +251,7 @@ class Issues(commands.Cog): color=Colours.soft_red, description=f"Too many issues/PRs! (maximum of {MAXIMUM_ISSUES})" ) - await message.channel.send(embed=embed).delete(delay=5) + await message.channel.send(embed=embed, delete_after=5) return for repo_issue in issues: -- cgit v1.2.3 From cd0153d36f081f2a0a41b610eac86ff52211e5c5 Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Thu, 8 Apr 2021 09:10:48 +0200 Subject: Issues: make use of invoke_help_command Co-authored-by: ToxicKidz <78174417+ToxicKidz@users.noreply.github.com> --- bot/exts/evergreen/issues.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'bot/exts/evergreen/issues.py') diff --git a/bot/exts/evergreen/issues.py b/bot/exts/evergreen/issues.py index bcaeee1f..d8b373d7 100644 --- a/bot/exts/evergreen/issues.py +++ b/bot/exts/evergreen/issues.py @@ -18,6 +18,7 @@ from bot.constants import ( WHITELISTED_CHANNELS ) from bot.utils.decorators import whitelist_override +from bot.utils.extensions import invoke_help_command log = logging.getLogger(__name__) @@ -206,7 +207,7 @@ class Issues(commands.Cog): description=f"Too many issues/PRs! (maximum of {MAXIMUM_ISSUES})" ) await ctx.send(embed=embed) - await ctx.invoke(self.bot.get_command('help'), 'issue') + await invoke_help_command(ctx) results = [await self.fetch_issues(number, repository, user) for number in numbers] await ctx.send(embed=self.format_embed(results, user, repository)) -- cgit v1.2.3 From a52eeb5e253a31c447d28fe663f3a60c0c9efcdc Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Thu, 8 Apr 2021 11:35:43 +0200 Subject: Issues: add accept header --- bot/exts/evergreen/issues.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'bot/exts/evergreen/issues.py') diff --git a/bot/exts/evergreen/issues.py b/bot/exts/evergreen/issues.py index d8b373d7..5ea8a6bf 100644 --- a/bot/exts/evergreen/issues.py +++ b/bot/exts/evergreen/issues.py @@ -26,7 +26,9 @@ BAD_RESPONSE = { 404: "Issue/pull request not located! Please enter a valid number!", 403: "Rate limit has been hit! Please try again later!" } -REQUEST_HEADERS = dict() +REQUEST_HEADERS = { + "Accept": "application/vnd.github.v3+json" +} REPOSITORY_ENDPOINT = "https://api.github.com/orgs/{org}/repos" ISSUE_ENDPOINT = "https://api.github.com/repos/{user}/{repository}/issues/{number}" -- cgit v1.2.3 From 37a3b571467bb889526b7d21db17f7644453a85e Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Thu, 8 Apr 2021 11:39:45 +0200 Subject: Issues: icon_url -> emoji_url --- bot/exts/evergreen/issues.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'bot/exts/evergreen/issues.py') diff --git a/bot/exts/evergreen/issues.py b/bot/exts/evergreen/issues.py index 5ea8a6bf..9b04c8c1 100644 --- a/bot/exts/evergreen/issues.py +++ b/bot/exts/evergreen/issues.py @@ -67,7 +67,7 @@ class IssueState: number: int url: str title: str - icon_url: str + emoji_url: str class Issues(commands.Cog): @@ -176,7 +176,7 @@ class Issues(commands.Cog): for result in results: if isinstance(result, IssueState): - description_list.append(f"{result.icon_url} [{result.title}]({result.url})") + description_list.append(f"{result.emoji_url} [{result.title}]({result.url})") elif isinstance(result, FetchError): description_list.append(f"[{result.return_code}] {result.message}") -- cgit v1.2.3 From c272bdf0149d741efad477b77a7e123a42569f68 Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Thu, 8 Apr 2021 11:41:23 +0200 Subject: Issues: add red cross emoji for failed issues Co-authored-by: Shivansh-007 --- bot/exts/evergreen/issues.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bot/exts/evergreen/issues.py') diff --git a/bot/exts/evergreen/issues.py b/bot/exts/evergreen/issues.py index 83cafcc8..5ad13628 100644 --- a/bot/exts/evergreen/issues.py +++ b/bot/exts/evergreen/issues.py @@ -178,7 +178,7 @@ class Issues(commands.Cog): if isinstance(result, IssueState): description_list.append(f"{result.emoji_url} [{result.title}]({result.url})") elif isinstance(result, FetchError): - description_list.append(f"[{result.return_code}] {result.message}") + description_list.append(f":x: [{result.return_code}] {result.message}") resp = discord.Embed( colour=Colours.bright_green, -- cgit v1.2.3 From 0ff42e4e2197822bd1e1a63e5ebd73d75588b1eb Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Thu, 8 Apr 2021 11:51:16 +0200 Subject: Issues: emoji_url -> emoji --- bot/exts/evergreen/issues.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) (limited to 'bot/exts/evergreen/issues.py') diff --git a/bot/exts/evergreen/issues.py b/bot/exts/evergreen/issues.py index 83cafcc8..10aa347d 100644 --- a/bot/exts/evergreen/issues.py +++ b/bot/exts/evergreen/issues.py @@ -67,7 +67,7 @@ class IssueState: number: int url: str title: str - emoji_url: str + emoji: str class Issues(commands.Cog): @@ -143,9 +143,9 @@ class Issues(commands.Cog): # need from the initial API call. if "issues" in json_data["html_url"]: if json_data.get("state") == "open": - icon_url = Emojis.issue + emoji = Emojis.issue else: - icon_url = Emojis.issue_closed + emoji = Emojis.issue_closed # If the 'issues' key is not contained in the API response and there is no error code, then # we know that a PR has been requested and a call to the pulls API endpoint is necessary @@ -154,16 +154,16 @@ class Issues(commands.Cog): log.trace(f"PR provided, querying GH pulls API for additional information: {merge_url}") async with self.bot.http_session.get(merge_url) as m: if json_data.get("state") == "open": - icon_url = Emojis.pull_request + emoji = Emojis.pull_request # When the status is 204 this means that the state of the PR is merged elif m.status == 204: - icon_url = Emojis.merge + emoji = Emojis.merge else: - icon_url = Emojis.pull_request_closed + emoji = Emojis.pull_request_closed issue_url = json_data.get("html_url") - return IssueState(repository, number, issue_url, json_data.get('title', ''), icon_url) + return IssueState(repository, number, issue_url, json_data.get('title', ''), emoji) @staticmethod def format_embed( @@ -176,7 +176,7 @@ class Issues(commands.Cog): for result in results: if isinstance(result, IssueState): - description_list.append(f"{result.emoji_url} [{result.title}]({result.url})") + description_list.append(f"{result.emoji} [{result.title}]({result.url})") elif isinstance(result, FetchError): description_list.append(f"[{result.return_code}] {result.message}") -- cgit v1.2.3 From c94a45a10bac63eff1ce066707fe2df0b1243daa Mon Sep 17 00:00:00 2001 From: ToxicKidz <78174417+ToxicKidz@users.noreply.github.com> Date: Thu, 8 Apr 2021 12:37:14 -0400 Subject: Use PRDraft emoji when the pr is a draft pr for the .issue|.pr command --- bot/constants.py | 1 + bot/exts/evergreen/issues.py | 13 ++++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) (limited to 'bot/exts/evergreen/issues.py') diff --git a/bot/constants.py b/bot/constants.py index 416dd0e7..e59fa641 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -168,6 +168,7 @@ class Emojis: issue_closed = "<:IssueClosed:629695470570307614>" pull_request = "<:PROpen:629695470175780875>" pull_request_closed = "<:PRClosed:629695470519713818>" + pull_request_draft = "<:PRDraft:829755345425399848>" merge = "<:PRMerged:629695470570176522>" number_emojis = { diff --git a/bot/exts/evergreen/issues.py b/bot/exts/evergreen/issues.py index 4a73d20b..e83f1a3e 100644 --- a/bot/exts/evergreen/issues.py +++ b/bot/exts/evergreen/issues.py @@ -100,7 +100,7 @@ class Issues(commands.Cog): for number in numbers: url = f"https://api.github.com/repos/{user}/{repository}/issues/{number}" - merge_url = f"https://api.github.com/repos/{user}/{repository}/pulls/{number}/merge" + pulls_url = f"https://api.github.com/repos/{user}/{repository}/pulls/{number}" log.trace(f"Querying GH issues API: {url}") async with self.bot.http_session.get(url, headers=REQUEST_HEADERS) as r: json_data = await r.json() @@ -123,12 +123,15 @@ class Issues(commands.Cog): # we know that a PR has been requested and a call to the pulls API endpoint is necessary # to get the desired information for the PR. else: - log.trace(f"PR provided, querying GH pulls API for additional information: {merge_url}") - async with self.bot.http_session.get(merge_url) as m: - if json_data.get("state") == "open": + log.trace(f"PR provided, querying GH pulls API for additional information: {pulls_url}") + async with self.bot.http_session.get(pulls_url) as p: + pull_data = await p.json() + if pull_data["draft"]: + icon_url = Emojis.pull_request_draft + elif pull_data["state"] == "open": icon_url = Emojis.pull_request # When the status is 204 this means that the state of the PR is merged - elif m.status == 204: + elif pull_data["merged_at"] is not None: icon_url = Emojis.merge else: icon_url = Emojis.pull_request_closed -- cgit v1.2.3 From a36a58d81b64412458a7b51fbfb4a37e88a060fe Mon Sep 17 00:00:00 2001 From: Chris Date: Thu, 8 Apr 2021 19:28:59 +0100 Subject: Add word boundaries to the issues regex --- bot/exts/evergreen/issues.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bot/exts/evergreen/issues.py') diff --git a/bot/exts/evergreen/issues.py b/bot/exts/evergreen/issues.py index 0d43326d..b653f7ae 100644 --- a/bot/exts/evergreen/issues.py +++ b/bot/exts/evergreen/issues.py @@ -226,7 +226,7 @@ class Issues(commands.Cog): return # `issues` will hold a list of two element tuples `(repository, issue_number)` - issues = re.findall(fr"({self.repo_regex})#(\d+)", message.content) + issues = re.findall(fr"\b({self.repo_regex})#(\d+)\b", message.content) links = [] if issues: -- cgit v1.2.3 From 0f2c076e6e64cf8f09fc8a89872767121acdfd83 Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Fri, 9 Apr 2021 09:16:40 +0200 Subject: Issues: use soft_red instead of red Co-authored-by: Shivansh-007 --- bot/exts/evergreen/issues.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bot/exts/evergreen/issues.py') diff --git a/bot/exts/evergreen/issues.py b/bot/exts/evergreen/issues.py index b653f7ae..3fb69127 100644 --- a/bot/exts/evergreen/issues.py +++ b/bot/exts/evergreen/issues.py @@ -239,7 +239,7 @@ class Issues(commands.Cog): "You can't retrieve issues from DMs. " f"Try again in <#{Channels.community_bot_commands}>" ), - colour=discord.Colour.red() + colour=Colours.soft_red ) ) return -- cgit v1.2.3 From edb6ffab317499689d7b8f5d0b884a8d10a01a76 Mon Sep 17 00:00:00 2001 From: ToxicKidz <78174417+ToxicKidz@users.noreply.github.com> Date: Fri, 9 Apr 2021 13:28:36 -0400 Subject: Update comment --- bot/exts/evergreen/issues.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bot/exts/evergreen/issues.py') diff --git a/bot/exts/evergreen/issues.py b/bot/exts/evergreen/issues.py index 7604c438..fa07b674 100644 --- a/bot/exts/evergreen/issues.py +++ b/bot/exts/evergreen/issues.py @@ -158,7 +158,7 @@ class Issues(commands.Cog): emoji = Emojis.pull_request_draft elif pull_data["state"] == "open": emoji = Emojis.pull_request - # When the status is 204 this means that the state of the PR is merged + # When 'merged_at' is not None, this means that the state of the PR is merged elif pull_data["merged_at"] is not None: emoji = Emojis.merge else: -- cgit v1.2.3 From 51d9b5413ceb8319e0ce9847ee048f112b21fdff Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Sun, 11 Apr 2021 16:48:25 +0200 Subject: Issues: add `type=public` This will make sure that even if a privileged token is passed we won't leak any private information. --- bot/exts/evergreen/issues.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bot/exts/evergreen/issues.py') diff --git a/bot/exts/evergreen/issues.py b/bot/exts/evergreen/issues.py index fa07b674..42ab97c3 100644 --- a/bot/exts/evergreen/issues.py +++ b/bot/exts/evergreen/issues.py @@ -30,7 +30,7 @@ REQUEST_HEADERS = { "Accept": "application/vnd.github.v3+json" } -REPOSITORY_ENDPOINT = "https://api.github.com/orgs/{org}/repos?per_page=100" +REPOSITORY_ENDPOINT = "https://api.github.com/orgs/{org}/repos?per_page=100&type=public" ISSUE_ENDPOINT = "https://api.github.com/repos/{user}/{repository}/issues/{number}" PR_ENDPOINT = "https://api.github.com/repos/{user}/{repository}/pulls/{number}" -- cgit v1.2.3 From e187a3c67bcc93e8a688bac1a49541914f8b4871 Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Mon, 12 Apr 2021 17:00:55 +0200 Subject: Allow automatic linking of issues outside of the python-discord organisation Actions python-discord/organisation#345 --- bot/exts/evergreen/issues.py | 75 ++++++++++++++++++++------------------------ 1 file changed, 34 insertions(+), 41 deletions(-) (limited to 'bot/exts/evergreen/issues.py') diff --git a/bot/exts/evergreen/issues.py b/bot/exts/evergreen/issues.py index 42ab97c3..6797559c 100644 --- a/bot/exts/evergreen/issues.py +++ b/bot/exts/evergreen/issues.py @@ -5,7 +5,7 @@ import typing as t from dataclasses import dataclass import discord -from discord.ext import commands, tasks +from discord.ext import commands from bot.constants import ( Categories, @@ -50,6 +50,21 @@ CODE_BLOCK_RE = re.compile( # Maximum number of issues in one message MAXIMUM_ISSUES = 5 +# Regex used when looking for automatic linking in messages +AUTOMATIC_REGEX = re.compile(r"((?P.+?)\/)?(?P.+?)#(?P.+?)") + + +@dataclass +class FoundIssue: + """Dataclass representing an issue found by the regex.""" + + organisation: t.Optional[str] + repository: str + number: str + + def __hash__(self) -> int: + return hash(self.organisation) + hash(self.repository) + hash(self.number) + @dataclass class FetchError: @@ -76,38 +91,11 @@ class Issues(commands.Cog): def __init__(self, bot: commands.Bot): self.bot = bot self.repos = [] - self.repo_regex = None - self.get_pydis_repos.start() - - @tasks.loop(minutes=30) - async def get_pydis_repos(self) -> None: - """ - Get all python-discord repositories on github. - - This task will update a pipe-separated list of repositories in self.repo_regex. - """ - async with self.bot.http_session.get( - REPOSITORY_ENDPOINT.format(org="python-discord"), - headers=REQUEST_HEADERS - ) as resp: - if resp.status == 200: - data = await resp.json() - for repo in data: - self.repos.append(repo["full_name"].split("/")[1]) - self.repo_regex = "|".join(self.repos) - else: - log.warning(f"Failed to get latest Pydis repositories. Status code {resp.status}") @staticmethod - def check_in_block(message: discord.Message, repo_issue: str) -> bool: - """Check whether the # is in codeblocks.""" - block = re.findall(CODE_BLOCK_RE, message.content) - - if not block: - return False - elif "#".join(repo_issue.split(" ")) in "".join([*block[0]]): - return True - return False + def remove_codeblocks(message: str) -> str: + """Remove any codeblock in a message.""" + return re.sub(CODE_BLOCK_RE, "", message) async def fetch_issues( self, @@ -219,17 +207,19 @@ class Issues(commands.Cog): @commands.Cog.listener() async def on_message(self, message: discord.Message) -> None: - """Command to retrieve issue(s) from a GitHub repository using automatic linking if matching #.""" - if not self.repo_regex: - log.warning("repo_regex isn't ready, cannot look for issues.") - return + """ + Automatic issue linking. + Listener to retrieve issue(s) from a GitHub repository using automatic linking if matching /#. + """ # Ignore bots if message.author.bot: return - # `issues` will hold a list of two element tuples `(repository, issue_number)` - issues = re.findall(fr"\b({self.repo_regex})#(\d+)\b", message.content) + issues = [ + FoundIssue(*match.group("org", "repo", "number")) + for match in AUTOMATIC_REGEX.finditer(self.remove_codeblocks(message.content)) + ] links = [] if issues: @@ -261,10 +251,13 @@ class Issues(commands.Cog): return for repo_issue in issues: - if not self.check_in_block(message, " ".join(repo_issue)): - result = await self.fetch_issues(repo_issue[1], repo_issue[0], "python-discord") - if isinstance(result, IssueState): - links.append(result) + result = await self.fetch_issues( + int(repo_issue.number), + repo_issue.repository, + repo_issue.organisation or "python-discord" + ) + if isinstance(result, IssueState): + links.append(result) if not links: return -- cgit v1.2.3 From 52dbe86fb8ff7d659e3ea7f7e49f1442e004e34d Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Mon, 12 Apr 2021 17:07:39 +0200 Subject: Issues: change hashing of FoundIssue to use a tuple --- bot/exts/evergreen/issues.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bot/exts/evergreen/issues.py') diff --git a/bot/exts/evergreen/issues.py b/bot/exts/evergreen/issues.py index 6797559c..bb6273bb 100644 --- a/bot/exts/evergreen/issues.py +++ b/bot/exts/evergreen/issues.py @@ -63,7 +63,7 @@ class FoundIssue: number: str def __hash__(self) -> int: - return hash(self.organisation) + hash(self.repository) + hash(self.number) + return hash((self.organisation, self.repository, self.number)) @dataclass -- cgit v1.2.3