Pipeline/backend/app/services/forgejo_issue_comment.py

122 lines
3.8 KiB
Python

"""Service for posting comments on Forgejo issues and updating local cache."""
from __future__ import annotations
from datetime import datetime
from typing import TYPE_CHECKING
from uuid import UUID
from sqlmodel import select
from app.core.logging import get_logger
from app.core.time import utcnow
from app.db import crud
from app.models.forgejo_connections import ForgejoConnection
from app.models.forgejo_issues import ForgejoIssue
from app.models.forgejo_repositories import ForgejoRepository
from app.services.forgejo_client import get_forgejo_client
if TYPE_CHECKING:
from sqlalchemy.ext.asyncio.session import AsyncSession
logger = get_logger(__name__)
class PostCommentError(Exception):
"""Base exception for post comment errors."""
class PostCommentNotFoundError(PostCommentError):
"""Raised when issue, repository, or connection is not found."""
class PostCommentRemoteError(PostCommentError):
"""Raised when the Forgejo API call fails."""
async def post_comment_on_issue(
session: AsyncSession,
issue: ForgejoIssue,
body: str,
actor_user_id: UUID | None = None,
actor_agent_id: UUID | None = None,
) -> dict[str, object]:
"""Post a comment on a Forgejo issue and append it to the local cache.
Raises:
PostCommentNotFoundError: If repository or connection not found.
PostCommentRemoteError: If the Forgejo API call fails.
"""
repository = await crud.get_by_id(session, ForgejoRepository, issue.repository_id)
if repository is None:
raise PostCommentNotFoundError("Repository not found")
connection = await crud.get_by_id(session, ForgejoConnection, repository.connection_id)
if connection is None:
raise PostCommentNotFoundError("Connection not found")
try:
async with get_forgejo_client(connection) as client:
created = await client.create_comment(
owner=repository.owner,
repo=repository.repo,
issue_number=issue.forgejo_issue_number,
body=body,
)
except Exception as exc:
raise PostCommentRemoteError(f"Failed to post comment on Forgejo: {exc}") from exc
existing = list(issue.forgejo_comments_payload or [])
existing.append(created)
issue.forgejo_comments_payload = existing
issue.last_synced_at = utcnow()
session.add(issue)
await session.flush()
raw_created_at = created.get("created_at")
created_at_str = (
raw_created_at.isoformat()
if isinstance(raw_created_at, datetime)
else str(raw_created_at or "")
)
logger.info(
"forgejo.issue.comment.posted",
extra={
"issue_id": str(issue.id),
"forgejo_issue_number": issue.forgejo_issue_number,
"repository_id": str(repository.id),
"comment_id": str(created.get("id", "")),
"actor_user_id": str(actor_user_id) if actor_user_id else None,
"actor_agent_id": str(actor_agent_id) if actor_agent_id else None,
},
)
return {
"success": True,
"issue_id": str(issue.id),
"comment_id": created.get("id"),
"body": body,
"created_at": created_at_str,
}
async def post_comment_by_issue_id(
session: AsyncSession,
issue_id: UUID,
body: str,
actor_user_id: UUID | None = None,
actor_agent_id: UUID | None = None,
) -> dict[str, object]:
"""Post a comment by issue ID."""
issue = (await session.exec(select(ForgejoIssue).where(ForgejoIssue.id == issue_id))).first()
if issue is None:
raise PostCommentNotFoundError("Issue not found")
return await post_comment_on_issue(
session=session,
issue=issue,
body=body,
actor_user_id=actor_user_id,
actor_agent_id=actor_agent_id,
)