From 773579e44a83c669453babcd5f4148e0e23f07e1 Mon Sep 17 00:00:00 2001 From: jingfelix Date: Tue, 12 Mar 2024 01:49:20 +0800 Subject: [PATCH] feat: port to kook Signed-off-by: jingfelix --- Dockerfile | 4 +- README.md | 4 +- pyproject.toml | 12 +- src/{repo2lark => repo2kook}/__init__.py | 6 +- src/repo2kook/config.py | 24 ++ src/{repo2lark => repo2kook}/middleware.py | 4 +- src/{repo2lark => repo2kook}/models.py | 0 src/{repo2lark => repo2kook}/router.py | 52 ++--- src/{repo2lark => repo2kook}/utils.py | 69 +++--- src/repo2lark/config.py | 29 --- src/repo2lark/template.json | 242 --------------------- 11 files changed, 103 insertions(+), 343 deletions(-) rename src/{repo2lark => repo2kook}/__init__.py (52%) create mode 100644 src/repo2kook/config.py rename src/{repo2lark => repo2kook}/middleware.py (85%) rename src/{repo2lark => repo2kook}/models.py (100%) rename src/{repo2lark => repo2kook}/router.py (75%) rename src/{repo2lark => repo2kook}/utils.py (57%) delete mode 100644 src/repo2lark/config.py delete mode 100644 src/repo2lark/template.json diff --git a/Dockerfile b/Dockerfile index bda9e5e..4bb3eb8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,6 +12,6 @@ ENV PYTHONUNBUFFERED=1 # Install pip requirements COPY requirements.txt . -RUN python -m pip install --no-cache-dir --upgrade repo2lark +RUN python -m pip install --no-cache-dir --upgrade repo2kook -CMD ["uvicorn", "repo2lark:app", "--port", "3030", "--host", "0.0.0.0", "--workers", "4"] +CMD ["uvicorn", "repo2kook:app", "--port", "3030", "--host", "0.0.0.0", "--workers", "4"] diff --git a/README.md b/README.md index f7247d4..340cb71 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@
-

Repo2Lark

+

Repo2Kook

PyPI PyPI - License pdm-managed Zeabur-Deployed
- 优雅地转发 GitHub Webhook 事件到飞书 + 优雅地转发 GitHub Webhook 事件到 Kook
diff --git a/pyproject.toml b/pyproject.toml index aa40134..16c1dec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] -name = "Repo2Lark" -version = "0.0.2" -description = "Default template for PDM package" +name = "Repo2Kook" +version = "0.0.1" +description = "Send GitHub Webhook to Kook" authors = [ {name = "jingfelix", email = "jingfelix@outlook.com"}, ] @@ -18,11 +18,11 @@ readme = "README.md" license = {text = "GPLv3"} [project.urls] -Homepage = "https://github.com/jingfelix/Repo2Lark" -"Bug Tracker" = "https://github.com/jingfelix/Repo2Lark" +Homepage = "https://github.com/jingfelix/Repo2Kook" +"Bug Tracker" = "https://github.com/jingfelix/Repo2Kook" [project.scripts] -repo2lark = "repo2lark.__init__:run" +repo2kook = "repo2kook.__init__:run" [build-system] requires = ["pdm-backend"] diff --git a/src/repo2lark/__init__.py b/src/repo2kook/__init__.py similarity index 52% rename from src/repo2lark/__init__.py rename to src/repo2kook/__init__.py index be02cb8..9fd3476 100644 --- a/src/repo2lark/__init__.py +++ b/src/repo2kook/__init__.py @@ -2,8 +2,8 @@ app = FastAPI(docs_url=None) -from repo2lark.middleware import VerifySignatureMiddleware -from repo2lark.router import router +from .middleware import VerifySignatureMiddleware +from .router import router app.include_router(router) app.add_middleware(VerifySignatureMiddleware) @@ -12,4 +12,4 @@ def run(): import uvicorn - uvicorn.run("repo2lark:app", host="0.0.0.0", port=3030, reload=True) + uvicorn.run("repo2kook:app", host="0.0.0.0", port=3030, reload=True) diff --git a/src/repo2kook/config.py b/src/repo2kook/config.py new file mode 100644 index 0000000..4b20f7d --- /dev/null +++ b/src/repo2kook/config.py @@ -0,0 +1,24 @@ +from functools import lru_cache + +from pydantic_settings import BaseSettings + + +@lru_cache() +def get_settings(): + return Settings() + + +class Settings(BaseSettings): + app_name: str = "r2kook" + + kook_api_base_url: str = "https://www.kookapp.cn/api/v3" + + kook_token: str = "" + + github_webhook_secret: str = "" + + class Config: + env_file = ".env" + + +settings = get_settings() diff --git a/src/repo2lark/middleware.py b/src/repo2kook/middleware.py similarity index 85% rename from src/repo2lark/middleware.py rename to src/repo2kook/middleware.py index 6f14646..b5fbbc1 100644 --- a/src/repo2lark/middleware.py +++ b/src/repo2kook/middleware.py @@ -1,8 +1,8 @@ from fastapi import Request from starlette.middleware.base import BaseHTTPMiddleware -from repo2lark.config import settings -from repo2lark.utils import get_body, verify_signature +from .config import settings +from .utils import get_body, verify_signature class VerifySignatureMiddleware(BaseHTTPMiddleware): diff --git a/src/repo2lark/models.py b/src/repo2kook/models.py similarity index 100% rename from src/repo2lark/models.py rename to src/repo2kook/models.py diff --git a/src/repo2lark/router.py b/src/repo2kook/router.py similarity index 75% rename from src/repo2lark/router.py rename to src/repo2kook/router.py index 1a9680f..e644978 100644 --- a/src/repo2lark/router.py +++ b/src/repo2kook/router.py @@ -1,22 +1,22 @@ import json import urllib.parse as urlparse -from typing import Optional from fastapi import APIRouter, BackgroundTasks, Request from fastapi.exceptions import HTTPException -from repo2lark.config import settings -from repo2lark.models import IssueCommentEvent, IssueEvent, PREvent, PushEvent -from repo2lark.utils import send_to_lark, truncate +from .config import settings +from .models import IssueCommentEvent, IssueEvent, PREvent, PushEvent +from .utils import send_to_kook, truncate router = APIRouter() -@router.post("/webhook") -@router.post("/open-apis/bot/v2/hook/{lark_webhook_token}") +# @router.post("/webhook") +# @router.post("/open-apis/bot/v2/hook/{lark_webhook_token}") +@router.post("/kook/{kook_channel_id}") async def webhook( request: Request, - lark_webhook_token: Optional[str] = None, + kook_channel_id: str = None, background_tasks: BackgroundTasks = None, ): headers = request.headers @@ -25,7 +25,7 @@ async def webhook( body = await request.body() return await webhook_urlencoded( request, - lark_webhook_token=lark_webhook_token, + kook_channel_id=kook_channel_id, payload=body.decode("utf-8"), background_tasks=background_tasks, ) @@ -35,7 +35,7 @@ async def webhook( payload = dict(urlparse.parse_qsl(body.decode("utf-8"))) return await webhook_urlencoded( request, - lark_webhook_token=lark_webhook_token, + kook_channel_id=kook_channel_id, payload=payload.get("payload", None), background_tasks=background_tasks, ) @@ -45,18 +45,14 @@ async def webhook( async def webhook_urlencoded( request: Request, - lark_webhook_token: Optional[str] = None, + kook_channel_id: str = None, payload: str = None, background_tasks: BackgroundTasks = None, ): headers = request.headers - if lark_webhook_token is not None: - lark_webhook_url = settings.lark_webhook_base_url + lark_webhook_token - lark_webhook_secret = None - else: - lark_webhook_url = settings.lark_webhook_url - lark_webhook_secret = settings.lark_webhook_secret + if kook_channel_id is None: + raise HTTPException(status_code=400, detail="kook_channel_id cannot be None") x_github_event = headers.get("X-GitHub-Event", None) if x_github_event is None: @@ -67,10 +63,8 @@ async def webhook_urlencoded( params = PushEvent(**json.loads(payload)) background_tasks.add_task( - send_to_lark, - settings.push_template_id, - lark_webhook_url=lark_webhook_url, - lark_webhook_secret=lark_webhook_secret, + send_to_kook, + kook_channel_id=kook_channel_id, variables={ "commiter": params.pusher.name, "repository": params.repository.full_name, @@ -87,10 +81,8 @@ async def webhook_urlencoded( params = IssueEvent(**json.loads(payload)) background_tasks.add_task( - send_to_lark, - settings.issue_template_id, - lark_webhook_url=lark_webhook_url, - lark_webhook_secret=lark_webhook_secret, + send_to_kook, + kook_channel_id=kook_channel_id, variables={ "action": params.action.capitalize(), "repository": params.repository.full_name, @@ -107,10 +99,8 @@ async def webhook_urlencoded( params = IssueCommentEvent(**json.loads(payload)) background_tasks.add_task( - send_to_lark, - settings.issue_comment_template_id, - lark_webhook_url=lark_webhook_url, - lark_webhook_secret=lark_webhook_secret, + send_to_kook, + kook_channel_id=kook_channel_id, variables={ "action": params.action.capitalize(), "user": params.comment.user.login, @@ -127,10 +117,8 @@ async def webhook_urlencoded( params = PREvent(**json.loads(payload)) background_tasks.add_task( - send_to_lark, - settings.pr_template_id, - lark_webhook_url=lark_webhook_url, - lark_webhook_secret=lark_webhook_secret, + send_to_kook, + kook_channel_id=kook_channel_id, variables={ "action": params.action.capitalize(), "user": params.pull_request.user.login, diff --git a/src/repo2lark/utils.py b/src/repo2kook/utils.py similarity index 57% rename from src/repo2lark/utils.py rename to src/repo2kook/utils.py index ae88429..a5f721d 100644 --- a/src/repo2lark/utils.py +++ b/src/repo2kook/utils.py @@ -8,6 +8,8 @@ from starlette.requests import Request from starlette.types import Message +from .config import settings + def verify_signature(payload_body, secret_token, signature_header) -> None: """Verify that the payload was sent from GitHub by validating SHA256. @@ -42,34 +44,51 @@ def gen_sign(timestamp, secret): return sign -async def send_to_lark( - template_id: str, lark_webhook_url: str, lark_webhook_secret: str, variables: dict -) -> None: - data = { - "msg_type": "interactive", - "card": { - "type": "template", - "data": { - "template_id": template_id, - "template_variable": variables, - }, - }, +# async def send_to_lark( +# template_id: str, lark_webhook_url: str, lark_webhook_secret: str, variables: dict +# ) -> None: +# data = { +# "msg_type": "interactive", +# "card": { +# "type": "template", +# "data": { +# "template_id": template_id, +# "template_variable": variables, +# }, +# }, +# } +# if lark_webhook_secret != "" and lark_webhook_secret is not None: +# timestamp = str(int(time.time())) +# sign = gen_sign(timestamp, lark_webhook_secret) +# data["timestamp"] = timestamp +# data["sign"] = sign + +# if lark_webhook_url == "" or lark_webhook_url is None: +# raise HTTPException(status_code=500, detail="lark_webhook_url is empty!") + +# # TODO 增加超时和重试 +# async with httpx.AsyncClient(timeout=15) as client: +# res = await client.post(lark_webhook_url, json=data) + +# if res.status_code != 200 or res.json()["code"] != 0: +# raise HTTPException(status_code=500, detail=res.text) + + +async def send_to_kook(kook_channel_id: str, variables: dict) -> None: + """Send card message to Kook.""" + + json = { + "type": 1, + "target_id": kook_channel_id, + "content": variables.get("action", "Action not Found"), } - if lark_webhook_secret != "" and lark_webhook_secret is not None: - timestamp = str(int(time.time())) - sign = gen_sign(timestamp, lark_webhook_secret) - data["timestamp"] = timestamp - data["sign"] = sign - - if lark_webhook_url == "" or lark_webhook_url is None: - raise HTTPException(status_code=500, detail="lark_webhook_url is empty!") - # TODO 增加超时和重试 async with httpx.AsyncClient(timeout=15) as client: - res = await client.post(lark_webhook_url, json=data) - - if res.status_code != 200 or res.json()["code"] != 0: - raise HTTPException(status_code=500, detail=res.text) + res = await client.post( + f"{settings.kook_api_base_url}/message/create", + json=json, + headers={"Authorization": f"Bot {settings.kook_token}"}, + ) def truncate(text: str, length: int = 80) -> str: diff --git a/src/repo2lark/config.py b/src/repo2lark/config.py deleted file mode 100644 index 1a76bb1..0000000 --- a/src/repo2lark/config.py +++ /dev/null @@ -1,29 +0,0 @@ -from functools import lru_cache - -from pydantic_settings import BaseSettings - - -@lru_cache() -def get_settings(): - return Settings() - - -class Settings(BaseSettings): - app_name: str = "r2lark" - - push_template_id: str = "ctp_AAydM4VlgIBl" - issue_template_id: str = "ctp_AAydOzv3yGRD" - issue_comment_template_id: str = "ctp_AAy45ODQkUZY" - pr_template_id: str = "ctp_AAyISOdEupq3" - - lark_webhook_base_url: str = "https://open.feishu.cn/open-apis/bot/v2/hook/" - lark_webhook_url: str = "" - lark_webhook_secret: str = "" - - github_webhook_secret: str = "" - - class Config: - env_file = ".env" - - -settings = get_settings() diff --git a/src/repo2lark/template.json b/src/repo2lark/template.json deleted file mode 100644 index c7cbe6c..0000000 --- a/src/repo2lark/template.json +++ /dev/null @@ -1,242 +0,0 @@ -{ - "action": "created", - "issue": { - "url": "https://api.github.com/repos/jingfelix/BiliFM/issues/12", - "repository_url": "https://api.github.com/repos/jingfelix/BiliFM", - "labels_url": "https://api.github.com/repos/jingfelix/BiliFM/issues/12/labels{/name}", - "comments_url": "https://api.github.com/repos/jingfelix/BiliFM/issues/12/comments", - "events_url": "https://api.github.com/repos/jingfelix/BiliFM/issues/12/events", - "html_url": "https://github.com/jingfelix/BiliFM/issues/12", - "id": 2053460106, - "node_id": "I_kwDOGHLuAc56ZVCK", - "number": 12, - "title": "Test webhook", - "user": { - "login": "jingfelix", - "id": 72600955, - "node_id": "MDQ6VXNlcjcyNjAwOTU1", - "avatar_url": "https://avatars.githubusercontent.com/u/72600955?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/jingfelix", - "html_url": "https://github.com/jingfelix", - "followers_url": "https://api.github.com/users/jingfelix/followers", - "following_url": "https://api.github.com/users/jingfelix/following{/other_user}", - "gists_url": "https://api.github.com/users/jingfelix/gists{/gist_id}", - "starred_url": "https://api.github.com/users/jingfelix/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/jingfelix/subscriptions", - "organizations_url": "https://api.github.com/users/jingfelix/orgs", - "repos_url": "https://api.github.com/users/jingfelix/repos", - "events_url": "https://api.github.com/users/jingfelix/events{/privacy}", - "received_events_url": "https://api.github.com/users/jingfelix/received_events", - "type": "User", - "site_admin": false - }, - "labels": [ - - ], - "state": "closed", - "locked": false, - "assignee": null, - "assignees": [ - - ], - "milestone": null, - "comments": 1, - "created_at": "2023-12-22T06:39:59Z", - "updated_at": "2023-12-24T12:15:01Z", - "closed_at": "2023-12-23T08:24:00Z", - "author_association": "OWNER", - "active_lock_reason": null, - "body": "Testing webhook", - "reactions": { - "url": "https://api.github.com/repos/jingfelix/BiliFM/issues/12/reactions", - "total_count": 0, - "+1": 0, - "-1": 0, - "laugh": 0, - "hooray": 0, - "confused": 0, - "heart": 0, - "rocket": 0, - "eyes": 0 - }, - "timeline_url": "https://api.github.com/repos/jingfelix/BiliFM/issues/12/timeline", - "performed_via_github_app": null, - "state_reason": "completed" - }, - "comment": { - "url": "https://api.github.com/repos/jingfelix/BiliFM/issues/comments/1868502830", - "html_url": "https://github.com/jingfelix/BiliFM/issues/12#issuecomment-1868502830", - "issue_url": "https://api.github.com/repos/jingfelix/BiliFM/issues/12", - "id": 1868502830, - "node_id": "IC_kwDOGHLuAc5vXxcu", - "user": { - "login": "jingfelix", - "id": 72600955, - "node_id": "MDQ6VXNlcjcyNjAwOTU1", - "avatar_url": "https://avatars.githubusercontent.com/u/72600955?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/jingfelix", - "html_url": "https://github.com/jingfelix", - "followers_url": "https://api.github.com/users/jingfelix/followers", - "following_url": "https://api.github.com/users/jingfelix/following{/other_user}", - "gists_url": "https://api.github.com/users/jingfelix/gists{/gist_id}", - "starred_url": "https://api.github.com/users/jingfelix/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/jingfelix/subscriptions", - "organizations_url": "https://api.github.com/users/jingfelix/orgs", - "repos_url": "https://api.github.com/users/jingfelix/repos", - "events_url": "https://api.github.com/users/jingfelix/events{/privacy}", - "received_events_url": "https://api.github.com/users/jingfelix/received_events", - "type": "User", - "site_admin": false - }, - "created_at": "2023-12-24T12:15:00Z", - "updated_at": "2023-12-24T12:15:00Z", - "author_association": "OWNER", - "body": "testing comment", - "reactions": { - "url": "https://api.github.com/repos/jingfelix/BiliFM/issues/comments/1868502830/reactions", - "total_count": 0, - "+1": 0, - "-1": 0, - "laugh": 0, - "hooray": 0, - "confused": 0, - "heart": 0, - "rocket": 0, - "eyes": 0 - }, - "performed_via_github_app": null - }, - "repository": { - "id": 410185217, - "node_id": "R_kgDOGHLuAQ", - "name": "BiliFM", - "full_name": "jingfelix/BiliFM", - "private": false, - "owner": { - "login": "jingfelix", - "id": 72600955, - "node_id": "MDQ6VXNlcjcyNjAwOTU1", - "avatar_url": "https://avatars.githubusercontent.com/u/72600955?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/jingfelix", - "html_url": "https://github.com/jingfelix", - "followers_url": "https://api.github.com/users/jingfelix/followers", - "following_url": "https://api.github.com/users/jingfelix/following{/other_user}", - "gists_url": "https://api.github.com/users/jingfelix/gists{/gist_id}", - "starred_url": "https://api.github.com/users/jingfelix/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/jingfelix/subscriptions", - "organizations_url": "https://api.github.com/users/jingfelix/orgs", - "repos_url": "https://api.github.com/users/jingfelix/repos", - "events_url": "https://api.github.com/users/jingfelix/events{/privacy}", - "received_events_url": "https://api.github.com/users/jingfelix/received_events", - "type": "User", - "site_admin": false - }, - "html_url": "https://github.com/jingfelix/BiliFM", - "description": "An script to download all audios of the Bilibili uploader you love. 下载指定up主全部或指定数量的音频。", - "fork": false, - "url": "https://api.github.com/repos/jingfelix/BiliFM", - "forks_url": "https://api.github.com/repos/jingfelix/BiliFM/forks", - "keys_url": "https://api.github.com/repos/jingfelix/BiliFM/keys{/key_id}", - "collaborators_url": "https://api.github.com/repos/jingfelix/BiliFM/collaborators{/collaborator}", - "teams_url": "https://api.github.com/repos/jingfelix/BiliFM/teams", - "hooks_url": "https://api.github.com/repos/jingfelix/BiliFM/hooks", - "issue_events_url": "https://api.github.com/repos/jingfelix/BiliFM/issues/events{/number}", - "events_url": "https://api.github.com/repos/jingfelix/BiliFM/events", - "assignees_url": "https://api.github.com/repos/jingfelix/BiliFM/assignees{/user}", - "branches_url": "https://api.github.com/repos/jingfelix/BiliFM/branches{/branch}", - "tags_url": "https://api.github.com/repos/jingfelix/BiliFM/tags", - "blobs_url": "https://api.github.com/repos/jingfelix/BiliFM/git/blobs{/sha}", - "git_tags_url": "https://api.github.com/repos/jingfelix/BiliFM/git/tags{/sha}", - "git_refs_url": "https://api.github.com/repos/jingfelix/BiliFM/git/refs{/sha}", - "trees_url": "https://api.github.com/repos/jingfelix/BiliFM/git/trees{/sha}", - "statuses_url": "https://api.github.com/repos/jingfelix/BiliFM/statuses/{sha}", - "languages_url": "https://api.github.com/repos/jingfelix/BiliFM/languages", - "stargazers_url": "https://api.github.com/repos/jingfelix/BiliFM/stargazers", - "contributors_url": "https://api.github.com/repos/jingfelix/BiliFM/contributors", - "subscribers_url": "https://api.github.com/repos/jingfelix/BiliFM/subscribers", - "subscription_url": "https://api.github.com/repos/jingfelix/BiliFM/subscription", - "commits_url": "https://api.github.com/repos/jingfelix/BiliFM/commits{/sha}", - "git_commits_url": "https://api.github.com/repos/jingfelix/BiliFM/git/commits{/sha}", - "comments_url": "https://api.github.com/repos/jingfelix/BiliFM/comments{/number}", - "issue_comment_url": "https://api.github.com/repos/jingfelix/BiliFM/issues/comments{/number}", - "contents_url": "https://api.github.com/repos/jingfelix/BiliFM/contents/{+path}", - "compare_url": "https://api.github.com/repos/jingfelix/BiliFM/compare/{base}...{head}", - "merges_url": "https://api.github.com/repos/jingfelix/BiliFM/merges", - "archive_url": "https://api.github.com/repos/jingfelix/BiliFM/{archive_format}{/ref}", - "downloads_url": "https://api.github.com/repos/jingfelix/BiliFM/downloads", - "issues_url": "https://api.github.com/repos/jingfelix/BiliFM/issues{/number}", - "pulls_url": "https://api.github.com/repos/jingfelix/BiliFM/pulls{/number}", - "milestones_url": "https://api.github.com/repos/jingfelix/BiliFM/milestones{/number}", - "notifications_url": "https://api.github.com/repos/jingfelix/BiliFM/notifications{?since,all,participating}", - "labels_url": "https://api.github.com/repos/jingfelix/BiliFM/labels{/name}", - "releases_url": "https://api.github.com/repos/jingfelix/BiliFM/releases{/id}", - "deployments_url": "https://api.github.com/repos/jingfelix/BiliFM/deployments", - "created_at": "2021-09-25T05:31:42Z", - "updated_at": "2023-11-30T04:38:58Z", - "pushed_at": "2023-12-22T02:19:32Z", - "git_url": "git://github.com/jingfelix/BiliFM.git", - "ssh_url": "git@github.com:jingfelix/BiliFM.git", - "clone_url": "https://github.com/jingfelix/BiliFM.git", - "svn_url": "https://github.com/jingfelix/BiliFM", - "homepage": "https://pypi.org/project/bilifm/", - "size": 109, - "stargazers_count": 13, - "watchers_count": 13, - "language": "Python", - "has_issues": true, - "has_projects": true, - "has_downloads": true, - "has_wiki": true, - "has_pages": false, - "has_discussions": false, - "forks_count": 1, - "mirror_url": null, - "archived": false, - "disabled": false, - "open_issues_count": 3, - "license": { - "key": "gpl-3.0", - "name": "GNU General Public License v3.0", - "spdx_id": "GPL-3.0", - "url": "https://api.github.com/licenses/gpl-3.0", - "node_id": "MDc6TGljZW5zZTk=" - }, - "allow_forking": true, - "is_template": false, - "web_commit_signoff_required": false, - "topics": [ - "audio-downloader", - "bilibili", - "bilibili-download", - "python" - ], - "visibility": "public", - "forks": 1, - "open_issues": 3, - "watchers": 13, - "default_branch": "main" - }, - "sender": { - "login": "jingfelix", - "id": 72600955, - "node_id": "MDQ6VXNlcjcyNjAwOTU1", - "avatar_url": "https://avatars.githubusercontent.com/u/72600955?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/jingfelix", - "html_url": "https://github.com/jingfelix", - "followers_url": "https://api.github.com/users/jingfelix/followers", - "following_url": "https://api.github.com/users/jingfelix/following{/other_user}", - "gists_url": "https://api.github.com/users/jingfelix/gists{/gist_id}", - "starred_url": "https://api.github.com/users/jingfelix/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/jingfelix/subscriptions", - "organizations_url": "https://api.github.com/users/jingfelix/orgs", - "repos_url": "https://api.github.com/users/jingfelix/repos", - "events_url": "https://api.github.com/users/jingfelix/events{/privacy}", - "received_events_url": "https://api.github.com/users/jingfelix/received_events", - "type": "User", - "site_admin": false - } - }