Files
mcp-nextcloud/nextcloud_mcp_server/client/webhooks.py
T
Chris CoutinhoandClaude Opus 4.7 f5f05b7c84 refactor(webhooks): address PR review on auth-pass
- webhook_receiver: always run hmac.compare_digest (drop the
  `not provided or` short-circuit) so the constant-time path is
  taken regardless of whether the Authorization header is present.
- client/webhooks: modernise the new `auth_data` type hint to
  `dict[str, str] | None` per CLAUDE.md.
- tests/client: rename `test_create_webhook_with_auth_headers` →
  `test_create_webhook_with_static_headers` and use
  `auth_method="header"` (NC's webhook_listeners only supports
  "none" and "header"; the previous "bearer" value was invalid).
- auth/webhook_routes: extract `_register_preset_webhooks` from
  `enable_webhook_preset` so the auth-threading behaviour is
  testable without standing up a Starlette app + auth middleware.
- tests/unit: new test_webhook_routes_register covering the helper
  with secret set / unset, and verifying ids round-trip in order.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 04:02:26 +02:00

120 lines
4.2 KiB
Python

"""Client for Nextcloud Webhook Listeners API operations."""
from typing import Any, Dict, List, Optional
from nextcloud_mcp_server.client.base import BaseNextcloudClient
class WebhooksClient(BaseNextcloudClient):
"""Client for Nextcloud webhook_listeners app API operations."""
app_name = "webhooks"
def _get_webhook_headers(
self, additional_headers: Optional[Dict[str, str]] = None
) -> Dict[str, str]:
"""Get standard headers required for Webhook Listeners API calls."""
headers = {"OCS-APIRequest": "true", "Accept": "application/json"}
if additional_headers:
headers.update(additional_headers)
return headers
async def list_webhooks(self) -> List[Dict[str, Any]]:
"""List all registered webhooks for the current user.
Returns:
List of webhook registrations with id, uri, event, filters, etc.
"""
headers = self._get_webhook_headers()
response = await self._make_request(
"GET",
"/ocs/v2.php/apps/webhook_listeners/api/v1/webhooks",
headers=headers,
)
data = response.json()["ocs"]["data"]
return data if isinstance(data, list) else []
async def create_webhook(
self,
event: str,
uri: str,
http_method: str = "POST",
auth_method: str = "none",
headers: Optional[Dict[str, str]] = None,
auth_data: dict[str, str] | None = None,
event_filter: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""Register a new webhook for the specified event.
Args:
event: Fully qualified event class name (e.g., "OCP\\Files\\Events\\Node\\NodeCreatedEvent")
uri: Webhook endpoint URL to receive event notifications
http_method: HTTP method for webhook delivery (default: "POST")
auth_method: Authentication method. Nextcloud's webhook_listeners
app accepts only ``"none"`` or ``"header"``.
headers: Optional static request headers attached to every
delivery (stored in clear text on the NC side).
auth_data: When ``auth_method="header"``, a dict of headers
holding the auth credentials. Stored encrypted at-rest in
Nextcloud's database and merged into the delivery request
at send time. Required when ``auth_method="header"``.
event_filter: JSON object specifying event filters (e.g., {"user.uid": "bob"})
Returns:
Webhook registration details including webhook ID
"""
data: Dict[str, Any] = {
"httpMethod": http_method,
"uri": uri,
"event": event,
"authMethod": auth_method,
}
if headers:
data["headers"] = headers
if auth_data:
data["authData"] = auth_data
if event_filter:
data["eventFilter"] = event_filter
request_headers = self._get_webhook_headers()
response = await self._make_request(
"POST",
"/ocs/v2.php/apps/webhook_listeners/api/v1/webhooks",
json=data,
headers=request_headers,
)
return response.json()["ocs"]["data"]
async def delete_webhook(self, webhook_id: int) -> None:
"""Delete a webhook registration.
Args:
webhook_id: ID of the webhook to delete
"""
headers = self._get_webhook_headers()
await self._make_request(
"DELETE",
f"/ocs/v2.php/apps/webhook_listeners/api/v1/webhooks/{webhook_id}",
headers=headers,
)
async def get_webhook(self, webhook_id: int) -> Dict[str, Any]:
"""Get details of a specific webhook registration.
Args:
webhook_id: ID of the webhook to retrieve
Returns:
Webhook registration details
"""
headers = self._get_webhook_headers()
response = await self._make_request(
"GET",
f"/ocs/v2.php/apps/webhook_listeners/api/v1/webhooks/{webhook_id}",
headers=headers,
)
return response.json()["ocs"]["data"]