-
Notifications
You must be signed in to change notification settings - Fork 7.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: google meet no show #17271
base: main
Are you sure you want to change the base?
feat: google meet no show #17271
Changes from all commits
1df2bbe
744a04c
9254ca3
9e2890b
47017b6
223cf4d
c7ac4a9
99c670b
a1c4fc7
e1f95a5
92f12f0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<ParticipantWithEmail[][]> { | ||
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, | ||
}, | ||
}), | ||
}); | ||
|
||
Comment on lines
+680
to
+687
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It was throwing type error when i used the Google auth client in this class so i had to create this one |
||
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()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
if (response.space === spaceName) { | ||
conferenceRecords.push(response); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we only want the conference record on our meeting url |
||
} | ||
} | ||
|
||
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`, | ||
Comment on lines
+723
to
+726
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fetch email here by id |
||
{ | ||
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<IntegrationCalendar[]> { | ||
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); | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Need this scope to fetch details about meeting and fetch participants |
||
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"; | ||
Comment on lines
+5
to
+8
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Need these scope to fetch user's workspace accounts email. |
||
|
||
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, | ||
]; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<any>[] = []; | ||
|
||
const subscribersHostsNoShowStarted = await getWebhooks({ | ||
const hostNoShowTriggerEvent = isDailyVideoLocation | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My previous comment applies here. This list will continue to grow larger and larger for the more video integrations we detect no shows for. Do we absolutely have to do this? |
||
? 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); | ||
|
||
Comment on lines
+61
to
+63
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we need the destination calendar of the organizer (that created the google meet url). |
||
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 } | ||
); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should these be separate per video type? Seems like it should be
AFTER_HOSTS_NO_SHOW
andAFTER_GUESTS_NO_SHOW
no matter which video type they used.