diff --git a/Dockerfile b/Dockerfile index b3b30c2..fd38b78 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM tiangolo/uvicorn-gunicorn-fastapi:python3.8 -RUN ./install_wkhtmltox buster_amd64 +RUN ./download_wkhtmltox buster_amd64 RUN dpkg -i wkhtmltox_*.deb diff --git a/docker-compose.yml b/docker-compose.yml index 077098f..fc3cbcc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,12 +6,12 @@ services: - "/etc/localtime:/etc/localtime" - "./:/app/" ports: - - "2333:2333" - env_file: - - ".env.prod" + - "$PORT:$PORT" environment: - ENVIRONMENT=prod + - HOST=$HOST + - PORT=$PORT - APP_MODULE=bot:app - - SECRET - - ACCESS_TOKEN + # - SECRET=$SECRET + # - ACCESS_TOKEN=$ACCESS_TOKEN network_mode: bridge diff --git a/src/libs/github/__init__.py b/src/libs/github/__init__.py new file mode 100644 index 0000000..5177e51 --- /dev/null +++ b/src/libs/github/__init__.py @@ -0,0 +1,322 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +@Author : yanyongyu +@Date : 2021-03-09 17:13:37 +@LastEditors : yanyongyu +@LastEditTime : 2021-03-09 18:36:19 +@Description : None +@GitHub : https://github.com/yanyongyu +""" +__author__ = "yanyongyu" + +import time +import datetime +from typing import List, Optional +from typing_extensions import Literal + +from .request import Requester + +DEFAULT_BASE_URL = "https://api.github.com" +DEFAULT_STATUS_URL = "https://status.github.com" +DEFAULT_TIMEOUT = 15 +DEFAULT_PER_PAGE = 30 + + +class Github: + + def __init__(self, + token_or_client_id: Optional[str] = None, + client_secret: Optional[str] = None, + base_url: str = DEFAULT_BASE_URL, + timeout: int = DEFAULT_TIMEOUT, + user_agent: str = "Python/GitHub", + per_page: int = DEFAULT_PER_PAGE, + retry: Optional[int] = None, + verify: bool = True): + self._requester = Requester(token_or_client_id, client_secret, base_url, + timeout, user_agent, per_page, verify, + retry) + + @property + def oauth_scopes(self): + """ + :type: list of string + """ + return self._requester.oauth_scopes + + def get_rate_limit(self): + """ + GET /rate_limit + + https://docs.github.com/en/rest/reference/rate-limit#get-rate-limit-status-for-the-authenticated-user + """ + headers, data = self._requester.requestJsonAndCheck( + "GET", "/rate_limit") + return RateLimit.RateLimit(self._requester, headers, data["resources"], + True) + + def get_license(self, key: str): + """ + GET /license/:license + + https://docs.github.com/en/rest/reference/licenses#get-a-license + """ + + assert isinstance(key, str), key + headers, data = self._requester.requestJsonAndCheck( + "GET", f"/licenses/{key}") + return github.License.License(self._requester, + headers, + data, + completed=True) + + def get_licenses(self): + """ + GET /licenses + + https://docs.github.com/en/rest/reference/licenses#get-all-commonly-used-licenses + """ + + url_parameters = dict() + + return github.PaginatedList.PaginatedList(github.License.License, + self._requester, "/licenses", + url_parameters) + + def get_events(self): + """ + GET /events + + https://docs.github.com/en/rest/reference/activity#list-public-events + """ + + return github.PaginatedList.PaginatedList(github.Event.Event, + self._requester, "/events", + None) + + def get_user(self, username: Optional[str] = None): + """ + GET /users/:user + + https://docs.github.com/en/rest/reference/users#get-a-user + + GET /user + https://docs.github.com/en/rest/reference/users#get-the-authenticated-user + """ + if not username: + return AuthenticatedUser.AuthenticatedUser(self._requester, {}, + {"url": "/user"}, + completed=False) + else: + headers, data = self._requester.requestJsonAndCheck( + "GET", f"/users/{username}") + return github.NamedUser.NamedUser(self._requester, + headers, + data, + completed=True) + + def get_users(self, since: Optional[int] = None): + """ + GET /users + + https://docs.github.com/en/rest/reference/users#list-users + """ + url_parameters = dict() + if since is not None: + url_parameters["since"] = since + return github.PaginatedList.PaginatedList(github.NamedUser.NamedUser, + self._requester, "/users", + url_parameters) + + def get_organization(self, org): + """ + GET /orgs/:org + + https://docs.github.com/en/rest/reference/orgs#get-an-organization + """ + headers, data = self._requester.requestJsonAndCheck( + "GET", f"/orgs/{org}") + return github.Organization.Organization(self._requester, + headers, + data, + completed=True) + + def get_organizations(self, since: Optional[int] = None): + """ + GET /organizations + + https://docs.github.com/en/rest/reference/orgs#list-organizations + """ + url_parameters = dict() + if since is not None: + url_parameters["since"] = since + return github.PaginatedList.PaginatedList( + github.Organization.Organization, + self._requester, + "/organizations", + url_parameters, + ) + + def get_repo(self, full_name: str, lazy: bool = False): + """ + GET /repos/:owner/:repo + + https://docs.github.com/en/rest/reference/repos#get-a-repository + """ + url = f"/repos/{full_name}" + if lazy: + return Repository.Repository(self._requester, {}, {"url": url}, + completed=False) + headers, data = self._requester.requestJsonAndCheck("GET", url) + return Repository.Repository(self._requester, + headers, + data, + completed=True) + + def get_repos( + self, + since: Optional[int] = None, + visibility: Literal["all", "public", "private", None] = None, + affiliation: Optional[List[Literal["owner", "collaborator", + "organization_member"]]] = None, + type: Optional[Literal["all", "owner", "public", "private", + "member"]] = None, + sort: Optional[Literal["created", "updated", "pushed", + "full_name"]] = None, + direction: Optional[Literal["asc", "desc"]] = None): + """ + GET /user/repos + + https://docs.github.com/en/rest/reference/repos#list-repositories-for-the-authenticated-user + """ + url_parameters = dict() + if since is not None: + url_parameters["since"] = since + if visibility is not None: + url_parameters["visibility"] = visibility + if affiliation is not None: + url_parameters["affiliation"] = ",".join(affiliation) + if type is not None: + url_parameters["type"] = type + if sort is not None: + url_parameters["sort"] = sort + if direction is not None: + url_parameters["direction"] = direction + return github.PaginatedList.PaginatedList( + github.Repository.Repository, + self._requester, + "/repositories", + url_parameters, + ) + + def get_project(self, id: int): + """ + GET /projects/:project_id + + https://docs.github.com/en/rest/reference/projects#get-a-project + """ + headers, data = self._requester.requestJsonAndCheck( + "GET", + f"/projects/{id}", + headers={"Accept": Consts.mediaTypeProjectsPreview}, + ) + return github.Project.Project(self._requester, + headers, + data, + completed=True) + + def get_project_column(self, id): + """ + GET /projects/columns/:column_id + + https://docs.github.com/en/rest/reference/projects#get-a-project-column + """ + headers, data = self._requester.requestJsonAndCheck( + "GET", + "/projects/columns/%d" % id, + headers={"Accept": Consts.mediaTypeProjectsPreview}, + ) + return github.ProjectColumn.ProjectColumn(self._requester, + headers, + data, + completed=True) + + def get_gist(self, id: str): + """ + GET /gists/:id + + https://docs.github.com/en/rest/reference/gists#get-a-gist + """ + headers, data = self._requester.requestJsonAndCheck( + "GET", f"/gists/{id}") + return github.Gist.Gist(self._requester, headers, data, completed=True) + + def get_gists(self, since: Optional[datetime.datetime] = None): + """ + GET /gists/public + + https://docs.github.com/en/rest/reference/gists#list-public-gists + """ + url_parameters = dict() + if since: + url_parameters["since"] = since.strftime("%Y-%m-%dT%H:%M:%SZ") + return github.PaginatedList.PaginatedList(github.Gist.Gist, + self._requester, + "/gists/public", + url_parameters) + + def render_markdown(self, + text: str, + context: Optional[github.Repository.Repository] = None): + """ + POST /markdown + + https://docs.github.com/en/rest/reference/markdown#render-a-markdown-document + """ + post_parameters = {"text": text} + if context: + post_parameters["mode"] = "gfm" + post_parameters["context"] = context._identity + status, headers, data = self._requester.requestJson( + "POST", "/markdown", input=post_parameters) + return data + + def get_hook(self, full_name: str, id: str): + """ + GET /repo/:full_name/hooks/:name + + https://docs.github.com/en/rest/reference/repos#get-a-repository-webhook + """ + headers, attributes = self._requester.requestJsonAndCheck( + "GET", f"/repos/{full_name}/hooks/{id}") + return HookDescription.HookDescription(self._requester, + headers, + attributes, + completed=True) + + def get_hooks(self): + """ + GET /repo/:full_name/hooks + + https://docs.github.com/en/rest/reference/repos#list-repository-webhooks + """ + headers, data = self._requester.requestJsonAndCheck( + "GET", f"/repo/{full_name}/hooks") + return [ + HookDescription.HookDescription(self._requester, + headers, + attributes, + completed=True) + for attributes in data + ] + + def get_emojis(self): + """ + GET /emojis + + https://docs.github.com/en/rest/reference/emojis#get-emojis + """ + headers, attributes = self._requester.requestJsonAndCheck( + "GET", "/emojis") + return attributes diff --git a/src/libs/github/request.py b/src/libs/github/request.py new file mode 100644 index 0000000..9e271de --- /dev/null +++ b/src/libs/github/request.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +@Author : yanyongyu +@Date : 2021-03-09 17:34:53 +@LastEditors : yanyongyu +@LastEditTime : 2021-03-09 18:36:58 +@Description : None +@GitHub : https://github.com/yanyongyu +""" +__author__ = "yanyongyu" + +from typing import Optional + + +class Requester: + + def __init__(self, token_or_client_id: Optional[str], + client_secret: Optional[str], base_url: str, timeout: int, + user_agent: str, per_page: int, retry: Optional[int], + verify: bool): + pass diff --git a/src/plugins/github/config.py b/src/plugins/github/config.py index 8700b0f..b29fb87 100644 --- a/src/plugins/github/config.py +++ b/src/plugins/github/config.py @@ -4,17 +4,22 @@ @Author : yanyongyu @Date : 2020-09-21 19:05:28 @LastEditors : yanyongyu -@LastEditTime : 2020-10-04 15:10:41 +@LastEditTime : 2021-03-09 16:23:44 @Description : None @GitHub : https://github.com/yanyongyu """ __author__ = "yanyongyu" +from typing import Optional + from pydantic import validator, BaseSettings class Config(BaseSettings): github_command_priority: int = 5 + github_client_id: Optional[str] = None + github_client_secret: Optional[str] = None + github_redirect_uri: Optional[str] = None @validator("github_command_priority") def validate_priority(cls, v): diff --git a/src/plugins/github/libs/__init__.py b/src/plugins/github/libs/__init__.py new file mode 100644 index 0000000..b83e77c --- /dev/null +++ b/src/plugins/github/libs/__init__.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +@Author : yanyongyu +@Date : 2021-03-09 16:14:00 +@LastEditors : yanyongyu +@LastEditTime : 2021-03-09 16:32:54 +@Description : None +@GitHub : https://github.com/yanyongyu +""" +__author__ = "yanyongyu" diff --git a/src/plugins/github/libs/auth.py b/src/plugins/github/libs/auth.py new file mode 100644 index 0000000..e39ade9 --- /dev/null +++ b/src/plugins/github/libs/auth.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +@Author : yanyongyu +@Date : 2021-03-09 16:30:16 +@LastEditors : yanyongyu +@LastEditTime : 2021-03-09 16:32:14 +@Description : None +@GitHub : https://github.com/yanyongyu +""" +__author__ = "yanyongyu" + +import urllib.parse + +from .. import github_config as config + +assert config.github_client_id and config.github_client_secret and config.github_redirect_uri, "GitHub OAuth Application info not fully provided! OAuth plugin will not work!" + + +async def get_auth_link(username: str) -> str: + query = { + "client_id": config.github_client_id, + "redirect_uri": config.github_redirect_uri, + # FIXME: encode username? + "state": username + } + return f"https://github.com/login/oauth/authorize?{urllib.parse.urlencode(query)}" diff --git a/src/plugins/github/libs/issue.py b/src/plugins/github/libs/issue.py new file mode 100644 index 0000000..921871a --- /dev/null +++ b/src/plugins/github/libs/issue.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +@Author : yanyongyu +@Date : 2021-03-09 16:45:25 +@LastEditors : yanyongyu +@LastEditTime : 2021-03-09 16:54:13 +@Description : None +@GitHub : https://github.com/yanyongyu +""" +__author__ = "yanyongyu" + + +async def get_issue(owner: str, repo: str, number: int): + # TODO + pass diff --git a/src/plugins/github/plugins/github_auth/__init__.py b/src/plugins/github/plugins/github_auth/__init__.py new file mode 100644 index 0000000..654f546 --- /dev/null +++ b/src/plugins/github/plugins/github_auth/__init__.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +@Author : yanyongyu +@Date : 2021-03-09 16:06:34 +@LastEditors : yanyongyu +@LastEditTime : 2021-03-09 16:35:50 +@Description : None +@GitHub : https://github.com/yanyongyu +""" +__author__ = "yanyongyu" + +from nonebot import on_command +from nonebot.adapters.cqhttp import Bot, PrivateMessageEvent, GroupMessageEvent + +from ...libs.auth import get_auth_link +from ... import github_config as config + +auth = on_command("auth", priority=config.github_command_priority) +auth.__doc__ = """ +/auth +登录 github 账号 +""" + + +@auth.handle() +async def handle_private(bot: Bot, event: PrivateMessageEvent): + await auth.finish("请前往以下链接进行授权:\n" + + await get_auth_link(event.get_user_id())) + + +@auth.handle() +async def handle_group(bot: Bot, event: GroupMessageEvent): + await auth.finish("请私聊我并使用 /auth 命令登录你的 GitHub 账号") diff --git a/src/plugins/github/plugins/github_help/__init__.py b/src/plugins/github/plugins/github_help/__init__.py index d7ec2fd..eceb651 100644 --- a/src/plugins/github/plugins/github_help/__init__.py +++ b/src/plugins/github/plugins/github_help/__init__.py @@ -4,7 +4,7 @@ @Author : yanyongyu @Date : 2020-09-21 00:05:16 @LastEditors : yanyongyu -@LastEditTime : 2021-03-06 22:26:21 +@LastEditTime : 2021-03-09 16:43:58 @Description : None @GitHub : https://github.com/yanyongyu """ @@ -14,7 +14,7 @@ import inspect from functools import reduce from nonebot import on_command -from nonebot.adapters import Bot +from nonebot.adapters.cqhttp import Bot from ... import _sub_plugins, github_config as config @@ -28,8 +28,8 @@ help.__doc__ = """ @help.handle() async def handle(bot: Bot): matchers = reduce(lambda x, y: x.union(y.matcher), _sub_plugins, set()) - docs = "命令列表:\n" - docs += "\n".join( + docs = "命令列表:\n\n" + docs += "\n\n".join( map(lambda x: inspect.cleandoc(x.__doc__), filter(lambda x: x.__doc__, matchers))) await help.finish(docs) diff --git a/src/plugins/github/plugins/github_issue/__init__.py b/src/plugins/github/plugins/github_issue/__init__.py new file mode 100644 index 0000000..b8d2c82 --- /dev/null +++ b/src/plugins/github/plugins/github_issue/__init__.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +@Author : yanyongyu +@Date : 2021-03-09 15:15:02 +@LastEditors : yanyongyu +@LastEditTime : 2021-03-09 16:37:02 +@Description : None +@GitHub : https://github.com/yanyongyu +""" +__author__ = "yanyongyu" + +import base64 +from typing import Dict + +from nonebot import on_regex +from nonebot.typing import T_State +from nonebot.adapters.cqhttp import Bot, MessageEvent, MessageSegment + +from src.libs import md2img +from ... import github_config as config + +issue = on_regex( + r"^(?P[a-zA-Z0-9][a-zA-Z0-9\-]*)/(?P[a-zA-Z0-9_\-]+)#(?P\d+)$", + priority=config.github_command_priority) +issue.__doc__ = """ +^owner/repo#number$ +获取指定仓库 issue / pr +""" + + +@issue.handle() +async def handle(bot: Bot, event: MessageEvent, state: T_State): + group: Dict[str, str] = state["_matched_dict"] + owner = group["owner"] + repo = group["repo"] + number = group["number"] + # TODO: Get user token (optional) + token = None + # TODO: Get issue content + issue_content = "" + img = await md2img.from_string(issue_content) + await issue.finish(MessageSegment.image(f"base64://{base64.b64encode(img)}") + ) diff --git a/src/plugins/nonebot_plugin_sentry/__init__.py b/src/plugins/nonebot_plugin_sentry/__init__.py index 94be69e..45b1115 100644 --- a/src/plugins/nonebot_plugin_sentry/__init__.py +++ b/src/plugins/nonebot_plugin_sentry/__init__.py @@ -4,7 +4,7 @@ @Author : yanyongyu @Date : 2020-11-23 18:44:25 @LastEditors : yanyongyu -@LastEditTime : 2020-11-23 22:18:01 +@LastEditTime : 2021-03-09 16:39:07 @Description : None @GitHub : https://github.com/yanyongyu """ @@ -21,6 +21,8 @@ driver = get_driver() global_config = driver.config config = Config(**global_config.dict()) +assert config.sentry_dsn, "Sentry DSN must provided!" + sentry_sdk.init(**{ key[7:]: value for key, value in config.dict().items()