diff options
| author | 2021-02-02 06:07:18 +0530 | |
|---|---|---|
| committer | 2021-02-02 06:07:18 +0530 | |
| commit | 41c8c013e13ae414023082be6de9c87640f21faa (patch) | |
| tree | f28643162060ebc841da9036e510b6c06e6d4dc6 | |
| parent | Allow cht.sh command to be used in occupied help channels (diff) | |
| parent | Fixes Issue Matching Regex (diff) | |
Merge branch 'master' into feature/cheat.sh
| -rw-r--r-- | bot/constants.py | 7 | ||||
| -rw-r--r-- | bot/exts/evergreen/issues.py | 160 | 
2 files changed, 136 insertions, 31 deletions
| diff --git a/bot/constants.py b/bot/constants.py index 23865272..a070dab3 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -9,6 +9,7 @@ __all__ = (      "AdventOfCode",      "Branding",      "Channels", +    "Categories",      "Client",      "Colours",      "Emojis", @@ -100,6 +101,7 @@ class Channels(NamedTuple):      big_brother_logs = 468507907357409333      bot = 267659945086812160      checkpoint_test = 422077681434099723 +    organisation = 551789653284356126      devalerts = 460181980097675264      devlog = int(environ.get("CHANNEL_DEVLOG", 622895325144940554))      dev_contrib = 635950537262759947 @@ -108,6 +110,8 @@ class Channels(NamedTuple):      message_log = 467752170159079424      mod_alerts = 473092532147060736      modlog = 282638479504965634 +    mod_meta = 775412552795947058 +    mod_tools = 775413915391098921      off_topic_0 = 291284109232308226      off_topic_1 = 463035241142026251      off_topic_2 = 463035268514185226 @@ -124,6 +128,9 @@ class Channels(NamedTuple):  class Categories(NamedTuple):      help_in_use = 696958401460043776 +    development = 411199786025484308 +    devprojects = 787641585624940544 +    media = 799054581991997460  class Client(NamedTuple): diff --git a/bot/exts/evergreen/issues.py b/bot/exts/evergreen/issues.py index e419a6f5..73ebe547 100644 --- a/bot/exts/evergreen/issues.py +++ b/bot/exts/evergreen/issues.py @@ -1,11 +1,13 @@  import logging  import random +import re +import typing as t +from enum import Enum  import discord -from discord.ext import commands +from discord.ext import commands, tasks -from bot.constants import Channels, Colours, ERROR_REPLIES, Emojis, Tokens, WHITELISTED_CHANNELS -from bot.utils.decorators import override_in_channel +from bot.constants import Categories, Channels, Colours, ERROR_REPLIES, Emojis, Tokens, WHITELISTED_CHANNELS  log = logging.getLogger(__name__) @@ -15,55 +17,86 @@ BAD_RESPONSE = {  }  MAX_REQUESTS = 10 -  REQUEST_HEADERS = dict() + +REPOS_API = "https://api.github.com/orgs/{org}/repos"  if GITHUB_TOKEN := Tokens.github:      REQUEST_HEADERS["Authorization"] = f"token {GITHUB_TOKEN}" +WHITELISTED_CATEGORIES = ( +    Categories.devprojects, Categories.media, Categories.development +) +WHITELISTED_CHANNELS_ON_MESSAGE = (Channels.organisation, Channels.mod_meta, Channels.mod_tools) + +CODE_BLOCK_RE = re.compile( +    r"^`([^`\n]+)`"  # Inline codeblock +    r"|```(.+?)```",  # Multiline codeblock +    re.DOTALL | re.MULTILINE +) + + +class FetchIssueErrors(Enum): +    """Errors returned in fetch issues.""" + +    value_error = "Numbers not found." +    max_requests = "Max requests hit." +  class Issues(commands.Cog):      """Cog that allows users to retrieve issues from GitHub."""      def __init__(self, bot: commands.Bot):          self.bot = bot - -    @commands.command(aliases=("pr",)) -    @override_in_channel(WHITELISTED_CHANNELS + (Channels.dev_contrib, Channels.dev_branding)) -    async def issue( -        self, -        ctx: commands.Context, -        numbers: commands.Greedy[int], -        repository: str = "sir-lancebot", -        user: str = "python-discord" -    ) -> None: -        """Command to retrieve issue(s) from a GitHub repository.""" +        self.repos = [] +        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: +            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}") + +    @staticmethod +    def check_in_block(message: discord.Message, repo_issue: str) -> bool: +        """Check whether the <repo>#<issue> 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 + +    async def fetch_issues( +            self, +            numbers: set, +            repository: str, +            user: str +    ) -> t.Union[FetchIssueErrors, str, list]: +        """Retrieve issue(s) from a GitHub repository."""          links = [] -        numbers = set(numbers)  # Convert from list to set to remove duplicates, if any -          if not numbers: -            await ctx.invoke(self.bot.get_command('help'), 'issue') -            return +            return FetchIssueErrors.value_error          if len(numbers) > MAX_REQUESTS: -            embed = discord.Embed( -                title=random.choice(ERROR_REPLIES), -                color=Colours.soft_red, -                description=f"Too many issues/PRs! (maximum of {MAX_REQUESTS})" -            ) -            await ctx.send(embed=embed) -            return +            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 await ctx.send(f"[{str(r.status)}] #{number} {BAD_RESPONSE.get(r.status)}") +                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 @@ -92,15 +125,80 @@ class Issues(commands.Cog):              issue_url = json_data.get("html_url")              links.append([icon_url, f"[{repository}] #{number} {json_data.get('title')}", issue_url]) -        # Issue/PR format: emoji to show if open/closed/merged, number and the title as a singular link. -        description_list = ["{0} [{1}]({2})".format(*link) for link in links] +        return links + +    @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]          resp = discord.Embed(              colour=Colours.bright_green,              description='\n'.join(description_list)          )          resp.set_author(name="GitHub", url=f"https://github.com/{user}/{repository}") -        await ctx.send(embed=resp) +        return resp + +    @commands.command(aliases=("pr",)) +    async def issue( +            self, +            ctx: commands.Context, +            numbers: commands.Greedy[int], +            repository: str = "sir-lancebot", +            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') + +        elif result == FetchIssueErrors.max_requests: +            embed = discord.Embed( +                title=random.choice(ERROR_REPLIES), +                color=Colours.soft_red, +                description=f"Too many issues/PRs! (maximum of {MAX_REQUESTS})" +            ) +            await ctx.send(embed=embed) + +        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) + +    @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 <repo>#<issue>.""" +        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 = [] + +        if 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") +                    if isinstance(result, list): +                        links.extend(result) + +        if not links: +            return + +        resp = self.get_embed(links, "python-discord") +        await message.channel.send(embed=resp)  def setup(bot: commands.Bot) -> None: | 
