新增了完整的GitHub集成插件,包含认证、会话管理、Webhook处理、消息收发、反应支持等核心功能,支持Issues、Discussions、Pull Request的交互与通知。
94 lines
3.1 KiB
Python
94 lines
3.1 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
import time
|
|
|
|
import httpx
|
|
import jwt
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
JWT_EXPIRY_SECONDS = 600
|
|
TOKEN_REFRESH_MARGIN_SECONDS = 120
|
|
INSTALLATION_TOKEN_TTL = 3600
|
|
|
|
|
|
class GitHubAuth:
|
|
def __init__(self, app_id: int, private_key: str):
|
|
self.app_id = app_id
|
|
self.private_key = private_key
|
|
self._token_cache: dict[int, tuple[str, float]] = {}
|
|
|
|
def _generate_jwt(self) -> str:
|
|
now = int(time.time())
|
|
payload = {
|
|
"iat": now - 60,
|
|
"exp": now + JWT_EXPIRY_SECONDS,
|
|
"iss": str(self.app_id),
|
|
}
|
|
return jwt.encode(payload, self.private_key, algorithm="RS256")
|
|
|
|
async def get_installation_token(self, installation_id: int) -> str:
|
|
now = time.time()
|
|
cached = self._token_cache.get(installation_id)
|
|
if cached and now < cached[1] - TOKEN_REFRESH_MARGIN_SECONDS:
|
|
return cached[0]
|
|
|
|
jwt_token = self._generate_jwt()
|
|
headers = {
|
|
"Authorization": f"Bearer {jwt_token}",
|
|
"Accept": "application/vnd.github+json",
|
|
}
|
|
|
|
async with httpx.AsyncClient(timeout=httpx.Timeout(30.0)) as client:
|
|
resp = await client.post(
|
|
f"https://api.github.com/app/installations/{installation_id}/access_tokens",
|
|
headers=headers,
|
|
)
|
|
resp.raise_for_status()
|
|
data = resp.json()
|
|
|
|
token = data["token"]
|
|
expires_at_str = data.get("expires_at", "")
|
|
expiry = now + INSTALLATION_TOKEN_TTL
|
|
if expires_at_str:
|
|
try:
|
|
from datetime import datetime as dt
|
|
parsed = dt.fromisoformat(expires_at_str.replace("Z", "+00:00"))
|
|
expiry = parsed.timestamp()
|
|
except (ValueError, OSError):
|
|
pass
|
|
|
|
self._token_cache[installation_id] = (token, expiry)
|
|
return token
|
|
|
|
def clear_cache(self):
|
|
self._token_cache.clear()
|
|
|
|
|
|
async def verify_credentials(account: dict, timeout_seconds: float = 10.0) -> dict | None:
|
|
auth = GitHubAuth(account["app_id"], account["private_key"])
|
|
try:
|
|
token = await auth.get_installation_token(account["installation_id"])
|
|
headers = {
|
|
"Authorization": f"Bearer {token}",
|
|
"Accept": "application/vnd.github+json",
|
|
}
|
|
async with httpx.AsyncClient(timeout=httpx.Timeout(timeout_seconds)) as client:
|
|
resp = await client.get("https://api.github.com/rate_limit", headers=headers)
|
|
resp.raise_for_status()
|
|
rate_data = resp.json()
|
|
core = rate_data.get("resources", {}).get("core", {})
|
|
return {
|
|
"ok": True,
|
|
"rate_limit": core.get("limit", 5000),
|
|
"rate_remaining": core.get("remaining", 5000),
|
|
"rate_reset": core.get("reset", 0),
|
|
}
|
|
except httpx.HTTPStatusError as e:
|
|
logger.warning("GitHub credential verification failed: HTTP %d", e.response.status_code)
|
|
return None
|
|
except Exception:
|
|
logger.exception("GitHub credential verification error")
|
|
return None
|