gabriel / musehub public
webhooks.py python
173 lines 6.8 KB
Raw
sha256:0997d6250ae6476362f6fe2025af7789f46d03df3e9f34356d5e8ee79b201923 fix(issues): use issue number as pagination cursor, not cre… Sonnet 4.6 patch 9 days ago
1 """Write executors for webhook operations: create, list, delete, redeliver."""
2
3 import logging
4
5 from musehub.db.database import AsyncSessionLocal
6 from musehub.models.musehub import WEBHOOK_EVENT_TYPES, WebhookResponse
7 from musehub.types.json_types import JSONObject
8 from musehub.services import musehub_repository, musehub_webhook_dispatcher
9 from musehub.services.musehub_mcp_executor import MusehubToolResult, _check_db_available
10 from musehub.mcp.write_tools.issues import _require_write_access
11
12 logger = logging.getLogger(__name__)
13
14 def _webhook_data(wh: WebhookResponse) -> JSONObject:
15 """Serialise a WebhookResponse to a ``JSONObject``."""
16 return {
17 "webhook_id": wh.webhook_id,
18 "repo_id": wh.repo_id,
19 "url": wh.url,
20 "events": list(wh.events),
21 "active": wh.active,
22 "created_at": wh.created_at.isoformat() if wh.created_at else None,
23 }
24
25 async def execute_create_webhook(
26 *,
27 repo_id: str,
28 url: str,
29 events: list[str],
30 secret: str = "",
31 actor: str = "",
32 ) -> MusehubToolResult:
33 """Register a new webhook subscription for a repository.
34
35 The caller must be the repo owner or a write/admin collaborator. Webhooks
36 receive HTTP POST payloads for each matching event type.
37
38 Args:
39 repo_id: sha256 genesis ID of the repository.
40 url: HTTPS URL that will receive event payloads.
41 events: Non-empty list of event types to subscribe to.
42 Valid types: push, proposal, issue, release, branch, tag, session, analysis.
43 secret: Optional HMAC-SHA256 signing secret (plaintext). Store this securely.
44 actor: Authenticated user ID (MSign handle).
45
46 Returns:
47 ``MusehubToolResult`` with ``data.webhook_id`` on success.
48 """
49 if (err := _check_db_available()) is not None:
50 return err
51
52 unknown = [e for e in events if e not in WEBHOOK_EVENT_TYPES]
53 if unknown:
54 return MusehubToolResult(
55 ok=False,
56 error_code="invalid_args",
57 error_message=f"Unknown event types: {unknown}. Valid types: {sorted(WEBHOOK_EVENT_TYPES)}",
58 )
59
60 try:
61 async with AsyncSessionLocal() as session:
62 repo = await musehub_repository.get_repo(session, repo_id)
63 if repo is None:
64 return MusehubToolResult(
65 ok=False,
66 error_code="repo_not_found",
67 error_message=f"Repository '{repo_id}' not found.",
68 hint="Call musehub_search_repos() to find available repositories.",
69 )
70 if (err := await _require_write_access(session, repo_id, actor, repo.owner)) is not None:
71 return err
72
73 wh = await musehub_webhook_dispatcher.create_webhook(
74 session,
75 repo_id=repo_id,
76 url=url,
77 events=events,
78 secret=secret,
79 )
80 await session.commit()
81 logger.info("MCP create_webhook %s for repo %s events=%s by %s", wh.webhook_id, repo_id, events, actor)
82 return MusehubToolResult(ok=True, data=_webhook_data(wh))
83 except Exception as exc:
84 logger.exception("MCP create_webhook failed: %s", exc)
85 return MusehubToolResult(ok=False, error_code="invalid_args", error_message=str(exc))
86
87 async def execute_list_webhooks(
88 *,
89 repo_id: str,
90 actor: str = "",
91 ) -> MusehubToolResult:
92 """List all webhook subscriptions for a repository.
93
94 Restricted to the repo owner and accepted write/admin collaborators.
95 Webhook payloads may contain sensitive callback URLs — listing them is
96 a privileged operation.
97
98 Args:
99 repo_id: sha256 genesis ID of the repository.
100 actor: Authenticated user ID (MSign handle).
101
102 Returns:
103 ``MusehubToolResult`` with ``data.webhooks`` list on success.
104 """
105 if (err := _check_db_available()) is not None:
106 return err
107
108 try:
109 async with AsyncSessionLocal() as session:
110 repo = await musehub_repository.get_repo(session, repo_id)
111 if repo is None:
112 return MusehubToolResult(
113 ok=False,
114 error_code="repo_not_found",
115 error_message=f"Repository '{repo_id}' not found.",
116 hint="Call musehub_search_repos() to find available repositories.",
117 )
118 if (err := await _require_write_access(session, repo_id, actor, repo.owner)) is not None:
119 return err
120 webhooks_result = await musehub_webhook_dispatcher.list_webhooks(session, repo_id)
121 data_list: list[JSONObject] = [_webhook_data(wh) for wh in webhooks_result.webhooks]
122 return MusehubToolResult(ok=True, data={"webhooks": data_list, "total": len(data_list)})
123 except Exception as exc:
124 logger.exception("MCP list_webhooks failed: %s", exc)
125 return MusehubToolResult(ok=False, error_code="invalid_args", error_message=str(exc))
126
127 async def execute_delete_webhook(
128 *,
129 repo_id: str,
130 webhook_id: str,
131 actor: str = "",
132 ) -> MusehubToolResult:
133 """Delete a webhook subscription and its full delivery history.
134
135 The caller must be the repo owner or a write/admin collaborator.
136
137 Args:
138 repo_id: sha256 genesis ID of the repository.
139 webhook_id: ID of the webhook to delete.
140 actor: Authenticated user ID (MSign handle).
141
142 Returns:
143 ``MusehubToolResult`` with ``data.deleted=true`` on success.
144 """
145 if (err := _check_db_available()) is not None:
146 return err
147
148 try:
149 async with AsyncSessionLocal() as session:
150 repo = await musehub_repository.get_repo(session, repo_id)
151 if repo is None:
152 return MusehubToolResult(
153 ok=False,
154 error_code="repo_not_found",
155 error_message=f"Repository '{repo_id}' not found.",
156 hint="Call musehub_search_repos() to find available repositories.",
157 )
158 if (err := await _require_write_access(session, repo_id, actor, repo.owner)) is not None:
159 return err
160
161 deleted = await musehub_webhook_dispatcher.delete_webhook(session, repo_id, webhook_id)
162 if not deleted:
163 return MusehubToolResult(
164 ok=False,
165 error_code="webhook_not_found",
166 error_message=f"Webhook '{webhook_id}' not found in repo '{repo_id}'.",
167 )
168 await session.commit()
169 logger.info("MCP delete_webhook %s from repo %s by %s", webhook_id, repo_id, actor)
170 return MusehubToolResult(ok=True, data={"deleted": True, "webhook_id": webhook_id})
171 except Exception as exc:
172 logger.exception("MCP delete_webhook failed: %s", exc)
173 return MusehubToolResult(ok=False, error_code="invalid_args", error_message=str(exc))
File History 1 commit
sha256:0997d6250ae6476362f6fe2025af7789f46d03df3e9f34356d5e8ee79b201923 fix(issues): use issue number as pagination cursor, not cre… Sonnet 4.6 patch 9 days ago