import json import logging from yuxi.channel.extensions.confluence.types import InboundConfluenceComment, PageContext logger = logging.getLogger(__name__) SUPPORTED_EVENTS = { "page_created", "page_updated", "page_removed", "blog_created", "blog_updated", "comment_created", "comment_updated", "comment_removed", "attachment_created", "attachment_updated", "attachment_removed", "label_added", "label_removed", "page_moved", "page_restored", "page_trashed", "content_created", "content_updated", "content_trashed", "content_restored", "space_created", "space_updated", "space_removed", } class ConfluenceWebhookReceiver: def __init__(self, on_page_changed=None, on_comment=None): self._on_page_changed = on_page_changed self._on_comment = on_comment def parse_webhook(self, body: bytes, headers: dict) -> dict | None: try: payload = json.loads(body) except json.JSONDecodeError: logger.warning("Invalid webhook JSON payload") return None event_type = payload.get("eventType", "") if event_type not in SUPPORTED_EVENTS: logger.debug("Ignored webhook event type: %s", event_type) return None if event_type.startswith("comment_"): return self._parse_comment_event(payload) if event_type.startswith(("attachment_", "label_")): return self._parse_resource_event(payload, event_type) page_id = payload.get("page", {}).get("id") page_title = payload.get("page", {}).get("title", "") space_key = payload.get("space", {}).get("key", "") if not page_id: logger.warning("Webhook payload missing page.id") return None return { "event_type": event_type, "page_id": page_id, "page_title": page_title, "space_key": space_key, "user_account_id": payload.get("userAccountId", ""), "timestamp": payload.get("timestamp", ""), } def _parse_comment_event(self, payload: dict) -> dict | None: comment = payload.get("comment", {}) comment_id = comment.get("id") if not comment_id: return None container = payload.get("container") or payload.get("page") or {} author = comment.get("createdBy", {}) or comment.get("lastUpdatedBy", {}) page_ctx = PageContext( page_id=container.get("id", ""), page_title=container.get("title", ""), space_key=payload.get("space", {}).get("key", ""), ) inbound = InboundConfluenceComment( comment_id=comment_id, author_id=author.get("accountId", ""), author_name=author.get("displayName", ""), body_text=self._extract_comment_body_text(comment), container_id=container.get("id", ""), parent_comment_id=comment.get("parentCommentId"), page_context=page_ctx, ) return { "event_type": payload.get("eventType", ""), "comment": inbound, "timestamp": payload.get("timestamp", ""), } @staticmethod def _extract_comment_body_text(comment: dict) -> str: body = comment.get("body", {}) view_value = body.get("view", {}).get("value", "") if view_value: import re clean = re.sub(r"<[^>]+>", "", view_value) clean = re.sub(r"&[a-z]+;", " ", clean) return re.sub(r"\s+", " ", clean).strip() return "" def _parse_resource_event(self, payload: dict, event_type: str) -> dict | None: if event_type.startswith("attachment_"): resource = payload.get("attachment", {}) return { "event_type": event_type, "attachment_id": resource.get("id"), "filename": resource.get("title"), "page_id": payload.get("page", {}).get("id"), "timestamp": payload.get("timestamp", ""), } if event_type.startswith("label_"): return { "event_type": event_type, "label_name": payload.get("label", {}).get("name"), "page_id": payload.get("page", {}).get("id"), "timestamp": payload.get("timestamp", ""), } return None async def handle_page_event(self, event: dict, account_id: str = "default"): if self._on_page_changed is None: return page_ctx = PageContext( page_id=event["page_id"], page_title=event["page_title"], space_key=event["space_key"], ) await self._on_page_changed( event_type=event["event_type"], page_context=page_ctx, user_account_id=event["user_account_id"], account_id=account_id, ) async def handle_comment_event(self, event: dict, account_id: str = "default"): if self._on_comment is None: return comment = event.get("comment") if comment is None: return await self._on_comment(comment, account_id=account_id)