Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Jira improvements #64

Merged
merged 9 commits into from
Sep 15, 2021
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion commanderbot/ext/jira/__init__.py
Original file line number Diff line number Diff line change
@@ -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)
88 changes: 88 additions & 0 deletions commanderbot/ext/jira/jira_client.py
Original file line number Diff line number Diff line change
@@ -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"],
)
132 changes: 36 additions & 96 deletions commanderbot/ext/jira/jira_cog.py
Original file line number Diff line number Diff line change
@@ -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)
54 changes: 54 additions & 0 deletions commanderbot/ext/jira/jira_issue.py
Original file line number Diff line number Diff line change
@@ -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"<t:{int(self.created.timestamp())}:R>",
"Updated": f"<t:{int(self.updated.timestamp())}:R>",
"Since version": self.since_version,
"Fix version": self.fix_version,
"Status": self.status,
"Resolution": self.resolution,
"Votes": self.votes,
}