Build Forgejo-backed community prototype

This commit is contained in:
kacper 2026-04-12 20:15:33 -04:00
parent 797ae5ea35
commit 6671a01d26
16 changed files with 2485 additions and 293 deletions

View file

@ -14,8 +14,9 @@ class ForgejoClientError(RuntimeError):
class ForgejoClient:
def __init__(self, settings: Settings) -> None:
def __init__(self, settings: Settings, forgejo_token: str | None = None) -> None:
self._settings = settings
self._forgejo_token = forgejo_token or settings.forgejo_token
self._client = httpx.AsyncClient(timeout=settings.forgejo_request_timeout_seconds)
async def __aenter__(self) -> ForgejoClient:
@ -30,6 +31,54 @@ class ForgejoClient:
absolute_url=True,
)
async def exchange_oauth_code(
self,
*,
token_endpoint: str,
client_id: str,
client_secret: str,
code: str,
redirect_uri: str,
code_verifier: str,
) -> dict[str, Any]:
response = await self._client.post(
token_endpoint,
data={
"grant_type": "authorization_code",
"client_id": client_id,
"client_secret": client_secret,
"code": code,
"redirect_uri": redirect_uri,
"code_verifier": code_verifier,
},
headers={"Accept": "application/json"},
)
if response.is_success:
payload = response.json()
if isinstance(payload, dict):
return payload
raise ForgejoClientError("Unexpected Forgejo OAuth token payload.")
message = self._extract_error_message(response)
raise ForgejoClientError(f"{response.status_code} from Forgejo OAuth: {message}")
async def fetch_userinfo(self, userinfo_endpoint: str, access_token: str) -> dict[str, Any]:
response = await self._client.get(
userinfo_endpoint,
headers={
"Accept": "application/json",
"Authorization": f"Bearer {access_token}",
},
)
if response.is_success:
payload = response.json()
if isinstance(payload, dict):
return payload
raise ForgejoClientError("Unexpected Forgejo UserInfo payload.")
message = self._extract_error_message(response)
raise ForgejoClientError(f"{response.status_code} from Forgejo UserInfo: {message}")
async def fetch_current_user(self) -> dict[str, Any]:
return await self._get_json("/api/v1/user", auth_required=True)
@ -39,22 +88,37 @@ class ForgejoClient:
params={
"limit": self._settings.forgejo_repo_scan_limit,
"page": 1,
"private": "false",
"is_private": "false",
},
auth_required=True,
)
data = payload.get("data", [])
return [repo for repo in data if isinstance(repo, dict)]
async def search_recent_issues(self) -> list[dict[str, Any]]:
async def fetch_repository(self, owner: str, repo: str) -> dict[str, Any]:
payload = await self._get_json(
"/api/v1/repos/issues/search",
f"/api/v1/repos/{owner}/{repo}",
)
if isinstance(payload, dict):
return payload
raise ForgejoClientError(f"Unexpected repository payload for {owner}/{repo}")
async def list_repo_issues(
self,
owner: str,
repo: str,
*,
limit: int,
) -> list[dict[str, Any]]:
payload = await self._get_json(
f"/api/v1/repos/{owner}/{repo}/issues",
params={
"state": "open",
"page": 1,
"limit": self._settings.forgejo_recent_issue_limit,
"limit": limit,
"type": "issues",
"sort": "recentupdate",
},
auth_required=True,
)
if isinstance(payload, list):
return [issue for issue in payload if isinstance(issue, dict)]
@ -65,7 +129,7 @@ class ForgejoClient:
if path:
endpoint = f"{endpoint}/{path.strip('/')}"
payload = await self._get_json(endpoint, auth_required=True)
payload = await self._get_json(endpoint)
if isinstance(payload, list):
return [entry for entry in payload if isinstance(entry, dict)]
if isinstance(payload, dict):
@ -80,16 +144,31 @@ class ForgejoClient:
) -> list[dict[str, Any]]:
payload = await self._get_json(
f"/api/v1/repos/{owner}/{repo}/issues/{issue_number}/comments",
auth_required=True,
)
if isinstance(payload, list):
return [comment for comment in payload if isinstance(comment, dict)]
return []
async def create_issue_comment(
self,
owner: str,
repo: str,
issue_number: int,
body: str,
) -> dict[str, Any]:
payload = await self._request_json(
"POST",
f"/api/v1/repos/{owner}/{repo}/issues/{issue_number}/comments",
json_payload={"body": body},
auth_required=True,
)
if isinstance(payload, dict):
return payload
raise ForgejoClientError(f"Unexpected comment payload for {owner}/{repo}#{issue_number}")
async def get_file_content(self, owner: str, repo: str, path: str) -> dict[str, str]:
payload = await self._get_json(
f"/api/v1/repos/{owner}/{repo}/contents/{path.strip('/')}",
auth_required=True,
)
if not isinstance(payload, dict):
raise ForgejoClientError(f"Unexpected file payload for {owner}/{repo}:{path}")
@ -118,17 +197,41 @@ class ForgejoClient:
params: Mapping[str, str | int] | None = None,
auth_required: bool = False,
) -> dict[str, Any] | list[Any]:
if auth_required and not self._settings.forgejo_token:
return await self._request_json(
"GET",
path,
absolute_url=absolute_url,
params=params,
auth_required=auth_required,
)
async def _request_json(
self,
method: str,
path: str,
*,
absolute_url: bool = False,
params: Mapping[str, str | int] | None = None,
json_payload: Mapping[str, object] | None = None,
auth_required: bool = False,
) -> dict[str, Any] | list[Any]:
if auth_required and not self._forgejo_token:
raise ForgejoClientError(
"This Forgejo instance requires an authenticated API token for repo and issue access.",
)
url = path if absolute_url else f"{self._settings.forgejo_base_url}{path}"
headers = {}
if self._settings.forgejo_token:
headers["Authorization"] = f"token {self._settings.forgejo_token}"
if self._forgejo_token:
headers["Authorization"] = f"token {self._forgejo_token}"
response = await self._client.get(url, params=params, headers=headers)
response = await self._client.request(
method,
url,
params=params,
headers=headers,
json=json_payload,
)
if response.is_success:
return response.json()