Ability to mention a GitHub issue

This commit is contained in:
JelNiSław 2022-06-03 22:40:54 +02:00
parent 121eae99da
commit 61dcdcd190
No known key found for this signature in database
GPG key ID: EA41571A0A88E97E
5 changed files with 146 additions and 14 deletions

View file

@ -29,7 +29,9 @@ class Wulkabot(commands.Bot):
async def on_connect(self) -> None: async def on_connect(self) -> None:
print(f"Connected as {self.user}") print(f"Connected as {self.user}")
async def on_command_error(self, context: commands.Context, exception: commands.errors.CommandError, /) -> None: async def on_command_error(
self, context: commands.Context, exception: commands.errors.CommandError, /
) -> None:
await context.send(f"Error! {exception}") await context.send(f"Error! {exception}")
async def close(self) -> None: async def close(self) -> None:

View file

@ -31,7 +31,9 @@ class Development(commands.Cog):
commands = await self.bot.tree.sync(guild=interaction.guild if current_guild else None) commands = await self.bot.tree.sync(guild=interaction.guild if current_guild else None)
commands_str = ", ".join(c.name for c in commands) commands_str = ", ".join(c.name for c in commands)
destination = "guild" if current_guild else "global" destination = "guild" if current_guild else "global"
await interaction.response.send_message(f"Synced **{len(commands)} {destination}** commands\n{commands_str}") await interaction.response.send_message(
f"Synced **{len(commands)} {destination}** commands\n{commands_str}"
)
@app_commands.command() @app_commands.command()
async def reload(self, interaction: discord.Interaction): async def reload(self, interaction: discord.Interaction):

View file

@ -6,6 +6,7 @@ Copyright (C) 2022-present Stanisław Jelnicki
import re import re
from typing import Any from typing import Any
import aiohttp
import discord import discord
from discord.ext import commands from discord.ext import commands
@ -13,12 +14,68 @@ from .. import bot
from ..utils import github from ..utils import github
from ..utils.views import DeleteButton from ..utils.views import DeleteButton
GITHUB_REPO = re.compile(r"(?:\s|^)(?P<owner>[\w-]+)/(?P<repo>[\w-]+)(?:\s|$)", re.ASCII) Repo = tuple[str, str]
DEFAULT_REPO: Repo = ("wulkanowy", "wulkanowy")
def match_repo(text: str) -> tuple[str, str] | None: def parse_repo(text: str, *, default_owner: str | None = None) -> Repo | None:
if match := GITHUB_REPO.search(text): """
return (match["owner"], match["repo"]) Parses repository name and owner
"wulkanowy/sdk" => ("wulkanowy", "sdk")
"sdk" => None
"sdk", default_owner="wulkanowy" => ("wulkanowy", "sdk")
"""
repo = text.split("/")
match len(repo):
case 1:
if default_owner is not None:
owner, repo = default_owner, repo[0]
else:
return None
case 2:
owner, repo = repo
case _:
return None
return (owner, repo)
def parse_issue(
text: str, *, default_owner: str | None = None, default_repo: str | None = None
) -> tuple[Repo, int] | None:
"""
Parses an issue or a pull request string
"""
if "#" not in text:
return None
repo, issue_number = text.rsplit("#", 1)
try:
issue_number = int(issue_number)
except ValueError:
return None
if issue_number <= 0:
return None
if repo:
repo = parse_repo(repo, default_owner=default_owner)
if repo is None:
return None
elif default_owner is None or default_repo is None:
return None
else:
repo = (default_owner, default_repo)
return (repo, issue_number)
def find_repo_in_channel_topic(topic: str) -> Repo | None:
key = "https://github.com/"
for word in topic.split():
if word.startswith(key):
return parse_repo(word[len(key) :])
class GitHub(commands.Cog): class GitHub(commands.Cog):
@ -56,6 +113,35 @@ class GitHub(commands.Cog):
.set_footer(text=footer) .set_footer(text=footer)
) )
def github_issue_embed(self, issue: dict[str, Any]) -> discord.Embed:
is_pull_request = "pull_request" in issue
title = f'{"Pull request" if is_pull_request else "Issue"} #{issue["number"]}\n{issue["title"][:128]}'
body = issue["body"]
if len(body) > 256:
body = None
color = None
if issue["state"] == "open":
color = 0x2DA44E # --color-open-emphasis
elif issue["state"] == "closed":
color = 0xCF222E # --color-closed-emphasis
if is_pull_request:
pull_request = issue["pull_request"]
if pull_request["merged_at"] is not None:
color = 0x8250DF # --color-done-emphasis
elif issue["draft"]:
color = 0x6E7781 # --color-neutral-emphasis
user = issue["user"]
comments = issue["comments"]
footer = f"💬 {comments}"
return (
discord.Embed(title=title, url=issue["html_url"], description=body, color=color)
.set_author(name=user["login"], url=user["html_url"], icon_url=user["avatar_url"])
.set_footer(text=footer)
)
def get_github_color(self, language: str | None) -> int | None: def get_github_color(self, language: str | None) -> int | None:
if language is None: if language is None:
return None return None
@ -69,11 +155,44 @@ class GitHub(commands.Cog):
if message.author.bot: if message.author.bot:
return return
if match := match_repo(message.content): words = message.content.split()
if repo := await self.github.fetch_repo(*match): embeds = []
view = DeleteButton(message.author)
reply = await message.reply(embed=self.github_repo_embed(repo), view=view) try:
view.message = reply topic = message.channel.topic # type: ignore
except AttributeError:
channel_repo = DEFAULT_REPO
else:
if topic is not None:
channel_repo = find_repo_in_channel_topic(topic)
if channel_repo is None:
channel_repo = DEFAULT_REPO
else:
channel_repo = DEFAULT_REPO
for word in words:
match = parse_issue(word, default_owner=channel_repo[0], default_repo=channel_repo[1])
if match is not None:
repo, issue_number = match
repo = repo or DEFAULT_REPO
try:
issue = await self.github.fetch_issue(*repo, issue_number)
except aiohttp.ClientResponseError:
continue
embeds.append(self.github_issue_embed(issue))
else:
match = parse_repo(word)
if match is not None:
try:
repo = await self.github.fetch_repo(*match)
except aiohttp.ClientResponseError:
continue
embeds.append(self.github_repo_embed(repo))
if embeds:
view = DeleteButton(message.author)
reply = await message.reply(embeds=embeds[:3], view=view)
view.message = reply
async def setup(bot: bot.Wulkabot): async def setup(bot: bot.Wulkabot):

View file

@ -3,14 +3,18 @@ Wulkabot
Copyright (C) 2022-present Stanisław Jelnicki Copyright (C) 2022-present Stanisław Jelnicki
""" """
from typing import Any
import aiohttp import aiohttp
class GitHub: class GitHub:
def __init__(self) -> None: def __init__(self) -> None:
self._http = aiohttp.ClientSession(base_url="https://api.github.com") self._http = aiohttp.ClientSession(
base_url="https://api.github.com", headers={"Accept": "application/vnd.github.v3+json"}
)
async def fetch_repo(self, owner: str, repo: str) -> dict[str, str | int | None]: async def fetch_repo(self, owner: str, repo: str) -> dict[str, Any]:
response = await self._http.get(f"/repos/{owner}/{repo}") response = await self._http.get(f"/repos/{owner}/{repo}")
response.raise_for_status() response.raise_for_status()
return await response.json() return await response.json()
@ -21,5 +25,10 @@ class GitHub:
branches = await response.json() branches = await response.json()
return [branch["name"] for branch in branches] return [branch["name"] for branch in branches]
async def fetch_issue(self, owner: str, repo: str, issue_number: int) -> dict[str, Any]:
response = await self._http.get(f"/repos/{owner}/{repo}/issues/{issue_number}")
response.raise_for_status()
return await response.json()
async def close(self): async def close(self):
await self._http.close() await self._http.close()

View file

@ -7,7 +7,7 @@ class DeleteButton(discord.ui.View):
self.invoker = invoker self.invoker = invoker
self.message: discord.Message | None = None self.message: discord.Message | None = None
def interaction_check(self, interaction: discord.Interaction) -> bool: async def interaction_check(self, interaction: discord.Interaction) -> bool:
return ( return (
interaction.user == self.invoker interaction.user == self.invoker
or isinstance(interaction.user, discord.Member) or isinstance(interaction.user, discord.Member)