diff --git a/packages/features/webhooks/lib/sendPayload.test.ts b/packages/features/webhooks/lib/sendPayload.test.ts index 9f5c06a6e87..5a4ccbed516 100644 --- a/packages/features/webhooks/lib/sendPayload.test.ts +++ b/packages/features/webhooks/lib/sendPayload.test.ts @@ -109,5 +109,152 @@ describe("sendPayload", () => { }); }); - + describe("videoCallData public URL rewriting", () => { + it("should rewrite daily_video videoCallData.url to the public Cal.com video link", async () => { + const webhook = { + subscriberUrl: "https://example.com/webhook", + appId: null, + payloadTemplate: null, + version: WebhookVersion.V_2021_10_20, + }; + + await sendPayload("test-secret", "BOOKING_CREATED", new Date().toISOString(), webhook, { + uid: "abc123", + title: "Test Booking", + startTime: "2024-01-01T10:00:00Z", + endTime: "2024-01-01T11:00:00Z", + organizer: { + email: "organizer@example.com", + name: "Organizer", + timeZone: "UTC", + language: { locale: "en" }, + }, + attendees: [], + type: "test-event", + description: "", + videoCallData: { + type: "daily_video", + id: "abc123", + password: "mock-token", + url: "https://meetco.daily.co/abc123", + }, + } as unknown as Parameters[4]); + + expect(mockFetch).toHaveBeenCalledTimes(1); + const [, options] = mockFetch.mock.calls[0]; + const body = JSON.parse(options.body); + + expect(body.payload.videoCallData.url).not.toContain("daily.co"); + expect(body.payload.videoCallData.url).toMatch(/\/video\/abc123$/); + expect(body.payload.videoCallData.type).toBe("daily_video"); + expect(body.payload.videoCallData.id).toBe("abc123"); + expect(body.payload.videoCallData.password).toBe("mock-token"); + }); + + it("should NOT rewrite non daily videoCallData.url", async () => { + const webhook = { + subscriberUrl: "https://example.com/webhook", + appId: null, + payloadTemplate: null, + version: WebhookVersion.V_2021_10_20, + }; + + const zoomUrl = "https://zoom.us/j/123456789"; + + await sendPayload("test-secret", "BOOKING_CREATED", new Date().toISOString(), webhook, { + uid: "zoom-uid", + title: "Test Booking", + startTime: "2024-01-01T10:00:00Z", + endTime: "2024-01-01T11:00:00Z", + organizer: { + email: "organizer@example.com", + name: "Organizer", + timeZone: "UTC", + language: { locale: "en" }, + }, + attendees: [], + type: "test-event", + description: "", + videoCallData: { + type: "zoom_video", + id: "123456789", + password: "zoom-pass", + url: zoomUrl, + }, + } as unknown as Parameters[4]); + + const [, options] = mockFetch.mock.calls[0]; + const body = JSON.parse(options.body); + + expect(body.payload.videoCallData.url).toBe(zoomUrl); + }); + + it("should leave payload untouched when videoCallData is absent", async () => { + const webhook = { + subscriberUrl: "https://example.com/webhook", + appId: null, + payloadTemplate: null, + version: WebhookVersion.V_2021_10_20, + }; + + await sendPayload("test-secret", "BOOKING_CREATED", new Date().toISOString(), webhook, { + uid: "no-video-uid", + title: "Test Booking", + startTime: "2024-01-01T10:00:00Z", + endTime: "2024-01-01T11:00:00Z", + organizer: { + email: "organizer@example.com", + name: "Organizer", + timeZone: "UTC", + language: { locale: "en" }, + }, + attendees: [], + type: "test-event", + description: "", + } as unknown as Parameters[4]); + + const [, options] = mockFetch.mock.calls[0]; + const body = JSON.parse(options.body); + + expect(body.payload.videoCallData).toBeUndefined(); + }); + + it("should not throw and not rewrite when getVideoCallUrlFromCalEvent returns the same URL already", async () => { + const webhook = { + subscriberUrl: "https://example.com/webhook", + appId: null, + payloadTemplate: null, + version: WebhookVersion.V_2021_10_20, + }; + + const alreadyPublicUrl = `${process.env.NEXT_PUBLIC_WEBAPP_URL ?? "http://localhost:3000"}/video/already-public-uid`; + + await sendPayload("test-secret", "BOOKING_CREATED", new Date().toISOString(), webhook, { + uid: "already-public-uid", + title: "Test Booking", + startTime: "2024-01-01T10:00:00Z", + endTime: "2024-01-01T11:00:00Z", + organizer: { + email: "organizer@example.com", + name: "Organizer", + timeZone: "UTC", + language: { locale: "en" }, + }, + attendees: [], + type: "test-event", + description: "", + videoCallData: { + type: "daily_video", + id: "already-public-uid", + password: "mock-token", + url: alreadyPublicUrl, + }, + } as unknown as Parameters[4]); + + const [, options] = mockFetch.mock.calls[0]; + const body = JSON.parse(options.body); + + expect(body.payload.videoCallData.url).toBe(alreadyPublicUrl); + }); }); +}); \ No newline at end of file diff --git a/packages/features/webhooks/lib/sendPayload.ts b/packages/features/webhooks/lib/sendPayload.ts index e361d8ddae1..2bcd38ab5c1 100644 --- a/packages/features/webhooks/lib/sendPayload.ts +++ b/packages/features/webhooks/lib/sendPayload.ts @@ -6,6 +6,7 @@ import { getHumanReadableLocationValue } from "@calcom/app-store/locations"; import type { WebhookSubscriber, PaymentData } from "@calcom/features/webhooks/lib/dto/types"; import { getUTCOffsetByTimezone } from "@calcom/lib/dayjs"; import type { CalendarEvent, Person } from "@calcom/types/Calendar"; +import { getVideoCallUrlFromCalEvent, isDailyVideoCall } from "@calcom/lib/CalEventParser"; // Minimal webhook shape for sending payloads (subset of WebhookSubscriber) type WebhookForPayload = Pick; @@ -110,6 +111,22 @@ export type WebhookPayloadType = type WebhookDataType = WebhookPayloadType & { triggerEvent: string; createdAt: string }; +function withPublicVideoUrl(data: EventPayloadType): EventPayloadType { + if (!data.videoCallData) return data; + if (!isDailyVideoCall(data?.videoCallData)) return data; + + const publicUrl = getVideoCallUrlFromCalEvent(data); + if (!publicUrl || publicUrl === data.videoCallData.url) return data; + + return { + ...data, + videoCallData: { + ...data.videoCallData, + url: publicUrl, + }, + }; +} + function addUTCOffset(data: WebhookPayloadType): WithUTCOffsetType { if (isEventPayload(data)) { if (data.organizer?.timeZone) { @@ -226,6 +243,10 @@ const sendPayload = async ( const contentType = !template || jsonParse(template) ? "application/json" : "application/x-www-form-urlencoded"; + if (isEventPayload(data)) { + data = withPublicVideoUrl(data); + } + data = addUTCOffset(data); let body; @@ -326,4 +347,4 @@ const _sendPayload = async ( }; }; -export default sendPayload; +export default sendPayload; \ No newline at end of file