feat: capture issues from mobile My Work (#159)
CI / lint (pull_request) Successful in 13s
CI / build-frontend (pull_request) Successful in 5s

This commit is contained in:
timmy committed 2026-08-07 02:58:14 +00:00
1 parent 8c889e3ffc
commit bb394be8b2
7 files changed
+451 -4

No files matched your search

+38
View File
@@ -364,6 +364,44 @@ async def comment_on_issue(repository: str, number: int, body: str) -> dict:
return _normalize_issue_comment(comment)
async def create_issue(repository: str, title: str, body: str, assignee: str) -> dict:
response = await _get_client().post(
f"/api/v1/repos/{repository}/issues",
headers=_auth(),
json={"title": title, "body": body, "assignee": assignee},
)
response.raise_for_status()
issue = response.json()
if not isinstance(issue, dict) or not isinstance(issue.get("number"), int):
raise ValueError("Gitea did not confirm issue creation")
assignees_value = issue.get("assignees")
assignees = assignees_value if isinstance(assignees_value, list) else []
confirmed_assignees = [
item["login"]
for item in assignees
if isinstance(item, dict) and isinstance(item.get("login"), str)
]
if assignee not in confirmed_assignees:
raise ValueError("Gitea did not confirm issue self-assignment")
return {
"id": issue.get("id"),
"number": issue["number"],
"title": issue.get("title", "")
if isinstance(issue.get("title"), str)
else "",
"state": issue.get("state", "")
if isinstance(issue.get("state"), str)
else "",
"repository": repository,
"labels": [],
"assignees": confirmed_assignees,
"updated_at": issue.get("updated_at", "")
if isinstance(issue.get("updated_at"), str)
else "",
"url": _safe_web_url(issue.get("html_url")),
}
async def issue_detail(repository: str, number: int) -> dict:
base = f"repos/{repository}/issues/{number}"
issue, comments = await asyncio.gather(
+54
View File
@@ -106,6 +106,24 @@ class IssueComment(BaseModel):
return value
class IssueCreation(BaseModel):
title: str = Field(min_length=1, max_length=255)
body: str = Field(default="", max_length=10_000)
@field_validator("title")
@classmethod
def strip_title(cls, value: str) -> str:
value = value.strip()
if not value:
raise ValueError("title must not be blank")
return value
@field_validator("body")
@classmethod
def strip_issue_body(cls, value: str) -> str:
return value.strip()
class PullReviewSubmission(BaseModel):
decision: str
body: str = Field(max_length=10_000)
@@ -177,6 +195,9 @@ async def prevent_live_api_caching(request, call_next):
) or request.url.path.startswith("/api/v1/notifications") or (
request.url.path.startswith("/api/v1/repos/")
and "/issues/" in request.url.path
) or (
request.url.path.startswith("/api/v1/repos/")
and request.url.path.endswith("/issues")
):
response.headers["Cache-Control"] = "no-store"
return response
@@ -709,6 +730,39 @@ async def assigned_issue_detail(owner: str, repo: str, number: int = PathParam(g
)
@app.post("/api/v1/repos/{owner}/{repo}/issues", status_code=201)
async def create_assigned_issue(creation: IssueCreation, owner: str, repo: str):
repository = f"{owner}/{repo}"
async def create_issue():
user, available = await asyncio.gather(
gitea_proxy.current_user(), gitea_proxy.repos()
)
login = user.get("login") if isinstance(user, dict) else None
accessible = {
item.get("full_name")
for item in (available if isinstance(available, list) else [])
if isinstance(item, dict)
}
if not login or repository not in accessible:
raise HTTPException(status_code=404, detail="Repository not found")
return await gitea_proxy.create_issue(
repository, creation.title, creation.body, login
)
try:
result = await asyncio.wait_for(create_issue(), timeout=ISSUE_ACTION_TIMEOUT_SECONDS)
except HTTPException:
raise
except Exception:
return JSONResponse(
{"error": "The issue could not be created. Your draft is safe; please retry."},
status_code=503,
headers={"Retry-After": "1"},
)
return JSONResponse(result, status_code=201)
@app.post("/api/v1/repos/{owner}/{repo}/issues/{number}/comments", status_code=201)
async def comment_on_assigned_issue(
comment: IssueComment, owner: str, repo: str, number: int = PathParam(gt=0)