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:
2025-08-29 10:07:49 -06:00
committed by GitHub
parent 6f0c7c1a5e
commit 88ed7cfa78
14 changed files with 1102 additions and 42 deletions

View File

@@ -91,6 +91,14 @@ export const $CreateRoom = {
type: "boolean",
title: "Is Shared",
},
webhook_url: {
type: "string",
title: "Webhook Url",
},
webhook_secret: {
type: "string",
title: "Webhook Secret",
},
},
type: "object",
required: [
@@ -103,6 +111,8 @@ export const $CreateRoom = {
"recording_type",
"recording_trigger",
"is_shared",
"webhook_url",
"webhook_secret",
],
title: "CreateRoom",
} as const;
@@ -809,11 +819,11 @@ export const $Page_GetTranscriptMinimal_ = {
title: "Page[GetTranscriptMinimal]",
} as const;
export const $Page_Room_ = {
export const $Page_RoomDetails_ = {
properties: {
items: {
items: {
$ref: "#/components/schemas/Room",
$ref: "#/components/schemas/RoomDetails",
},
type: "array",
title: "Items",
@@ -869,7 +879,7 @@ export const $Page_Room_ = {
},
type: "object",
required: ["items", "page", "size"],
title: "Page[Room]",
title: "Page[RoomDetails]",
} as const;
export const $Participant = {
@@ -969,6 +979,86 @@ export const $Room = {
title: "Room",
} 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 = {
properties: {
sdp: {
@@ -1351,6 +1441,14 @@ export const $UpdateRoom = {
type: "boolean",
title: "Is Shared",
},
webhook_url: {
type: "string",
title: "Webhook Url",
},
webhook_secret: {
type: "string",
title: "Webhook Secret",
},
},
type: "object",
required: [
@@ -1363,6 +1461,8 @@ export const $UpdateRoom = {
"recording_type",
"recording_trigger",
"is_shared",
"webhook_url",
"webhook_secret",
],
title: "UpdateRoom",
} as const;
@@ -1541,6 +1641,50 @@ export const $ValidationError = {
title: "ValidationError",
} 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 = {
properties: {
apiVersion: {

View File

@@ -10,12 +10,16 @@ import type {
V1RoomsListResponse,
V1RoomsCreateData,
V1RoomsCreateResponse,
V1RoomsGetData,
V1RoomsGetResponse,
V1RoomsUpdateData,
V1RoomsUpdateResponse,
V1RoomsDeleteData,
V1RoomsDeleteResponse,
V1RoomsCreateMeetingData,
V1RoomsCreateMeetingResponse,
V1RoomsTestWebhookData,
V1RoomsTestWebhookResponse,
V1TranscriptsListData,
V1TranscriptsListResponse,
V1TranscriptsCreateData,
@@ -118,7 +122,7 @@ export class DefaultService {
* @param data The data for the request.
* @param data.page Page number
* @param data.size Page size
* @returns Page_Room_ Successful Response
* @returns Page_RoomDetails_ Successful Response
* @throws ApiError
*/
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
* @param data The data for the request.
* @param data.roomId
* @param data.requestBody
* @returns Room Successful Response
* @returns RoomDetails Successful Response
* @throws ApiError
*/
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
* @param data The data for the request.

View File

@@ -24,6 +24,8 @@ export type CreateRoom = {
recording_type: string;
recording_trigger: string;
is_shared: boolean;
webhook_url: string;
webhook_secret: string;
};
export type CreateTranscript = {
@@ -147,8 +149,8 @@ export type Page_GetTranscriptMinimal_ = {
pages?: number | null;
};
export type Page_Room_ = {
items: Array<Room>;
export type Page_RoomDetails_ = {
items: Array<RoomDetails>;
total?: number | null;
page: number | null;
size: number | null;
@@ -176,6 +178,23 @@ export type Room = {
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 = {
sdp: string;
type: string;
@@ -281,6 +300,8 @@ export type UpdateRoom = {
recording_type: string;
recording_trigger: string;
is_shared: boolean;
webhook_url: string;
webhook_secret: string;
};
export type UpdateTranscript = {
@@ -307,6 +328,14 @@ export type ValidationError = {
type: string;
};
export type WebhookTestResult = {
success: boolean;
message?: string;
error?: string;
status_code?: number | null;
response_preview?: string | null;
};
export type WherebyWebhookEvent = {
apiVersion: string;
id: string;
@@ -350,7 +379,7 @@ export type V1RoomsListData = {
size?: number;
};
export type V1RoomsListResponse = Page_Room_;
export type V1RoomsListResponse = Page_RoomDetails_;
export type V1RoomsCreateData = {
requestBody: CreateRoom;
@@ -358,12 +387,18 @@ export type V1RoomsCreateData = {
export type V1RoomsCreateResponse = Room;
export type V1RoomsGetData = {
roomId: string;
};
export type V1RoomsGetResponse = RoomDetails;
export type V1RoomsUpdateData = {
requestBody: UpdateRoom;
roomId: string;
};
export type V1RoomsUpdateResponse = Room;
export type V1RoomsUpdateResponse = RoomDetails;
export type V1RoomsDeleteData = {
roomId: string;
@@ -377,6 +412,12 @@ export type V1RoomsCreateMeetingData = {
export type V1RoomsCreateMeetingResponse = Meeting;
export type V1RoomsTestWebhookData = {
roomId: string;
};
export type V1RoomsTestWebhookResponse = WebhookTestResult;
export type V1TranscriptsListData = {
/**
* Page number
@@ -613,7 +654,7 @@ export type $OpenApiTs = {
/**
* Successful Response
*/
200: Page_Room_;
200: Page_RoomDetails_;
/**
* Validation Error
*/
@@ -635,13 +676,26 @@ export type $OpenApiTs = {
};
};
"/v1/rooms/{room_id}": {
get: {
req: V1RoomsGetData;
res: {
/**
* Successful Response
*/
200: RoomDetails;
/**
* Validation Error
*/
422: HTTPValidationError;
};
};
patch: {
req: V1RoomsUpdateData;
res: {
/**
* Successful Response
*/
200: Room;
200: RoomDetails;
/**
* 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": {
get: {
req: V1TranscriptsListData;