🚧 add github api

This commit is contained in:
yanyongyu 2021-03-09 18:39:46 +08:00
parent f93241afdc
commit 6a4ef989c9
12 changed files with 495 additions and 12 deletions

View File

@ -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

View File

@ -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

322
src/libs/github/__init__.py Normal file
View File

@ -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

View File

@ -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

View File

@ -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):

View File

@ -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"

View File

@ -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)}"

View File

@ -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

View File

@ -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 账号")

View File

@ -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)

View File

@ -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<owner>[a-zA-Z0-9][a-zA-Z0-9\-]*)/(?P<repo>[a-zA-Z0-9_\-]+)#(?P<number>\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)}")
)

View File

@ -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()