Skip to content

Commit

Permalink
Make Google Meet default when DWD for google is enabled
Browse files Browse the repository at this point in the history
  • Loading branch information
hariombalhara committed Jan 17, 2025
1 parent 033332e commit d9425a3
Show file tree
Hide file tree
Showing 6 changed files with 243 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
### Follow-up release
- [ ] Confirmation for DwD toggling off
- [ ] Confirmation for DwD deletion - Deletion isn't there at the moment.
- [ ] Make Google Meet "show up" as default conferencing app when DWD for Google is enabled.
- [ ] Profile pic from Google with DWD might not be working - Fix it.

### Security
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,10 @@ Last Step (To Be Taken By Cal.com organization Owner/Admin): Assign Specific API


## Onboarding Improvement
- Just adding a member to the organization would allow the member to receive events in their calendar, even if they don't login to their account and complete the onboarding process.
- Just adding a member to the organization would do the following:
- Member to receive events in their calendar, even if they don't login to their account and complete the onboarding process.
- The booking location would be Google Meet, even if the user hasn't set it as default(Though Cal Video would show up as default, but we still use Google Meet in this case. We will fix it later.)
- It would still not use their calendar for conflict checking, but user can complete the onboarding(just select one calendar there for conflict checking)
- It would still use CalVideo as the default location as the user hasn't confirmed their preferred conferencing app as Google Meet. They could do that in the onboarding process by just clicking 'Set Default'
- Onboarding process: Google Calendar is pre-installed for any new member of the organization(assuming the user has an email of the DWD domain) and Destination Calendar and Selected Calendar are configurable. On next step, Google Meet is pre-installed and shown at the top and could be set as default.

## Restrictions after enabling DWD
Expand Down
20 changes: 14 additions & 6 deletions packages/features/bookings/lib/handleNewBooking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import {
import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser";
import { isRerouting, shouldIgnoreContactOwner } from "@calcom/lib/bookings/routing/utils";
import { getDefaultEvent, getUsernameList } from "@calcom/lib/defaultEvents";
import { getFirstDwdConferencingCredentialAppLocation } from "@calcom/lib/domainWideDelegation/server";
import {
enrichHostsWithDwdCredentials,
enrichUsersWithDwdCredentials,
Expand Down Expand Up @@ -505,12 +506,12 @@ async function handler(
isSameHostReschedule: !!(eventType.rescheduleWithSameRoundRobinHost && reqBody.rescheduleUid),
});

const firstUser: (typeof allHostUsersWithoutHavingDwdCredentials)[number] | undefined =
const firstUserWithoutDwdCredentials: (typeof allHostUsersWithoutHavingDwdCredentials)[number] | undefined =
allHostUsersWithoutHavingDwdCredentials[0];

// We use first user's org ID assuming that each and every member would be within the same organization.
const firstUserOrgId = await getOrgIdFromMemberOrTeamId({
memberId: firstUser?.id ?? null,
memberId: firstUserWithoutDwdCredentials?.id ?? null,
teamId: eventType.teamId,
});

Expand All @@ -521,12 +522,12 @@ async function handler(

// We filter out users but ensure allHostUsers remain same.
let users = allHostUsers;

let { locationBodyString, organizerOrFirstDynamicGroupMemberDefaultLocationUrl } = getLocationValuesForDb(
const firstUser = users[0];
let { locationBodyString, organizerOrFirstDynamicGroupMemberDefaultLocationUrl } = getLocationValuesForDb({
dynamicUserList,
users,
location
);
location,
});

await monitorCallbackAsync(checkBookingAndDurationLimits, {
eventType,
Expand Down Expand Up @@ -819,6 +820,11 @@ async function handler(
locationBodyString = OrganizerDefaultConferencingAppType;
}
}

const organizationDefaultLocation = getFirstDwdConferencingCredentialAppLocation({
credentials: firstUser.credentials,
});

// use host default
if (locationBodyString == OrganizerDefaultConferencingAppType) {
const metadataParseResult = userMetadataSchema.safeParse(organizerUser.metadata);
Expand All @@ -830,6 +836,8 @@ async function handler(
organizerOrFirstDynamicGroupMemberDefaultLocationUrl =
organizerMetadata?.defaultConferencingApp?.appLink;
}
} else if (organizationDefaultLocation) {
locationBodyString = organizationDefaultLocation;
} else {
locationBodyString = "integrations:daily";
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,30 +1,52 @@
import { getFirstDwdConferencingCredentialAppLocation } from "@calcom/lib/domainWideDelegation/server";
import { userMetadata as userMetadataSchema } from "@calcom/prisma/zod-utils";

import type { loadAndValidateUsers } from "./loadAndValidateUsers";

type Users = Awaited<ReturnType<typeof loadAndValidateUsers>>;

const sortUsersByDynamicList = (users: Users, dynamicUserList: string[]) => {
const sortUsersByDynamicList = <TUser extends { username: string | null }>(
users: TUser[],
dynamicUserList: string[]
) => {
return users.sort((a, b) => {
const aIndex = (a.username && dynamicUserList.indexOf(a.username)) || 0;
const bIndex = (b.username && dynamicUserList.indexOf(b.username)) || 0;
return aIndex - bIndex;
});
};

export const getLocationValuesForDb = (
dynamicUserList: string[],
users: Users,
locationBodyString: string
) => {
export const getLocationValuesForDb = <
TUser extends {
username: string | null;
metadata: Prisma.JsonValue;
credentials: CredentialForCalendarService[];
}
>({
dynamicUserList,
users,
location: locationBodyString,
}: {
dynamicUserList: string[];
users: TUser[];
location: string;
}) => {
const isDynamicGroupBookingCase = dynamicUserList.length > 1;
let firstDynamicGroupMemberDefaultLocationUrl;
// TODO: It's definition should be moved to getLocationValueForDb
let organizerOrFirstDynamicGroupMemberDefaultLocationUrl;
if (dynamicUserList.length > 1) {
if (isDynamicGroupBookingCase) {
users = sortUsersByDynamicList(users, dynamicUserList);
const firstUsersMetadata = userMetadataSchema.parse(users[0].metadata);
locationBodyString = firstUsersMetadata?.defaultConferencingApp?.appLink || locationBodyString;
organizerOrFirstDynamicGroupMemberDefaultLocationUrl =
firstUsersMetadata?.defaultConferencingApp?.appLink;
const firstDynamicGroupMember = users[0];
const firstDynamicGroupMemberMetadata = userMetadataSchema.parse(firstDynamicGroupMember.metadata);
const firstDynamicGroupMemberDwdConferencingAppLocation = getFirstDwdConferencingCredentialAppLocation({
credentials: firstDynamicGroupMember.credentials,
});

firstDynamicGroupMemberDefaultLocationUrl =
firstDynamicGroupMemberMetadata?.defaultConferencingApp?.appLink ||
firstDynamicGroupMemberDwdConferencingAppLocation;

locationBodyString = firstDynamicGroupMemberDefaultLocationUrl || locationBodyString;
}
return { locationBodyString, organizerOrFirstDynamicGroupMemberDefaultLocationUrl };

return {
locationBodyString,
organizerOrFirstDynamicGroupMemberDefaultLocationUrl: firstDynamicGroupMemberDefaultLocationUrl,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import {
createBookingScenario,
TestData,
getDate,
getOrganizer,
getBooker,
getScenarioData,
Expand All @@ -31,7 +32,10 @@ import {
expectBookingToNotHaveReference,
expectNoAttemptToCreateCalendarEvent,
} from "@calcom/web/test/utils/bookingScenario/expects";
import { getMockRequestDataForBooking } from "@calcom/web/test/utils/bookingScenario/getMockRequestDataForBooking";
import {
getMockRequestDataForBooking,
getMockRequestDataForDynamicGroupBooking,
} from "@calcom/web/test/utils/bookingScenario/getMockRequestDataForBooking";
import { setupAndTeardown } from "@calcom/web/test/utils/bookingScenario/setupAndTeardown";

import { describe, expect } from "vitest";
Expand All @@ -51,6 +55,7 @@ describe("handleNewBooking", () => {
`should create a successful booking using the domain wide delegation credential
1. Should create a booking in the database with reference having DWD credential
2. Should create an event in calendar with DWD credential
3. Should use Google Meet as the location even when not explicitly set.
`,
async ({ emails }) => {
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
Expand Down Expand Up @@ -97,7 +102,6 @@ describe("handleNewBooking", () => {
id: 1,
slotInterval: 30,
length: 30,
location: BookingLocations.GoogleMeet,
users: [
{
id: organizer.id,
Expand Down Expand Up @@ -394,5 +398,165 @@ describe("handleNewBooking", () => {
},
timeout
);

test(
`should create a successful dynamic group booking using the domain wide delegation credential
1. Should create a booking in the database with reference having DWD credential
2. Should create an event in calendar with DWD credential for both users
3. Should use Google Meet as the location even when not explicitly set.
`,
async () => {
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;

const org = await createOrganization({
name: "Test Org",
slug: "testorg",
});

const payloadToMakePartOfOrganization = [
{
membership: {
accepted: true,
role: MembershipRole.ADMIN,
},
team: {
id: org.id,
name: "Test Org",
slug: "testorg",
},
},
];

const booker = getBooker({
email: "[email protected]",
name: "Booker",
});

const groupUser1 = getOrganizer({
name: "group-user-1",
username: "group-user-1",
email: "[email protected]",
id: 101,
schedules: [TestData.schedules.IstWorkHours],
selectedCalendars: [TestData.selectedCalendars.google],
teams: payloadToMakePartOfOrganization,
credentials: [],
destinationCalendar: [TestData.selectedCalendars.google],
});

const groupUser2 = getOrganizer({
name: "group-user-2",
username: "group-user-2",
email: "[email protected]",
id: 102,
schedules: [TestData.schedules.IstWorkHours],
selectedCalendars: [TestData.selectedCalendars.google],
teams: payloadToMakePartOfOrganization,
credentials: [],
destinationCalendar: [TestData.selectedCalendars.google],
});

const dwd = await createDwdCredential(org.id);

await createBookingScenario(
getScenarioData({
webhooks: [
{
userId: groupUser1.id,
eventTriggers: ["BOOKING_CREATED"],
subscriberUrl: "http://my-webhook.example.com",
active: true,
eventTypeId: 0,
appId: null,
},
],
workflows: [
{
userId: groupUser1.id,
trigger: "NEW_EVENT",
action: "EMAIL_HOST",
template: "REMINDER",
activeOn: [0],
},
],
eventTypes: [],
users: [groupUser1, groupUser2],
apps: [TestData.apps["daily-video"], TestData.apps["google-calendar"]],
})
);

// Mock a Scenario where iCalUID isn't returned by Google Calendar in which case booking UID is used as the ics UID
const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", {
create: {
id: "GOOGLE_CALENDAR_EVENT_ID",
uid: "MOCK_ID",
appSpecificData: {
googleCalendar: {
hangoutLink: "https://GOOGLE_MEET_URL_IN_CALENDAR_EVENT",
},
},
},
});

const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });

const mockBookingData = getMockRequestDataForDynamicGroupBooking({
data: {
start: `${plus1DateString}T05:00:00.000Z`,
end: `${plus1DateString}T05:30:00.000Z`,
eventTypeId: 0,
eventTypeSlug: "group-user-1+group-user-2",
user: "group-user-1+group-user-2",
responses: {
email: booker.email,
name: booker.name,
// There is no location option during booking for Dynamic Group Bookings
// location: { optionValue: "", value: BookingLocations.CalVideo },
},
},
});

const { req } = createMockNextJsRequest({
method: "POST",
body: mockBookingData,
});

const createdBooking = await handleNewBooking(req);

expect(createdBooking.responses).toEqual(
expect.objectContaining({
email: booker.email,
name: booker.name,
})
);

expect(createdBooking).toEqual(
expect.objectContaining({
location: BookingLocations.GoogleMeet,
})
);

await expectBookingToBeInDatabase({
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
uid: createdBooking.uid!,
eventTypeId: null,
status: BookingStatus.ACCEPTED,
location: BookingLocations.GoogleMeet,
references: [
{
type: appStoreMetadata.googlecalendar.type,
uid: "GOOGLE_CALENDAR_EVENT_ID",
meetingId: "GOOGLE_CALENDAR_EVENT_ID",
meetingPassword: "MOCK_PASSWORD",
meetingUrl: "https://GOOGLE_MEET_URL_IN_CALENDAR_EVENT",
// Verify DWD credential was used
domainWideDelegationCredentialId: dwd.id,
},
],
iCalUID: createdBooking.iCalUID,
});
},
timeout
);
});
});
20 changes: 20 additions & 0 deletions packages/lib/domainWideDelegation/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,3 +395,23 @@ export function getDwdOrRegularCredential<TCredential extends { delegatedToId?:
}) || null
);
}

export function getFirstDwdConferencingCredential({
credentials,
}: {
credentials: CredentialForCalendarService[];
}) {
return credentials.find((credential) => _isConferencingCredential(credential));
}

export function getFirstDwdConferencingCredentialAppLocation({
credentials,
}: {
credentials: CredentialForCalendarService[];
}) {
const dwdConferencingCredential = getFirstDwdConferencingCredential({ credentials });
if (dwdConferencingCredential?.appId === googleMeetMetadata.slug) {
return googleMeetMetadata.appData.location.type;
}
return null;
}

0 comments on commit d9425a3

Please sign in to comment.