chore: add webhook architecture skeleton (#23247)
* add migration guide and dto * add factory * add notifier * add repo * add services * coderabbit review --1 * coderabbit review --2 * coderabbit review --3 * further improvement * -- * fix * bookingWebhookFactory consideration and type fixes * cleanup * fix types * DI part1 * DI --part 2 * remove migrationGuide as we're WIP * using evyweb for DI -- 1 * DI --final * separate func instead of private class * adds a todo migration file * adjust structure * address feedback * remove todo_migrate * --1 * fix type * address feedback * add TODO comment * address requested changes --1 * address feedback --2 * restructure as per feedback * rename camelcase
This commit is contained in:
@@ -0,0 +1,513 @@
|
||||
import type { TGetTranscriptAccessLink } from "@calcom/app-store/dailyvideo/zod";
|
||||
import type { FORM_SUBMITTED_WEBHOOK_RESPONSES } from "@calcom/app-store/routing-forms/trpc/utils";
|
||||
import type { TimeUnit, WebhookTriggerEvents } from "@calcom/prisma/enums";
|
||||
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
||||
|
||||
export interface BaseEventDTO {
|
||||
triggerEvent: WebhookTriggerEvents;
|
||||
createdAt: string;
|
||||
bookingId?: number;
|
||||
eventTypeId?: number | null;
|
||||
userId?: number | null;
|
||||
teamId?: number | null;
|
||||
orgId?: number | null;
|
||||
platformClientId?: string | null;
|
||||
}
|
||||
|
||||
export interface BookingCreatedDTO extends BaseEventDTO {
|
||||
triggerEvent: typeof WebhookTriggerEvents.BOOKING_CREATED;
|
||||
evt: CalendarEvent;
|
||||
eventType: EventTypeInfo & {
|
||||
id: number;
|
||||
};
|
||||
booking: {
|
||||
id: number;
|
||||
eventTypeId: number | null;
|
||||
userId: number | null;
|
||||
startTime: Date;
|
||||
smsReminderNumber?: string | null;
|
||||
};
|
||||
status: "ACCEPTED" | "PENDING";
|
||||
metadata?: Record<string, unknown>;
|
||||
platformParams?: {
|
||||
platformClientId?: string;
|
||||
platformRescheduleUrl?: string;
|
||||
platformCancelUrl?: string;
|
||||
platformBookingUrl?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface BookingCancelledDTO extends BaseEventDTO {
|
||||
triggerEvent: typeof WebhookTriggerEvents.BOOKING_CANCELLED;
|
||||
evt: CalendarEvent;
|
||||
eventType: EventTypeInfo & {
|
||||
id: number;
|
||||
};
|
||||
booking: {
|
||||
id: number;
|
||||
eventTypeId: number | null;
|
||||
userId: number | null;
|
||||
smsReminderNumber?: string | null;
|
||||
};
|
||||
cancelledBy?: string;
|
||||
cancellationReason?: string;
|
||||
}
|
||||
|
||||
export interface BookingRejectedDTO extends BaseEventDTO {
|
||||
triggerEvent: typeof WebhookTriggerEvents.BOOKING_REJECTED;
|
||||
evt: CalendarEvent;
|
||||
eventType: EventTypeInfo & {
|
||||
id: number;
|
||||
};
|
||||
booking: {
|
||||
id: number;
|
||||
eventTypeId: number | null;
|
||||
userId: number | null;
|
||||
smsReminderNumber?: string | null;
|
||||
};
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface BookingRequestedDTO extends BaseEventDTO {
|
||||
triggerEvent: typeof WebhookTriggerEvents.BOOKING_REQUESTED;
|
||||
evt: CalendarEvent;
|
||||
eventType: EventTypeInfo & {
|
||||
id: number;
|
||||
};
|
||||
booking: {
|
||||
id: number;
|
||||
eventTypeId: number | null;
|
||||
userId: number | null;
|
||||
};
|
||||
}
|
||||
|
||||
export interface BookingRescheduledDTO extends BaseEventDTO {
|
||||
triggerEvent: typeof WebhookTriggerEvents.BOOKING_RESCHEDULED;
|
||||
evt: CalendarEvent;
|
||||
eventType: EventTypeInfo & {
|
||||
id: number;
|
||||
};
|
||||
booking: {
|
||||
id: number;
|
||||
eventTypeId: number | null;
|
||||
userId: number | null;
|
||||
smsReminderNumber?: string | null;
|
||||
};
|
||||
rescheduleId?: number;
|
||||
rescheduleUid?: string;
|
||||
rescheduleStartTime?: string;
|
||||
rescheduleEndTime?: string;
|
||||
rescheduledBy?: string;
|
||||
}
|
||||
|
||||
export interface BookingPaidDTO extends BaseEventDTO {
|
||||
triggerEvent: typeof WebhookTriggerEvents.BOOKING_PAID;
|
||||
evt: CalendarEvent;
|
||||
eventType: EventTypeInfo & {
|
||||
id: number;
|
||||
};
|
||||
booking: {
|
||||
id: number;
|
||||
eventTypeId: number | null;
|
||||
userId: number | null;
|
||||
};
|
||||
paymentId?: number;
|
||||
paymentData?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface BookingPaymentInitiatedDTO extends BaseEventDTO {
|
||||
triggerEvent: typeof WebhookTriggerEvents.BOOKING_PAYMENT_INITIATED;
|
||||
evt: CalendarEvent;
|
||||
eventType: EventTypeInfo & {
|
||||
id: number;
|
||||
};
|
||||
booking: {
|
||||
id: number;
|
||||
eventTypeId: number | null;
|
||||
userId: number | null;
|
||||
};
|
||||
paymentId?: number;
|
||||
paymentData?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface BookingNoShowDTO extends BaseEventDTO {
|
||||
triggerEvent: typeof WebhookTriggerEvents.BOOKING_NO_SHOW_UPDATED;
|
||||
message: string;
|
||||
bookingUid: string;
|
||||
bookingId?: number;
|
||||
attendees: { email: string; noShow: boolean }[];
|
||||
}
|
||||
|
||||
export interface OOOCreatedDTO extends BaseEventDTO {
|
||||
triggerEvent: typeof WebhookTriggerEvents.OOO_CREATED;
|
||||
oooEntry: {
|
||||
id: number;
|
||||
start: string;
|
||||
end: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
notes: string | null;
|
||||
reason: {
|
||||
emoji?: string;
|
||||
reason?: string;
|
||||
};
|
||||
reasonId: number;
|
||||
user: {
|
||||
id: number;
|
||||
name: string | null;
|
||||
username: string | null;
|
||||
timeZone: string;
|
||||
email: string;
|
||||
};
|
||||
toUser: {
|
||||
id: number;
|
||||
name?: string | null;
|
||||
username?: string | null;
|
||||
timeZone?: string;
|
||||
email?: string;
|
||||
} | null;
|
||||
uuid: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface FormSubmittedDTO extends BaseEventDTO {
|
||||
triggerEvent: typeof WebhookTriggerEvents.FORM_SUBMITTED;
|
||||
form: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
response: {
|
||||
id: number;
|
||||
data: FORM_SUBMITTED_WEBHOOK_RESPONSES;
|
||||
};
|
||||
}
|
||||
|
||||
export interface RecordingReadyDTO extends BaseEventDTO {
|
||||
triggerEvent: typeof WebhookTriggerEvents.RECORDING_READY;
|
||||
evt: CalendarEvent;
|
||||
downloadLink: string;
|
||||
}
|
||||
|
||||
export interface TranscriptionGeneratedDTO extends BaseEventDTO {
|
||||
triggerEvent: typeof WebhookTriggerEvents.RECORDING_TRANSCRIPTION_GENERATED;
|
||||
evt: CalendarEvent;
|
||||
downloadLinks?: {
|
||||
transcription?: Array<{
|
||||
format: string;
|
||||
link: string;
|
||||
}>;
|
||||
recording?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface FormSubmittedNoEventDTO extends BaseEventDTO {
|
||||
triggerEvent: typeof WebhookTriggerEvents.FORM_SUBMITTED_NO_EVENT;
|
||||
form: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
response: {
|
||||
id: number;
|
||||
data: FORM_SUBMITTED_WEBHOOK_RESPONSES;
|
||||
};
|
||||
}
|
||||
|
||||
export interface MeetingStartedDTO extends BaseEventDTO {
|
||||
triggerEvent: typeof WebhookTriggerEvents.MEETING_STARTED;
|
||||
booking: {
|
||||
id: number;
|
||||
startTime: Date;
|
||||
endTime: Date;
|
||||
title: string;
|
||||
description: string | null;
|
||||
customInputs: Record<string, unknown> | null;
|
||||
responses: Record<string, unknown> | null;
|
||||
userFieldsResponses: Record<string, unknown> | null;
|
||||
location: string | null;
|
||||
status: string;
|
||||
user: {
|
||||
username: string | null;
|
||||
name: string | null;
|
||||
email: string;
|
||||
timeZone: string;
|
||||
locale: string | null;
|
||||
} | null;
|
||||
eventType: {
|
||||
title: string;
|
||||
description: string | null;
|
||||
requiresConfirmation: boolean;
|
||||
price: number;
|
||||
currency: string;
|
||||
length: number;
|
||||
team: {
|
||||
logoUrl: string | null;
|
||||
parent: {
|
||||
logoUrl: string | null;
|
||||
name: string;
|
||||
} | null;
|
||||
} | null;
|
||||
} | null;
|
||||
attendees: {
|
||||
name: string;
|
||||
email: string;
|
||||
timeZone: string;
|
||||
}[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface MeetingEndedDTO extends BaseEventDTO {
|
||||
triggerEvent: typeof WebhookTriggerEvents.MEETING_ENDED;
|
||||
booking: {
|
||||
id: number;
|
||||
startTime: Date;
|
||||
endTime: Date;
|
||||
title: string;
|
||||
description: string | null;
|
||||
customInputs: Record<string, unknown> | null;
|
||||
responses: Record<string, unknown> | null;
|
||||
userFieldsResponses: Record<string, unknown> | null;
|
||||
location: string | null;
|
||||
status: string;
|
||||
user: {
|
||||
username: string | null;
|
||||
name: string | null;
|
||||
email: string;
|
||||
timeZone: string;
|
||||
locale: string | null;
|
||||
} | null;
|
||||
eventType: {
|
||||
title: string;
|
||||
description: string | null;
|
||||
requiresConfirmation: boolean;
|
||||
price: number;
|
||||
currency: string;
|
||||
length: number;
|
||||
team: {
|
||||
logoUrl: string | null;
|
||||
parent: {
|
||||
logoUrl: string | null;
|
||||
name: string;
|
||||
} | null;
|
||||
} | null;
|
||||
} | null;
|
||||
attendees: {
|
||||
name: string;
|
||||
email: string;
|
||||
timeZone: string;
|
||||
}[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface InstantMeetingDTO extends BaseEventDTO {
|
||||
triggerEvent: typeof WebhookTriggerEvents.INSTANT_MEETING;
|
||||
title: string;
|
||||
body: string;
|
||||
icon?: string;
|
||||
url?: string;
|
||||
actions?: { action: string; title: string; type: string; image: string | null }[];
|
||||
requireInteraction?: boolean;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface AfterHostsNoShowDTO extends BaseEventDTO {
|
||||
triggerEvent: typeof WebhookTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW;
|
||||
bookingId: number;
|
||||
webhook: {
|
||||
id: string;
|
||||
subscriberUrl: string;
|
||||
time: number;
|
||||
timeUnit: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AfterGuestsNoShowDTO extends BaseEventDTO {
|
||||
triggerEvent: typeof WebhookTriggerEvents.AFTER_GUESTS_CAL_VIDEO_NO_SHOW;
|
||||
bookingId: number;
|
||||
webhook: {
|
||||
id: string;
|
||||
subscriberUrl: string;
|
||||
time: number;
|
||||
timeUnit: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type WebhookEventDTO =
|
||||
| BookingCreatedDTO
|
||||
| BookingCancelledDTO
|
||||
| BookingRejectedDTO
|
||||
| BookingRequestedDTO
|
||||
| BookingRescheduledDTO
|
||||
| BookingPaidDTO
|
||||
| BookingPaymentInitiatedDTO
|
||||
| BookingNoShowDTO
|
||||
| OOOCreatedDTO
|
||||
| FormSubmittedDTO
|
||||
| FormSubmittedNoEventDTO
|
||||
| RecordingReadyDTO
|
||||
| TranscriptionGeneratedDTO
|
||||
| MeetingStartedDTO
|
||||
| MeetingEndedDTO
|
||||
| InstantMeetingDTO
|
||||
| AfterHostsNoShowDTO
|
||||
| AfterGuestsNoShowDTO;
|
||||
|
||||
// Service layer interfaces
|
||||
export interface WebhookTriggerArgs {
|
||||
trigger: WebhookTriggerEvents;
|
||||
evt: CalendarEvent;
|
||||
booking: {
|
||||
id: number;
|
||||
eventTypeId: number | null;
|
||||
userId: number | null;
|
||||
startTime?: Date;
|
||||
smsReminderNumber?: string | null;
|
||||
};
|
||||
eventType: {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string | null;
|
||||
requiresConfirmation: boolean;
|
||||
price: number;
|
||||
currency: string;
|
||||
length: number;
|
||||
teamId?: number | null;
|
||||
} | null;
|
||||
teamId?: number | null;
|
||||
orgId?: number | null;
|
||||
platformClientId?: string;
|
||||
isDryRun?: boolean;
|
||||
status?: "ACCEPTED" | "PENDING";
|
||||
metadata?: Record<string, unknown>;
|
||||
platformParams?: {
|
||||
platformClientId?: string;
|
||||
platformRescheduleUrl?: string;
|
||||
platformCancelUrl?: string;
|
||||
platformBookingUrl?: string;
|
||||
};
|
||||
cancelledBy?: string;
|
||||
cancellationReason?: string;
|
||||
rescheduleId?: number;
|
||||
rescheduleUid?: string;
|
||||
rescheduleStartTime?: string;
|
||||
rescheduleEndTime?: string;
|
||||
rescheduledBy?: string;
|
||||
paymentId?: number;
|
||||
paymentData?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface WebhookSubscriber {
|
||||
id: string;
|
||||
subscriberUrl: string;
|
||||
payloadTemplate: string | null;
|
||||
appId: string | null;
|
||||
secret: string | null;
|
||||
time?: number | null;
|
||||
timeUnit?: TimeUnit | null;
|
||||
eventTriggers: WebhookTriggerEvents[];
|
||||
}
|
||||
|
||||
export interface WebhookDeliveryResult {
|
||||
ok: boolean;
|
||||
status: number;
|
||||
message?: string;
|
||||
duration?: number;
|
||||
subscriberUrl: string;
|
||||
webhookId: string;
|
||||
}
|
||||
|
||||
// Legacy types moved from sendPayload.ts for backward compatibility
|
||||
export type EventTypeInfo = {
|
||||
eventTitle?: string | null;
|
||||
eventDescription?: string | null;
|
||||
requiresConfirmation?: boolean | null;
|
||||
price?: number | null;
|
||||
currency?: string | null;
|
||||
length?: number | null;
|
||||
};
|
||||
|
||||
export type UTCOffset = {
|
||||
utcOffset?: number | null;
|
||||
};
|
||||
|
||||
export type WithUTCOffsetType<T> = T & {
|
||||
user?: Person & UTCOffset;
|
||||
} & {
|
||||
organizer?: Person & UTCOffset;
|
||||
} & {
|
||||
attendees?: (Person & UTCOffset)[];
|
||||
};
|
||||
|
||||
export type BookingNoShowUpdatedPayload = {
|
||||
message: string;
|
||||
bookingUid: string;
|
||||
bookingId?: number;
|
||||
attendees: { email: string; noShow: boolean }[];
|
||||
};
|
||||
|
||||
export type TranscriptionGeneratedPayload = {
|
||||
downloadLinks?: {
|
||||
transcription: TGetTranscriptAccessLink["transcription"];
|
||||
recording: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type OOOEntryPayloadType = {
|
||||
oooEntry: {
|
||||
id: number;
|
||||
start: string;
|
||||
end: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
notes: string | null;
|
||||
reason: {
|
||||
emoji?: string;
|
||||
reason?: string;
|
||||
};
|
||||
reasonId: number;
|
||||
user: {
|
||||
id: number;
|
||||
name: string | null;
|
||||
username: string | null;
|
||||
timeZone: string;
|
||||
email: string;
|
||||
};
|
||||
toUser: {
|
||||
id: number;
|
||||
name?: string | null;
|
||||
username?: string | null;
|
||||
timeZone?: string;
|
||||
email?: string;
|
||||
} | null;
|
||||
uuid: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type EventPayloadType = CalendarEvent &
|
||||
TranscriptionGeneratedPayload &
|
||||
EventTypeInfo & {
|
||||
metadata?: { [key: string]: string | number | boolean | null };
|
||||
bookingId?: number;
|
||||
status?: string;
|
||||
smsReminderNumber?: string;
|
||||
rescheduleId?: number;
|
||||
rescheduleUid?: string;
|
||||
rescheduleStartTime?: string;
|
||||
rescheduleEndTime?: string;
|
||||
downloadLink?: string;
|
||||
paymentId?: number;
|
||||
rescheduledBy?: string;
|
||||
cancelledBy?: string;
|
||||
paymentData?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
// dto/types.ts
|
||||
|
||||
export type BookingWebhookEventDTO =
|
||||
| BookingCreatedDTO
|
||||
| BookingCancelledDTO
|
||||
| BookingRequestedDTO
|
||||
| BookingRescheduledDTO
|
||||
| BookingPaidDTO
|
||||
| BookingPaymentInitiatedDTO
|
||||
| BookingRejectedDTO
|
||||
| BookingNoShowDTO;
|
||||
@@ -0,0 +1,209 @@
|
||||
import { getUTCOffsetByTimezone } from "@calcom/lib/dayjs";
|
||||
import { BookingStatus, WebhookTriggerEvents } from "@calcom/prisma/enums";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
|
||||
import type { EventTypeInfo, BookingWebhookEventDTO } from "../dto/types";
|
||||
import type { WebhookPayload } from "./types";
|
||||
|
||||
type BookingExtraDataMap = {
|
||||
[WebhookTriggerEvents.BOOKING_CREATED]: null;
|
||||
[WebhookTriggerEvents.BOOKING_CANCELLED]: { cancelledBy?: string; cancellationReason?: string };
|
||||
[WebhookTriggerEvents.BOOKING_REQUESTED]: null;
|
||||
[WebhookTriggerEvents.BOOKING_REJECTED]: null;
|
||||
[WebhookTriggerEvents.BOOKING_RESCHEDULED]: {
|
||||
rescheduleId?: number;
|
||||
rescheduleUid?: string;
|
||||
rescheduleStartTime?: string;
|
||||
rescheduleEndTime?: string;
|
||||
rescheduledBy?: string;
|
||||
};
|
||||
[WebhookTriggerEvents.BOOKING_PAID]: { paymentId?: number; paymentData?: Record<string, unknown> };
|
||||
[WebhookTriggerEvents.BOOKING_PAYMENT_INITIATED]: {
|
||||
paymentId?: number;
|
||||
paymentData?: Record<string, unknown>;
|
||||
};
|
||||
};
|
||||
|
||||
interface CreateBookingWebhookPayloadParams<T extends keyof BookingExtraDataMap> {
|
||||
booking: {
|
||||
id: number;
|
||||
eventTypeId: number | null;
|
||||
userId: number | null;
|
||||
smsReminderNumber?: string | null;
|
||||
};
|
||||
eventType: EventTypeInfo;
|
||||
evt: CalendarEvent;
|
||||
status: BookingStatus;
|
||||
triggerEvent: WebhookTriggerEvents;
|
||||
createdAt: string;
|
||||
extra?: BookingExtraDataMap[T];
|
||||
}
|
||||
|
||||
function createBookingWebhookPayload<T extends keyof BookingExtraDataMap>(
|
||||
params: CreateBookingWebhookPayloadParams<T>
|
||||
): WebhookPayload {
|
||||
const utcOffsetOrganizer = getUTCOffsetByTimezone(params.evt.organizer?.timeZone, params.evt.startTime);
|
||||
const organizer = { ...params.evt.organizer, utcOffset: utcOffsetOrganizer };
|
||||
|
||||
return {
|
||||
triggerEvent: params.triggerEvent,
|
||||
createdAt: params.createdAt,
|
||||
payload: {
|
||||
...params.evt,
|
||||
bookingId: params.booking.id,
|
||||
startTime: params.evt.startTime,
|
||||
endTime: params.evt.endTime,
|
||||
title: params.evt.title,
|
||||
type: params.evt.type,
|
||||
organizer,
|
||||
attendees:
|
||||
params.evt.attendees?.map((a) => ({
|
||||
...a,
|
||||
utcOffset: getUTCOffsetByTimezone(a.timeZone, params.evt.startTime),
|
||||
})) ?? [],
|
||||
location: params.evt.location,
|
||||
uid: params.evt.uid,
|
||||
customInputs: params.evt.customInputs,
|
||||
responses: params.evt.responses,
|
||||
userFieldsResponses: params.evt.userFieldsResponses,
|
||||
status: params.status,
|
||||
eventTitle: params.eventType?.eventTitle,
|
||||
eventDescription: params.eventType?.eventDescription,
|
||||
requiresConfirmation: params.eventType?.requiresConfirmation,
|
||||
price: params.eventType?.price,
|
||||
currency: params.eventType?.currency,
|
||||
length: params.eventType?.length,
|
||||
smsReminderNumber: params.booking.smsReminderNumber || undefined,
|
||||
description: params.evt.description || params.evt.additionalNotes,
|
||||
...(params.extra || {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const BOOKING_WEBHOOK_EVENTS: WebhookTriggerEvents[] = [
|
||||
WebhookTriggerEvents.BOOKING_CREATED,
|
||||
WebhookTriggerEvents.BOOKING_CANCELLED,
|
||||
WebhookTriggerEvents.BOOKING_REQUESTED,
|
||||
WebhookTriggerEvents.BOOKING_RESCHEDULED,
|
||||
WebhookTriggerEvents.BOOKING_REJECTED,
|
||||
WebhookTriggerEvents.BOOKING_PAID,
|
||||
WebhookTriggerEvents.BOOKING_PAYMENT_INITIATED,
|
||||
WebhookTriggerEvents.BOOKING_NO_SHOW_UPDATED,
|
||||
];
|
||||
|
||||
export class BookingPayloadBuilder {
|
||||
canHandle(triggerEvent: WebhookTriggerEvents): boolean {
|
||||
return BOOKING_WEBHOOK_EVENTS.includes(triggerEvent);
|
||||
}
|
||||
|
||||
build(dto: BookingWebhookEventDTO): WebhookPayload {
|
||||
switch (dto.triggerEvent) {
|
||||
case WebhookTriggerEvents.BOOKING_CREATED:
|
||||
return createBookingWebhookPayload({
|
||||
booking: dto.booking,
|
||||
eventType: dto.eventType,
|
||||
evt: dto.evt,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
triggerEvent: dto.triggerEvent,
|
||||
createdAt: dto.createdAt,
|
||||
});
|
||||
|
||||
case WebhookTriggerEvents.BOOKING_CANCELLED:
|
||||
return createBookingWebhookPayload({
|
||||
booking: dto.booking,
|
||||
eventType: dto.eventType,
|
||||
evt: dto.evt,
|
||||
status: BookingStatus.CANCELLED,
|
||||
triggerEvent: dto.triggerEvent,
|
||||
createdAt: dto.createdAt,
|
||||
extra: {
|
||||
cancelledBy: dto.cancelledBy,
|
||||
cancellationReason: dto.cancellationReason,
|
||||
},
|
||||
});
|
||||
|
||||
case WebhookTriggerEvents.BOOKING_REQUESTED:
|
||||
return createBookingWebhookPayload({
|
||||
booking: dto.booking,
|
||||
eventType: dto.eventType,
|
||||
evt: dto.evt,
|
||||
status: BookingStatus.PENDING,
|
||||
triggerEvent: dto.triggerEvent,
|
||||
createdAt: dto.createdAt,
|
||||
});
|
||||
|
||||
case WebhookTriggerEvents.BOOKING_REJECTED:
|
||||
return createBookingWebhookPayload({
|
||||
booking: dto.booking,
|
||||
eventType: dto.eventType,
|
||||
evt: dto.evt,
|
||||
status: BookingStatus.REJECTED,
|
||||
triggerEvent: dto.triggerEvent,
|
||||
createdAt: dto.createdAt,
|
||||
});
|
||||
|
||||
case WebhookTriggerEvents.BOOKING_RESCHEDULED:
|
||||
return createBookingWebhookPayload({
|
||||
booking: dto.booking,
|
||||
eventType: dto.eventType,
|
||||
evt: dto.evt,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
triggerEvent: dto.triggerEvent,
|
||||
createdAt: dto.createdAt,
|
||||
extra: {
|
||||
rescheduleId: dto.rescheduleId,
|
||||
rescheduleUid: dto.rescheduleUid,
|
||||
rescheduleStartTime: dto.rescheduleStartTime,
|
||||
rescheduleEndTime: dto.rescheduleEndTime,
|
||||
rescheduledBy: dto.rescheduledBy,
|
||||
},
|
||||
});
|
||||
|
||||
case WebhookTriggerEvents.BOOKING_PAID:
|
||||
return createBookingWebhookPayload({
|
||||
booking: dto.booking,
|
||||
eventType: dto.eventType,
|
||||
evt: dto.evt,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
triggerEvent: dto.triggerEvent,
|
||||
createdAt: dto.createdAt,
|
||||
extra: {
|
||||
paymentId: dto.paymentId,
|
||||
paymentData: dto.paymentData,
|
||||
},
|
||||
});
|
||||
|
||||
case WebhookTriggerEvents.BOOKING_PAYMENT_INITIATED:
|
||||
return createBookingWebhookPayload({
|
||||
booking: dto.booking,
|
||||
eventType: dto.eventType,
|
||||
evt: dto.evt,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
triggerEvent: dto.triggerEvent,
|
||||
createdAt: dto.createdAt,
|
||||
extra: {
|
||||
paymentId: dto.paymentId,
|
||||
paymentData: dto.paymentData,
|
||||
},
|
||||
});
|
||||
|
||||
case WebhookTriggerEvents.BOOKING_NO_SHOW_UPDATED:
|
||||
return {
|
||||
triggerEvent: dto.triggerEvent,
|
||||
createdAt: dto.createdAt,
|
||||
payload: {
|
||||
bookingUid: dto.bookingUid,
|
||||
bookingId: dto.bookingId,
|
||||
attendees: dto.attendees,
|
||||
message: dto.message,
|
||||
},
|
||||
};
|
||||
|
||||
default: {
|
||||
// TypeScript exhaustiveness check - this should never happen if all cases are covered
|
||||
const _exhaustiveCheck: never = dto;
|
||||
throw new Error(`Unsupported booking trigger: ${JSON.stringify(_exhaustiveCheck)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import type { FORM_SUBMITTED_WEBHOOK_RESPONSES } from "@calcom/app-store/routing-forms/trpc/utils";
|
||||
import { WebhookTriggerEvents } from "@calcom/prisma/enums";
|
||||
|
||||
import type { FormSubmittedDTO, FormSubmittedNoEventDTO } from "../dto/types";
|
||||
import type { WebhookPayload } from "./types";
|
||||
|
||||
export class FormPayloadBuilder {
|
||||
canHandle(triggerEvent: WebhookTriggerEvents): boolean {
|
||||
return (
|
||||
triggerEvent === WebhookTriggerEvents.FORM_SUBMITTED ||
|
||||
triggerEvent === WebhookTriggerEvents.FORM_SUBMITTED_NO_EVENT
|
||||
);
|
||||
}
|
||||
|
||||
build(dto: FormSubmittedDTO | FormSubmittedNoEventDTO): WebhookPayload {
|
||||
// Type the responses properly using the routing-forms type
|
||||
const responses = dto.response.data;
|
||||
|
||||
// Create properly typed payload with primitive types
|
||||
const payload: {
|
||||
formId: string;
|
||||
formName: string;
|
||||
teamId: number | null;
|
||||
responses: FORM_SUBMITTED_WEBHOOK_RESPONSES;
|
||||
[key: string]: unknown; // For backward compatibility fields
|
||||
} = {
|
||||
formId: dto.form.id,
|
||||
formName: dto.form.name,
|
||||
teamId: dto.teamId ?? null,
|
||||
responses,
|
||||
};
|
||||
|
||||
// Add unwrapped response fields at root level for backwards compatibility
|
||||
// This ensures both `value` (deprecated) and `response` (new) are available
|
||||
Object.entries(responses).forEach(([fieldKey, fieldValue]) => {
|
||||
if (fieldValue && typeof fieldValue === "object") {
|
||||
// Each field should have both `value` (deprecated) and `response` (new)
|
||||
const responseField = fieldValue as {
|
||||
value?: unknown;
|
||||
response?: unknown;
|
||||
};
|
||||
|
||||
// Add the field value directly to payload root for backward compatibility
|
||||
// This preserves the legacy behavior where field values were at root level
|
||||
if (responseField.value !== undefined) {
|
||||
payload[fieldKey] = responseField.value;
|
||||
} else if (responseField.response !== undefined) {
|
||||
// Fallback to response if value is not present
|
||||
payload[fieldKey] = responseField.response;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
triggerEvent: dto.triggerEvent,
|
||||
createdAt: dto.createdAt,
|
||||
payload,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { WebhookTriggerEvents } from "@calcom/prisma/enums";
|
||||
|
||||
import type { InstantMeetingDTO } from "../dto/types";
|
||||
import type { WebhookPayload } from "./types";
|
||||
|
||||
export class InstantMeetingBuilder {
|
||||
canHandle(triggerEvent: WebhookTriggerEvents): boolean {
|
||||
return triggerEvent === WebhookTriggerEvents.INSTANT_MEETING;
|
||||
}
|
||||
|
||||
build(dto: InstantMeetingDTO): WebhookPayload {
|
||||
return {
|
||||
triggerEvent: dto.triggerEvent,
|
||||
createdAt: dto.createdAt,
|
||||
payload: {
|
||||
title: dto.title,
|
||||
body: dto.body,
|
||||
icon: dto.icon,
|
||||
url: dto.url,
|
||||
actions: dto.actions,
|
||||
requireInteraction: dto.requireInteraction,
|
||||
type: dto.type,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { WebhookTriggerEvents } from "@calcom/prisma/enums";
|
||||
|
||||
import type { MeetingStartedDTO, MeetingEndedDTO } from "../dto/types";
|
||||
import type { WebhookPayload } from "./types";
|
||||
|
||||
export class MeetingPayloadBuilder {
|
||||
canHandle(triggerEvent: WebhookTriggerEvents): boolean {
|
||||
return (
|
||||
triggerEvent === WebhookTriggerEvents.MEETING_STARTED ||
|
||||
triggerEvent === WebhookTriggerEvents.MEETING_ENDED
|
||||
);
|
||||
}
|
||||
|
||||
build(dto: MeetingStartedDTO | MeetingEndedDTO): WebhookPayload {
|
||||
return {
|
||||
triggerEvent: dto.triggerEvent,
|
||||
createdAt: dto.createdAt,
|
||||
payload: { ...dto.booking },
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { WebhookTriggerEvents } from "@calcom/prisma/enums";
|
||||
|
||||
import type { OOOCreatedDTO } from "../dto/types";
|
||||
import type { WebhookPayload } from "./types";
|
||||
|
||||
export class OOOPayloadBuilder {
|
||||
canHandle(triggerEvent: WebhookTriggerEvents): boolean {
|
||||
return triggerEvent === WebhookTriggerEvents.OOO_CREATED;
|
||||
}
|
||||
|
||||
build(dto: OOOCreatedDTO): WebhookPayload {
|
||||
return {
|
||||
triggerEvent: dto.triggerEvent,
|
||||
createdAt: dto.createdAt,
|
||||
payload: { oooEntry: dto.oooEntry },
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { WebhookTriggerEvents } from "@calcom/prisma/enums";
|
||||
|
||||
import type { RecordingReadyDTO, TranscriptionGeneratedDTO } from "../dto/types";
|
||||
import type { WebhookPayload } from "./types";
|
||||
|
||||
export class RecordingPayloadBuilder {
|
||||
canHandle(triggerEvent: WebhookTriggerEvents): boolean {
|
||||
return (
|
||||
triggerEvent === WebhookTriggerEvents.RECORDING_READY ||
|
||||
triggerEvent === WebhookTriggerEvents.RECORDING_TRANSCRIPTION_GENERATED
|
||||
);
|
||||
}
|
||||
|
||||
build(dto: RecordingReadyDTO | TranscriptionGeneratedDTO): WebhookPayload {
|
||||
if (dto.triggerEvent === WebhookTriggerEvents.RECORDING_READY) {
|
||||
return {
|
||||
triggerEvent: dto.triggerEvent,
|
||||
createdAt: dto.createdAt,
|
||||
payload: { downloadLink: dto.downloadLink },
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
triggerEvent: dto.triggerEvent,
|
||||
createdAt: dto.createdAt,
|
||||
payload: {
|
||||
downloadLinks: dto.downloadLinks,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import type { EventPayloadType, OOOEntryPayloadType, BookingNoShowUpdatedPayload } from "../dto/types";
|
||||
|
||||
export interface FormSubmittedPayload {
|
||||
formId: string;
|
||||
formName: string;
|
||||
teamId?: number | null;
|
||||
redirect?: Record<string, unknown>;
|
||||
responses: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface RecordingPayload {
|
||||
downloadLink?: string;
|
||||
downloadLinks?: {
|
||||
transcription?: Array<{
|
||||
format: string;
|
||||
link: string;
|
||||
}>;
|
||||
recording?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface MeetingPayload {
|
||||
id: number;
|
||||
startTime: Date;
|
||||
endTime: Date;
|
||||
title: string;
|
||||
description: string | null;
|
||||
customInputs: Record<string, unknown> | null;
|
||||
responses: Record<string, unknown> | null;
|
||||
userFieldsResponses: Record<string, unknown> | null;
|
||||
location: string | null;
|
||||
status: string;
|
||||
user: {
|
||||
username: string | null;
|
||||
name: string | null;
|
||||
email: string;
|
||||
timeZone: string;
|
||||
locale: string | null;
|
||||
} | null;
|
||||
eventType: {
|
||||
title: string;
|
||||
description: string | null;
|
||||
requiresConfirmation: boolean;
|
||||
price: number;
|
||||
currency: string;
|
||||
length: number;
|
||||
team: {
|
||||
logoUrl: string | null;
|
||||
parent: {
|
||||
logoUrl: string | null;
|
||||
name: string;
|
||||
} | null;
|
||||
} | null;
|
||||
} | null;
|
||||
attendees: {
|
||||
name: string;
|
||||
email: string;
|
||||
timeZone: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface InstantMeetingPayload {
|
||||
title: string;
|
||||
body: string;
|
||||
icon?: string;
|
||||
url?: string;
|
||||
actions?: { action: string; title: string; type: string; image: string | null }[];
|
||||
requireInteraction?: boolean;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface NoShowWebhookPayload {
|
||||
bookingId: number;
|
||||
webhook: {
|
||||
id: string;
|
||||
subscriberUrl: string;
|
||||
time: number;
|
||||
timeUnit: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface WebhookPayload {
|
||||
triggerEvent: string;
|
||||
createdAt: string;
|
||||
payload:
|
||||
| EventPayloadType
|
||||
| OOOEntryPayloadType
|
||||
| BookingNoShowUpdatedPayload
|
||||
| FormSubmittedPayload
|
||||
| RecordingPayload
|
||||
| MeetingPayload
|
||||
| InstantMeetingPayload
|
||||
| NoShowWebhookPayload;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
export interface ITasker {
|
||||
create(
|
||||
taskName: string,
|
||||
payload: string,
|
||||
options?: { scheduledAt?: Date; referenceUid?: string }
|
||||
): Promise<string>;
|
||||
cancelWithReference(referenceUid: string, taskName: string): Promise<string | null>;
|
||||
}
|
||||
|
||||
export interface ILogger {
|
||||
debug(message: string, meta?: Record<string, unknown>): void;
|
||||
info(message: string, meta?: Record<string, unknown>): void;
|
||||
warn(message: string, meta?: Record<string, unknown>): void;
|
||||
error(message: string, meta?: Record<string, unknown>): void;
|
||||
getSubLogger(options: { prefix: string[] }): ILogger;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { WebhookTriggerEvents } from "@calcom/prisma/enums";
|
||||
|
||||
import type { WebhookSubscriber } from "../dto/types";
|
||||
|
||||
export interface GetSubscribersOptions {
|
||||
userId?: number | null;
|
||||
eventTypeId?: number | null;
|
||||
triggerEvent: WebhookTriggerEvents;
|
||||
teamId?: number | number[] | null;
|
||||
orgId?: number | null;
|
||||
oAuthClientId?: string | null;
|
||||
}
|
||||
|
||||
export interface IWebhookRepository {
|
||||
getSubscribers(options: GetSubscribersOptions): Promise<WebhookSubscriber[]>;
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
import type { WebhookTriggerEvents } from "@calcom/prisma/enums";
|
||||
|
||||
import type { WebhookSubscriber } from "../dto/types";
|
||||
import type { WebhookPayload } from "../factory/types";
|
||||
import type {
|
||||
BookingPaymentInitiatedParams,
|
||||
BookingCreatedParams,
|
||||
BookingCancelledParams,
|
||||
BookingRequestedParams,
|
||||
BookingRescheduledParams,
|
||||
BookingPaidParams,
|
||||
BookingNoShowParams,
|
||||
BookingRejectedParams,
|
||||
ScheduleMeetingWebhooksParams,
|
||||
CancelScheduledMeetingWebhooksParams,
|
||||
ScheduleNoShowWebhooksParams,
|
||||
} from "../types/params";
|
||||
|
||||
export interface GetSubscribersOptions {
|
||||
userId?: number | null;
|
||||
eventTypeId?: number | null;
|
||||
triggerEvent: WebhookTriggerEvents;
|
||||
teamId?: number | number[] | null;
|
||||
orgId?: number | null;
|
||||
oAuthClientId?: string | null;
|
||||
}
|
||||
|
||||
// Repository Interface - Data access operations
|
||||
export interface IWebhookRepository {
|
||||
getSubscribers(options: GetSubscribersOptions): Promise<WebhookSubscriber[]>;
|
||||
}
|
||||
|
||||
// Processing Interface - Webhook processing operations
|
||||
export interface IWebhookProcessor {
|
||||
processWebhooks(
|
||||
trigger: WebhookTriggerEvents,
|
||||
payload: WebhookPayload,
|
||||
subscribers: WebhookSubscriber[]
|
||||
): Promise<void>;
|
||||
}
|
||||
|
||||
// Scheduling Interface - Webhook scheduling operations
|
||||
export interface IWebhookScheduler {
|
||||
scheduleTimeBasedWebhook(
|
||||
trigger: WebhookTriggerEvents,
|
||||
scheduledAt: Date,
|
||||
bookingData: Record<string, unknown>,
|
||||
subscriber: WebhookSubscriber,
|
||||
evt: Record<string, unknown>,
|
||||
isDryRun?: boolean
|
||||
): Promise<void>;
|
||||
cancelScheduledWebhooks(
|
||||
bookingId: number,
|
||||
triggers: WebhookTriggerEvents[],
|
||||
isDryRun?: boolean
|
||||
): Promise<void>;
|
||||
scheduleDelayedWebhooks(
|
||||
trigger: WebhookTriggerEvents,
|
||||
payload: WebhookPayload,
|
||||
scheduledAt: Date,
|
||||
options?: { teamId?: number | number[] | null; orgId?: number | null },
|
||||
subscribers?: WebhookSubscriber[],
|
||||
isDryRun?: boolean
|
||||
): Promise<void>;
|
||||
}
|
||||
|
||||
// Combined interface for backward compatibility (can be removed later)
|
||||
export interface IWebhookService extends IWebhookRepository, IWebhookProcessor, IWebhookScheduler {}
|
||||
|
||||
// Event Emission Interface - Direct parameter-based methods
|
||||
export interface IBookingEventEmitter {
|
||||
emitBookingPaymentInitiated(params: BookingPaymentInitiatedParams): Promise<void>;
|
||||
emitBookingCreated(params: BookingCreatedParams): Promise<void>;
|
||||
emitBookingCancelled(params: BookingCancelledParams): Promise<void>;
|
||||
emitBookingRequested(params: BookingRequestedParams): Promise<void>;
|
||||
emitBookingRescheduled(params: BookingRescheduledParams): Promise<void>;
|
||||
emitBookingPaid(params: BookingPaidParams): Promise<void>;
|
||||
emitBookingNoShow(params: BookingNoShowParams): Promise<void>;
|
||||
emitBookingRejected(params: BookingRejectedParams): Promise<void>;
|
||||
}
|
||||
|
||||
// Scheduling Interface - Webhook scheduling operations
|
||||
export interface IBookingScheduler {
|
||||
scheduleMeetingWebhooks(params: ScheduleMeetingWebhooksParams): Promise<void>;
|
||||
cancelScheduledMeetingWebhooks(params: CancelScheduledMeetingWebhooksParams): Promise<void>;
|
||||
scheduleNoShowWebhooks(params: ScheduleNoShowWebhooksParams): Promise<void>;
|
||||
}
|
||||
|
||||
// Combined interface for backward compatibility (can be removed later)
|
||||
export interface IBookingWebhookService extends IBookingEventEmitter, IBookingScheduler {}
|
||||
|
||||
// Form Event Emission Interface - Form event emission operations
|
||||
export interface IFormEventEmitter {
|
||||
emitFormSubmitted(params: {
|
||||
form: { id: string; name: string };
|
||||
response: { id: number; data: Record<string, unknown> };
|
||||
eventTypeId?: number | null;
|
||||
userId?: number | null;
|
||||
teamId?: number | null;
|
||||
orgId?: number | null;
|
||||
platformClientId?: string;
|
||||
isDryRun?: boolean;
|
||||
}): Promise<void>;
|
||||
|
||||
emitFormSubmittedNoEvent(params: {
|
||||
form: { id: string; name: string };
|
||||
response: { id: number; data: Record<string, unknown> };
|
||||
userId?: number | null;
|
||||
teamId?: number | null;
|
||||
orgId?: number | null;
|
||||
platformClientId?: string;
|
||||
isDryRun?: boolean;
|
||||
}): Promise<void>;
|
||||
}
|
||||
|
||||
// Form Scheduling Interface - Form webhook scheduling operations
|
||||
export interface IFormScheduler {
|
||||
scheduleDelayedFormWebhooks(params: {
|
||||
responseId: number;
|
||||
form: {
|
||||
id: string;
|
||||
name: string;
|
||||
teamId?: number | null;
|
||||
};
|
||||
responses: Record<string, unknown>;
|
||||
redirect?: Record<string, unknown>;
|
||||
teamId?: number | null;
|
||||
orgId?: number | null;
|
||||
delayMinutes?: number;
|
||||
}): Promise<void>;
|
||||
}
|
||||
|
||||
// Combined interface for backward compatibility (can be removed later)
|
||||
export interface IFormWebhookService extends IFormEventEmitter, IFormScheduler {}
|
||||
|
||||
export interface IRecordingWebhookService {
|
||||
emitRecordingReady(params: {
|
||||
evt: import("@calcom/types/Calendar").CalendarEvent;
|
||||
downloadLink: string;
|
||||
booking?: {
|
||||
id: number;
|
||||
eventTypeId?: number | null;
|
||||
userId?: number | null;
|
||||
};
|
||||
teamId?: number | null;
|
||||
orgId?: number | null;
|
||||
platformClientId?: string;
|
||||
isDryRun?: boolean;
|
||||
}): Promise<void>;
|
||||
|
||||
emitTranscriptionGenerated(params: {
|
||||
evt: import("@calcom/types/Calendar").CalendarEvent;
|
||||
downloadLinks?: {
|
||||
transcription?: Array<{
|
||||
format: string;
|
||||
link: string;
|
||||
}>;
|
||||
recording?: string;
|
||||
};
|
||||
booking?: {
|
||||
id: number;
|
||||
eventTypeId?: number | null;
|
||||
userId?: number | null;
|
||||
};
|
||||
teamId?: number | null;
|
||||
orgId?: number | null;
|
||||
platformClientId?: string;
|
||||
isDryRun?: boolean;
|
||||
}): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { WebhookEventDTO } from "../dto/types";
|
||||
|
||||
export interface IWebhookNotifier {
|
||||
emitWebhook(dto: WebhookEventDTO, isDryRun?: boolean): Promise<void>;
|
||||
}
|
||||
export interface IWebhookNotificationHandler {
|
||||
handleNotification(dto: WebhookEventDTO, isDryRun?: boolean): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
import { withReporting } from "@calcom/lib/sentryWrapper";
|
||||
import { prisma as defaultPrisma } from "@calcom/prisma";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import type { TimeUnit, WebhookTriggerEvents } from "@calcom/prisma/enums";
|
||||
|
||||
import type { WebhookSubscriber } from "../dto/types";
|
||||
import type { IWebhookRepository } from "../interface/repository";
|
||||
import type { GetSubscribersOptions } from "./types";
|
||||
|
||||
// Type for raw query results from the database
|
||||
interface WebhookQueryResult {
|
||||
id: string;
|
||||
subscriberUrl: string;
|
||||
payloadTemplate: string | null;
|
||||
appId: string | null;
|
||||
secret: string | null;
|
||||
time: number | null;
|
||||
timeUnit: TimeUnit | null;
|
||||
eventTriggers: WebhookTriggerEvents[];
|
||||
priority: number; // This field is added by the query and removed before returning
|
||||
}
|
||||
|
||||
export class WebhookRepository implements IWebhookRepository {
|
||||
constructor(private prisma: PrismaClient = defaultPrisma) {}
|
||||
|
||||
async getSubscribers(options: GetSubscribersOptions): Promise<WebhookSubscriber[]> {
|
||||
const teamId = options.teamId;
|
||||
const userId = options.userId;
|
||||
const eventTypeId = options.eventTypeId;
|
||||
const teamIds = Array.isArray(teamId) ? teamId : teamId ? [teamId] : undefined;
|
||||
const orgId = options.orgId;
|
||||
const oAuthClientId = options.oAuthClientId;
|
||||
|
||||
let managedParentEventTypeId: number | undefined;
|
||||
if (eventTypeId) {
|
||||
const managedChildEventType = await this.prisma.eventType.findFirst({
|
||||
where: {
|
||||
id: eventTypeId,
|
||||
parentId: {
|
||||
not: null,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
parentId: true,
|
||||
},
|
||||
});
|
||||
managedParentEventTypeId = managedChildEventType?.parentId ?? undefined;
|
||||
}
|
||||
|
||||
const webhooks = await this.getSubscribersRaw({
|
||||
userId,
|
||||
eventTypeId,
|
||||
managedParentEventTypeId,
|
||||
teamIds: teamIds && orgId ? [...teamIds, orgId] : teamIds || (orgId ? [orgId] : undefined),
|
||||
oAuthClientId,
|
||||
triggerEvent: options.triggerEvent,
|
||||
});
|
||||
|
||||
return webhooks.map((webhook) => ({
|
||||
id: webhook.id,
|
||||
subscriberUrl: webhook.subscriberUrl,
|
||||
payloadTemplate: webhook.payloadTemplate,
|
||||
appId: webhook.appId,
|
||||
secret: webhook.secret,
|
||||
time: webhook.time,
|
||||
timeUnit: webhook.timeUnit as TimeUnit | null,
|
||||
eventTriggers: webhook.eventTriggers as WebhookTriggerEvents[],
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Raw SQL query using UNION for better index utilization than complex ORs
|
||||
* Each UNION branch can use its own optimal index
|
||||
*/
|
||||
private async getSubscribersRaw(params: {
|
||||
userId?: number | null;
|
||||
eventTypeId?: number | null;
|
||||
managedParentEventTypeId?: number | null;
|
||||
teamIds?: number[];
|
||||
oAuthClientId?: string | null;
|
||||
triggerEvent: WebhookTriggerEvents;
|
||||
}): Promise<WebhookSubscriber[]> {
|
||||
const { userId, eventTypeId, managedParentEventTypeId, teamIds, oAuthClientId, triggerEvent } = params;
|
||||
|
||||
// Use static SQL with IS NOT NULL guards and PostgreSQL ANY() for arrays
|
||||
const results = await this.prisma.$queryRaw<WebhookQueryResult[]>`
|
||||
-- Platform webhooks (highest priority)
|
||||
SELECT
|
||||
id, "subscriberUrl", "payloadTemplate", "appId", secret, time, "timeUnit", "eventTriggers",
|
||||
1 as priority
|
||||
FROM "Webhook"
|
||||
WHERE active = true
|
||||
AND platform = true
|
||||
AND ${triggerEvent}::"WebhookTriggerEvents" = ANY("eventTriggers")
|
||||
|
||||
UNION ALL
|
||||
|
||||
-- User-specific webhooks (only if userId provided)
|
||||
SELECT
|
||||
id, "subscriberUrl", "payloadTemplate", "appId", secret, time, "timeUnit", "eventTriggers",
|
||||
2 as priority
|
||||
FROM "Webhook"
|
||||
WHERE active = true
|
||||
AND ${userId} IS NOT NULL
|
||||
AND "userId" = ${userId}
|
||||
AND ${triggerEvent}::"WebhookTriggerEvents" = ANY("eventTriggers")
|
||||
AND platform = false
|
||||
|
||||
UNION ALL
|
||||
|
||||
-- Event type webhooks (only if eventTypeId provided)
|
||||
SELECT
|
||||
id, "subscriberUrl", "payloadTemplate", "appId", secret, time, "timeUnit", "eventTriggers",
|
||||
3 as priority
|
||||
FROM "Webhook"
|
||||
WHERE active = true
|
||||
AND ${eventTypeId} IS NOT NULL
|
||||
AND "eventTypeId" = ${eventTypeId}
|
||||
AND ${triggerEvent}::"WebhookTriggerEvents" = ANY("eventTriggers")
|
||||
AND platform = false
|
||||
|
||||
UNION ALL
|
||||
|
||||
-- Parent event type webhooks (only if managedParentEventTypeId provided)
|
||||
SELECT
|
||||
id, "subscriberUrl", "payloadTemplate", "appId", secret, time, "timeUnit", "eventTriggers",
|
||||
4 as priority
|
||||
FROM "Webhook"
|
||||
WHERE active = true
|
||||
AND ${managedParentEventTypeId} IS NOT NULL
|
||||
AND "eventTypeId" = ${managedParentEventTypeId}
|
||||
AND ${triggerEvent}::"WebhookTriggerEvents" = ANY("eventTriggers")
|
||||
AND platform = false
|
||||
|
||||
UNION ALL
|
||||
|
||||
-- Team webhooks (only if teamIds provided and not empty)
|
||||
SELECT
|
||||
id, "subscriberUrl", "payloadTemplate", "appId", secret, time, "timeUnit", "eventTriggers",
|
||||
5 as priority
|
||||
FROM "Webhook"
|
||||
WHERE active = true
|
||||
AND ${teamIds} IS NOT NULL
|
||||
AND cardinality(${teamIds}::int[]) > 0
|
||||
AND "teamId" = ANY(${teamIds}::int[])
|
||||
AND ${triggerEvent}::"WebhookTriggerEvents" = ANY("eventTriggers")
|
||||
AND platform = false
|
||||
|
||||
UNION ALL
|
||||
|
||||
-- OAuth client webhooks (only if oAuthClientId provided)
|
||||
SELECT
|
||||
id, "subscriberUrl", "payloadTemplate", "appId", secret, time, "timeUnit", "eventTriggers",
|
||||
6 as priority
|
||||
FROM "Webhook"
|
||||
WHERE active = true
|
||||
AND ${oAuthClientId} IS NOT NULL
|
||||
AND "platformOAuthClientId" = ${oAuthClientId}
|
||||
AND ${triggerEvent}::"WebhookTriggerEvents" = ANY("eventTriggers")
|
||||
AND platform = false
|
||||
|
||||
ORDER BY priority, id
|
||||
`;
|
||||
|
||||
const uniqueWebhooks = new Map<string, WebhookSubscriber>();
|
||||
for (const webhook of results) {
|
||||
if (!uniqueWebhooks.has(webhook.id)) {
|
||||
const { priority: _priority, ...webhookData } = webhook;
|
||||
uniqueWebhooks.set(webhook.id, webhookData);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(uniqueWebhooks.values());
|
||||
}
|
||||
|
||||
async getWebhookById(id: string): Promise<WebhookSubscriber | null> {
|
||||
const webhook = await this.prisma.webhook.findUnique({
|
||||
where: { id },
|
||||
select: {
|
||||
id: true,
|
||||
subscriberUrl: true,
|
||||
payloadTemplate: true,
|
||||
appId: true,
|
||||
secret: true,
|
||||
time: true,
|
||||
timeUnit: true,
|
||||
eventTriggers: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!webhook) return null;
|
||||
|
||||
return {
|
||||
id: webhook.id,
|
||||
subscriberUrl: webhook.subscriberUrl,
|
||||
payloadTemplate: webhook.payloadTemplate,
|
||||
appId: webhook.appId,
|
||||
secret: webhook.secret,
|
||||
time: webhook.time,
|
||||
timeUnit: webhook.timeUnit as TimeUnit | null,
|
||||
eventTriggers: webhook.eventTriggers,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const webhookRepository = withReporting(
|
||||
(options: GetSubscribersOptions) => new WebhookRepository().getSubscribers(options),
|
||||
"WebhookRepository.getSubscribers"
|
||||
);
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { WebhookTriggerEvents } from "@calcom/prisma/enums";
|
||||
|
||||
export interface GetSubscribersOptions {
|
||||
userId?: number | null;
|
||||
eventTypeId?: number | null;
|
||||
triggerEvent: WebhookTriggerEvents;
|
||||
teamId?: number | number[] | null;
|
||||
orgId?: number | null;
|
||||
oAuthClientId?: string | null;
|
||||
}
|
||||
@@ -0,0 +1,438 @@
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import type { TimeUnit } from "@calcom/prisma/enums";
|
||||
import { WebhookTriggerEvents } from "@calcom/prisma/enums";
|
||||
|
||||
import type {
|
||||
BookingCreatedDTO,
|
||||
BookingCancelledDTO,
|
||||
BookingRequestedDTO,
|
||||
BookingRescheduledDTO,
|
||||
BookingPaidDTO,
|
||||
BookingPaymentInitiatedDTO,
|
||||
BookingNoShowDTO,
|
||||
BookingRejectedDTO,
|
||||
WebhookSubscriber,
|
||||
} from "../dto/types";
|
||||
import type { IWebhookNotifier, IWebhookService, ITasker, IBookingWebhookService } from "../interface";
|
||||
import type { ILogger } from "../interface/infrastructure";
|
||||
import type {
|
||||
BookingCreatedParams,
|
||||
BookingCancelledParams,
|
||||
BookingRequestedParams,
|
||||
BookingRescheduledParams,
|
||||
BookingPaidParams,
|
||||
BookingPaymentInitiatedParams,
|
||||
BookingNoShowParams,
|
||||
BookingRejectedParams,
|
||||
ScheduleMeetingWebhooksParams,
|
||||
CancelScheduledMeetingWebhooksParams,
|
||||
ScheduleNoShowWebhooksParams,
|
||||
} from "../types";
|
||||
|
||||
export class BookingWebhookService implements IBookingWebhookService {
|
||||
private readonly log: ILogger;
|
||||
|
||||
constructor(
|
||||
private readonly webhookNotifier: IWebhookNotifier,
|
||||
private readonly webhookService: IWebhookService,
|
||||
private readonly tasker: ITasker,
|
||||
logger: ILogger
|
||||
) {
|
||||
this.log = logger.getSubLogger({ prefix: ["[BookingWebhookService]"] });
|
||||
}
|
||||
|
||||
private getTasker(): ITasker {
|
||||
return this.tasker;
|
||||
}
|
||||
|
||||
async emitBookingPaymentInitiated(params: BookingPaymentInitiatedParams): Promise<void> {
|
||||
if (!params.eventType) {
|
||||
throw new Error("eventType is required for booking webhook events");
|
||||
}
|
||||
|
||||
const dto: BookingPaymentInitiatedDTO = {
|
||||
triggerEvent: WebhookTriggerEvents.BOOKING_PAYMENT_INITIATED,
|
||||
createdAt: new Date().toISOString(),
|
||||
bookingId: params.booking.id,
|
||||
eventTypeId: params.eventType.id,
|
||||
userId: params.booking.userId,
|
||||
teamId: params.teamId,
|
||||
orgId: params.orgId,
|
||||
platformClientId: params.platformClientId,
|
||||
evt: params.evt,
|
||||
eventType: params.eventType,
|
||||
booking: params.booking,
|
||||
paymentId: params.paymentId,
|
||||
paymentData: params.paymentData,
|
||||
};
|
||||
|
||||
await this.webhookNotifier.emitWebhook(dto, params.isDryRun);
|
||||
}
|
||||
|
||||
async emitBookingCreated(params: BookingCreatedParams): Promise<void> {
|
||||
if (!params.eventType) {
|
||||
throw new Error("eventType is required for booking webhook events");
|
||||
}
|
||||
|
||||
const dto: BookingCreatedDTO = {
|
||||
triggerEvent: WebhookTriggerEvents.BOOKING_CREATED,
|
||||
createdAt: new Date().toISOString(),
|
||||
bookingId: params.booking.id,
|
||||
eventTypeId: params.eventType.id,
|
||||
userId: params.booking.userId,
|
||||
teamId: params.teamId,
|
||||
orgId: params.orgId,
|
||||
platformClientId: params.platformClientId ?? params.platformParams?.platformClientId,
|
||||
evt: params.evt,
|
||||
eventType: params.eventType,
|
||||
booking: {
|
||||
...params.booking,
|
||||
startTime: params.booking.startTime ?? new Date(params.evt.startTime),
|
||||
},
|
||||
status: params.status || "ACCEPTED",
|
||||
metadata: params.metadata,
|
||||
platformParams: params.platformParams,
|
||||
};
|
||||
|
||||
await this.webhookNotifier.emitWebhook(dto, params.isDryRun);
|
||||
}
|
||||
|
||||
async emitBookingCancelled(params: BookingCancelledParams): Promise<void> {
|
||||
if (!params.eventType) {
|
||||
throw new Error("eventType is required for booking webhook events");
|
||||
}
|
||||
|
||||
const dto: BookingCancelledDTO = {
|
||||
triggerEvent: WebhookTriggerEvents.BOOKING_CANCELLED,
|
||||
createdAt: new Date().toISOString(),
|
||||
bookingId: params.booking.id,
|
||||
eventTypeId: params.eventType.id,
|
||||
userId: params.booking.userId,
|
||||
teamId: params.teamId,
|
||||
orgId: params.orgId,
|
||||
platformClientId: params.platformClientId,
|
||||
evt: params.evt,
|
||||
eventType: params.eventType,
|
||||
booking: params.booking,
|
||||
cancelledBy: params.cancelledBy,
|
||||
cancellationReason: params.cancellationReason,
|
||||
};
|
||||
|
||||
await this.webhookNotifier.emitWebhook(dto, params.isDryRun);
|
||||
}
|
||||
|
||||
async emitBookingRequested(params: BookingRequestedParams): Promise<void> {
|
||||
if (!params.eventType) {
|
||||
throw new Error("eventType is required for booking webhook events");
|
||||
}
|
||||
|
||||
const dto: BookingRequestedDTO = {
|
||||
triggerEvent: WebhookTriggerEvents.BOOKING_REQUESTED,
|
||||
createdAt: new Date().toISOString(),
|
||||
bookingId: params.booking.id,
|
||||
eventTypeId: params.eventType.id,
|
||||
userId: params.booking.userId,
|
||||
teamId: params.teamId,
|
||||
orgId: params.orgId,
|
||||
platformClientId: params.platformClientId,
|
||||
evt: params.evt,
|
||||
eventType: params.eventType,
|
||||
booking: params.booking,
|
||||
};
|
||||
await this.webhookNotifier.emitWebhook(dto, params.isDryRun);
|
||||
}
|
||||
|
||||
async emitBookingRescheduled(params: BookingRescheduledParams): Promise<void> {
|
||||
if (!params.eventType) {
|
||||
throw new Error("eventType is required for booking webhook events");
|
||||
}
|
||||
|
||||
const dto: BookingRescheduledDTO = {
|
||||
triggerEvent: WebhookTriggerEvents.BOOKING_RESCHEDULED,
|
||||
createdAt: new Date().toISOString(),
|
||||
bookingId: params.booking.id,
|
||||
eventTypeId: params.eventType.id,
|
||||
userId: params.booking.userId,
|
||||
teamId: params.teamId,
|
||||
orgId: params.orgId,
|
||||
platformClientId: params.platformClientId,
|
||||
evt: params.evt,
|
||||
eventType: params.eventType,
|
||||
booking: params.booking,
|
||||
rescheduleId: params.rescheduleId,
|
||||
rescheduleUid: params.rescheduleUid,
|
||||
rescheduleStartTime: params.rescheduleStartTime,
|
||||
rescheduleEndTime: params.rescheduleEndTime,
|
||||
rescheduledBy: params.rescheduledBy,
|
||||
};
|
||||
|
||||
await this.webhookNotifier.emitWebhook(dto, params.isDryRun);
|
||||
}
|
||||
|
||||
async emitBookingPaid(params: BookingPaidParams): Promise<void> {
|
||||
if (!params.eventType) {
|
||||
throw new Error("eventType is required for booking webhook events");
|
||||
}
|
||||
|
||||
const dto: BookingPaidDTO = {
|
||||
triggerEvent: WebhookTriggerEvents.BOOKING_PAID,
|
||||
createdAt: new Date().toISOString(),
|
||||
bookingId: params.booking.id,
|
||||
eventTypeId: params.eventType.id,
|
||||
userId: params.booking.userId,
|
||||
teamId: params.teamId,
|
||||
orgId: params.orgId,
|
||||
platformClientId: params.platformClientId,
|
||||
evt: params.evt,
|
||||
eventType: params.eventType,
|
||||
booking: params.booking,
|
||||
paymentId: params.paymentId,
|
||||
paymentData: params.paymentData,
|
||||
};
|
||||
|
||||
await this.webhookNotifier.emitWebhook(dto, params.isDryRun);
|
||||
}
|
||||
|
||||
async emitBookingNoShow(params: BookingNoShowParams): Promise<void> {
|
||||
const dto: BookingNoShowDTO = {
|
||||
triggerEvent: WebhookTriggerEvents.BOOKING_NO_SHOW_UPDATED,
|
||||
createdAt: new Date().toISOString(),
|
||||
bookingId: params.bookingId,
|
||||
eventTypeId: params.eventTypeId,
|
||||
userId: params.userId,
|
||||
teamId: params.teamId,
|
||||
orgId: params.orgId,
|
||||
platformClientId: params.platformClientId,
|
||||
message: params.message,
|
||||
bookingUid: params.bookingUid,
|
||||
attendees: params.attendees,
|
||||
};
|
||||
|
||||
await this.webhookNotifier.emitWebhook(dto, params.isDryRun);
|
||||
}
|
||||
|
||||
async emitBookingRejected(params: BookingRejectedParams): Promise<void> {
|
||||
if (!params.eventType) {
|
||||
throw new Error("eventType is required for booking webhook events");
|
||||
}
|
||||
|
||||
const dto: BookingRejectedDTO = {
|
||||
triggerEvent: WebhookTriggerEvents.BOOKING_REJECTED,
|
||||
createdAt: new Date().toISOString(),
|
||||
bookingId: params.booking.id,
|
||||
eventTypeId: params.eventType.id,
|
||||
userId: params.booking.userId,
|
||||
teamId: params.teamId,
|
||||
orgId: params.orgId,
|
||||
status: "REJECTED",
|
||||
platformClientId: params.platformClientId,
|
||||
evt: params.evt,
|
||||
eventType: params.eventType,
|
||||
booking: params.booking,
|
||||
};
|
||||
|
||||
await this.webhookNotifier.emitWebhook(dto, params.isDryRun);
|
||||
}
|
||||
|
||||
async scheduleMeetingWebhooks(params: ScheduleMeetingWebhooksParams): Promise<void> {
|
||||
const webhookService = this.webhookService;
|
||||
|
||||
const subscribersMeetingStarted = await webhookService.getSubscribers({
|
||||
userId: params.booking.userId,
|
||||
eventTypeId: params.booking.eventTypeId,
|
||||
triggerEvent: WebhookTriggerEvents.MEETING_STARTED,
|
||||
teamId: params.teamId,
|
||||
orgId: params.orgId,
|
||||
oAuthClientId: params.oAuthClientId,
|
||||
});
|
||||
|
||||
const subscribersMeetingEnded = await webhookService.getSubscribers({
|
||||
userId: params.booking.userId,
|
||||
eventTypeId: params.booking.eventTypeId,
|
||||
triggerEvent: WebhookTriggerEvents.MEETING_ENDED,
|
||||
teamId: params.teamId,
|
||||
orgId: params.orgId,
|
||||
oAuthClientId: params.oAuthClientId,
|
||||
});
|
||||
|
||||
const schedulePromises: Promise<void>[] = [];
|
||||
|
||||
for (const subscriber of subscribersMeetingStarted) {
|
||||
schedulePromises.push(
|
||||
webhookService.scheduleTimeBasedWebhook(
|
||||
WebhookTriggerEvents.MEETING_STARTED,
|
||||
params.booking.startTime,
|
||||
{
|
||||
id: params.booking.id,
|
||||
uid: params.booking.uid,
|
||||
eventTypeId: params.booking.eventTypeId,
|
||||
userId: params.booking.userId,
|
||||
teamId: params.teamId,
|
||||
responses: params.booking.responses,
|
||||
},
|
||||
subscriber,
|
||||
params.evt as unknown as Record<string, unknown>,
|
||||
params.isDryRun
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
for (const subscriber of subscribersMeetingEnded) {
|
||||
schedulePromises.push(
|
||||
webhookService.scheduleTimeBasedWebhook(
|
||||
WebhookTriggerEvents.MEETING_ENDED,
|
||||
params.booking.endTime,
|
||||
{
|
||||
id: params.booking.id,
|
||||
uid: params.booking.uid,
|
||||
eventTypeId: params.booking.eventTypeId,
|
||||
userId: params.booking.userId,
|
||||
teamId: params.teamId,
|
||||
responses: params.booking.responses,
|
||||
},
|
||||
subscriber,
|
||||
params.evt as unknown as Record<string, unknown>,
|
||||
params.isDryRun
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Use Promise.allSettled to prevent single webhook scheduling failure from killing entire processor
|
||||
const results = await Promise.allSettled(schedulePromises);
|
||||
|
||||
// Log summary for monitoring
|
||||
const successCount = results.filter((result) => result.status === "fulfilled").length;
|
||||
const failureCount = results.filter((result) => result.status === "rejected").length;
|
||||
|
||||
this.log.info(`Meeting webhook scheduling completed`, {
|
||||
bookingId: params.booking.id,
|
||||
totalWebhooks: schedulePromises.length,
|
||||
successful: successCount,
|
||||
failed: failureCount,
|
||||
});
|
||||
|
||||
// Log individual failures for debugging
|
||||
if (failureCount > 0) {
|
||||
results.forEach((result, index) => {
|
||||
if (result.status === "rejected") {
|
||||
this.log.error(`Meeting webhook scheduling failed`, {
|
||||
bookingId: params.booking.id,
|
||||
index,
|
||||
error: result.reason,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async cancelScheduledMeetingWebhooks(params: CancelScheduledMeetingWebhooksParams): Promise<void> {
|
||||
const webhookService = this.webhookService;
|
||||
await webhookService.cancelScheduledWebhooks(
|
||||
params.bookingId,
|
||||
[WebhookTriggerEvents.MEETING_STARTED, WebhookTriggerEvents.MEETING_ENDED],
|
||||
params.isDryRun
|
||||
);
|
||||
}
|
||||
|
||||
async scheduleNoShowWebhooks(params: ScheduleNoShowWebhooksParams): Promise<void> {
|
||||
const webhookService = this.webhookService;
|
||||
|
||||
try {
|
||||
const tasker = await this.getTasker();
|
||||
const noShowPromises: Promise<string | void>[] = [];
|
||||
|
||||
const subscribersHostsNoShow = await webhookService.getSubscribers({
|
||||
userId: params.triggerForUser ? params.organizerUser.id : null,
|
||||
eventTypeId: params.booking.eventTypeId,
|
||||
triggerEvent: WebhookTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW,
|
||||
teamId: params.teamId,
|
||||
orgId: params.orgId,
|
||||
oAuthClientId: params.oAuthClientId,
|
||||
});
|
||||
|
||||
noShowPromises.push(
|
||||
...subscribersHostsNoShow.map((webhook: WebhookSubscriber) => {
|
||||
if (params.booking.startTime && webhook.time && webhook.timeUnit) {
|
||||
const scheduledAt = dayjs
|
||||
.utc(params.booking.startTime)
|
||||
.add(webhook.time, webhook.timeUnit.toLowerCase() as dayjs.ManipulateType)
|
||||
.toDate();
|
||||
|
||||
return tasker.create(
|
||||
"triggerHostNoShowWebhook",
|
||||
JSON.stringify({
|
||||
triggerEvent: WebhookTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW,
|
||||
bookingId: params.booking.id,
|
||||
webhook: { ...webhook, time: webhook.time, timeUnit: webhook.timeUnit as TimeUnit },
|
||||
}),
|
||||
{ scheduledAt }
|
||||
);
|
||||
}
|
||||
return Promise.resolve();
|
||||
})
|
||||
);
|
||||
|
||||
const subscribersGuestsNoShow = await webhookService.getSubscribers({
|
||||
userId: params.triggerForUser ? params.organizerUser.id : null,
|
||||
eventTypeId: params.booking.eventTypeId,
|
||||
triggerEvent: WebhookTriggerEvents.AFTER_GUESTS_CAL_VIDEO_NO_SHOW,
|
||||
teamId: params.teamId,
|
||||
orgId: params.orgId,
|
||||
oAuthClientId: params.oAuthClientId,
|
||||
});
|
||||
|
||||
noShowPromises.push(
|
||||
...subscribersGuestsNoShow.map((webhook: WebhookSubscriber) => {
|
||||
if (params.booking.startTime && webhook.time && webhook.timeUnit) {
|
||||
const scheduledAt = dayjs
|
||||
.utc(params.booking.startTime)
|
||||
.add(webhook.time, webhook.timeUnit.toLowerCase() as dayjs.ManipulateType)
|
||||
.toDate();
|
||||
|
||||
return tasker.create(
|
||||
"triggerGuestNoShowWebhook",
|
||||
JSON.stringify({
|
||||
triggerEvent: WebhookTriggerEvents.AFTER_GUESTS_CAL_VIDEO_NO_SHOW,
|
||||
bookingId: params.booking.id,
|
||||
webhook: { ...webhook, time: webhook.time, timeUnit: webhook.timeUnit as TimeUnit },
|
||||
}),
|
||||
{ scheduledAt }
|
||||
);
|
||||
}
|
||||
return Promise.resolve();
|
||||
})
|
||||
);
|
||||
|
||||
// Use Promise.allSettled to prevent single webhook scheduling failure from killing entire processor
|
||||
const results = await Promise.allSettled(noShowPromises);
|
||||
|
||||
// Log summary for monitoring
|
||||
const successCount = results.filter((result) => result.status === "fulfilled").length;
|
||||
const failureCount = results.filter((result) => result.status === "rejected").length;
|
||||
|
||||
this.log.info(`No-show webhook scheduling completed`, {
|
||||
bookingId: params.booking.id,
|
||||
totalWebhooks: noShowPromises.length,
|
||||
successful: successCount,
|
||||
failed: failureCount,
|
||||
});
|
||||
|
||||
// Log individual failures for debugging
|
||||
if (failureCount > 0) {
|
||||
results.forEach((result, index) => {
|
||||
if (result.status === "rejected") {
|
||||
this.log.error(`No-show webhook scheduling failed`, {
|
||||
bookingId: params.booking.id,
|
||||
index,
|
||||
error: result.reason,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
this.log.error("Failed to schedule no-show webhooks:", {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import type { FORM_SUBMITTED_WEBHOOK_RESPONSES } from "@calcom/app-store/routing-forms/trpc/utils";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { WebhookTriggerEvents } from "@calcom/prisma/enums";
|
||||
|
||||
import type { FormSubmittedDTO, FormSubmittedNoEventDTO } from "../dto/types";
|
||||
import type { FormSubmittedPayload } from "../factory/types";
|
||||
import type { IWebhookService, IFormWebhookService } from "../interface/services";
|
||||
import type { IWebhookNotifier } from "../interface/webhook";
|
||||
|
||||
export class FormWebhookService implements IFormWebhookService {
|
||||
constructor(
|
||||
private readonly webhookNotifier: IWebhookNotifier,
|
||||
private readonly webhookService: IWebhookService
|
||||
) {}
|
||||
|
||||
async emitFormSubmitted(params: {
|
||||
form: { id: string; name: string };
|
||||
response: { id: number; data: FORM_SUBMITTED_WEBHOOK_RESPONSES };
|
||||
eventTypeId?: number | null;
|
||||
userId?: number | null;
|
||||
teamId?: number | null;
|
||||
orgId?: number | null;
|
||||
platformClientId?: string;
|
||||
isDryRun?: boolean;
|
||||
}): Promise<void> {
|
||||
const dto: FormSubmittedDTO = {
|
||||
triggerEvent: WebhookTriggerEvents.FORM_SUBMITTED,
|
||||
createdAt: new Date().toISOString(),
|
||||
eventTypeId: params.eventTypeId,
|
||||
userId: params.userId,
|
||||
teamId: params.teamId,
|
||||
orgId: params.orgId,
|
||||
platformClientId: params.platformClientId,
|
||||
form: params.form,
|
||||
response: params.response,
|
||||
};
|
||||
|
||||
await this.webhookNotifier.emitWebhook(dto, params.isDryRun);
|
||||
}
|
||||
|
||||
async emitFormSubmittedNoEvent(params: {
|
||||
form: { id: string; name: string };
|
||||
response: { id: number; data: FORM_SUBMITTED_WEBHOOK_RESPONSES };
|
||||
userId?: number | null;
|
||||
teamId?: number | null;
|
||||
orgId?: number | null;
|
||||
platformClientId?: string;
|
||||
isDryRun?: boolean;
|
||||
}): Promise<void> {
|
||||
const dto: FormSubmittedNoEventDTO = {
|
||||
triggerEvent: WebhookTriggerEvents.FORM_SUBMITTED_NO_EVENT,
|
||||
createdAt: new Date().toISOString(),
|
||||
userId: params.userId,
|
||||
teamId: params.teamId,
|
||||
orgId: params.orgId,
|
||||
platformClientId: params.platformClientId,
|
||||
form: params.form,
|
||||
response: params.response,
|
||||
};
|
||||
|
||||
await this.webhookNotifier.emitWebhook(dto, params.isDryRun);
|
||||
}
|
||||
|
||||
async scheduleDelayedFormWebhooks(params: {
|
||||
responseId: number;
|
||||
form: {
|
||||
id: string;
|
||||
name: string;
|
||||
teamId?: number | null;
|
||||
};
|
||||
responses: FORM_SUBMITTED_WEBHOOK_RESPONSES;
|
||||
redirect?: Record<string, unknown>;
|
||||
teamId?: number | null;
|
||||
orgId?: number | null;
|
||||
delayMinutes?: number;
|
||||
}): Promise<void> {
|
||||
const delayMinutes = params.delayMinutes || 15;
|
||||
const scheduledAt = dayjs().add(delayMinutes, "minute").toDate();
|
||||
|
||||
const payload = {
|
||||
triggerEvent: WebhookTriggerEvents.FORM_SUBMITTED_NO_EVENT,
|
||||
createdAt: new Date().toISOString(),
|
||||
payload: {
|
||||
formId: params.form.id,
|
||||
formName: params.form.name,
|
||||
teamId: params.teamId ?? null,
|
||||
redirect: params.redirect,
|
||||
responses: params.responses,
|
||||
} as FormSubmittedPayload,
|
||||
};
|
||||
|
||||
await this.webhookService.scheduleDelayedWebhooks(
|
||||
WebhookTriggerEvents.FORM_SUBMITTED_NO_EVENT,
|
||||
payload,
|
||||
scheduledAt,
|
||||
{ teamId: params.teamId, orgId: params.orgId }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { WebhookTriggerEvents } from "@calcom/prisma/enums";
|
||||
|
||||
import type { OOOCreatedDTO } from "../dto/types";
|
||||
import type { ITasker, ILogger } from "../interface/infrastructure";
|
||||
import type { IWebhookRepository } from "../interface/services";
|
||||
import type { IWebhookNotifier } from "../interface/webhook";
|
||||
import { WebhookService } from "./WebhookService";
|
||||
|
||||
export class OOOWebhookService extends WebhookService {
|
||||
constructor(
|
||||
private readonly notifier: IWebhookNotifier,
|
||||
repository: IWebhookRepository,
|
||||
tasker: ITasker,
|
||||
logger: ILogger
|
||||
) {
|
||||
super(repository, tasker, logger);
|
||||
}
|
||||
|
||||
async emitOOOCreated(params: {
|
||||
oooEntry: {
|
||||
id: number;
|
||||
start: string;
|
||||
end: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
notes: string | null;
|
||||
reason: {
|
||||
emoji?: string;
|
||||
reason?: string;
|
||||
};
|
||||
reasonId: number;
|
||||
user: {
|
||||
id: number;
|
||||
name: string | null;
|
||||
username: string | null;
|
||||
timeZone: string;
|
||||
email: string;
|
||||
};
|
||||
toUser: {
|
||||
id: number;
|
||||
name?: string | null;
|
||||
username?: string | null;
|
||||
email?: string;
|
||||
timeZone?: string;
|
||||
} | null;
|
||||
uuid: string;
|
||||
};
|
||||
userId?: number | null;
|
||||
teamId?: number | null;
|
||||
orgId?: number | null;
|
||||
platformClientId?: string;
|
||||
isDryRun?: boolean;
|
||||
}): Promise<void> {
|
||||
const dto: OOOCreatedDTO = {
|
||||
triggerEvent: WebhookTriggerEvents.OOO_CREATED,
|
||||
createdAt: new Date().toISOString(),
|
||||
userId: params.userId,
|
||||
teamId: params.teamId,
|
||||
orgId: params.orgId,
|
||||
platformClientId: params.platformClientId,
|
||||
oooEntry: params.oooEntry,
|
||||
};
|
||||
|
||||
await this.notifier.emitWebhook(dto, params.isDryRun);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { WebhookTriggerEvents } from "@calcom/prisma/enums";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
|
||||
import type { RecordingReadyDTO, TranscriptionGeneratedDTO } from "../dto/types";
|
||||
import type { IRecordingWebhookService } from "../interface/services";
|
||||
import type { IWebhookNotifier } from "../interface/webhook";
|
||||
|
||||
export class RecordingWebhookService implements IRecordingWebhookService {
|
||||
constructor(private readonly webhookNotifier: IWebhookNotifier) {}
|
||||
|
||||
async emitRecordingReady(params: {
|
||||
evt: CalendarEvent;
|
||||
downloadLink: string;
|
||||
booking?: {
|
||||
id: number;
|
||||
eventTypeId?: number | null;
|
||||
userId?: number | null;
|
||||
};
|
||||
teamId?: number | null;
|
||||
orgId?: number | null;
|
||||
platformClientId?: string;
|
||||
isDryRun?: boolean;
|
||||
}): Promise<void> {
|
||||
const dto: RecordingReadyDTO = {
|
||||
triggerEvent: WebhookTriggerEvents.RECORDING_READY,
|
||||
createdAt: new Date().toISOString(),
|
||||
bookingId: params.booking?.id,
|
||||
eventTypeId: params.booking?.eventTypeId,
|
||||
userId: params.booking?.userId,
|
||||
teamId: params.teamId,
|
||||
orgId: params.orgId,
|
||||
platformClientId: params.platformClientId,
|
||||
evt: params.evt,
|
||||
downloadLink: params.downloadLink,
|
||||
};
|
||||
await this.webhookNotifier.emitWebhook(dto, params.isDryRun);
|
||||
}
|
||||
|
||||
async emitTranscriptionGenerated(params: {
|
||||
evt: CalendarEvent;
|
||||
downloadLinks?: {
|
||||
transcription?: Array<{
|
||||
format: string;
|
||||
link: string;
|
||||
}>;
|
||||
recording?: string;
|
||||
};
|
||||
booking?: {
|
||||
id: number;
|
||||
eventTypeId?: number | null;
|
||||
userId?: number | null;
|
||||
};
|
||||
teamId?: number | null;
|
||||
orgId?: number | null;
|
||||
platformClientId?: string;
|
||||
isDryRun?: boolean;
|
||||
}): Promise<void> {
|
||||
const dto: TranscriptionGeneratedDTO = {
|
||||
triggerEvent: WebhookTriggerEvents.RECORDING_TRANSCRIPTION_GENERATED,
|
||||
createdAt: new Date().toISOString(),
|
||||
bookingId: params.booking?.id,
|
||||
eventTypeId: params.booking?.eventTypeId,
|
||||
userId: params.booking?.userId,
|
||||
teamId: params.teamId,
|
||||
orgId: params.orgId,
|
||||
platformClientId: params.platformClientId,
|
||||
evt: params.evt,
|
||||
downloadLinks: params.downloadLinks,
|
||||
};
|
||||
|
||||
await this.webhookNotifier.emitWebhook(dto, params.isDryRun);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import { WebhookTriggerEvents } from "@calcom/prisma/enums";
|
||||
|
||||
import type { WebhookEventDTO } from "../dto/types";
|
||||
import type { BookingPayloadBuilder } from "../factory/BookingPayloadBuilder";
|
||||
import type { FormPayloadBuilder } from "../factory/FormPayloadBuilder";
|
||||
import type { InstantMeetingBuilder } from "../factory/InstantMeetingBuilder";
|
||||
import type { MeetingPayloadBuilder } from "../factory/MeetingPayloadBuilder";
|
||||
import type { OOOPayloadBuilder } from "../factory/OOOPayloadBuilder";
|
||||
import type { RecordingPayloadBuilder } from "../factory/RecordingPayloadBuilder";
|
||||
import type { WebhookPayload } from "../factory/types";
|
||||
import type { ILogger } from "../interface/infrastructure";
|
||||
import type { IWebhookService } from "../interface/services";
|
||||
import type { IWebhookNotificationHandler } from "../interface/webhook";
|
||||
|
||||
export class WebhookNotificationHandler implements IWebhookNotificationHandler {
|
||||
private readonly log: ILogger;
|
||||
|
||||
constructor(
|
||||
private readonly webhookService: IWebhookService,
|
||||
private readonly bookingPayloadBuilder: BookingPayloadBuilder,
|
||||
private readonly formPayloadBuilder: FormPayloadBuilder,
|
||||
private readonly oooPayloadBuilder: OOOPayloadBuilder,
|
||||
private readonly recordingPayloadBuilder: RecordingPayloadBuilder,
|
||||
private readonly meetingPayloadBuilder: MeetingPayloadBuilder,
|
||||
private readonly instantMeetingBuilder: InstantMeetingBuilder,
|
||||
logger: ILogger
|
||||
) {
|
||||
this.log = logger.getSubLogger({ prefix: ["[WebhookNotificationHandler]"] });
|
||||
}
|
||||
|
||||
async handleNotification(dto: WebhookEventDTO, isDryRun = false): Promise<void> {
|
||||
const trigger = dto.triggerEvent;
|
||||
|
||||
try {
|
||||
if (isDryRun) {
|
||||
this.log.debug(`Dry run mode - skipping webhook notification for: ${trigger}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const subscriptionParams = {
|
||||
userId: dto.userId,
|
||||
eventTypeId: dto.eventTypeId,
|
||||
triggerEvent: trigger,
|
||||
teamId: dto.teamId,
|
||||
orgId: dto.orgId,
|
||||
oAuthClientId: dto.platformClientId,
|
||||
};
|
||||
|
||||
this.log.debug(`Querying for webhook subscribers with params:`, subscriptionParams);
|
||||
|
||||
const subscribers = await this.webhookService.getSubscribers(subscriptionParams);
|
||||
|
||||
if (subscribers.length === 0) {
|
||||
this.log.debug(`No subscribers found for event: ${trigger}`, {
|
||||
bookingId: dto.bookingId,
|
||||
eventTypeId: dto.eventTypeId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const webhookPayload = this.createPayload(dto);
|
||||
|
||||
await this.webhookService.processWebhooks(trigger, webhookPayload, subscribers);
|
||||
|
||||
this.log.debug(`Successfully processed webhook notification: ${trigger}`, {
|
||||
subscriberCount: subscribers.length,
|
||||
bookingId: dto.bookingId,
|
||||
});
|
||||
} catch (error) {
|
||||
this.log.error(`Error handling webhook notification: ${trigger}`, {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
bookingId: dto.bookingId,
|
||||
eventTypeId: dto.eventTypeId,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private createPayload(dto: WebhookEventDTO): WebhookPayload {
|
||||
switch (dto.triggerEvent) {
|
||||
// Booking events
|
||||
case WebhookTriggerEvents.BOOKING_CREATED:
|
||||
case WebhookTriggerEvents.BOOKING_CANCELLED:
|
||||
case WebhookTriggerEvents.BOOKING_REQUESTED:
|
||||
case WebhookTriggerEvents.BOOKING_RESCHEDULED:
|
||||
case WebhookTriggerEvents.BOOKING_REJECTED:
|
||||
case WebhookTriggerEvents.BOOKING_PAID:
|
||||
case WebhookTriggerEvents.BOOKING_PAYMENT_INITIATED:
|
||||
case WebhookTriggerEvents.BOOKING_NO_SHOW_UPDATED:
|
||||
return this.bookingPayloadBuilder.build(dto);
|
||||
|
||||
// Form events
|
||||
case WebhookTriggerEvents.FORM_SUBMITTED:
|
||||
case WebhookTriggerEvents.FORM_SUBMITTED_NO_EVENT:
|
||||
return this.formPayloadBuilder.build(dto);
|
||||
|
||||
// OOO events
|
||||
case WebhookTriggerEvents.OOO_CREATED:
|
||||
return this.oooPayloadBuilder.build(dto);
|
||||
|
||||
// Recording events
|
||||
case WebhookTriggerEvents.RECORDING_READY:
|
||||
case WebhookTriggerEvents.RECORDING_TRANSCRIPTION_GENERATED:
|
||||
return this.recordingPayloadBuilder.build(dto);
|
||||
|
||||
// Meeting events
|
||||
case WebhookTriggerEvents.MEETING_STARTED:
|
||||
case WebhookTriggerEvents.MEETING_ENDED:
|
||||
return this.meetingPayloadBuilder.build(dto);
|
||||
|
||||
// Instant meeting events
|
||||
case WebhookTriggerEvents.INSTANT_MEETING:
|
||||
return this.instantMeetingBuilder.build(dto);
|
||||
|
||||
// No-show events (special handling)
|
||||
case WebhookTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW:
|
||||
case WebhookTriggerEvents.AFTER_GUESTS_CAL_VIDEO_NO_SHOW: {
|
||||
return {
|
||||
triggerEvent: dto.triggerEvent,
|
||||
createdAt: dto.createdAt,
|
||||
payload: {
|
||||
bookingId: dto.bookingId,
|
||||
webhook: dto.webhook,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
default: {
|
||||
// TypeScript exhaustiveness check - this should never happen if all cases are covered
|
||||
const _exhaustiveCheck: never = dto;
|
||||
throw new Error(`Unsupported triggerEvent: ${JSON.stringify(_exhaustiveCheck)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { WebhookEventDTO } from "../dto/types";
|
||||
import type { ILogger } from "../interface/infrastructure";
|
||||
import type { IWebhookNotifier, IWebhookNotificationHandler } from "../interface/webhook";
|
||||
|
||||
export class WebhookNotifier implements IWebhookNotifier {
|
||||
private readonly log: ILogger;
|
||||
|
||||
constructor(private readonly handler: IWebhookNotificationHandler, logger: ILogger) {
|
||||
this.log = logger.getSubLogger({ prefix: ["[WebhookNotifier]"] });
|
||||
}
|
||||
|
||||
async emitWebhook(dto: WebhookEventDTO, isDryRun = false): Promise<void> {
|
||||
const trigger = dto.triggerEvent;
|
||||
|
||||
try {
|
||||
this.log.debug(`Emitting webhook event: ${trigger}`, {
|
||||
bookingId: dto.bookingId,
|
||||
eventTypeId: dto.eventTypeId,
|
||||
isDryRun,
|
||||
});
|
||||
|
||||
await this.handler.handleNotification(dto, isDryRun);
|
||||
|
||||
this.log.debug(`Successfully emitted webhook event: ${trigger}`, {
|
||||
bookingId: dto.bookingId,
|
||||
});
|
||||
} catch (error) {
|
||||
this.log.error(`Failed to emit webhook event: ${trigger}`, {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
bookingId: dto.bookingId,
|
||||
eventTypeId: dto.eventTypeId,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
import { createHmac } from "crypto";
|
||||
|
||||
import { WebhookTriggerEvents } from "@calcom/prisma/enums";
|
||||
|
||||
import type { WebhookSubscriber, WebhookDeliveryResult } from "../dto/types";
|
||||
import type { WebhookPayload } from "../factory/types";
|
||||
import type { ITasker, ILogger } from "../interface/infrastructure";
|
||||
import type { IWebhookRepository, IWebhookService } from "../interface/services";
|
||||
|
||||
export class WebhookService implements IWebhookService {
|
||||
private readonly log: ILogger;
|
||||
|
||||
constructor(
|
||||
private readonly repository: IWebhookRepository,
|
||||
private readonly tasker: ITasker,
|
||||
logger: ILogger
|
||||
) {
|
||||
this.log = logger.getSubLogger({ prefix: ["[WebhookService]"] });
|
||||
}
|
||||
|
||||
protected async sendWebhook(
|
||||
trigger: WebhookTriggerEvents,
|
||||
payload: WebhookPayload,
|
||||
subscriber: WebhookSubscriber
|
||||
): Promise<WebhookDeliveryResult> {
|
||||
try {
|
||||
// TODO: Ideally we should inject this flag as well. Would be awesome when we would be able to unit test without mocking env variables too. Also with trigger.dev, this would be further worked on, so we can leave this as is for now
|
||||
if (process.env.TASKER_ENABLE_WEBHOOKS === "1") {
|
||||
return await this.scheduleWebhook(trigger, payload, subscriber);
|
||||
} else {
|
||||
return await this.sendWebhookDirectly(trigger, payload, subscriber);
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
status: 0,
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
duration: 0,
|
||||
subscriberUrl: subscriber.subscriberUrl,
|
||||
webhookId: subscriber.id,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async scheduleWebhook(
|
||||
trigger: WebhookTriggerEvents,
|
||||
payload: WebhookPayload,
|
||||
subscriber: WebhookSubscriber
|
||||
): Promise<WebhookDeliveryResult> {
|
||||
if (!this.tasker) {
|
||||
throw new Error("Tasker not injected - ensure proper DI configuration");
|
||||
}
|
||||
|
||||
await this.tasker.create(
|
||||
"sendWebhook",
|
||||
JSON.stringify({
|
||||
secretKey: subscriber.secret,
|
||||
triggerEvent: trigger,
|
||||
createdAt: new Date().toISOString(),
|
||||
webhook: subscriber,
|
||||
data: payload.payload,
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
message: "Webhook scheduled successfully",
|
||||
duration: 0,
|
||||
subscriberUrl: subscriber.subscriberUrl,
|
||||
webhookId: subscriber.id,
|
||||
};
|
||||
}
|
||||
|
||||
private async sendWebhookDirectly(
|
||||
trigger: WebhookTriggerEvents,
|
||||
payload: WebhookPayload,
|
||||
subscriber: WebhookSubscriber
|
||||
): Promise<WebhookDeliveryResult> {
|
||||
const { subscriberUrl, payloadTemplate } = subscriber;
|
||||
if (!subscriberUrl) throw new Error("Missing subscriber URL");
|
||||
|
||||
const contentType =
|
||||
!payloadTemplate || this.isJsonTemplate(payloadTemplate)
|
||||
? "application/json"
|
||||
: "application/x-www-form-urlencoded";
|
||||
|
||||
const body = JSON.stringify({
|
||||
triggerEvent: trigger,
|
||||
createdAt: payload.createdAt,
|
||||
payload: payload.payload,
|
||||
});
|
||||
|
||||
const signature = subscriber.secret
|
||||
? createHmac("sha256", subscriber.secret).update(body).digest("hex")
|
||||
: "no-secret-provided";
|
||||
|
||||
const response = await fetch(subscriberUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": contentType,
|
||||
"X-Cal-Signature-256": signature,
|
||||
},
|
||||
redirect: "manual",
|
||||
body,
|
||||
});
|
||||
|
||||
const responseText = await response.text();
|
||||
|
||||
return {
|
||||
ok: response.ok,
|
||||
status: response.status,
|
||||
message: responseText || undefined,
|
||||
duration: 0,
|
||||
subscriberUrl: subscriberUrl,
|
||||
webhookId: subscriber.id,
|
||||
};
|
||||
}
|
||||
|
||||
private isJsonTemplate(template: string | null): boolean {
|
||||
if (!template) return true;
|
||||
try {
|
||||
JSON.parse(template);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async getSubscribers(options: Parameters<IWebhookRepository["getSubscribers"]>[0]) {
|
||||
return this.repository.getSubscribers(options);
|
||||
}
|
||||
|
||||
async processWebhooks(
|
||||
trigger: WebhookTriggerEvents,
|
||||
payload: WebhookPayload,
|
||||
subscribers: WebhookSubscriber[]
|
||||
): Promise<void> {
|
||||
if (subscribers.length === 0) {
|
||||
this.log.debug("No subscribers to process for trigger:", { trigger });
|
||||
return;
|
||||
}
|
||||
|
||||
const promises = subscribers.map(async (subscriber) => {
|
||||
try {
|
||||
this.log.debug("Processing webhook", {
|
||||
trigger,
|
||||
webhookId: subscriber.id,
|
||||
subscriberUrl: subscriber.subscriberUrl,
|
||||
});
|
||||
|
||||
const result = await this.sendWebhook(trigger, payload, subscriber);
|
||||
|
||||
if (result.ok) {
|
||||
this.log.debug(`Webhook sent successfully`, {
|
||||
trigger,
|
||||
webhookId: subscriber.id,
|
||||
statusCode: result.status,
|
||||
});
|
||||
} else {
|
||||
this.log.error(`Webhook failed`, {
|
||||
error: result.message,
|
||||
trigger,
|
||||
webhookId: subscriber.id,
|
||||
statusCode: result.status,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
this.log.error("Error sending webhook", {
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
trigger,
|
||||
webhookId: subscriber.id,
|
||||
});
|
||||
// Re-throw to ensure Promise.allSettled captures the failure
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
// Use Promise.allSettled to prevent single webhook failure from killing entire processor
|
||||
const results = await Promise.allSettled(promises);
|
||||
|
||||
// Log summary for monitoring
|
||||
const successCount = results.filter((result) => result.status === "fulfilled").length;
|
||||
const failureCount = results.filter((result) => result.status === "rejected").length;
|
||||
|
||||
this.log.info(`Webhook processing completed for ${trigger}`, {
|
||||
totalSubscribers: subscribers.length,
|
||||
successful: successCount,
|
||||
failed: failureCount,
|
||||
});
|
||||
|
||||
// Log individual failures for debugging
|
||||
results.forEach((result, index) => {
|
||||
if (result.status === "rejected") {
|
||||
const subscriber = subscribers[index];
|
||||
this.log.error(`Webhook processing failed for subscriber`, {
|
||||
trigger,
|
||||
webhookId: subscriber?.id,
|
||||
subscriberUrl: subscriber?.subscriberUrl,
|
||||
error: result.reason,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async scheduleTimeBasedWebhook(
|
||||
trigger: WebhookTriggerEvents,
|
||||
scheduledAt: Date,
|
||||
bookingData: {
|
||||
id: number;
|
||||
uid: string;
|
||||
eventTypeId: number | null;
|
||||
userId: number | null;
|
||||
teamId?: number | null;
|
||||
responses?: Record<string, unknown>;
|
||||
},
|
||||
subscriber: WebhookSubscriber,
|
||||
evt: Record<string, unknown>,
|
||||
isDryRun = false
|
||||
): Promise<void> {
|
||||
if (isDryRun) return;
|
||||
|
||||
try {
|
||||
if (!this.tasker) {
|
||||
throw new Error("Tasker not injected - ensure proper DI configuration");
|
||||
}
|
||||
await this.tasker.create(
|
||||
"sendWebhook",
|
||||
JSON.stringify({
|
||||
secretKey: subscriber.secret,
|
||||
triggerEvent: trigger,
|
||||
createdAt: new Date().toISOString(),
|
||||
webhook: subscriber,
|
||||
data: {
|
||||
bookingId: bookingData.id,
|
||||
eventTypeId: bookingData.eventTypeId,
|
||||
userId: bookingData.userId,
|
||||
teamId: bookingData.teamId,
|
||||
responses: bookingData.responses,
|
||||
evt,
|
||||
},
|
||||
}),
|
||||
{ scheduledAt, referenceUid: `booking-${bookingData.id}-${trigger}` }
|
||||
);
|
||||
} catch (error) {
|
||||
this.log.error("Failed to schedule time-based webhook", {
|
||||
trigger,
|
||||
bookingId: bookingData.id,
|
||||
subscriberUrl: subscriber.subscriberUrl,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async cancelScheduledWebhooks(
|
||||
bookingId: number,
|
||||
triggers: WebhookTriggerEvents[] = [
|
||||
WebhookTriggerEvents.MEETING_STARTED,
|
||||
WebhookTriggerEvents.MEETING_ENDED,
|
||||
],
|
||||
isDryRun = false
|
||||
): Promise<void> {
|
||||
if (isDryRun) return;
|
||||
|
||||
try {
|
||||
if (!this.tasker) {
|
||||
throw new Error("Tasker not injected - ensure proper DI configuration");
|
||||
}
|
||||
const cancellationPromises = triggers.map(async (trigger) => {
|
||||
const referenceUid = `booking-${bookingId}-${trigger}`;
|
||||
try {
|
||||
await this.tasker.cancelWithReference(referenceUid, "sendWebhook");
|
||||
return { trigger, success: true };
|
||||
} catch (error) {
|
||||
this.log.warn(`Failed to cancel webhook for trigger ${trigger}:`, {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
return { trigger, success: false, error };
|
||||
}
|
||||
});
|
||||
|
||||
const results = await Promise.allSettled(cancellationPromises);
|
||||
|
||||
// Log any failures for monitoring
|
||||
const failures = results.filter((result) => result.status === "rejected");
|
||||
if (failures.length > 0) {
|
||||
this.log.warn(`Some webhook cancellations failed:`, { failureCount: failures.length });
|
||||
}
|
||||
} catch (error) {
|
||||
this.log.error("Failed to cancel scheduled webhooks", {
|
||||
bookingId,
|
||||
triggers,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async scheduleDelayedWebhooks(
|
||||
trigger: WebhookTriggerEvents,
|
||||
payload: WebhookPayload,
|
||||
scheduledAt: Date,
|
||||
options?: { teamId?: number | number[] | null; orgId?: number | null },
|
||||
subscribers?: WebhookSubscriber[],
|
||||
isDryRun = false
|
||||
): Promise<void> {
|
||||
if (isDryRun) return;
|
||||
|
||||
const webhookSubscribers =
|
||||
subscribers ||
|
||||
(await this.repository.getSubscribers({
|
||||
userId: null,
|
||||
eventTypeId: null,
|
||||
triggerEvent: trigger,
|
||||
teamId: options?.teamId,
|
||||
orgId: options?.orgId,
|
||||
oAuthClientId: undefined,
|
||||
}));
|
||||
|
||||
if (!webhookSubscribers.length) return;
|
||||
|
||||
try {
|
||||
if (!this.tasker) {
|
||||
throw new Error("Tasker not injected - ensure proper DI configuration");
|
||||
}
|
||||
const createdAt = new Date().toISOString();
|
||||
|
||||
await Promise.all(
|
||||
webhookSubscribers.map((subscriber) =>
|
||||
this.tasker.create(
|
||||
"sendWebhook",
|
||||
JSON.stringify({
|
||||
secretKey: subscriber.secret,
|
||||
triggerEvent: trigger,
|
||||
createdAt,
|
||||
webhook: subscriber,
|
||||
data: payload.payload,
|
||||
}),
|
||||
{ scheduledAt }
|
||||
)
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
this.log.error("Failed to schedule delayed webhooks", {
|
||||
trigger,
|
||||
scheduledAt,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
|
||||
/**
|
||||
* Parameter type definitions for webhook service methods
|
||||
*/
|
||||
|
||||
export interface BookingCreatedParams {
|
||||
evt: CalendarEvent;
|
||||
booking: {
|
||||
id: number;
|
||||
eventTypeId: number | null;
|
||||
userId: number | null;
|
||||
startTime?: Date;
|
||||
smsReminderNumber?: string | null;
|
||||
};
|
||||
eventType: {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string | null;
|
||||
requiresConfirmation: boolean;
|
||||
price: number;
|
||||
currency: string;
|
||||
length: number;
|
||||
teamId?: number | null;
|
||||
};
|
||||
status?: "ACCEPTED" | "PENDING";
|
||||
metadata?: Record<string, unknown>;
|
||||
platformParams?: {
|
||||
platformClientId?: string;
|
||||
platformRescheduleUrl?: string;
|
||||
platformCancelUrl?: string;
|
||||
platformBookingUrl?: string;
|
||||
};
|
||||
platformClientId?: string;
|
||||
teamId?: number | null;
|
||||
orgId?: number | null;
|
||||
isDryRun?: boolean;
|
||||
}
|
||||
|
||||
export interface BookingCancelledParams {
|
||||
evt: CalendarEvent;
|
||||
booking: {
|
||||
id: number;
|
||||
eventTypeId: number | null;
|
||||
userId: number | null;
|
||||
smsReminderNumber?: string | null;
|
||||
};
|
||||
eventType: {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string | null;
|
||||
requiresConfirmation: boolean;
|
||||
price: number;
|
||||
currency: string;
|
||||
length: number;
|
||||
teamId?: number | null;
|
||||
};
|
||||
cancelledBy?: string;
|
||||
cancellationReason?: string;
|
||||
teamId?: number | null;
|
||||
orgId?: number | null;
|
||||
platformClientId?: string;
|
||||
isDryRun?: boolean;
|
||||
}
|
||||
|
||||
export interface BookingRequestedParams {
|
||||
evt: CalendarEvent;
|
||||
booking: {
|
||||
id: number;
|
||||
eventTypeId: number | null;
|
||||
userId: number | null;
|
||||
};
|
||||
eventType: {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string | null;
|
||||
requiresConfirmation: boolean;
|
||||
price: number;
|
||||
currency: string;
|
||||
length: number;
|
||||
teamId?: number | null;
|
||||
};
|
||||
teamId?: number | null;
|
||||
orgId?: number | null;
|
||||
platformClientId?: string;
|
||||
isDryRun?: boolean;
|
||||
}
|
||||
|
||||
export interface BookingRescheduledParams {
|
||||
evt: CalendarEvent;
|
||||
booking: {
|
||||
id: number;
|
||||
eventTypeId: number | null;
|
||||
userId: number | null;
|
||||
smsReminderNumber?: string | null;
|
||||
};
|
||||
eventType: {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string | null;
|
||||
requiresConfirmation: boolean;
|
||||
price: number;
|
||||
currency: string;
|
||||
length: number;
|
||||
teamId?: number | null;
|
||||
};
|
||||
rescheduleId?: number;
|
||||
rescheduleUid?: string;
|
||||
rescheduleStartTime?: string;
|
||||
rescheduleEndTime?: string;
|
||||
rescheduledBy?: string;
|
||||
teamId?: number | null;
|
||||
orgId?: number | null;
|
||||
platformClientId?: string;
|
||||
isDryRun?: boolean;
|
||||
}
|
||||
|
||||
export interface BookingPaidParams {
|
||||
evt: CalendarEvent;
|
||||
booking: {
|
||||
id: number;
|
||||
eventTypeId: number | null;
|
||||
userId: number | null;
|
||||
};
|
||||
eventType: {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string | null;
|
||||
requiresConfirmation: boolean;
|
||||
price: number;
|
||||
currency: string;
|
||||
length: number;
|
||||
teamId?: number | null;
|
||||
};
|
||||
paymentId?: number;
|
||||
paymentData?: Record<string, unknown>;
|
||||
teamId?: number | null;
|
||||
orgId?: number | null;
|
||||
platformClientId?: string;
|
||||
isDryRun?: boolean;
|
||||
}
|
||||
|
||||
export interface BookingPaymentInitiatedParams {
|
||||
evt: CalendarEvent;
|
||||
booking: {
|
||||
id: number;
|
||||
eventTypeId: number | null;
|
||||
userId: number | null;
|
||||
};
|
||||
eventType: {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string | null;
|
||||
requiresConfirmation: boolean;
|
||||
price: number;
|
||||
currency: string;
|
||||
length: number;
|
||||
teamId?: number | null;
|
||||
};
|
||||
paymentId?: number;
|
||||
paymentData?: Record<string, unknown>;
|
||||
teamId?: number | null;
|
||||
orgId?: number | null;
|
||||
platformClientId?: string;
|
||||
isDryRun?: boolean;
|
||||
}
|
||||
|
||||
export interface BookingNoShowParams {
|
||||
message: string;
|
||||
bookingUid: string;
|
||||
bookingId?: number;
|
||||
attendees: { email: string; noShow: boolean }[];
|
||||
userId?: number | null;
|
||||
eventTypeId?: number | null;
|
||||
teamId?: number | null;
|
||||
orgId?: number | null;
|
||||
platformClientId?: string;
|
||||
isDryRun?: boolean;
|
||||
}
|
||||
|
||||
export interface BookingRejectedParams {
|
||||
evt: CalendarEvent;
|
||||
booking: {
|
||||
id: number;
|
||||
eventTypeId: number | null;
|
||||
userId: number | null;
|
||||
smsReminderNumber?: string | null;
|
||||
};
|
||||
eventType: {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string | null;
|
||||
requiresConfirmation: boolean;
|
||||
price: number;
|
||||
currency: string;
|
||||
length: number;
|
||||
teamId?: number | null;
|
||||
};
|
||||
teamId?: number | null;
|
||||
orgId?: number | null;
|
||||
platformClientId?: string;
|
||||
isDryRun?: boolean;
|
||||
}
|
||||
|
||||
export interface ScheduleMeetingWebhooksParams {
|
||||
booking: {
|
||||
id: number;
|
||||
uid: string;
|
||||
eventTypeId: number | null;
|
||||
userId: number | null;
|
||||
startTime: Date;
|
||||
endTime: Date;
|
||||
responses?: Record<string, unknown>;
|
||||
};
|
||||
evt: CalendarEvent;
|
||||
teamId?: number | null;
|
||||
orgId?: number | null;
|
||||
oAuthClientId?: string;
|
||||
isDryRun?: boolean;
|
||||
}
|
||||
|
||||
export interface CancelScheduledMeetingWebhooksParams {
|
||||
bookingId: number;
|
||||
isDryRun?: boolean;
|
||||
}
|
||||
|
||||
export interface ScheduleNoShowWebhooksParams {
|
||||
booking: {
|
||||
id: number;
|
||||
startTime: Date;
|
||||
eventTypeId: number | null;
|
||||
userId: number | null;
|
||||
uid: string;
|
||||
};
|
||||
organizerUser: {
|
||||
id: number;
|
||||
username: string | null;
|
||||
};
|
||||
eventType?: {
|
||||
teamId?: number | null;
|
||||
};
|
||||
teamId?: number | null;
|
||||
orgId?: number | null;
|
||||
oAuthClientId?: string;
|
||||
triggerForUser?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { createModule } from "@evyweb/ioctopus";
|
||||
|
||||
import type { ILogger } from "@calcom/features/webhooks/lib/interface/infrastructure";
|
||||
|
||||
import { SHARED_TOKENS } from "../shared.tokens";
|
||||
|
||||
export const loggerServiceModule = createModule();
|
||||
|
||||
// Bind logger with proper factory that respects IoC
|
||||
loggerServiceModule.bind(SHARED_TOKENS.LOGGER).toFactory(async (): Promise<ILogger> => {
|
||||
const loggerModule = await import("@calcom/lib/logger");
|
||||
return loggerModule.default;
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import { createModule } from "@evyweb/ioctopus";
|
||||
|
||||
import type { ITasker } from "@calcom/features/webhooks/lib/interface/infrastructure";
|
||||
|
||||
import { SHARED_TOKENS } from "../shared.tokens";
|
||||
|
||||
export const taskerServiceModule = createModule();
|
||||
|
||||
// Bind tasker with proper async factory that respects IoC
|
||||
taskerServiceModule.bind(SHARED_TOKENS.TASKER).toFactory(async (): Promise<ITasker> => {
|
||||
const taskerModule = await import("@calcom/features/tasker");
|
||||
return taskerModule.default;
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
export const SHARED_TOKENS = {
|
||||
// Infrastructure services
|
||||
TASKER: Symbol("ITasker"),
|
||||
LOGGER: Symbol("ILogger"),
|
||||
} as const;
|
||||
@@ -0,0 +1,21 @@
|
||||
export const WEBHOOK_TOKENS = {
|
||||
// Core interfaces
|
||||
WEBHOOK_SERVICE: Symbol("IWebhookService"),
|
||||
BOOKING_WEBHOOK_SERVICE: Symbol("IBookingWebhookService"),
|
||||
FORM_WEBHOOK_SERVICE: Symbol("IFormWebhookService"),
|
||||
RECORDING_WEBHOOK_SERVICE: Symbol("IRecordingWebhookService"),
|
||||
WEBHOOK_NOTIFIER: Symbol("IWebhookNotifier"),
|
||||
OOO_WEBHOOK_SERVICE: Symbol("OOO_WEBHOOK_SERVICE"),
|
||||
WEBHOOK_NOTIFICATION_HANDLER: Symbol("WebhookNotificationHandler"),
|
||||
|
||||
// Payload builders
|
||||
BOOKING_PAYLOAD_BUILDER: Symbol("BookingPayloadBuilder"),
|
||||
FORM_PAYLOAD_BUILDER: Symbol("FormPayloadBuilder"),
|
||||
OOO_PAYLOAD_BUILDER: Symbol("OOOPayloadBuilder"),
|
||||
RECORDING_PAYLOAD_BUILDER: Symbol("RecordingPayloadBuilder"),
|
||||
MEETING_PAYLOAD_BUILDER: Symbol("MeetingPayloadBuilder"),
|
||||
INSTANT_MEETING_BUILDER: Symbol("InstantMeetingBuilder"),
|
||||
|
||||
// Repositories
|
||||
WEBHOOK_REPOSITORY: Symbol("IWebhookRepository"),
|
||||
} as const;
|
||||
@@ -0,0 +1,46 @@
|
||||
import { createContainer } from "@evyweb/ioctopus";
|
||||
|
||||
import { loggerServiceModule } from "../../shared/services/logger.service";
|
||||
import { taskerServiceModule } from "../../shared/services/tasker.service";
|
||||
import { SHARED_TOKENS } from "../../shared/shared.tokens";
|
||||
import { WEBHOOK_TOKENS } from "../Webhooks.tokens";
|
||||
import { webhookModule } from "../modules/Webhook.module";
|
||||
|
||||
export const webhookContainer = createContainer();
|
||||
|
||||
// Load shared infrastructure
|
||||
webhookContainer.load(SHARED_TOKENS.LOGGER, loggerServiceModule);
|
||||
webhookContainer.load(SHARED_TOKENS.TASKER, taskerServiceModule);
|
||||
|
||||
// Load webhook module
|
||||
webhookContainer.load(WEBHOOK_TOKENS.WEBHOOK_REPOSITORY, webhookModule);
|
||||
webhookContainer.load(WEBHOOK_TOKENS.WEBHOOK_SERVICE, webhookModule);
|
||||
webhookContainer.load(WEBHOOK_TOKENS.BOOKING_WEBHOOK_SERVICE, webhookModule);
|
||||
webhookContainer.load(WEBHOOK_TOKENS.FORM_WEBHOOK_SERVICE, webhookModule);
|
||||
webhookContainer.load(WEBHOOK_TOKENS.RECORDING_WEBHOOK_SERVICE, webhookModule);
|
||||
webhookContainer.load(WEBHOOK_TOKENS.OOO_WEBHOOK_SERVICE, webhookModule);
|
||||
webhookContainer.load(WEBHOOK_TOKENS.BOOKING_PAYLOAD_BUILDER, webhookModule);
|
||||
webhookContainer.load(WEBHOOK_TOKENS.FORM_PAYLOAD_BUILDER, webhookModule);
|
||||
webhookContainer.load(WEBHOOK_TOKENS.OOO_PAYLOAD_BUILDER, webhookModule);
|
||||
webhookContainer.load(WEBHOOK_TOKENS.RECORDING_PAYLOAD_BUILDER, webhookModule);
|
||||
webhookContainer.load(WEBHOOK_TOKENS.MEETING_PAYLOAD_BUILDER, webhookModule);
|
||||
webhookContainer.load(WEBHOOK_TOKENS.INSTANT_MEETING_BUILDER, webhookModule);
|
||||
webhookContainer.load(WEBHOOK_TOKENS.WEBHOOK_NOTIFICATION_HANDLER, webhookModule);
|
||||
webhookContainer.load(WEBHOOK_TOKENS.WEBHOOK_NOTIFIER, webhookModule);
|
||||
|
||||
// Service getters
|
||||
export function getBookingWebhookService() {
|
||||
return webhookContainer.get(WEBHOOK_TOKENS.BOOKING_WEBHOOK_SERVICE);
|
||||
}
|
||||
|
||||
export function getFormWebhookService() {
|
||||
return webhookContainer.get(WEBHOOK_TOKENS.FORM_WEBHOOK_SERVICE);
|
||||
}
|
||||
|
||||
export function getRecordingWebhookService() {
|
||||
return webhookContainer.get(WEBHOOK_TOKENS.RECORDING_WEBHOOK_SERVICE);
|
||||
}
|
||||
|
||||
export function getWebhookNotifier() {
|
||||
return webhookContainer.get(WEBHOOK_TOKENS.WEBHOOK_NOTIFIER);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { createModule } from "@evyweb/ioctopus";
|
||||
|
||||
import { BookingPayloadBuilder } from "@calcom/features/webhooks/lib/factory/BookingPayloadBuilder";
|
||||
import { FormPayloadBuilder } from "@calcom/features/webhooks/lib/factory/FormPayloadBuilder";
|
||||
import { InstantMeetingBuilder } from "@calcom/features/webhooks/lib/factory/InstantMeetingBuilder";
|
||||
import { MeetingPayloadBuilder } from "@calcom/features/webhooks/lib/factory/MeetingPayloadBuilder";
|
||||
import { OOOPayloadBuilder } from "@calcom/features/webhooks/lib/factory/OOOPayloadBuilder";
|
||||
import { RecordingPayloadBuilder } from "@calcom/features/webhooks/lib/factory/RecordingPayloadBuilder";
|
||||
import { WebhookRepository } from "@calcom/features/webhooks/lib/repository/WebhookRepository";
|
||||
import { BookingWebhookService } from "@calcom/features/webhooks/lib/service/BookingWebhookService";
|
||||
import { FormWebhookService } from "@calcom/features/webhooks/lib/service/FormWebhookService";
|
||||
import { OOOWebhookService } from "@calcom/features/webhooks/lib/service/OOOWebhookService";
|
||||
import { RecordingWebhookService } from "@calcom/features/webhooks/lib/service/RecordingWebhookService";
|
||||
import { WebhookNotificationHandler } from "@calcom/features/webhooks/lib/service/WebhookNotificationHandler";
|
||||
import { WebhookNotifier } from "@calcom/features/webhooks/lib/service/WebhookNotifier";
|
||||
import { WebhookService } from "@calcom/features/webhooks/lib/service/WebhookService";
|
||||
|
||||
import { SHARED_TOKENS } from "../../shared/shared.tokens";
|
||||
import { WEBHOOK_TOKENS } from "../Webhooks.tokens";
|
||||
|
||||
export const webhookModule = createModule();
|
||||
|
||||
// Bind repository
|
||||
webhookModule.bind(WEBHOOK_TOKENS.WEBHOOK_REPOSITORY).toClass(WebhookRepository);
|
||||
|
||||
// Bind services
|
||||
webhookModule
|
||||
.bind(WEBHOOK_TOKENS.WEBHOOK_SERVICE)
|
||||
.toClass(WebhookService, [WEBHOOK_TOKENS.WEBHOOK_REPOSITORY, SHARED_TOKENS.TASKER, SHARED_TOKENS.LOGGER]);
|
||||
|
||||
webhookModule
|
||||
.bind(WEBHOOK_TOKENS.BOOKING_WEBHOOK_SERVICE)
|
||||
.toClass(BookingWebhookService, [
|
||||
WEBHOOK_TOKENS.WEBHOOK_NOTIFIER,
|
||||
WEBHOOK_TOKENS.WEBHOOK_SERVICE,
|
||||
SHARED_TOKENS.TASKER,
|
||||
SHARED_TOKENS.LOGGER,
|
||||
]);
|
||||
|
||||
webhookModule
|
||||
.bind(WEBHOOK_TOKENS.FORM_WEBHOOK_SERVICE)
|
||||
.toClass(FormWebhookService, [WEBHOOK_TOKENS.WEBHOOK_NOTIFIER, WEBHOOK_TOKENS.WEBHOOK_SERVICE]);
|
||||
|
||||
webhookModule
|
||||
.bind(WEBHOOK_TOKENS.RECORDING_WEBHOOK_SERVICE)
|
||||
.toClass(RecordingWebhookService, [WEBHOOK_TOKENS.WEBHOOK_NOTIFIER]);
|
||||
|
||||
webhookModule
|
||||
.bind(WEBHOOK_TOKENS.OOO_WEBHOOK_SERVICE)
|
||||
.toClass(OOOWebhookService, [
|
||||
WEBHOOK_TOKENS.WEBHOOK_NOTIFIER,
|
||||
WEBHOOK_TOKENS.WEBHOOK_REPOSITORY,
|
||||
SHARED_TOKENS.TASKER,
|
||||
SHARED_TOKENS.LOGGER,
|
||||
]);
|
||||
|
||||
// Bind payload builders
|
||||
webhookModule.bind(WEBHOOK_TOKENS.BOOKING_PAYLOAD_BUILDER).toClass(BookingPayloadBuilder);
|
||||
webhookModule.bind(WEBHOOK_TOKENS.FORM_PAYLOAD_BUILDER).toClass(FormPayloadBuilder);
|
||||
webhookModule.bind(WEBHOOK_TOKENS.OOO_PAYLOAD_BUILDER).toClass(OOOPayloadBuilder);
|
||||
webhookModule.bind(WEBHOOK_TOKENS.RECORDING_PAYLOAD_BUILDER).toClass(RecordingPayloadBuilder);
|
||||
webhookModule.bind(WEBHOOK_TOKENS.MEETING_PAYLOAD_BUILDER).toClass(MeetingPayloadBuilder);
|
||||
webhookModule.bind(WEBHOOK_TOKENS.INSTANT_MEETING_BUILDER).toClass(InstantMeetingBuilder);
|
||||
|
||||
// Bind notification handler
|
||||
webhookModule
|
||||
.bind(WEBHOOK_TOKENS.WEBHOOK_NOTIFICATION_HANDLER)
|
||||
.toClass(WebhookNotificationHandler, [
|
||||
WEBHOOK_TOKENS.WEBHOOK_SERVICE,
|
||||
WEBHOOK_TOKENS.BOOKING_PAYLOAD_BUILDER,
|
||||
WEBHOOK_TOKENS.FORM_PAYLOAD_BUILDER,
|
||||
WEBHOOK_TOKENS.OOO_PAYLOAD_BUILDER,
|
||||
WEBHOOK_TOKENS.RECORDING_PAYLOAD_BUILDER,
|
||||
WEBHOOK_TOKENS.MEETING_PAYLOAD_BUILDER,
|
||||
WEBHOOK_TOKENS.INSTANT_MEETING_BUILDER,
|
||||
SHARED_TOKENS.LOGGER,
|
||||
]);
|
||||
|
||||
// Bind notifier
|
||||
webhookModule
|
||||
.bind(WEBHOOK_TOKENS.WEBHOOK_NOTIFIER)
|
||||
.toClass(WebhookNotifier, [WEBHOOK_TOKENS.WEBHOOK_NOTIFICATION_HANDLER, SHARED_TOKENS.LOGGER]);
|
||||
@@ -0,0 +1,9 @@
|
||||
import { createModule } from "@evyweb/ioctopus";
|
||||
|
||||
import { WebhookRepository } from "@calcom/features/webhooks/lib/repository/WebhookRepository";
|
||||
|
||||
import { WEBHOOK_TOKENS } from "../webhooks.tokens";
|
||||
|
||||
export const webhookRepositoryModule = createModule();
|
||||
|
||||
webhookRepositoryModule.bind(WEBHOOK_TOKENS.WEBHOOK_REPOSITORY).toClass(WebhookRepository);
|
||||
@@ -0,0 +1,42 @@
|
||||
import { createModule } from "@evyweb/ioctopus";
|
||||
|
||||
import { BookingWebhookService } from "@calcom/features/webhooks/lib/service/BookingWebhookService";
|
||||
import { FormWebhookService } from "@calcom/features/webhooks/lib/service/FormWebhookService";
|
||||
import { OOOWebhookService } from "@calcom/features/webhooks/lib/service/OOOWebhookService";
|
||||
import { RecordingWebhookService } from "@calcom/features/webhooks/lib/service/RecordingWebhookService";
|
||||
import { WebhookService } from "@calcom/features/webhooks/lib/service/WebhookService";
|
||||
|
||||
import { SHARED_TOKENS } from "../../shared/shared.tokens";
|
||||
import { WEBHOOK_TOKENS } from "../webhooks.tokens";
|
||||
|
||||
export const webhookServicesModule = createModule();
|
||||
|
||||
webhookServicesModule
|
||||
.bind(WEBHOOK_TOKENS.WEBHOOK_SERVICE)
|
||||
.toClass(WebhookService, [WEBHOOK_TOKENS.WEBHOOK_REPOSITORY, SHARED_TOKENS.TASKER, SHARED_TOKENS.LOGGER]);
|
||||
|
||||
webhookServicesModule
|
||||
.bind(WEBHOOK_TOKENS.BOOKING_WEBHOOK_SERVICE)
|
||||
.toClass(BookingWebhookService, [
|
||||
WEBHOOK_TOKENS.WEBHOOK_NOTIFIER,
|
||||
WEBHOOK_TOKENS.WEBHOOK_SERVICE,
|
||||
SHARED_TOKENS.TASKER,
|
||||
SHARED_TOKENS.LOGGER,
|
||||
]);
|
||||
|
||||
webhookServicesModule
|
||||
.bind(WEBHOOK_TOKENS.FORM_WEBHOOK_SERVICE)
|
||||
.toClass(FormWebhookService, [WEBHOOK_TOKENS.WEBHOOK_NOTIFIER, WEBHOOK_TOKENS.WEBHOOK_SERVICE]);
|
||||
|
||||
webhookServicesModule
|
||||
.bind(WEBHOOK_TOKENS.RECORDING_WEBHOOK_SERVICE)
|
||||
.toClass(RecordingWebhookService, [WEBHOOK_TOKENS.WEBHOOK_NOTIFIER]);
|
||||
|
||||
webhookServicesModule
|
||||
.bind(WEBHOOK_TOKENS.OOO_WEBHOOK_SERVICE)
|
||||
.toClass(OOOWebhookService, [
|
||||
WEBHOOK_TOKENS.WEBHOOK_NOTIFIER,
|
||||
WEBHOOK_TOKENS.WEBHOOK_REPOSITORY,
|
||||
SHARED_TOKENS.TASKER,
|
||||
SHARED_TOKENS.LOGGER,
|
||||
]);
|
||||
Reference in New Issue
Block a user