mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2025-12-20 12:19:06 +00:00
feat(rooms): add webhook for transcript completion (#578)
* feat(rooms): add webhook notifications for transcript completion
- Add webhook_url and webhook_secret fields to rooms table
- Create Celery task with 24-hour retry window using exponential backoff
- Send transcript metadata, diarized text, topics, and summaries via webhook
- Add HMAC signature verification for webhook security
- Add test endpoint POST /v1/rooms/{room_id}/webhook/test
- Update frontend with webhook configuration UI and test button
- Auto-generate webhook secret if not provided
- Trigger webhook after successful file pipeline processing for room recordings
* style: linting
* fix: remove unwanted files
* fix: update openapi gen
* fix: self-review
* docs: add comprehensive webhook documentation
- Document webhook configuration, events, and payloads
- Include transcript.completed and test event examples
- Add security considerations and best practices
- Provide example webhook receiver implementation
- Document retry policy and signature verification
* fix: remove audio_mp3_url from webhook payload
- Remove audio download URL generation from webhook
- Update documentation to reflect the change
- Keep only frontend_url for accessing transcripts
* docs: remove unwanted section
* fix: correct API method name and type imports for rooms
- Fix v1RoomsRetrieve to v1RoomsGet
- Update Room type to RoomDetails throughout frontend
- Fix type imports in useRoomList, RoomList, RoomTable, and RoomCards
* feat: add show/hide toggle for webhook secret field
- Add eye icon button to reveal/hide webhook secret when editing
- Show password dots when webhook secret is hidden
- Reset visibility state when opening/closing dialog
- Only show toggle button when editing existing room with secret
* fix: resolve event loop conflict in webhook test endpoint
- Extract webhook test logic into shared async function
- Call async function directly from FastAPI endpoint
- Keep Celery task wrapper for background processing
- Fixes RuntimeError: event loop already running
* refactor: remove unnecessary Celery task for webhook testing
- Webhook testing is synchronous and provides immediate feedback
- No need for background processing via Celery
- Keep only the async function called directly from API endpoint
* feat: improve webhook test error messages and display
- Show HTTP status code in error messages
- Parse JSON error responses to extract meaningful messages
- Improved UI layout for webhook test results
- Added colored background for success/error states
- Better text wrapping for long error messages
* docs: adjust doc
* fix: review
* fix: update attempts to match close 24h
* fix: add event_id
* fix: changed to uuid, to have new event_id when reprocess.
* style: linting
* fix: alembic revision
This commit is contained in:
212
server/docs/webhook.md
Normal file
212
server/docs/webhook.md
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
# Reflector Webhook Documentation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Reflector supports webhook notifications to notify external systems when transcript processing is completed. Webhooks can be configured per room and are triggered automatically after a transcript is successfully processed.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Webhooks are configured at the room level with two fields:
|
||||||
|
- `webhook_url`: The HTTPS endpoint to receive webhook notifications
|
||||||
|
- `webhook_secret`: Optional secret key for HMAC signature verification (auto-generated if not provided)
|
||||||
|
|
||||||
|
## Events
|
||||||
|
|
||||||
|
### `transcript.completed`
|
||||||
|
|
||||||
|
Triggered when a transcript has been fully processed, including transcription, diarization, summarization, and topic detection.
|
||||||
|
|
||||||
|
### `test`
|
||||||
|
|
||||||
|
A test event that can be triggered manually to verify webhook configuration.
|
||||||
|
|
||||||
|
## Webhook Request Format
|
||||||
|
|
||||||
|
### Headers
|
||||||
|
|
||||||
|
All webhook requests include the following headers:
|
||||||
|
|
||||||
|
| Header | Description | Example |
|
||||||
|
|--------|-------------|---------|
|
||||||
|
| `Content-Type` | Always `application/json` | `application/json` |
|
||||||
|
| `User-Agent` | Identifies Reflector as the source | `Reflector-Webhook/1.0` |
|
||||||
|
| `X-Webhook-Event` | The event type | `transcript.completed` or `test` |
|
||||||
|
| `X-Webhook-Retry` | Current retry attempt number | `0`, `1`, `2`... |
|
||||||
|
| `X-Webhook-Signature` | HMAC signature (if secret configured) | `t=1735306800,v1=abc123...` |
|
||||||
|
|
||||||
|
### Signature Verification
|
||||||
|
|
||||||
|
If a webhook secret is configured, Reflector includes an HMAC-SHA256 signature in the `X-Webhook-Signature` header to verify the webhook authenticity.
|
||||||
|
|
||||||
|
The signature format is: `t={timestamp},v1={signature}`
|
||||||
|
|
||||||
|
To verify the signature:
|
||||||
|
1. Extract the timestamp and signature from the header
|
||||||
|
2. Create the signed payload: `{timestamp}.{request_body}`
|
||||||
|
3. Compute HMAC-SHA256 of the signed payload using your webhook secret
|
||||||
|
4. Compare the computed signature with the received signature
|
||||||
|
|
||||||
|
Example verification (Python):
|
||||||
|
```python
|
||||||
|
import hmac
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
def verify_webhook_signature(payload: bytes, signature_header: str, secret: str) -> bool:
|
||||||
|
# Parse header: "t=1735306800,v1=abc123..."
|
||||||
|
parts = dict(part.split("=") for part in signature_header.split(","))
|
||||||
|
timestamp = parts["t"]
|
||||||
|
received_signature = parts["v1"]
|
||||||
|
|
||||||
|
# Create signed payload
|
||||||
|
signed_payload = f"{timestamp}.{payload.decode('utf-8')}"
|
||||||
|
|
||||||
|
# Compute expected signature
|
||||||
|
expected_signature = hmac.new(
|
||||||
|
secret.encode("utf-8"),
|
||||||
|
signed_payload.encode("utf-8"),
|
||||||
|
hashlib.sha256
|
||||||
|
).hexdigest()
|
||||||
|
|
||||||
|
# Compare signatures
|
||||||
|
return hmac.compare_digest(expected_signature, received_signature)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Event Payloads
|
||||||
|
|
||||||
|
### `transcript.completed` Event
|
||||||
|
|
||||||
|
This event includes a convenient URL for accessing the transcript:
|
||||||
|
- `frontend_url`: Direct link to view the transcript in the web interface
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event": "transcript.completed",
|
||||||
|
"event_id": "transcript.completed-abc-123-def-456",
|
||||||
|
"timestamp": "2025-08-27T12:34:56.789012Z",
|
||||||
|
"transcript": {
|
||||||
|
"id": "abc-123-def-456",
|
||||||
|
"room_id": "room-789",
|
||||||
|
"created_at": "2025-08-27T12:00:00Z",
|
||||||
|
"duration": 1800.5,
|
||||||
|
"title": "Q3 Product Planning Meeting",
|
||||||
|
"short_summary": "Team discussed Q3 product roadmap, prioritizing mobile app features and API improvements.",
|
||||||
|
"long_summary": "The product team met to finalize the Q3 roadmap. Key decisions included...",
|
||||||
|
"webvtt": "WEBVTT\n\n00:00:00.000 --> 00:00:05.000\n<v Speaker 1>Welcome everyone to today's meeting...",
|
||||||
|
"topics": [
|
||||||
|
{
|
||||||
|
"title": "Introduction and Agenda",
|
||||||
|
"summary": "Meeting kickoff with agenda review",
|
||||||
|
"timestamp": 0.0,
|
||||||
|
"duration": 120.0,
|
||||||
|
"webvtt": "WEBVTT\n\n00:00:00.000 --> 00:00:05.000\n<v Speaker 1>Welcome everyone..."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Mobile App Features Discussion",
|
||||||
|
"summary": "Team reviewed proposed mobile app features for Q3",
|
||||||
|
"timestamp": 120.0,
|
||||||
|
"duration": 600.0,
|
||||||
|
"webvtt": "WEBVTT\n\n00:02:00.000 --> 00:02:10.000\n<v Speaker 2>Let's talk about the mobile app..."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"participants": [
|
||||||
|
{
|
||||||
|
"id": "participant-1",
|
||||||
|
"name": "John Doe",
|
||||||
|
"speaker": "Speaker 1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "participant-2",
|
||||||
|
"name": "Jane Smith",
|
||||||
|
"speaker": "Speaker 2"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source_language": "en",
|
||||||
|
"target_language": "en",
|
||||||
|
"status": "completed",
|
||||||
|
"frontend_url": "https://app.reflector.com/transcripts/abc-123-def-456"
|
||||||
|
},
|
||||||
|
"room": {
|
||||||
|
"id": "room-789",
|
||||||
|
"name": "Product Team Room"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `test` Event
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event": "test",
|
||||||
|
"event_id": "test.2025-08-27T12:34:56.789012Z",
|
||||||
|
"timestamp": "2025-08-27T12:34:56.789012Z",
|
||||||
|
"message": "This is a test webhook from Reflector",
|
||||||
|
"room": {
|
||||||
|
"id": "room-789",
|
||||||
|
"name": "Product Team Room"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Retry Policy
|
||||||
|
|
||||||
|
Webhooks are delivered with automatic retry logic to handle transient failures. When a webhook delivery fails due to server errors or network issues, Reflector will automatically retry the delivery multiple times over an extended period.
|
||||||
|
|
||||||
|
### Retry Mechanism
|
||||||
|
|
||||||
|
Reflector implements an exponential backoff strategy for webhook retries:
|
||||||
|
|
||||||
|
- **Initial retry delay**: 60 seconds after the first failure
|
||||||
|
- **Exponential backoff**: Each subsequent retry waits approximately twice as long as the previous one
|
||||||
|
- **Maximum retry interval**: 1 hour (backoff is capped at this duration)
|
||||||
|
- **Maximum retry attempts**: 30 attempts total
|
||||||
|
- **Total retry duration**: Retries continue for approximately 24 hours
|
||||||
|
|
||||||
|
### How Retries Work
|
||||||
|
|
||||||
|
When a webhook fails, Reflector will:
|
||||||
|
1. Wait 60 seconds, then retry (attempt #1)
|
||||||
|
2. If it fails again, wait ~2 minutes, then retry (attempt #2)
|
||||||
|
3. Continue doubling the wait time up to a maximum of 1 hour between attempts
|
||||||
|
4. Keep retrying at 1-hour intervals until successful or 30 attempts are exhausted
|
||||||
|
|
||||||
|
The `X-Webhook-Retry` header indicates the current retry attempt number (0 for the initial attempt, 1 for first retry, etc.), allowing your endpoint to track retry attempts.
|
||||||
|
|
||||||
|
### Retry Behavior by HTTP Status Code
|
||||||
|
|
||||||
|
| Status Code | Behavior |
|
||||||
|
|-------------|----------|
|
||||||
|
| 2xx (Success) | No retry, webhook marked as delivered |
|
||||||
|
| 4xx (Client Error) | No retry, request is considered permanently failed |
|
||||||
|
| 5xx (Server Error) | Automatic retry with exponential backoff |
|
||||||
|
| Network/Timeout Error | Automatic retry with exponential backoff |
|
||||||
|
|
||||||
|
**Important Notes:**
|
||||||
|
- Webhooks timeout after 30 seconds. If your endpoint takes longer to respond, it will be considered a timeout error and retried.
|
||||||
|
- During the retry period (~24 hours), you may receive the same webhook multiple times if your endpoint experiences intermittent failures.
|
||||||
|
- There is no mechanism to manually retry failed webhooks after the retry period expires.
|
||||||
|
|
||||||
|
## Testing Webhooks
|
||||||
|
|
||||||
|
You can test your webhook configuration before processing transcripts:
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /v1/rooms/{room_id}/webhook/test
|
||||||
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"status_code": 200,
|
||||||
|
"message": "Webhook test successful",
|
||||||
|
"response_preview": "OK"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Or in case of failure:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": "Webhook request timed out (10 seconds)"
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
"""Add webhook fields to rooms
|
||||||
|
|
||||||
|
Revision ID: 0194f65cd6d3
|
||||||
|
Revises: 5a8907fd1d78
|
||||||
|
Create Date: 2025-08-27 09:03:19.610995
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = "0194f65cd6d3"
|
||||||
|
down_revision: Union[str, None] = "5a8907fd1d78"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table("room", schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column("webhook_url", sa.String(), nullable=True))
|
||||||
|
batch_op.add_column(sa.Column("webhook_secret", sa.String(), nullable=True))
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table("room", schema=None) as batch_op:
|
||||||
|
batch_op.drop_column("webhook_secret")
|
||||||
|
batch_op.drop_column("webhook_url")
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import secrets
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from sqlite3 import IntegrityError
|
from sqlite3 import IntegrityError
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
@@ -40,6 +41,8 @@ rooms = sqlalchemy.Table(
|
|||||||
sqlalchemy.Column(
|
sqlalchemy.Column(
|
||||||
"is_shared", sqlalchemy.Boolean, nullable=False, server_default=false()
|
"is_shared", sqlalchemy.Boolean, nullable=False, server_default=false()
|
||||||
),
|
),
|
||||||
|
sqlalchemy.Column("webhook_url", sqlalchemy.String),
|
||||||
|
sqlalchemy.Column("webhook_secret", sqlalchemy.String),
|
||||||
sqlalchemy.Index("idx_room_is_shared", "is_shared"),
|
sqlalchemy.Index("idx_room_is_shared", "is_shared"),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -59,6 +62,8 @@ class Room(BaseModel):
|
|||||||
"none", "prompt", "automatic", "automatic-2nd-participant"
|
"none", "prompt", "automatic", "automatic-2nd-participant"
|
||||||
] = "automatic-2nd-participant"
|
] = "automatic-2nd-participant"
|
||||||
is_shared: bool = False
|
is_shared: bool = False
|
||||||
|
webhook_url: str = ""
|
||||||
|
webhook_secret: str = ""
|
||||||
|
|
||||||
|
|
||||||
class RoomController:
|
class RoomController:
|
||||||
@@ -107,10 +112,15 @@ class RoomController:
|
|||||||
recording_type: str,
|
recording_type: str,
|
||||||
recording_trigger: str,
|
recording_trigger: str,
|
||||||
is_shared: bool,
|
is_shared: bool,
|
||||||
|
webhook_url: str = "",
|
||||||
|
webhook_secret: str = "",
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Add a new room
|
Add a new room
|
||||||
"""
|
"""
|
||||||
|
if webhook_url and not webhook_secret:
|
||||||
|
webhook_secret = secrets.token_urlsafe(32)
|
||||||
|
|
||||||
room = Room(
|
room = Room(
|
||||||
name=name,
|
name=name,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
@@ -122,6 +132,8 @@ class RoomController:
|
|||||||
recording_type=recording_type,
|
recording_type=recording_type,
|
||||||
recording_trigger=recording_trigger,
|
recording_trigger=recording_trigger,
|
||||||
is_shared=is_shared,
|
is_shared=is_shared,
|
||||||
|
webhook_url=webhook_url,
|
||||||
|
webhook_secret=webhook_secret,
|
||||||
)
|
)
|
||||||
query = rooms.insert().values(**room.model_dump())
|
query = rooms.insert().values(**room.model_dump())
|
||||||
try:
|
try:
|
||||||
@@ -134,6 +146,9 @@ class RoomController:
|
|||||||
"""
|
"""
|
||||||
Update a room fields with key/values in values
|
Update a room fields with key/values in values
|
||||||
"""
|
"""
|
||||||
|
if values.get("webhook_url") and not values.get("webhook_secret"):
|
||||||
|
values["webhook_secret"] = secrets.token_urlsafe(32)
|
||||||
|
|
||||||
query = rooms.update().where(rooms.c.id == room.id).values(**values)
|
query = rooms.update().where(rooms.c.id == room.id).values(**values)
|
||||||
try:
|
try:
|
||||||
await get_database().execute(query)
|
await get_database().execute(query)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ Uses parallel processing for transcription, diarization, and waveform generation
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import uuid
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import av
|
import av
|
||||||
@@ -14,7 +15,9 @@ import structlog
|
|||||||
from celery import shared_task
|
from celery import shared_task
|
||||||
|
|
||||||
from reflector.asynctask import asynctask
|
from reflector.asynctask import asynctask
|
||||||
|
from reflector.db.rooms import rooms_controller
|
||||||
from reflector.db.transcripts import (
|
from reflector.db.transcripts import (
|
||||||
|
SourceKind,
|
||||||
Transcript,
|
Transcript,
|
||||||
TranscriptStatus,
|
TranscriptStatus,
|
||||||
transcripts_controller,
|
transcripts_controller,
|
||||||
@@ -48,6 +51,7 @@ from reflector.processors.types import (
|
|||||||
)
|
)
|
||||||
from reflector.settings import settings
|
from reflector.settings import settings
|
||||||
from reflector.storage import get_transcripts_storage
|
from reflector.storage import get_transcripts_storage
|
||||||
|
from reflector.worker.webhook import send_transcript_webhook
|
||||||
|
|
||||||
|
|
||||||
class EmptyPipeline:
|
class EmptyPipeline:
|
||||||
@@ -385,7 +389,6 @@ async def task_pipeline_file_process(*, transcript_id: str):
|
|||||||
raise Exception(f"Transcript {transcript_id} not found")
|
raise Exception(f"Transcript {transcript_id} not found")
|
||||||
|
|
||||||
pipeline = PipelineMainFile(transcript_id=transcript_id)
|
pipeline = PipelineMainFile(transcript_id=transcript_id)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await pipeline.set_status(transcript_id, "processing")
|
await pipeline.set_status(transcript_id, "processing")
|
||||||
|
|
||||||
@@ -402,3 +405,17 @@ async def task_pipeline_file_process(*, transcript_id: str):
|
|||||||
except Exception:
|
except Exception:
|
||||||
await pipeline.set_status(transcript_id, "error")
|
await pipeline.set_status(transcript_id, "error")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
# Trigger webhook if this is a room recording with webhook configured
|
||||||
|
if transcript.source_kind == SourceKind.ROOM and transcript.room_id:
|
||||||
|
room = await rooms_controller.get_by_id(transcript.room_id)
|
||||||
|
if room and room.webhook_url:
|
||||||
|
logger.info(
|
||||||
|
"Dispatching webhook task",
|
||||||
|
transcript_id=transcript_id,
|
||||||
|
room_id=room.id,
|
||||||
|
webhook_url=room.webhook_url,
|
||||||
|
)
|
||||||
|
send_transcript_webhook.delay(
|
||||||
|
transcript_id, room.id, event_id=uuid.uuid4().hex
|
||||||
|
)
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from reflector.db.meetings import meetings_controller
|
|||||||
from reflector.db.rooms import rooms_controller
|
from reflector.db.rooms import rooms_controller
|
||||||
from reflector.settings import settings
|
from reflector.settings import settings
|
||||||
from reflector.whereby import create_meeting, upload_logo
|
from reflector.whereby import create_meeting, upload_logo
|
||||||
|
from reflector.worker.webhook import test_webhook
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -44,6 +45,11 @@ class Room(BaseModel):
|
|||||||
is_shared: bool
|
is_shared: bool
|
||||||
|
|
||||||
|
|
||||||
|
class RoomDetails(Room):
|
||||||
|
webhook_url: str
|
||||||
|
webhook_secret: str
|
||||||
|
|
||||||
|
|
||||||
class Meeting(BaseModel):
|
class Meeting(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
room_name: str
|
room_name: str
|
||||||
@@ -64,6 +70,8 @@ class CreateRoom(BaseModel):
|
|||||||
recording_type: str
|
recording_type: str
|
||||||
recording_trigger: str
|
recording_trigger: str
|
||||||
is_shared: bool
|
is_shared: bool
|
||||||
|
webhook_url: str
|
||||||
|
webhook_secret: str
|
||||||
|
|
||||||
|
|
||||||
class UpdateRoom(BaseModel):
|
class UpdateRoom(BaseModel):
|
||||||
@@ -76,16 +84,26 @@ class UpdateRoom(BaseModel):
|
|||||||
recording_type: str
|
recording_type: str
|
||||||
recording_trigger: str
|
recording_trigger: str
|
||||||
is_shared: bool
|
is_shared: bool
|
||||||
|
webhook_url: str
|
||||||
|
webhook_secret: str
|
||||||
|
|
||||||
|
|
||||||
class DeletionStatus(BaseModel):
|
class DeletionStatus(BaseModel):
|
||||||
status: str
|
status: str
|
||||||
|
|
||||||
|
|
||||||
@router.get("/rooms", response_model=Page[Room])
|
class WebhookTestResult(BaseModel):
|
||||||
|
success: bool
|
||||||
|
message: str = ""
|
||||||
|
error: str = ""
|
||||||
|
status_code: int | None = None
|
||||||
|
response_preview: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/rooms", response_model=Page[RoomDetails])
|
||||||
async def rooms_list(
|
async def rooms_list(
|
||||||
user: Annotated[Optional[auth.UserInfo], Depends(auth.current_user_optional)],
|
user: Annotated[Optional[auth.UserInfo], Depends(auth.current_user_optional)],
|
||||||
) -> list[Room]:
|
) -> list[RoomDetails]:
|
||||||
if not user and not settings.PUBLIC_MODE:
|
if not user and not settings.PUBLIC_MODE:
|
||||||
raise HTTPException(status_code=401, detail="Not authenticated")
|
raise HTTPException(status_code=401, detail="Not authenticated")
|
||||||
|
|
||||||
@@ -99,6 +117,18 @@ async def rooms_list(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/rooms/{room_id}", response_model=RoomDetails)
|
||||||
|
async def rooms_get(
|
||||||
|
room_id: str,
|
||||||
|
user: Annotated[Optional[auth.UserInfo], Depends(auth.current_user_optional)],
|
||||||
|
):
|
||||||
|
user_id = user["sub"] if user else None
|
||||||
|
room = await rooms_controller.get_by_id_for_http(room_id, user_id=user_id)
|
||||||
|
if not room:
|
||||||
|
raise HTTPException(status_code=404, detail="Room not found")
|
||||||
|
return room
|
||||||
|
|
||||||
|
|
||||||
@router.post("/rooms", response_model=Room)
|
@router.post("/rooms", response_model=Room)
|
||||||
async def rooms_create(
|
async def rooms_create(
|
||||||
room: CreateRoom,
|
room: CreateRoom,
|
||||||
@@ -117,10 +147,12 @@ async def rooms_create(
|
|||||||
recording_type=room.recording_type,
|
recording_type=room.recording_type,
|
||||||
recording_trigger=room.recording_trigger,
|
recording_trigger=room.recording_trigger,
|
||||||
is_shared=room.is_shared,
|
is_shared=room.is_shared,
|
||||||
|
webhook_url=room.webhook_url,
|
||||||
|
webhook_secret=room.webhook_secret,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.patch("/rooms/{room_id}", response_model=Room)
|
@router.patch("/rooms/{room_id}", response_model=RoomDetails)
|
||||||
async def rooms_update(
|
async def rooms_update(
|
||||||
room_id: str,
|
room_id: str,
|
||||||
info: UpdateRoom,
|
info: UpdateRoom,
|
||||||
@@ -209,3 +241,24 @@ async def rooms_create_meeting(
|
|||||||
meeting.host_room_url = ""
|
meeting.host_room_url = ""
|
||||||
|
|
||||||
return meeting
|
return meeting
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/rooms/{room_id}/webhook/test", response_model=WebhookTestResult)
|
||||||
|
async def rooms_test_webhook(
|
||||||
|
room_id: str,
|
||||||
|
user: Annotated[Optional[auth.UserInfo], Depends(auth.current_user_optional)],
|
||||||
|
):
|
||||||
|
"""Test webhook configuration by sending a sample payload."""
|
||||||
|
user_id = user["sub"] if user else None
|
||||||
|
|
||||||
|
room = await rooms_controller.get_by_id(room_id)
|
||||||
|
if not room:
|
||||||
|
raise HTTPException(status_code=404, detail="Room not found")
|
||||||
|
|
||||||
|
if user_id and room.user_id != user_id:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=403, detail="Not authorized to test this room's webhook"
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await test_webhook(room_id)
|
||||||
|
return WebhookTestResult(**result)
|
||||||
|
|||||||
258
server/reflector/worker/webhook.py
Normal file
258
server/reflector/worker/webhook.py
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
"""Webhook task for sending transcript notifications."""
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
import json
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
import structlog
|
||||||
|
from celery import shared_task
|
||||||
|
from celery.utils.log import get_task_logger
|
||||||
|
|
||||||
|
from reflector.db.rooms import rooms_controller
|
||||||
|
from reflector.db.transcripts import transcripts_controller
|
||||||
|
from reflector.pipelines.main_live_pipeline import asynctask
|
||||||
|
from reflector.settings import settings
|
||||||
|
from reflector.utils.webvtt import topics_to_webvtt
|
||||||
|
|
||||||
|
logger = structlog.wrap_logger(get_task_logger(__name__))
|
||||||
|
|
||||||
|
|
||||||
|
def generate_webhook_signature(payload: bytes, secret: str, timestamp: str) -> str:
|
||||||
|
"""Generate HMAC signature for webhook payload."""
|
||||||
|
signed_payload = f"{timestamp}.{payload.decode('utf-8')}"
|
||||||
|
hmac_obj = hmac.new(
|
||||||
|
secret.encode("utf-8"),
|
||||||
|
signed_payload.encode("utf-8"),
|
||||||
|
hashlib.sha256,
|
||||||
|
)
|
||||||
|
return hmac_obj.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task(
|
||||||
|
bind=True,
|
||||||
|
max_retries=30,
|
||||||
|
default_retry_delay=60,
|
||||||
|
retry_backoff=True,
|
||||||
|
retry_backoff_max=3600, # Max 1 hour between retries
|
||||||
|
)
|
||||||
|
@asynctask
|
||||||
|
async def send_transcript_webhook(
|
||||||
|
self,
|
||||||
|
transcript_id: str,
|
||||||
|
room_id: str,
|
||||||
|
event_id: str,
|
||||||
|
):
|
||||||
|
log = logger.bind(
|
||||||
|
transcript_id=transcript_id,
|
||||||
|
room_id=room_id,
|
||||||
|
retry_count=self.request.retries,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Fetch transcript and room
|
||||||
|
transcript = await transcripts_controller.get_by_id(transcript_id)
|
||||||
|
if not transcript:
|
||||||
|
log.error("Transcript not found, skipping webhook")
|
||||||
|
return
|
||||||
|
|
||||||
|
room = await rooms_controller.get_by_id(room_id)
|
||||||
|
if not room:
|
||||||
|
log.error("Room not found, skipping webhook")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not room.webhook_url:
|
||||||
|
log.info("No webhook URL configured for room, skipping")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Generate WebVTT content from topics
|
||||||
|
topics_data = []
|
||||||
|
|
||||||
|
if transcript.topics:
|
||||||
|
# Build topics data with diarized content per topic
|
||||||
|
for topic in transcript.topics:
|
||||||
|
topic_webvtt = topics_to_webvtt([topic]) if topic.words else ""
|
||||||
|
topics_data.append(
|
||||||
|
{
|
||||||
|
"title": topic.title,
|
||||||
|
"summary": topic.summary,
|
||||||
|
"timestamp": topic.timestamp,
|
||||||
|
"duration": topic.duration,
|
||||||
|
"webvtt": topic_webvtt,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Build webhook payload
|
||||||
|
frontend_url = f"{settings.UI_BASE_URL}/transcripts/{transcript.id}"
|
||||||
|
participants = [
|
||||||
|
{"id": p.id, "name": p.name, "speaker": p.speaker}
|
||||||
|
for p in (transcript.participants or [])
|
||||||
|
]
|
||||||
|
payload_data = {
|
||||||
|
"event": "transcript.completed",
|
||||||
|
"event_id": event_id,
|
||||||
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||||
|
"transcript": {
|
||||||
|
"id": transcript.id,
|
||||||
|
"room_id": transcript.room_id,
|
||||||
|
"created_at": transcript.created_at.isoformat(),
|
||||||
|
"duration": transcript.duration,
|
||||||
|
"title": transcript.title,
|
||||||
|
"short_summary": transcript.short_summary,
|
||||||
|
"long_summary": transcript.long_summary,
|
||||||
|
"webvtt": transcript.webvtt,
|
||||||
|
"topics": topics_data,
|
||||||
|
"participants": participants,
|
||||||
|
"source_language": transcript.source_language,
|
||||||
|
"target_language": transcript.target_language,
|
||||||
|
"status": transcript.status,
|
||||||
|
"frontend_url": frontend_url,
|
||||||
|
},
|
||||||
|
"room": {
|
||||||
|
"id": room.id,
|
||||||
|
"name": room.name,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Convert to JSON
|
||||||
|
payload_json = json.dumps(payload_data, separators=(",", ":"))
|
||||||
|
payload_bytes = payload_json.encode("utf-8")
|
||||||
|
|
||||||
|
# Generate signature if secret is configured
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"User-Agent": "Reflector-Webhook/1.0",
|
||||||
|
"X-Webhook-Event": "transcript.completed",
|
||||||
|
"X-Webhook-Retry": str(self.request.retries),
|
||||||
|
}
|
||||||
|
|
||||||
|
if room.webhook_secret:
|
||||||
|
timestamp = str(int(datetime.now(timezone.utc).timestamp()))
|
||||||
|
signature = generate_webhook_signature(
|
||||||
|
payload_bytes, room.webhook_secret, timestamp
|
||||||
|
)
|
||||||
|
headers["X-Webhook-Signature"] = f"t={timestamp},v1={signature}"
|
||||||
|
|
||||||
|
# Send webhook with timeout
|
||||||
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||||
|
log.info(
|
||||||
|
"Sending webhook",
|
||||||
|
url=room.webhook_url,
|
||||||
|
payload_size=len(payload_bytes),
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await client.post(
|
||||||
|
room.webhook_url,
|
||||||
|
content=payload_bytes,
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
"Webhook sent successfully",
|
||||||
|
status_code=response.status_code,
|
||||||
|
response_size=len(response.content),
|
||||||
|
)
|
||||||
|
|
||||||
|
except httpx.HTTPStatusError as e:
|
||||||
|
log.error(
|
||||||
|
"Webhook failed with HTTP error",
|
||||||
|
status_code=e.response.status_code,
|
||||||
|
response_text=e.response.text[:500], # First 500 chars
|
||||||
|
)
|
||||||
|
|
||||||
|
# Don't retry on client errors (4xx)
|
||||||
|
if 400 <= e.response.status_code < 500:
|
||||||
|
log.error("Client error, not retrying")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Retry on server errors (5xx)
|
||||||
|
raise self.retry(exc=e)
|
||||||
|
|
||||||
|
except (httpx.ConnectError, httpx.TimeoutException) as e:
|
||||||
|
# Retry on network errors
|
||||||
|
log.error("Webhook failed with connection error", error=str(e))
|
||||||
|
raise self.retry(exc=e)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# Retry on unexpected errors
|
||||||
|
log.exception("Unexpected error in webhook task", error=str(e))
|
||||||
|
raise self.retry(exc=e)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_webhook(room_id: str) -> dict:
|
||||||
|
"""
|
||||||
|
Test webhook configuration by sending a sample payload.
|
||||||
|
Returns immediately with success/failure status.
|
||||||
|
This is the shared implementation used by both the API endpoint and Celery task.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
room = await rooms_controller.get_by_id(room_id)
|
||||||
|
if not room:
|
||||||
|
return {"success": False, "error": "Room not found"}
|
||||||
|
|
||||||
|
if not room.webhook_url:
|
||||||
|
return {"success": False, "error": "No webhook URL configured"}
|
||||||
|
|
||||||
|
now = (datetime.now(timezone.utc).isoformat(),)
|
||||||
|
payload_data = {
|
||||||
|
"event": "test",
|
||||||
|
"event_id": uuid.uuid4().hex,
|
||||||
|
"timestamp": now,
|
||||||
|
"message": "This is a test webhook from Reflector",
|
||||||
|
"room": {
|
||||||
|
"id": room.id,
|
||||||
|
"name": room.name,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
payload_json = json.dumps(payload_data, separators=(",", ":"))
|
||||||
|
payload_bytes = payload_json.encode("utf-8")
|
||||||
|
|
||||||
|
# Generate headers with signature
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"User-Agent": "Reflector-Webhook/1.0",
|
||||||
|
"X-Webhook-Event": "test",
|
||||||
|
}
|
||||||
|
|
||||||
|
if room.webhook_secret:
|
||||||
|
timestamp = str(int(datetime.now(timezone.utc).timestamp()))
|
||||||
|
signature = generate_webhook_signature(
|
||||||
|
payload_bytes, room.webhook_secret, timestamp
|
||||||
|
)
|
||||||
|
headers["X-Webhook-Signature"] = f"t={timestamp},v1={signature}"
|
||||||
|
|
||||||
|
# Send test webhook with short timeout
|
||||||
|
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||||
|
response = await client.post(
|
||||||
|
room.webhook_url,
|
||||||
|
content=payload_bytes,
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": response.is_success,
|
||||||
|
"status_code": response.status_code,
|
||||||
|
"message": f"Webhook test {'successful' if response.is_success else 'failed'}",
|
||||||
|
"response_preview": response.text if response.text else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
except httpx.TimeoutException:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": "Webhook request timed out (10 seconds)",
|
||||||
|
}
|
||||||
|
except httpx.ConnectError as e:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"Could not connect to webhook URL: {str(e)}",
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"Unexpected error: {str(e)}",
|
||||||
|
}
|
||||||
@@ -12,11 +12,11 @@ import {
|
|||||||
HStack,
|
HStack,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { LuLink } from "react-icons/lu";
|
import { LuLink } from "react-icons/lu";
|
||||||
import { Room } from "../../../api";
|
import { RoomDetails } from "../../../api";
|
||||||
import { RoomActionsMenu } from "./RoomActionsMenu";
|
import { RoomActionsMenu } from "./RoomActionsMenu";
|
||||||
|
|
||||||
interface RoomCardsProps {
|
interface RoomCardsProps {
|
||||||
rooms: Room[];
|
rooms: RoomDetails[];
|
||||||
linkCopied: string;
|
linkCopied: string;
|
||||||
onCopyUrl: (roomName: string) => void;
|
onCopyUrl: (roomName: string) => void;
|
||||||
onEdit: (roomId: string, roomData: any) => void;
|
onEdit: (roomId: string, roomData: any) => void;
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Box, Heading, Text, VStack } from "@chakra-ui/react";
|
import { Box, Heading, Text, VStack } from "@chakra-ui/react";
|
||||||
import { Room } from "../../../api";
|
import { RoomDetails } from "../../../api";
|
||||||
import { RoomTable } from "./RoomTable";
|
import { RoomTable } from "./RoomTable";
|
||||||
import { RoomCards } from "./RoomCards";
|
import { RoomCards } from "./RoomCards";
|
||||||
|
|
||||||
interface RoomListProps {
|
interface RoomListProps {
|
||||||
title: string;
|
title: string;
|
||||||
rooms: Room[];
|
rooms: RoomDetails[];
|
||||||
linkCopied: string;
|
linkCopied: string;
|
||||||
onCopyUrl: (roomName: string) => void;
|
onCopyUrl: (roomName: string) => void;
|
||||||
onEdit: (roomId: string, roomData: any) => void;
|
onEdit: (roomId: string, roomData: any) => void;
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ import {
|
|||||||
Spinner,
|
Spinner,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { LuLink } from "react-icons/lu";
|
import { LuLink } from "react-icons/lu";
|
||||||
import { Room } from "../../../api";
|
import { RoomDetails } from "../../../api";
|
||||||
import { RoomActionsMenu } from "./RoomActionsMenu";
|
import { RoomActionsMenu } from "./RoomActionsMenu";
|
||||||
|
|
||||||
interface RoomTableProps {
|
interface RoomTableProps {
|
||||||
rooms: Room[];
|
rooms: RoomDetails[];
|
||||||
linkCopied: string;
|
linkCopied: string;
|
||||||
onCopyUrl: (roomName: string) => void;
|
onCopyUrl: (roomName: string) => void;
|
||||||
onEdit: (roomId: string, roomData: any) => void;
|
onEdit: (roomId: string, roomData: any) => void;
|
||||||
|
|||||||
@@ -11,13 +11,15 @@ import {
|
|||||||
Input,
|
Input,
|
||||||
Select,
|
Select,
|
||||||
Spinner,
|
Spinner,
|
||||||
|
IconButton,
|
||||||
createListCollection,
|
createListCollection,
|
||||||
useDisclosure,
|
useDisclosure,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { LuEye, LuEyeOff } from "react-icons/lu";
|
||||||
import useApi from "../../lib/useApi";
|
import useApi from "../../lib/useApi";
|
||||||
import useRoomList from "./useRoomList";
|
import useRoomList from "./useRoomList";
|
||||||
import { ApiError, Room } from "../../api";
|
import { ApiError, RoomDetails } from "../../api";
|
||||||
import { RoomList } from "./_components/RoomList";
|
import { RoomList } from "./_components/RoomList";
|
||||||
import { PaginationPage } from "../browse/_components/Pagination";
|
import { PaginationPage } from "../browse/_components/Pagination";
|
||||||
|
|
||||||
@@ -55,6 +57,8 @@ const roomInitialState = {
|
|||||||
recordingType: "cloud",
|
recordingType: "cloud",
|
||||||
recordingTrigger: "automatic-2nd-participant",
|
recordingTrigger: "automatic-2nd-participant",
|
||||||
isShared: false,
|
isShared: false,
|
||||||
|
webhookUrl: "",
|
||||||
|
webhookSecret: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RoomsList() {
|
export default function RoomsList() {
|
||||||
@@ -83,6 +87,11 @@ export default function RoomsList() {
|
|||||||
const [topics, setTopics] = useState<Topic[]>([]);
|
const [topics, setTopics] = useState<Topic[]>([]);
|
||||||
const [nameError, setNameError] = useState("");
|
const [nameError, setNameError] = useState("");
|
||||||
const [linkCopied, setLinkCopied] = useState("");
|
const [linkCopied, setLinkCopied] = useState("");
|
||||||
|
const [testingWebhook, setTestingWebhook] = useState(false);
|
||||||
|
const [webhookTestResult, setWebhookTestResult] = useState<string | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
const [showWebhookSecret, setShowWebhookSecret] = useState(false);
|
||||||
interface Stream {
|
interface Stream {
|
||||||
stream_id: number;
|
stream_id: number;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -155,6 +164,69 @@ export default function RoomsList() {
|
|||||||
}, 2000);
|
}, 2000);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCloseDialog = () => {
|
||||||
|
setShowWebhookSecret(false);
|
||||||
|
setWebhookTestResult(null);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTestWebhook = async () => {
|
||||||
|
if (!room.webhookUrl || !editRoomId) {
|
||||||
|
setWebhookTestResult("Please enter a webhook URL first");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTestingWebhook(true);
|
||||||
|
setWebhookTestResult(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await api?.v1RoomsTestWebhook({
|
||||||
|
roomId: editRoomId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response?.success) {
|
||||||
|
setWebhookTestResult(
|
||||||
|
`✅ Webhook test successful! Status: ${response.status_code}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let errorMsg = `❌ Webhook test failed`;
|
||||||
|
if (response?.status_code) {
|
||||||
|
errorMsg += ` (Status: ${response.status_code})`;
|
||||||
|
}
|
||||||
|
if (response?.error) {
|
||||||
|
errorMsg += `: ${response.error}`;
|
||||||
|
} else if (response?.response_preview) {
|
||||||
|
// Try to parse and extract meaningful error from response
|
||||||
|
// Specific to N8N at the moment, as there is no specification for that
|
||||||
|
// We could just display as is, but decided here to dig a little bit more.
|
||||||
|
try {
|
||||||
|
const preview = JSON.parse(response.response_preview);
|
||||||
|
if (preview.message) {
|
||||||
|
errorMsg += `: ${preview.message}`;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// If not JSON, just show the preview text (truncated)
|
||||||
|
const previewText = response.response_preview.substring(0, 150);
|
||||||
|
errorMsg += `: ${previewText}`;
|
||||||
|
}
|
||||||
|
} else if (response?.message) {
|
||||||
|
errorMsg += `: ${response.message}`;
|
||||||
|
}
|
||||||
|
setWebhookTestResult(errorMsg);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error testing webhook:", error);
|
||||||
|
setWebhookTestResult("❌ Failed to test webhook. Please check your URL.");
|
||||||
|
} finally {
|
||||||
|
setTestingWebhook(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear result after 5 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
setWebhookTestResult(null);
|
||||||
|
}, 5000);
|
||||||
|
};
|
||||||
|
|
||||||
const handleSaveRoom = async () => {
|
const handleSaveRoom = async () => {
|
||||||
try {
|
try {
|
||||||
if (RESERVED_PATHS.includes(room.name)) {
|
if (RESERVED_PATHS.includes(room.name)) {
|
||||||
@@ -172,6 +244,8 @@ export default function RoomsList() {
|
|||||||
recording_type: room.recordingType,
|
recording_type: room.recordingType,
|
||||||
recording_trigger: room.recordingTrigger,
|
recording_trigger: room.recordingTrigger,
|
||||||
is_shared: room.isShared,
|
is_shared: room.isShared,
|
||||||
|
webhook_url: room.webhookUrl,
|
||||||
|
webhook_secret: room.webhookSecret,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
@@ -190,7 +264,7 @@ export default function RoomsList() {
|
|||||||
setEditRoomId("");
|
setEditRoomId("");
|
||||||
setNameError("");
|
setNameError("");
|
||||||
refetch();
|
refetch();
|
||||||
onClose();
|
handleCloseDialog();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (
|
if (
|
||||||
err instanceof ApiError &&
|
err instanceof ApiError &&
|
||||||
@@ -206,18 +280,46 @@ export default function RoomsList() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEditRoom = (roomId, roomData) => {
|
const handleEditRoom = async (roomId, roomData) => {
|
||||||
setRoom({
|
// Reset states
|
||||||
name: roomData.name,
|
setShowWebhookSecret(false);
|
||||||
zulipAutoPost: roomData.zulip_auto_post,
|
setWebhookTestResult(null);
|
||||||
zulipStream: roomData.zulip_stream,
|
|
||||||
zulipTopic: roomData.zulip_topic,
|
// Fetch full room details to get webhook fields
|
||||||
isLocked: roomData.is_locked,
|
try {
|
||||||
roomMode: roomData.room_mode,
|
const detailedRoom = await api?.v1RoomsGet({ roomId });
|
||||||
recordingType: roomData.recording_type,
|
if (detailedRoom) {
|
||||||
recordingTrigger: roomData.recording_trigger,
|
setRoom({
|
||||||
isShared: roomData.is_shared,
|
name: detailedRoom.name,
|
||||||
});
|
zulipAutoPost: detailedRoom.zulip_auto_post,
|
||||||
|
zulipStream: detailedRoom.zulip_stream,
|
||||||
|
zulipTopic: detailedRoom.zulip_topic,
|
||||||
|
isLocked: detailedRoom.is_locked,
|
||||||
|
roomMode: detailedRoom.room_mode,
|
||||||
|
recordingType: detailedRoom.recording_type,
|
||||||
|
recordingTrigger: detailedRoom.recording_trigger,
|
||||||
|
isShared: detailedRoom.is_shared,
|
||||||
|
webhookUrl: detailedRoom.webhook_url || "",
|
||||||
|
webhookSecret: detailedRoom.webhook_secret || "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch room details, using list data:", error);
|
||||||
|
// Fallback to using the data from the list
|
||||||
|
setRoom({
|
||||||
|
name: roomData.name,
|
||||||
|
zulipAutoPost: roomData.zulip_auto_post,
|
||||||
|
zulipStream: roomData.zulip_stream,
|
||||||
|
zulipTopic: roomData.zulip_topic,
|
||||||
|
isLocked: roomData.is_locked,
|
||||||
|
roomMode: roomData.room_mode,
|
||||||
|
recordingType: roomData.recording_type,
|
||||||
|
recordingTrigger: roomData.recording_trigger,
|
||||||
|
isShared: roomData.is_shared,
|
||||||
|
webhookUrl: roomData.webhook_url || "",
|
||||||
|
webhookSecret: roomData.webhook_secret || "",
|
||||||
|
});
|
||||||
|
}
|
||||||
setEditRoomId(roomId);
|
setEditRoomId(roomId);
|
||||||
setIsEditing(true);
|
setIsEditing(true);
|
||||||
setNameError("");
|
setNameError("");
|
||||||
@@ -250,9 +352,9 @@ export default function RoomsList() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const myRooms: Room[] =
|
const myRooms: RoomDetails[] =
|
||||||
response?.items.filter((roomData) => !roomData.is_shared) || [];
|
response?.items.filter((roomData) => !roomData.is_shared) || [];
|
||||||
const sharedRooms: Room[] =
|
const sharedRooms: RoomDetails[] =
|
||||||
response?.items.filter((roomData) => roomData.is_shared) || [];
|
response?.items.filter((roomData) => roomData.is_shared) || [];
|
||||||
|
|
||||||
if (loading && !response)
|
if (loading && !response)
|
||||||
@@ -287,6 +389,8 @@ export default function RoomsList() {
|
|||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
setRoom(roomInitialState);
|
setRoom(roomInitialState);
|
||||||
setNameError("");
|
setNameError("");
|
||||||
|
setShowWebhookSecret(false);
|
||||||
|
setWebhookTestResult(null);
|
||||||
onOpen();
|
onOpen();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -296,7 +400,7 @@ export default function RoomsList() {
|
|||||||
|
|
||||||
<Dialog.Root
|
<Dialog.Root
|
||||||
open={open}
|
open={open}
|
||||||
onOpenChange={(e) => (e.open ? onOpen() : onClose())}
|
onOpenChange={(e) => (e.open ? onOpen() : handleCloseDialog())}
|
||||||
size="lg"
|
size="lg"
|
||||||
>
|
>
|
||||||
<Dialog.Backdrop />
|
<Dialog.Backdrop />
|
||||||
@@ -533,6 +637,109 @@ export default function RoomsList() {
|
|||||||
</Select.Positioner>
|
</Select.Positioner>
|
||||||
</Select.Root>
|
</Select.Root>
|
||||||
</Field.Root>
|
</Field.Root>
|
||||||
|
|
||||||
|
{/* Webhook Configuration Section */}
|
||||||
|
<Field.Root mt={8}>
|
||||||
|
<Field.Label>Webhook URL</Field.Label>
|
||||||
|
<Input
|
||||||
|
name="webhookUrl"
|
||||||
|
type="url"
|
||||||
|
placeholder="https://example.com/webhook"
|
||||||
|
value={room.webhookUrl}
|
||||||
|
onChange={handleRoomChange}
|
||||||
|
/>
|
||||||
|
<Field.HelperText>
|
||||||
|
Optional: URL to receive notifications when transcripts are
|
||||||
|
ready
|
||||||
|
</Field.HelperText>
|
||||||
|
</Field.Root>
|
||||||
|
|
||||||
|
{room.webhookUrl && (
|
||||||
|
<>
|
||||||
|
<Field.Root mt={4}>
|
||||||
|
<Field.Label>Webhook Secret</Field.Label>
|
||||||
|
<Flex gap={2}>
|
||||||
|
<Input
|
||||||
|
name="webhookSecret"
|
||||||
|
type={showWebhookSecret ? "text" : "password"}
|
||||||
|
value={room.webhookSecret}
|
||||||
|
onChange={handleRoomChange}
|
||||||
|
placeholder={
|
||||||
|
isEditing && room.webhookSecret
|
||||||
|
? "••••••••"
|
||||||
|
: "Leave empty to auto-generate"
|
||||||
|
}
|
||||||
|
flex="1"
|
||||||
|
/>
|
||||||
|
{isEditing && room.webhookSecret && (
|
||||||
|
<IconButton
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
aria-label={
|
||||||
|
showWebhookSecret ? "Hide secret" : "Show secret"
|
||||||
|
}
|
||||||
|
onClick={() =>
|
||||||
|
setShowWebhookSecret(!showWebhookSecret)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{showWebhookSecret ? <LuEyeOff /> : <LuEye />}
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
<Field.HelperText>
|
||||||
|
Used for HMAC signature verification (auto-generated if
|
||||||
|
left empty)
|
||||||
|
</Field.HelperText>
|
||||||
|
</Field.Root>
|
||||||
|
|
||||||
|
{isEditing && (
|
||||||
|
<>
|
||||||
|
<Flex
|
||||||
|
mt={2}
|
||||||
|
gap={2}
|
||||||
|
alignItems="flex-start"
|
||||||
|
direction="column"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleTestWebhook}
|
||||||
|
disabled={testingWebhook || !room.webhookUrl}
|
||||||
|
>
|
||||||
|
{testingWebhook ? (
|
||||||
|
<>
|
||||||
|
<Spinner size="xs" mr={2} />
|
||||||
|
Testing...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"Test Webhook"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
{webhookTestResult && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: "14px",
|
||||||
|
wordBreak: "break-word",
|
||||||
|
maxWidth: "100%",
|
||||||
|
padding: "8px",
|
||||||
|
borderRadius: "4px",
|
||||||
|
backgroundColor: webhookTestResult.startsWith(
|
||||||
|
"✅",
|
||||||
|
)
|
||||||
|
? "#f0fdf4"
|
||||||
|
: "#fef2f2",
|
||||||
|
border: `1px solid ${webhookTestResult.startsWith("✅") ? "#86efac" : "#fca5a5"}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{webhookTestResult}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<Field.Root mt={4}>
|
<Field.Root mt={4}>
|
||||||
<Checkbox.Root
|
<Checkbox.Root
|
||||||
name="isShared"
|
name="isShared"
|
||||||
@@ -557,7 +764,7 @@ export default function RoomsList() {
|
|||||||
</Field.Root>
|
</Field.Root>
|
||||||
</Dialog.Body>
|
</Dialog.Body>
|
||||||
<Dialog.Footer>
|
<Dialog.Footer>
|
||||||
<Button variant="ghost" onClick={onClose}>
|
<Button variant="ghost" onClick={handleCloseDialog}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useError } from "../../(errors)/errorContext";
|
import { useError } from "../../(errors)/errorContext";
|
||||||
import useApi from "../../lib/useApi";
|
import useApi from "../../lib/useApi";
|
||||||
import { Page_Room_ } from "../../api";
|
import { Page_RoomDetails_ } from "../../api";
|
||||||
import { PaginationPage } from "../browse/_components/Pagination";
|
import { PaginationPage } from "../browse/_components/Pagination";
|
||||||
|
|
||||||
type RoomList = {
|
type RoomList = {
|
||||||
response: Page_Room_ | null;
|
response: Page_RoomDetails_ | null;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
error: Error | null;
|
error: Error | null;
|
||||||
refetch: () => void;
|
refetch: () => void;
|
||||||
@@ -13,7 +13,7 @@ type RoomList = {
|
|||||||
|
|
||||||
//always protected
|
//always protected
|
||||||
const useRoomList = (page: PaginationPage): RoomList => {
|
const useRoomList = (page: PaginationPage): RoomList => {
|
||||||
const [response, setResponse] = useState<Page_Room_ | null>(null);
|
const [response, setResponse] = useState<Page_RoomDetails_ | null>(null);
|
||||||
const [loading, setLoading] = useState<boolean>(true);
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
const [error, setErrorState] = useState<Error | null>(null);
|
const [error, setErrorState] = useState<Error | null>(null);
|
||||||
const { setError } = useError();
|
const { setError } = useError();
|
||||||
|
|||||||
@@ -91,6 +91,14 @@ export const $CreateRoom = {
|
|||||||
type: "boolean",
|
type: "boolean",
|
||||||
title: "Is Shared",
|
title: "Is Shared",
|
||||||
},
|
},
|
||||||
|
webhook_url: {
|
||||||
|
type: "string",
|
||||||
|
title: "Webhook Url",
|
||||||
|
},
|
||||||
|
webhook_secret: {
|
||||||
|
type: "string",
|
||||||
|
title: "Webhook Secret",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
type: "object",
|
type: "object",
|
||||||
required: [
|
required: [
|
||||||
@@ -103,6 +111,8 @@ export const $CreateRoom = {
|
|||||||
"recording_type",
|
"recording_type",
|
||||||
"recording_trigger",
|
"recording_trigger",
|
||||||
"is_shared",
|
"is_shared",
|
||||||
|
"webhook_url",
|
||||||
|
"webhook_secret",
|
||||||
],
|
],
|
||||||
title: "CreateRoom",
|
title: "CreateRoom",
|
||||||
} as const;
|
} as const;
|
||||||
@@ -809,11 +819,11 @@ export const $Page_GetTranscriptMinimal_ = {
|
|||||||
title: "Page[GetTranscriptMinimal]",
|
title: "Page[GetTranscriptMinimal]",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const $Page_Room_ = {
|
export const $Page_RoomDetails_ = {
|
||||||
properties: {
|
properties: {
|
||||||
items: {
|
items: {
|
||||||
items: {
|
items: {
|
||||||
$ref: "#/components/schemas/Room",
|
$ref: "#/components/schemas/RoomDetails",
|
||||||
},
|
},
|
||||||
type: "array",
|
type: "array",
|
||||||
title: "Items",
|
title: "Items",
|
||||||
@@ -869,7 +879,7 @@ export const $Page_Room_ = {
|
|||||||
},
|
},
|
||||||
type: "object",
|
type: "object",
|
||||||
required: ["items", "page", "size"],
|
required: ["items", "page", "size"],
|
||||||
title: "Page[Room]",
|
title: "Page[RoomDetails]",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const $Participant = {
|
export const $Participant = {
|
||||||
@@ -969,6 +979,86 @@ export const $Room = {
|
|||||||
title: "Room",
|
title: "Room",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export const $RoomDetails = {
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: "string",
|
||||||
|
title: "Id",
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: "string",
|
||||||
|
title: "Name",
|
||||||
|
},
|
||||||
|
user_id: {
|
||||||
|
type: "string",
|
||||||
|
title: "User Id",
|
||||||
|
},
|
||||||
|
created_at: {
|
||||||
|
type: "string",
|
||||||
|
format: "date-time",
|
||||||
|
title: "Created At",
|
||||||
|
},
|
||||||
|
zulip_auto_post: {
|
||||||
|
type: "boolean",
|
||||||
|
title: "Zulip Auto Post",
|
||||||
|
},
|
||||||
|
zulip_stream: {
|
||||||
|
type: "string",
|
||||||
|
title: "Zulip Stream",
|
||||||
|
},
|
||||||
|
zulip_topic: {
|
||||||
|
type: "string",
|
||||||
|
title: "Zulip Topic",
|
||||||
|
},
|
||||||
|
is_locked: {
|
||||||
|
type: "boolean",
|
||||||
|
title: "Is Locked",
|
||||||
|
},
|
||||||
|
room_mode: {
|
||||||
|
type: "string",
|
||||||
|
title: "Room Mode",
|
||||||
|
},
|
||||||
|
recording_type: {
|
||||||
|
type: "string",
|
||||||
|
title: "Recording Type",
|
||||||
|
},
|
||||||
|
recording_trigger: {
|
||||||
|
type: "string",
|
||||||
|
title: "Recording Trigger",
|
||||||
|
},
|
||||||
|
is_shared: {
|
||||||
|
type: "boolean",
|
||||||
|
title: "Is Shared",
|
||||||
|
},
|
||||||
|
webhook_url: {
|
||||||
|
type: "string",
|
||||||
|
title: "Webhook Url",
|
||||||
|
},
|
||||||
|
webhook_secret: {
|
||||||
|
type: "string",
|
||||||
|
title: "Webhook Secret",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type: "object",
|
||||||
|
required: [
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"user_id",
|
||||||
|
"created_at",
|
||||||
|
"zulip_auto_post",
|
||||||
|
"zulip_stream",
|
||||||
|
"zulip_topic",
|
||||||
|
"is_locked",
|
||||||
|
"room_mode",
|
||||||
|
"recording_type",
|
||||||
|
"recording_trigger",
|
||||||
|
"is_shared",
|
||||||
|
"webhook_url",
|
||||||
|
"webhook_secret",
|
||||||
|
],
|
||||||
|
title: "RoomDetails",
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const $RtcOffer = {
|
export const $RtcOffer = {
|
||||||
properties: {
|
properties: {
|
||||||
sdp: {
|
sdp: {
|
||||||
@@ -1351,6 +1441,14 @@ export const $UpdateRoom = {
|
|||||||
type: "boolean",
|
type: "boolean",
|
||||||
title: "Is Shared",
|
title: "Is Shared",
|
||||||
},
|
},
|
||||||
|
webhook_url: {
|
||||||
|
type: "string",
|
||||||
|
title: "Webhook Url",
|
||||||
|
},
|
||||||
|
webhook_secret: {
|
||||||
|
type: "string",
|
||||||
|
title: "Webhook Secret",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
type: "object",
|
type: "object",
|
||||||
required: [
|
required: [
|
||||||
@@ -1363,6 +1461,8 @@ export const $UpdateRoom = {
|
|||||||
"recording_type",
|
"recording_type",
|
||||||
"recording_trigger",
|
"recording_trigger",
|
||||||
"is_shared",
|
"is_shared",
|
||||||
|
"webhook_url",
|
||||||
|
"webhook_secret",
|
||||||
],
|
],
|
||||||
title: "UpdateRoom",
|
title: "UpdateRoom",
|
||||||
} as const;
|
} as const;
|
||||||
@@ -1541,6 +1641,50 @@ export const $ValidationError = {
|
|||||||
title: "ValidationError",
|
title: "ValidationError",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export const $WebhookTestResult = {
|
||||||
|
properties: {
|
||||||
|
success: {
|
||||||
|
type: "boolean",
|
||||||
|
title: "Success",
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
type: "string",
|
||||||
|
title: "Message",
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
type: "string",
|
||||||
|
title: "Error",
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
status_code: {
|
||||||
|
anyOf: [
|
||||||
|
{
|
||||||
|
type: "integer",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "null",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
title: "Status Code",
|
||||||
|
},
|
||||||
|
response_preview: {
|
||||||
|
anyOf: [
|
||||||
|
{
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "null",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
title: "Response Preview",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type: "object",
|
||||||
|
required: ["success"],
|
||||||
|
title: "WebhookTestResult",
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const $WherebyWebhookEvent = {
|
export const $WherebyWebhookEvent = {
|
||||||
properties: {
|
properties: {
|
||||||
apiVersion: {
|
apiVersion: {
|
||||||
|
|||||||
@@ -10,12 +10,16 @@ import type {
|
|||||||
V1RoomsListResponse,
|
V1RoomsListResponse,
|
||||||
V1RoomsCreateData,
|
V1RoomsCreateData,
|
||||||
V1RoomsCreateResponse,
|
V1RoomsCreateResponse,
|
||||||
|
V1RoomsGetData,
|
||||||
|
V1RoomsGetResponse,
|
||||||
V1RoomsUpdateData,
|
V1RoomsUpdateData,
|
||||||
V1RoomsUpdateResponse,
|
V1RoomsUpdateResponse,
|
||||||
V1RoomsDeleteData,
|
V1RoomsDeleteData,
|
||||||
V1RoomsDeleteResponse,
|
V1RoomsDeleteResponse,
|
||||||
V1RoomsCreateMeetingData,
|
V1RoomsCreateMeetingData,
|
||||||
V1RoomsCreateMeetingResponse,
|
V1RoomsCreateMeetingResponse,
|
||||||
|
V1RoomsTestWebhookData,
|
||||||
|
V1RoomsTestWebhookResponse,
|
||||||
V1TranscriptsListData,
|
V1TranscriptsListData,
|
||||||
V1TranscriptsListResponse,
|
V1TranscriptsListResponse,
|
||||||
V1TranscriptsCreateData,
|
V1TranscriptsCreateData,
|
||||||
@@ -118,7 +122,7 @@ export class DefaultService {
|
|||||||
* @param data The data for the request.
|
* @param data The data for the request.
|
||||||
* @param data.page Page number
|
* @param data.page Page number
|
||||||
* @param data.size Page size
|
* @param data.size Page size
|
||||||
* @returns Page_Room_ Successful Response
|
* @returns Page_RoomDetails_ Successful Response
|
||||||
* @throws ApiError
|
* @throws ApiError
|
||||||
*/
|
*/
|
||||||
public v1RoomsList(
|
public v1RoomsList(
|
||||||
@@ -158,12 +162,34 @@ export class DefaultService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rooms Get
|
||||||
|
* @param data The data for the request.
|
||||||
|
* @param data.roomId
|
||||||
|
* @returns RoomDetails Successful Response
|
||||||
|
* @throws ApiError
|
||||||
|
*/
|
||||||
|
public v1RoomsGet(
|
||||||
|
data: V1RoomsGetData,
|
||||||
|
): CancelablePromise<V1RoomsGetResponse> {
|
||||||
|
return this.httpRequest.request({
|
||||||
|
method: "GET",
|
||||||
|
url: "/v1/rooms/{room_id}",
|
||||||
|
path: {
|
||||||
|
room_id: data.roomId,
|
||||||
|
},
|
||||||
|
errors: {
|
||||||
|
422: "Validation Error",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rooms Update
|
* Rooms Update
|
||||||
* @param data The data for the request.
|
* @param data The data for the request.
|
||||||
* @param data.roomId
|
* @param data.roomId
|
||||||
* @param data.requestBody
|
* @param data.requestBody
|
||||||
* @returns Room Successful Response
|
* @returns RoomDetails Successful Response
|
||||||
* @throws ApiError
|
* @throws ApiError
|
||||||
*/
|
*/
|
||||||
public v1RoomsUpdate(
|
public v1RoomsUpdate(
|
||||||
@@ -227,6 +253,29 @@ export class DefaultService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rooms Test Webhook
|
||||||
|
* Test webhook configuration by sending a sample payload.
|
||||||
|
* @param data The data for the request.
|
||||||
|
* @param data.roomId
|
||||||
|
* @returns WebhookTestResult Successful Response
|
||||||
|
* @throws ApiError
|
||||||
|
*/
|
||||||
|
public v1RoomsTestWebhook(
|
||||||
|
data: V1RoomsTestWebhookData,
|
||||||
|
): CancelablePromise<V1RoomsTestWebhookResponse> {
|
||||||
|
return this.httpRequest.request({
|
||||||
|
method: "POST",
|
||||||
|
url: "/v1/rooms/{room_id}/webhook/test",
|
||||||
|
path: {
|
||||||
|
room_id: data.roomId,
|
||||||
|
},
|
||||||
|
errors: {
|
||||||
|
422: "Validation Error",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transcripts List
|
* Transcripts List
|
||||||
* @param data The data for the request.
|
* @param data The data for the request.
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ export type CreateRoom = {
|
|||||||
recording_type: string;
|
recording_type: string;
|
||||||
recording_trigger: string;
|
recording_trigger: string;
|
||||||
is_shared: boolean;
|
is_shared: boolean;
|
||||||
|
webhook_url: string;
|
||||||
|
webhook_secret: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CreateTranscript = {
|
export type CreateTranscript = {
|
||||||
@@ -147,8 +149,8 @@ export type Page_GetTranscriptMinimal_ = {
|
|||||||
pages?: number | null;
|
pages?: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Page_Room_ = {
|
export type Page_RoomDetails_ = {
|
||||||
items: Array<Room>;
|
items: Array<RoomDetails>;
|
||||||
total?: number | null;
|
total?: number | null;
|
||||||
page: number | null;
|
page: number | null;
|
||||||
size: number | null;
|
size: number | null;
|
||||||
@@ -176,6 +178,23 @@ export type Room = {
|
|||||||
is_shared: boolean;
|
is_shared: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type RoomDetails = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
user_id: string;
|
||||||
|
created_at: string;
|
||||||
|
zulip_auto_post: boolean;
|
||||||
|
zulip_stream: string;
|
||||||
|
zulip_topic: string;
|
||||||
|
is_locked: boolean;
|
||||||
|
room_mode: string;
|
||||||
|
recording_type: string;
|
||||||
|
recording_trigger: string;
|
||||||
|
is_shared: boolean;
|
||||||
|
webhook_url: string;
|
||||||
|
webhook_secret: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type RtcOffer = {
|
export type RtcOffer = {
|
||||||
sdp: string;
|
sdp: string;
|
||||||
type: string;
|
type: string;
|
||||||
@@ -281,6 +300,8 @@ export type UpdateRoom = {
|
|||||||
recording_type: string;
|
recording_type: string;
|
||||||
recording_trigger: string;
|
recording_trigger: string;
|
||||||
is_shared: boolean;
|
is_shared: boolean;
|
||||||
|
webhook_url: string;
|
||||||
|
webhook_secret: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UpdateTranscript = {
|
export type UpdateTranscript = {
|
||||||
@@ -307,6 +328,14 @@ export type ValidationError = {
|
|||||||
type: string;
|
type: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type WebhookTestResult = {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
error?: string;
|
||||||
|
status_code?: number | null;
|
||||||
|
response_preview?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
export type WherebyWebhookEvent = {
|
export type WherebyWebhookEvent = {
|
||||||
apiVersion: string;
|
apiVersion: string;
|
||||||
id: string;
|
id: string;
|
||||||
@@ -350,7 +379,7 @@ export type V1RoomsListData = {
|
|||||||
size?: number;
|
size?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type V1RoomsListResponse = Page_Room_;
|
export type V1RoomsListResponse = Page_RoomDetails_;
|
||||||
|
|
||||||
export type V1RoomsCreateData = {
|
export type V1RoomsCreateData = {
|
||||||
requestBody: CreateRoom;
|
requestBody: CreateRoom;
|
||||||
@@ -358,12 +387,18 @@ export type V1RoomsCreateData = {
|
|||||||
|
|
||||||
export type V1RoomsCreateResponse = Room;
|
export type V1RoomsCreateResponse = Room;
|
||||||
|
|
||||||
|
export type V1RoomsGetData = {
|
||||||
|
roomId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type V1RoomsGetResponse = RoomDetails;
|
||||||
|
|
||||||
export type V1RoomsUpdateData = {
|
export type V1RoomsUpdateData = {
|
||||||
requestBody: UpdateRoom;
|
requestBody: UpdateRoom;
|
||||||
roomId: string;
|
roomId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type V1RoomsUpdateResponse = Room;
|
export type V1RoomsUpdateResponse = RoomDetails;
|
||||||
|
|
||||||
export type V1RoomsDeleteData = {
|
export type V1RoomsDeleteData = {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
@@ -377,6 +412,12 @@ export type V1RoomsCreateMeetingData = {
|
|||||||
|
|
||||||
export type V1RoomsCreateMeetingResponse = Meeting;
|
export type V1RoomsCreateMeetingResponse = Meeting;
|
||||||
|
|
||||||
|
export type V1RoomsTestWebhookData = {
|
||||||
|
roomId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type V1RoomsTestWebhookResponse = WebhookTestResult;
|
||||||
|
|
||||||
export type V1TranscriptsListData = {
|
export type V1TranscriptsListData = {
|
||||||
/**
|
/**
|
||||||
* Page number
|
* Page number
|
||||||
@@ -613,7 +654,7 @@ export type $OpenApiTs = {
|
|||||||
/**
|
/**
|
||||||
* Successful Response
|
* Successful Response
|
||||||
*/
|
*/
|
||||||
200: Page_Room_;
|
200: Page_RoomDetails_;
|
||||||
/**
|
/**
|
||||||
* Validation Error
|
* Validation Error
|
||||||
*/
|
*/
|
||||||
@@ -635,13 +676,26 @@ export type $OpenApiTs = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
"/v1/rooms/{room_id}": {
|
"/v1/rooms/{room_id}": {
|
||||||
|
get: {
|
||||||
|
req: V1RoomsGetData;
|
||||||
|
res: {
|
||||||
|
/**
|
||||||
|
* Successful Response
|
||||||
|
*/
|
||||||
|
200: RoomDetails;
|
||||||
|
/**
|
||||||
|
* Validation Error
|
||||||
|
*/
|
||||||
|
422: HTTPValidationError;
|
||||||
|
};
|
||||||
|
};
|
||||||
patch: {
|
patch: {
|
||||||
req: V1RoomsUpdateData;
|
req: V1RoomsUpdateData;
|
||||||
res: {
|
res: {
|
||||||
/**
|
/**
|
||||||
* Successful Response
|
* Successful Response
|
||||||
*/
|
*/
|
||||||
200: Room;
|
200: RoomDetails;
|
||||||
/**
|
/**
|
||||||
* Validation Error
|
* Validation Error
|
||||||
*/
|
*/
|
||||||
@@ -677,6 +731,21 @@ export type $OpenApiTs = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
"/v1/rooms/{room_id}/webhook/test": {
|
||||||
|
post: {
|
||||||
|
req: V1RoomsTestWebhookData;
|
||||||
|
res: {
|
||||||
|
/**
|
||||||
|
* Successful Response
|
||||||
|
*/
|
||||||
|
200: WebhookTestResult;
|
||||||
|
/**
|
||||||
|
* Validation Error
|
||||||
|
*/
|
||||||
|
422: HTTPValidationError;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
"/v1/transcripts": {
|
"/v1/transcripts": {
|
||||||
get: {
|
get: {
|
||||||
req: V1TranscriptsListData;
|
req: V1TranscriptsListData;
|
||||||
|
|||||||
Reference in New Issue
Block a user