diff --git a/CHANGELOG.md b/CHANGELOG.md index 9051b0c..63a6c1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - `automod`: Added support for JSON paths for easier configuration of rules +### Changed + +- `jira`: Overhauled the issue requesting logic and the embed colors now reflect the status of the bug + ## [0.17.0] - 2021-09-02 ### Added diff --git a/commanderbot/ext/jira/__init__.py b/commanderbot/ext/jira/__init__.py index eab85ed..cd77c27 100644 --- a/commanderbot/ext/jira/__init__.py +++ b/commanderbot/ext/jira/__init__.py @@ -1,7 +1,8 @@ from discord.ext.commands import Bot +from commanderbot.core.utils import add_configured_cog from commanderbot.ext.jira.jira_cog import JiraCog def setup(bot: Bot): - bot.add_cog(JiraCog(bot)) + add_configured_cog(bot, __name__, JiraCog) diff --git a/commanderbot/ext/jira/jira_client.py b/commanderbot/ext/jira/jira_client.py new file mode 100644 index 0000000..e55d364 --- /dev/null +++ b/commanderbot/ext/jira/jira_client.py @@ -0,0 +1,88 @@ +from datetime import datetime + +import aiohttp + +from commanderbot.ext.jira.jira_issue import JiraIssue, StatusColor +from commanderbot.lib.responsive_exception import ResponsiveException + + +class JiraException(ResponsiveException): + pass + + +class IssueNotFound(JiraException): + def __init__(self, issue_id: str): + self.issue_id = issue_id + super().__init__(f"`{self.issue_id}` does not exist or it may be private") + + +class ConnectionError(JiraException): + def __init__(self, url: str): + self.url = url + super().__init__(f"Could not connect to `{self.url}`") + + +class RequestError(JiraException): + def __init__(self, issue_id: str): + self.issue_id = issue_id + super().__init__(f"There was an error while requesting `{self.issue_id}`") + + +class JiraClient: + def __init__(self, url: str): + self.url = url + + async def _request_issue_data(self, issue_id: str) -> dict: + try: + issue_url: str = f"{self.url}/rest/api/latest/issue/{issue_id}" + async with aiohttp.ClientSession() as session: + async with session.get(issue_url, raise_for_status=True) as response: + return await response.json() + + except aiohttp.ClientResponseError: + raise IssueNotFound(issue_id) + + except aiohttp.ClientConnectorError: + raise ConnectionError(self.url) + + except aiohttp.ClientError: + raise RequestError(issue_id) + + async def get_issue(self, issue_id: str) -> JiraIssue: + data: dict = await self._request_issue_data(issue_id) + fields: dict = data["fields"] + + assignee: str = "Unassigned" + if user := fields.get("assignee"): + assignee = user["displayName"] + + resolution: str = "Unresolved" + if res := fields.get("resolution"): + resolution = res["name"] + + since_version: str = "None" + if ver := fields.get("versions"): + since_version = ver[0]["name"] + + fix_version: str = "None" + if ver := fields.get("fixVersions"): + fix_version = ver[-1]["name"] + + return JiraIssue( + issue_id=issue_id, + url=f"{self.url}/browse/{issue_id}", + icon_url=f"{self.url}/jira-favicon-hires.png", + summary=fields["summary"], + reporter=fields["reporter"]["displayName"], + assignee=assignee, + created=datetime.strptime(fields["created"], "%Y-%m-%dT%H:%M:%S.%f%z"), + updated=datetime.strptime(fields["updated"], "%Y-%m-%dT%H:%M:%S.%f%z"), + status=fields["status"]["name"], + status_color=StatusColor.from_str( + fields["status"]["statusCategory"]["colorName"] + ), + resolution=resolution, + since_version=since_version, + fix_version=fix_version, + votes=fields["votes"]["votes"], + ) diff --git a/commanderbot/ext/jira/jira_cog.py b/commanderbot/ext/jira/jira_cog.py index 4904c7e..7d57592 100644 --- a/commanderbot/ext/jira/jira_cog.py +++ b/commanderbot/ext/jira/jira_cog.py @@ -1,109 +1,49 @@ -import aiohttp +from logging import Logger, getLogger + from discord import Embed from discord.ext.commands import Bot, Cog, Context, command +from commanderbot.ext.jira.jira_client import JiraClient +from commanderbot.ext.jira.jira_issue import JiraIssue + class JiraCog(Cog, name="commanderbot.ext.jira"): - def __init__(self, bot: Bot): + def __init__(self, bot: Bot, **options): self.bot: Bot = bot + self.log: Logger = getLogger(self.qualified_name) - self.resolution_table = { - "https://bugs.mojang.com/rest/api/2/resolution/1": "Fixed", - "https://bugs.mojang.com/rest/api/2/resolution/2": "Won't Fix", - "https://bugs.mojang.com/rest/api/2/resolution/3": "Duplicate", - "https://bugs.mojang.com/rest/api/2/resolution/4": "Incomplete", - "https://bugs.mojang.com/rest/api/2/resolution/5": "Cannot Reproduce", - "https://bugs.mojang.com/rest/api/2/resolution/6": "Works as Intended", - "https://bugs.mojang.com/rest/api/2/resolution/7": "Invalid", - "https://bugs.mojang.com/rest/api/2/resolution/10001": "Awaiting Response", - "https://bugs.mojang.com/rest/api/2/resolution/10003": "Done", - } - - self.status_table = { - "https://bugs.mojang.com/rest/api/2/status/1": "Open", - "https://bugs.mojang.com/rest/api/2/status/3": "In Progress", - "https://bugs.mojang.com/rest/api/2/status/4": "Reopened", - "https://bugs.mojang.com/rest/api/2/status/5": "Resolved", - "https://bugs.mojang.com/rest/api/2/status/6": "Closed", - "https://bugs.mojang.com/rest/api/2/status/10200": "Postponed", - } + # Get the URL from the config + url = options.get("url", "") + if not url: + # Log an error if the URL doesn't exist + self.log.error("No Jira URL was given in the bot config") - # TODO think about keeping a global `session` object for the cog - async def _request_data(self, url): - async with aiohttp.ClientSession() as session: - async with session.get(url) as response: - return await response.json() - - # TODO remove hardcoded status and resolve values and add config for JIRA URL and bug ID format + # Create the Jira client + self.jira_client: JiraClient = JiraClient(url) + @command(name="jira", aliases=["bug"]) - async def cmd_jira(self, ctx: Context, bug_id: str): - # Assume the parameter is a URL, so get the ID from it - if "/" in bug_id: - bug_id = bug_id.split("/")[-1] - - try: - data = await self._request_data( - f"https://bugs.mojang.com/rest/api/latest/issue/{bug_id}" - ) - report_data = data["fields"] - - # Bug report doesn't exist - except KeyError: - await ctx.send( - f"**{bug_id.upper()}** is not accessible." - " This may be due to it being private or it may not exist." - ) - return - - title = f"[{bug_id.upper()}] {report_data['summary']}" - if report_data["assignee"] is None: - assignee = "Unassigned" - else: - assignee = report_data["assignee"]["name"] - reporter = report_data["reporter"]["displayName"] - creation_date = report_data["created"][:10] - since_version = report_data["versions"][0]["name"] - - jira_embed = Embed( - title=title, url=f"https://bugs.mojang.com/browse/{bug_id}", color=0x00ACED + async def cmd_jira(self, ctx: Context, issue_id: str): + # Make uppercase so the project ID is valid + issue_id = issue_id.upper() + + # Try to get the issue + issue: JiraIssue = await self.jira_client.get_issue(issue_id) + + # Create embed title and limit it to 256 characters + title: str = f"[{issue.issue_id}] {issue.summary}" + if len(title) > 256: + title = f"{title[:253]}..." + + # Create issue embed + issue_embed: Embed = Embed( + title=title, + url=issue.url, + color=issue.status_color.value, ) - jira_embed.add_field(name="Reporter", value=reporter, inline=True) - jira_embed.add_field(name="Assignee", value=assignee, inline=True) - jira_embed.add_field(name="Created On", value=creation_date, inline=True) - jira_embed.add_field(name="Since Version", value=since_version, inline=True) - - # The bug report is still open - if report_data["resolution"] is None: - status = self.status_table[report_data["status"]["self"]] - votes = report_data["votes"]["votes"] - """ - if not report_data["customfield_10500"]: - confirmation = "Unconfirmed" - confirmation = report_data["customfield_10500"]["value"] - """ + issue_embed.set_thumbnail(url=issue.icon_url) - jira_embed.add_field(name="Status", value=status, inline=True) - jira_embed.add_field(name="Votes", value=votes, inline=True) - # jira_embed.add_field(name="Confirmation", value=confirmation, inline=True) + for k, v in issue.fields.items(): + issue_embed.add_field(name=k, value=v) - # The bug report is closed - else: - resolution_status = self.resolution_table[report_data["resolution"]["self"]] - resolve_date = report_data["resolutiondate"][:10] - if not report_data["fixVersions"]: - fix_version = "None" - else: - fix_version = report_data["fixVersions"][0]["name"] - - jira_embed.add_field( - name="Resolution", value=resolution_status, inline=True - ) - jira_embed.add_field(name="Resolved On", value=resolve_date, inline=True) - jira_embed.add_field(name="Fix Version", value=fix_version, inline=True) - - jira_embed.set_footer( - text=str(ctx.message.author), - icon_url=str(ctx.message.author.display_avatar.url), - ) - await ctx.send(embed=jira_embed) + await ctx.send(embed=issue_embed) diff --git a/commanderbot/ext/jira/jira_issue.py b/commanderbot/ext/jira/jira_issue.py new file mode 100644 index 0000000..d3fbd77 --- /dev/null +++ b/commanderbot/ext/jira/jira_issue.py @@ -0,0 +1,54 @@ +from dataclasses import dataclass +from datetime import datetime +from enum import Enum + + +class StatusColor(Enum): + UNKNOWN = 0x00ACED + MEDIUM_GRAY = 0x42526E + BLUE_GRAY = 0x42526E + GREEN = 0x00875A + WARM_RED = 0xDE350B + YELLOW = 0x0052CC + BROWN = 0xFF991F + + @classmethod + def from_str(cls, color: str): + color = color.replace("-", "_").upper() + try: + return cls[color] + except KeyError: + return cls.UNKNOWN + + +@dataclass +class JiraIssue: + issue_id: str + url: str + icon_url: str + summary: str + reporter: str + assignee: str + created: datetime + updated: datetime + status: str + status_color: StatusColor + resolution: str + since_version: str + fix_version: str + votes: int + + @property + def fields(self) -> dict: + return { + "Reported by": self.reporter, + "Assigned to": self.assignee, + "Created": f"", + "Updated": f"", + "Since version": self.since_version, + "Fix version": self.fix_version, + "Status": self.status, + "Resolution": self.resolution, + "Votes": self.votes, + } + \ No newline at end of file