diff --git a/apps/web/modules/settings/platform/oauth-clients/[clientId]/edit/edit-webhooks-view.tsx b/apps/web/modules/settings/platform/oauth-clients/[clientId]/edit/edit-webhooks-view.tsx index 35583707afdec4..82b2c60b79c4d2 100644 --- a/apps/web/modules/settings/platform/oauth-clients/[clientId]/edit/edit-webhooks-view.tsx +++ b/apps/web/modules/settings/platform/oauth-clients/[clientId]/edit/edit-webhooks-view.tsx @@ -77,6 +77,14 @@ export default function EditOAuthClientWebhooks() { value: WebhookTriggerEvents.AFTER_GUESTS_CAL_VIDEO_NO_SHOW, label: "after_guests_cal_video_no_show", }, + { + value: WebhookTriggerEvents.AFTER_HOSTS_GOOGLE_MEET_NO_SHOW, + label: "after_hosts_google_meet_no_show", + }, + { + value: WebhookTriggerEvents.AFTER_GUESTS_GOOGLE_MEET_NO_SHOW, + label: "after_guests_google_meet_no_show", + }, ]} onSubmit={async (data) => { try { diff --git a/apps/web/package.json b/apps/web/package.json index 2760d95a57df63..2fbbe9e03da215 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -146,6 +146,7 @@ "@babel/core": "^7.19.6", "@calcom/config": "*", "@calcom/types": "*", + "@google-apps/meet": "^0.3.0", "@microsoft/microsoft-graph-types-beta": "0.15.0-preview", "@playwright/test": "^1.45.3", "@testing-library/react": "^13.3.0", @@ -177,6 +178,7 @@ "deasync": "^0.1.30", "detect-port": "^1.3.0", "env-cmd": "^10.1.0", + "google-auth-library": "^9.14.2", "module-alias": "^2.2.2", "msw": "^0.42.3", "node-html-parser": "^6.1.10", diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 849be281595f14..b56020fcf4eafe 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -1234,6 +1234,8 @@ "event_cancelled_trigger": "when event is canceled", "after_hosts_cal_video_no_show": "After hosts don't join cal video", "after_guests_cal_video_no_show": "After guests don't join cal video", + "after_hosts_google_meet_no_show": "After hosts don't join google meet", + "after_guests_google_meet_no_show": "After guests don't join google meet", "new_event_trigger": "when new event is booked", "email_host_action": "send email to host", "email_attendee_action": "send email to attendees", diff --git a/packages/app-store/googlecalendar/lib/CalendarService.ts b/packages/app-store/googlecalendar/lib/CalendarService.ts index 20ed7ce00f7141..cf86c18580af5e 100644 --- a/packages/app-store/googlecalendar/lib/CalendarService.ts +++ b/packages/app-store/googlecalendar/lib/CalendarService.ts @@ -1,5 +1,8 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import type { protos } from "@google-apps/meet"; +import { ConferenceRecordsServiceClient, SpacesServiceClient } from "@google-apps/meet"; import type { Prisma } from "@prisma/client"; +import { GoogleAuth, OAuth2Client } from "google-auth-library"; import type { calendar_v3 } from "googleapis"; import { google } from "googleapis"; import { RRule } from "rrule"; @@ -42,6 +45,10 @@ interface GoogleCalError extends Error { code?: number; } +export interface ParticipantWithEmail extends protos.google.apps.meet.v2.IParticipant { + email?: string; +} + const ONE_MINUTE_MS = 60 * 1000; const CACHING_TIME = ONE_MINUTE_MS; @@ -334,6 +341,7 @@ export default class GoogleCalendarService implements Calendar { conferenceDataVersion: 1, sendUpdates: "none", }); + event = eventResponse.data; if (event.recurrence) { if (event.recurrence.length > 0) { @@ -663,6 +671,86 @@ export default class GoogleCalendarService implements Calendar { } } + async getMeetParticipants(videoCallUrl: string | null): Promise { + const { token } = await this.oAuthManagerInstance.getTokenObjectOrFetch(); + if (!token) { + throw new Error("Invalid grant for Google Calendar app"); + } + + const googleAuth = new GoogleAuth({ + authClient: new OAuth2Client({ + credentials: { + access_token: token.access_token, + }, + }), + }); + + const meetClient = new ConferenceRecordsServiceClient({ + auth: googleAuth as ConferenceRecordsServiceClient["auth"], + }); + + const spacesClient = new SpacesServiceClient({ + auth: googleAuth as SpacesServiceClient["auth"], + }); + + const meetingCode = videoCallUrl ? new URL(videoCallUrl).pathname.split("/").pop() : null; + + const spaceInfo = await spacesClient.getSpace({ name: `spaces/${meetingCode}` }); + const spaceName = spaceInfo[0].name; + + const conferenceRecords = []; + for await (const response of meetClient.listConferenceRecordsAsync()) { + if (response.space === spaceName) { + conferenceRecords.push(response); + } + } + + const participantsByConferenceRecord = await Promise.all( + conferenceRecords.map(async (conferenceRecord) => { + const participants = []; + for await (const participant of meetClient.listParticipantsAsync({ parent: conferenceRecord.name })) { + participants.push(participant); + } + return participants; + }) + ); + + const participantsWithEmails = await Promise.all( + participantsByConferenceRecord.map(async (participants) => { + return Promise.all( + participants.map(async (participant) => { + try { + const response = await fetch( + `https://people.googleapis.com/v1/people/${ + participant.signedinUser?.user?.split("/")[1] + }?personFields=emailAddresses`, + { + headers: { + Authorization: `Bearer ${token.access_token}`, + Accept: "application/json", + }, + } + ); + + const data = await response.json(); + const emailAddresses = data.emailAddresses; + + return { + ...participant, + email: emailAddresses ? emailAddresses[0].value : undefined, + }; + } catch (err) { + console.error("Error fetching email for participant:", err); + return participant; + } + }) + ); + }) + ); + + return participantsWithEmails; + } + async listCalendars(): Promise { this.log.debug("Listing calendars"); const calendar = await this.authedCalendar(); @@ -693,7 +781,7 @@ export default class GoogleCalendarService implements Calendar { } } -class MyGoogleAuth extends google.auth.OAuth2 { +export class MyGoogleAuth extends google.auth.OAuth2 { constructor(client_id: string, client_secret: string, redirect_uri: string) { super(client_id, client_secret, redirect_uri); } diff --git a/packages/app-store/googlecalendar/lib/constants.ts b/packages/app-store/googlecalendar/lib/constants.ts index 5c285ce803bd88..5fc6bce0dd57d4 100644 --- a/packages/app-store/googlecalendar/lib/constants.ts +++ b/packages/app-store/googlecalendar/lib/constants.ts @@ -1,7 +1,18 @@ export const SCOPE_USERINFO_PROFILE = "https://www.googleapis.com/auth/userinfo.profile"; export const SCOPE_CALENDAR_READONLY = "https://www.googleapis.com/auth/calendar.readonly"; export const SCOPE_CALENDAR_EVENT = "https://www.googleapis.com/auth/calendar.events"; +export const GOOGLE_MEET_API = "https://www.googleapis.com/auth/meetings.space.readonly"; +export const SCOPE_USERINFO_EMAIL = "https://www.googleapis.com/auth/userinfo.email"; -export const REQUIRED_SCOPES = [SCOPE_CALENDAR_READONLY, SCOPE_CALENDAR_EVENT]; +export const SCOPE_USER_CONTACTS = "https://www.googleapis.com/auth/contacts.readonly"; +export const SCOPE_OTHER_USER_CONTACTS = "https://www.googleapis.com/auth/contacts.other.readonly"; -export const SCOPES = [...REQUIRED_SCOPES, SCOPE_USERINFO_PROFILE]; +export const REQUIRED_SCOPES = [SCOPE_CALENDAR_READONLY, SCOPE_CALENDAR_EVENT, GOOGLE_MEET_API]; + +export const SCOPES = [ + ...REQUIRED_SCOPES, + SCOPE_USERINFO_EMAIL, + SCOPE_USERINFO_PROFILE, + SCOPE_USER_CONTACTS, + SCOPE_OTHER_USER_CONTACTS, +]; diff --git a/packages/features/bookings/lib/handleConfirmation.ts b/packages/features/bookings/lib/handleConfirmation.ts index eb949e11a8131d..ec725b734c4764 100644 --- a/packages/features/bookings/lib/handleConfirmation.ts +++ b/packages/features/bookings/lib/handleConfirmation.ts @@ -40,6 +40,7 @@ export async function handleConfirmation(args: { booking: { startTime: Date; id: number; + location: string | null; eventType: { currency: string; description: string | null; @@ -401,12 +402,14 @@ export async function handleConfirmation(args: { booking: { startTime: booking.startTime, id: booking.id, + location: booking.location, }, triggerForUser, organizerUser: { id: booking.userId }, eventTypeId: booking.eventTypeId, teamId, orgId, + destinationCalendars: evt.destinationCalendar, }); const eventTypeInfo: EventTypeInfo = { diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index 22af828b4e173a..c7d619e267b1eb 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -12,7 +12,6 @@ import { OrganizerDefaultConferencingAppType, getLocationValueForDB, } from "@calcom/app-store/locations"; -import { DailyLocationType } from "@calcom/app-store/locations"; import { getAppFromSlug } from "@calcom/app-store/utils"; import EventManager from "@calcom/core/EventManager"; import { getEventName } from "@calcom/core/event"; @@ -1249,6 +1248,7 @@ async function handler( } else if (isConfirmedByDefault) { // Use EventManager to conditionally use all needed integrations. const createManager = await eventManager.create(evt); + console.log("createManager", JSON.stringify(createManager)); if (evt.location) { booking.location = evt.location; } @@ -1647,17 +1647,19 @@ async function handler( } try { - if (isConfirmedByDefault && (booking.location === DailyLocationType || booking.location?.trim() === "")) { + if (isConfirmedByDefault) { await scheduleNoShowTriggers({ - booking: { startTime: booking.startTime, id: booking.id }, + booking: { startTime: booking.startTime, id: booking.id, location: booking.location }, triggerForUser, organizerUser: { id: organizerUser.id }, eventTypeId, teamId, orgId, + destinationCalendars: evt.destinationCalendar, }); } } catch (error) { + console.log("error", error); loggerWithEventDetails.error("Error while scheduling no show triggers", JSON.stringify({ error })); } diff --git a/packages/features/bookings/lib/handleNewBooking/scheduleNoShowTriggers.ts b/packages/features/bookings/lib/handleNewBooking/scheduleNoShowTriggers.ts index 49906aef079279..7555b8aceb1a7a 100644 --- a/packages/features/bookings/lib/handleNewBooking/scheduleNoShowTriggers.ts +++ b/packages/features/bookings/lib/handleNewBooking/scheduleNoShowTriggers.ts @@ -1,3 +1,6 @@ +import type { DestinationCalendar } from "@prisma/client"; + +import { DailyLocationType } from "@calcom/app-store/locations"; import dayjs from "@calcom/dayjs"; import tasker from "@calcom/features/tasker"; import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks"; @@ -7,28 +10,57 @@ type ScheduleNoShowTriggersArgs = { booking: { startTime: Date; id: number; + location: string | null; }; triggerForUser?: number | true | null; organizerUser: { id: number | null }; eventTypeId: number | null; teamId?: number | null; orgId?: number | null; + destinationCalendars?: DestinationCalendar[] | null; }; export const scheduleNoShowTriggers = async (args: ScheduleNoShowTriggersArgs) => { - const { booking, triggerForUser, organizerUser, eventTypeId, teamId, orgId } = args; + const { booking, triggerForUser, organizerUser, eventTypeId, teamId, orgId, destinationCalendars } = args; + + const isDailyVideoLocation = booking.location === DailyLocationType || booking.location?.trim() === ""; + const isGoogleMeetLocation = booking.location === "integrations:google:meet"; + + if (!isGoogleMeetLocation && !isDailyVideoLocation) return; - // Add task for automatic no show in cal video const noShowPromises: Promise[] = []; - const subscribersHostsNoShowStarted = await getWebhooks({ + const hostNoShowTriggerEvent = isDailyVideoLocation + ? WebhookTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW + : WebhookTriggerEvents.AFTER_HOSTS_GOOGLE_MEET_NO_SHOW; + + const guestNoShowTriggerEvent = isDailyVideoLocation + ? WebhookTriggerEvents.AFTER_GUESTS_CAL_VIDEO_NO_SHOW + : WebhookTriggerEvents.AFTER_GUESTS_GOOGLE_MEET_NO_SHOW; + + const subscribersHostsNoShowStartedPromises = getWebhooks({ + userId: triggerForUser ? organizerUser.id : null, + eventTypeId, + triggerEvent: hostNoShowTriggerEvent, + teamId, + orgId, + }); + const subscribersGuestsNoShowStartedPromises = getWebhooks({ userId: triggerForUser ? organizerUser.id : null, eventTypeId, - triggerEvent: WebhookTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW, + triggerEvent: guestNoShowTriggerEvent, teamId, orgId, }); + const [subscribersHostsNoShowStarted, subscribersGuestsNoShowStarted] = await Promise.all([ + subscribersHostsNoShowStartedPromises, + subscribersGuestsNoShowStartedPromises, + ]); + + // TODO: is this correct? + const destinationCalendar = destinationCalendars?.find((cal) => cal.userId === organizerUser.id); + noShowPromises.push( ...subscribersHostsNoShowStarted.map((webhook) => { if (booking?.startTime && webhook.time && webhook.timeUnit) { @@ -38,10 +70,11 @@ export const scheduleNoShowTriggers = async (args: ScheduleNoShowTriggersArgs) = return tasker.create( "triggerHostNoShowWebhook", { - triggerEvent: WebhookTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW, + triggerEvent: hostNoShowTriggerEvent, bookingId: booking.id, // Prevents null values from being serialized webhook: { ...webhook, time: webhook.time, timeUnit: webhook.timeUnit }, + destinationCalendar, }, { scheduledAt } ); @@ -50,14 +83,6 @@ export const scheduleNoShowTriggers = async (args: ScheduleNoShowTriggersArgs) = }) ); - const subscribersGuestsNoShowStarted = await getWebhooks({ - userId: triggerForUser ? organizerUser.id : null, - eventTypeId, - triggerEvent: WebhookTriggerEvents.AFTER_GUESTS_CAL_VIDEO_NO_SHOW, - teamId, - orgId, - }); - noShowPromises.push( ...subscribersGuestsNoShowStarted.map((webhook) => { if (booking?.startTime && webhook.time && webhook.timeUnit) { @@ -68,10 +93,11 @@ export const scheduleNoShowTriggers = async (args: ScheduleNoShowTriggersArgs) = return tasker.create( "triggerGuestNoShowWebhook", { - triggerEvent: WebhookTriggerEvents.AFTER_GUESTS_CAL_VIDEO_NO_SHOW, + triggerEvent: guestNoShowTriggerEvent, bookingId: booking.id, // Prevents null values from being serialized webhook: { ...webhook, time: webhook.time, timeUnit: webhook.timeUnit }, + destinationCalendar, }, { scheduledAt } ); diff --git a/packages/features/tasker/tasks/triggerNoShow/common.ts b/packages/features/tasker/tasks/triggerNoShow/common.ts index 1bf8fc0c372b34..f07d4dfb5bf29f 100644 --- a/packages/features/tasker/tasks/triggerNoShow/common.ts +++ b/packages/features/tasker/tasks/triggerNoShow/common.ts @@ -1,13 +1,19 @@ +import type { protos } from "@google-apps/meet"; + +import { getCalendar } from "@calcom/app-store/_utils/getCalendar"; +import { DailyLocationType } from "@calcom/app-store/locations"; import dayjs from "@calcom/dayjs"; import { sendGenericWebhookPayload } from "@calcom/features/webhooks/lib/sendPayload"; import logger from "@calcom/lib/logger"; import { safeStringify } from "@calcom/lib/safeStringify"; +import { prisma } from "@calcom/prisma"; import type { TimeUnit } from "@calcom/prisma/enums"; import { BookingStatus, WebhookTriggerEvents } from "@calcom/prisma/enums"; +import { bookingMetadataSchema } from "@calcom/prisma/zod-utils"; import { getBooking } from "./getBooking"; import { getMeetingSessionsFromRoomName } from "./getMeetingSessionsFromRoomName"; -import type { TWebhook, TTriggerNoShowPayloadSchema } from "./schema"; +import type { TWebhook, TTriggerNoShowPayloadSchema, TSendNoShowWebhookPayloadSchema } from "./schema"; import { ZSendNoShowWebhookPayloadSchema } from "./schema"; export type Host = { @@ -15,9 +21,87 @@ export type Host = { email: string; }; +interface ParticipantWithEmail extends protos.google.apps.meet.v2.IParticipant { + email?: string; +} + export type Booking = Awaited>; type Webhook = TWebhook; export type Participants = TTriggerNoShowPayloadSchema["data"][number]["participants"]; +export type DestinationCalendar = TSendNoShowWebhookPayloadSchema["destinationCalendar"]; + +const getGoogleCalendarCredential = async (destinationCalendar: DestinationCalendar) => { + if (destinationCalendar?.credentialId) { + const credential = await prisma.credential.findUnique({ + where: { + id: destinationCalendar.credentialId, + }, + select: { + id: true, + type: true, + key: true, + userId: true, + teamId: true, + subscriptionId: true, + billingCycleStart: true, + appId: true, + paymentStatus: true, + invalid: true, + user: { + select: { + email: true, + }, + }, + }, + }); + + if (credential) { + return credential; + } + } + + // If for some reason credentialId is deleted, we find the destinationCalendar by userId and externalId + const destinationCalendars = await prisma.destinationCalendar.findMany({ + where: { + integration: "google_calendar", + userId: destinationCalendar?.userId, + externalId: destinationCalendar?.externalId, + }, + select: { + credentialId: true, + }, + }); + + if (!destinationCalendars.length) { + return null; + } + + if (!destinationCalendars?.[0]?.credentialId) { + return null; + } + + const newCredential = await prisma.credential.findUnique({ + where: { + id: destinationCalendars[0].credentialId, + }, + select: { + id: true, + type: true, + key: true, + userId: true, + teamId: true, + appId: true, + invalid: true, + user: { + select: { + email: true, + }, + }, + }, + }); + + return newCredential; +}; export function getHosts(booking: Booking): Host[] { const hostMap = new Map(); @@ -105,13 +189,16 @@ export const prepareNoShowTrigger = async ( hostsThatDidntJoinTheCall: Host[]; numberOfHostsThatJoined: number; didGuestJoinTheCall: boolean; + triggerEvent: WebhookTriggerEvents; } | void> => { - const { bookingId, webhook } = ZSendNoShowWebhookPayloadSchema.parse(JSON.parse(payload)); + const { bookingId, webhook, destinationCalendar, triggerEvent } = ZSendNoShowWebhookPayloadSchema.parse( + JSON.parse(payload) + ); const booking = await getBooking(bookingId); if (booking.status !== BookingStatus.ACCEPTED) { - log.debug( + log.info( "Booking is not accepted", safeStringify({ bookingId, @@ -122,32 +209,84 @@ export const prepareNoShowTrigger = async ( return; } + const hosts = getHosts(booking); + const dailyVideoReference = booking.references.find((reference) => reference.type === "daily_video"); - if (!dailyVideoReference) { + if ( + !!dailyVideoReference && + (booking.location === DailyLocationType || booking.location?.trim() === "" || !booking.location) + ) { + const meetingDetails = await getMeetingSessionsFromRoomName(dailyVideoReference.uid); + + const allParticipants = meetingDetails.data.flatMap((meeting) => meeting.participants); + + const hostsThatDidntJoinTheCall = hosts.filter( + (host) => !checkIfUserJoinedTheCall(host.id, allParticipants) + ); + + const numberOfHostsThatJoined = hosts.length - hostsThatDidntJoinTheCall.length; + + const didGuestJoinTheCall = meetingDetails.data.some( + (meeting) => meeting.max_participants < numberOfHostsThatJoined + ); + + return { + hostsThatDidntJoinTheCall, + booking, + numberOfHostsThatJoined, + webhook, + didGuestJoinTheCall, + triggerEvent, + }; + } else if (booking.location === "integrations:google:meet") { + const googleCalendarCredentials = await getGoogleCalendarCredential(destinationCalendar); + if (!googleCalendarCredentials) { + log.error( + "No google calendar credentials found", + safeStringify({ + bookingId, + webhook: { id: webhook.id }, + destinationCalendar, + }) + ); + return; + } + + const calendar = await getCalendar(googleCalendarCredentials); + const videoCallUrl = bookingMetadataSchema.parse(booking.metadata ?? null)?.videoCallUrl ?? null; + const allParticipantGroups = (await calendar?.getMeetParticipants?.(videoCallUrl)) ?? []; + + const allParticipants: ParticipantWithEmail[] = allParticipantGroups.flat(); + + const hostsThatDidntJoinTheCall = hosts.filter( + (host) => !allParticipants?.some((participant) => participant.email === host.email) + ); + + const numberOfHostsThatJoined = hosts.length - hostsThatDidntJoinTheCall.length; + + const maxParticipants = allParticipantGroups.reduce((max, participantGroup) => { + return Math.max(max, participantGroup.length); + }, 0); + + const didGuestJoinTheCall = maxParticipants < numberOfHostsThatJoined; + + return { + hostsThatDidntJoinTheCall, + booking, + numberOfHostsThatJoined, + webhook, + didGuestJoinTheCall, + triggerEvent, + }; + } else { log.error( - "Daily video reference not found", + "No valid location found in triggerNoShowWebhook", safeStringify({ bookingId, webhook: { id: webhook.id }, }) ); - throw new Error(`Daily video reference not found in triggerHostNoShow with bookingId ${bookingId}`); + throw new Error(`No valid location found in triggerNoShowWebhook with bookingId ${bookingId}`); } - const meetingDetails = await getMeetingSessionsFromRoomName(dailyVideoReference.uid); - - const hosts = getHosts(booking); - const allParticipants = meetingDetails.data.flatMap((meeting) => meeting.participants); - - const hostsThatDidntJoinTheCall = hosts.filter( - (host) => !checkIfUserJoinedTheCall(host.id, allParticipants) - ); - - const numberOfHostsThatJoined = hosts.length - hostsThatDidntJoinTheCall.length; - - const didGuestJoinTheCall = meetingDetails.data.some( - (meeting) => meeting.max_participants < numberOfHostsThatJoined - ); - - return { hostsThatDidntJoinTheCall, booking, numberOfHostsThatJoined, webhook, didGuestJoinTheCall }; }; diff --git a/packages/features/tasker/tasks/triggerNoShow/schema.ts b/packages/features/tasker/tasks/triggerNoShow/schema.ts index 355b6cbd0c02b7..a998330153e2e8 100644 --- a/packages/features/tasker/tasks/triggerNoShow/schema.ts +++ b/packages/features/tasker/tasks/triggerNoShow/schema.ts @@ -7,6 +7,8 @@ const commonSchema = z.object({ triggerEvent: z.enum([ WebhookTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW, WebhookTriggerEvents.AFTER_GUESTS_CAL_VIDEO_NO_SHOW, + WebhookTriggerEvents.AFTER_HOSTS_GOOGLE_MEET_NO_SHOW, + WebhookTriggerEvents.AFTER_GUESTS_GOOGLE_MEET_NO_SHOW, ]), bookingId: z.number(), }); @@ -48,10 +50,23 @@ export const triggerNoShowPayloadSchema = z.object({ ), }); +const ZDestinationCalendar = z + .object({ + id: z.number(), + integration: z.string(), + externalId: z.string(), + primaryEmail: z.string().nullable(), + credentialId: z.number().nullable(), + userId: z.number().nullable(), + }) + .passthrough() + .optional(); + export type TTriggerNoShowPayloadSchema = z.infer; export const ZSendNoShowWebhookPayloadSchema = commonSchema.extend({ webhook: ZWebhook, + destinationCalendar: ZDestinationCalendar, }); export type TSendNoShowWebhookPayloadSchema = z.infer; diff --git a/packages/features/tasker/tasks/triggerNoShow/triggerGuestNoShow.ts b/packages/features/tasker/tasks/triggerNoShow/triggerGuestNoShow.ts index 3c9f82e4d1a4ee..ce0a450aa023f8 100644 --- a/packages/features/tasker/tasks/triggerNoShow/triggerGuestNoShow.ts +++ b/packages/features/tasker/tasks/triggerNoShow/triggerGuestNoShow.ts @@ -1,21 +1,14 @@ -import { WebhookTriggerEvents } from "@calcom/prisma/enums"; - import { calculateMaxStartTime, sendWebhookPayload, prepareNoShowTrigger } from "./common"; export async function triggerGuestNoShow(payload: string): Promise { const result = await prepareNoShowTrigger(payload); if (!result) return; - const { webhook, booking, didGuestJoinTheCall } = result; + const { webhook, booking, didGuestJoinTheCall, triggerEvent } = result; const maxStartTime = calculateMaxStartTime(booking.startTime, webhook.time, webhook.timeUnit); if (!didGuestJoinTheCall) { - await sendWebhookPayload( - webhook, - WebhookTriggerEvents.AFTER_GUESTS_CAL_VIDEO_NO_SHOW, - booking, - maxStartTime - ); + await sendWebhookPayload(webhook, triggerEvent, booking, maxStartTime); } } diff --git a/packages/features/tasker/tasks/triggerNoShow/triggerHostNoShow.ts b/packages/features/tasker/tasks/triggerNoShow/triggerHostNoShow.ts index dee91eb04bfa50..f268a0594a21d1 100644 --- a/packages/features/tasker/tasks/triggerNoShow/triggerHostNoShow.ts +++ b/packages/features/tasker/tasks/triggerNoShow/triggerHostNoShow.ts @@ -1,5 +1,4 @@ import { prisma } from "@calcom/prisma"; -import { WebhookTriggerEvents } from "@calcom/prisma/enums"; import type { Booking, Host } from "./common"; import { calculateMaxStartTime, sendWebhookPayload, prepareNoShowTrigger, log } from "./common"; @@ -37,18 +36,12 @@ export async function triggerHostNoShow(payload: string): Promise { const result = await prepareNoShowTrigger(payload); if (!result) return; - const { booking, webhook, hostsThatDidntJoinTheCall } = result; + const { booking, webhook, hostsThatDidntJoinTheCall, triggerEvent } = result; const maxStartTime = calculateMaxStartTime(booking.startTime, webhook.time, webhook.timeUnit); const hostsNoShowPromises = hostsThatDidntJoinTheCall.map((host) => { - return sendWebhookPayload( - webhook, - WebhookTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW, - booking, - maxStartTime, - host.email - ); + return sendWebhookPayload(webhook, triggerEvent, booking, maxStartTime, host.email); }); await Promise.all(hostsNoShowPromises); diff --git a/packages/features/webhooks/components/WebhookForm.tsx b/packages/features/webhooks/components/WebhookForm.tsx index 9301e911c7d9dd..bb83b6a464fec6 100644 --- a/packages/features/webhooks/components/WebhookForm.tsx +++ b/packages/features/webhooks/components/WebhookForm.tsx @@ -57,6 +57,11 @@ const WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP_V2: Record { + console.log("event", event); onChange(event.map((selection) => selection.value)); const noShowWebhookTriggerExists = !!event.find( (trigger) => trigger.value === WebhookTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW || - trigger.value === WebhookTriggerEvents.AFTER_GUESTS_CAL_VIDEO_NO_SHOW + trigger.value === WebhookTriggerEvents.AFTER_GUESTS_CAL_VIDEO_NO_SHOW || + trigger.value === WebhookTriggerEvents.AFTER_HOSTS_GOOGLE_MEET_NO_SHOW || + trigger.value === WebhookTriggerEvents.AFTER_GUESTS_GOOGLE_MEET_NO_SHOW ); if (noShowWebhookTriggerExists) { diff --git a/packages/features/webhooks/lib/constants.ts b/packages/features/webhooks/lib/constants.ts index bd9ea21b2bf636..6b27e7bdfa52bc 100644 --- a/packages/features/webhooks/lib/constants.ts +++ b/packages/features/webhooks/lib/constants.ts @@ -20,6 +20,8 @@ export const WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP = { WebhookTriggerEvents.OOO_CREATED, WebhookTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW, WebhookTriggerEvents.AFTER_GUESTS_CAL_VIDEO_NO_SHOW, + WebhookTriggerEvents.AFTER_HOSTS_GOOGLE_MEET_NO_SHOW, + WebhookTriggerEvents.AFTER_GUESTS_GOOGLE_MEET_NO_SHOW, ] as const, "routing-forms": [ WebhookTriggerEvents.FORM_SUBMITTED, diff --git a/packages/features/webhooks/lib/scheduleTrigger.ts b/packages/features/webhooks/lib/scheduleTrigger.ts index 6529e8e7994e90..35bc47a63d541d 100644 --- a/packages/features/webhooks/lib/scheduleTrigger.ts +++ b/packages/features/webhooks/lib/scheduleTrigger.ts @@ -442,7 +442,9 @@ export async function updateTriggerForExistingBookings( return addedEventTriggers.map((triggerEvent) => { if ( triggerEvent === WebhookTriggerEvents.AFTER_GUESTS_CAL_VIDEO_NO_SHOW || - triggerEvent === WebhookTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW + triggerEvent === WebhookTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW || + triggerEvent === WebhookTriggerEvents.AFTER_HOSTS_GOOGLE_MEET_NO_SHOW || + triggerEvent === WebhookTriggerEvents.AFTER_GUESTS_GOOGLE_MEET_NO_SHOW ) return Promise.resolve(); diff --git a/packages/prisma/migrations/20241023095517_add_webhook_google_meet_no_show/migration.sql b/packages/prisma/migrations/20241023095517_add_webhook_google_meet_no_show/migration.sql new file mode 100644 index 00000000000000..992825ca9485bc --- /dev/null +++ b/packages/prisma/migrations/20241023095517_add_webhook_google_meet_no_show/migration.sql @@ -0,0 +1,10 @@ +-- AlterEnum +-- This migration adds more than one value to an enum. +-- With PostgreSQL versions 11 and earlier, this is not possible +-- in a single migration. This can be worked around by creating +-- multiple migrations, each migration adding only one value to +-- the enum. + + +ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'AFTER_HOSTS_GOOGLE_MEET_NO_SHOW'; +ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'AFTER_GUESTS_GOOGLE_MEET_NO_SHOW'; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 810336f11a38d8..7b772a94766195 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -778,6 +778,8 @@ enum WebhookTriggerEvents { OOO_CREATED AFTER_HOSTS_CAL_VIDEO_NO_SHOW AFTER_GUESTS_CAL_VIDEO_NO_SHOW + AFTER_HOSTS_GOOGLE_MEET_NO_SHOW + AFTER_GUESTS_GOOGLE_MEET_NO_SHOW FORM_SUBMITTED_NO_EVENT } diff --git a/packages/types/Calendar.d.ts b/packages/types/Calendar.d.ts index 0f904b9cdf92e1..3960e9cf0f45d7 100644 --- a/packages/types/Calendar.d.ts +++ b/packages/types/Calendar.d.ts @@ -1,3 +1,4 @@ +import type { protos } from "@google-apps/meet"; import type { BookingSeat, DestinationCalendar, Prisma, SelectedCalendar } from "@prisma/client"; import type { Dayjs } from "dayjs"; import type { calendar_v3 } from "googleapis"; @@ -14,6 +15,10 @@ import type { CredentialPayload } from "@calcom/types/Credential"; import type { Ensure } from "./utils"; +interface ParticipantWithEmail extends protos.google.apps.meet.v2.IParticipant { + email?: string; +} + export type { VideoCallData } from "./VideoApiAdapter"; type PaymentInfo = { @@ -260,6 +265,8 @@ export interface Calendar { ): Promise; listCalendars(event?: CalendarEvent): Promise; + + getMeetParticipants?(videoCallUrl: string | null): Promise; } /**