Ability to mention a GitHub issue
This commit is contained in:
parent
121eae99da
commit
61dcdcd190
5 changed files with 146 additions and 14 deletions
|
@ -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:
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue