chore: enable platform emails (#14471)
* feat: enable platform emails * fixup! feat: enable platform emails * fixup! fixup! feat: enable platform emails * fixup! Merge branch 'main' into chore-platform-emails * fixup! fixup! Merge branch 'main' into chore-platform-emails * fixup! fixup! fixup! Merge branch 'main' into chore-platform-emails * chore: add oauth client repo * chore: enable platform emails using oauth client * fix: call getConnectedCalendar with dbWrite since it creates destinationCalendar for new credentials * chore: update doc * fixup! chore: update doc * fixup! fixup! chore: update doc
This commit is contained in:
@@ -11,6 +11,8 @@ import * as Sentry from "@sentry/node";
|
||||
import * as cookieParser from "cookie-parser";
|
||||
import helmet from "helmet";
|
||||
|
||||
import { X_CAL_CLIENT_ID, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
|
||||
|
||||
import { TRPCExceptionFilter } from "./filters/trpc-exception.filter";
|
||||
|
||||
export const bootstrap = (app: NestExpressApplication): NestExpressApplication => {
|
||||
@@ -26,7 +28,7 @@ export const bootstrap = (app: NestExpressApplication): NestExpressApplication =
|
||||
app.enableCors({
|
||||
origin: "*",
|
||||
methods: ["GET", "PATCH", "DELETE", "HEAD", "POST", "PUT", "OPTIONS"],
|
||||
allowedHeaders: ["Accept", "Authorization", "Content-Type", "Origin"],
|
||||
allowedHeaders: [X_CAL_CLIENT_ID, X_CAL_SECRET_KEY, "Accept", "Authorization", "Content-Type", "Origin"],
|
||||
maxAge: 86_400,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { BookingsController } from "@/ee/bookings/controllers/bookings.controller";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
@@ -7,7 +8,7 @@ import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, TokensModule],
|
||||
providers: [TokensRepository, OAuthFlowService],
|
||||
providers: [TokensRepository, OAuthFlowService, OAuthClientRepository],
|
||||
controllers: [BookingsController],
|
||||
})
|
||||
export class BookingsModule {}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import {
|
||||
@@ -15,6 +16,7 @@ import {
|
||||
Req,
|
||||
InternalServerErrorException,
|
||||
Body,
|
||||
Headers,
|
||||
HttpException,
|
||||
Param,
|
||||
Get,
|
||||
@@ -27,6 +29,7 @@ import { User } from "@prisma/client";
|
||||
import { Request } from "express";
|
||||
import { NextApiRequest } from "next/types";
|
||||
|
||||
import { X_CAL_CLIENT_ID } from "@calcom/platform-constants";
|
||||
import { BOOKING_READ, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import {
|
||||
getAllUserBookings,
|
||||
@@ -45,6 +48,25 @@ import { GetBookingsInput, CancelBookingInput, Status } from "@calcom/platform-t
|
||||
import { ApiResponse } from "@calcom/platform-types";
|
||||
import { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
type BookingRequest = Request & {
|
||||
userId?: number;
|
||||
};
|
||||
|
||||
type OAuthRequestParams = {
|
||||
platformClientId: string;
|
||||
platformRescheduleUrl: string;
|
||||
platformCancelUrl: string;
|
||||
platformBookingUrl: string;
|
||||
};
|
||||
|
||||
const DEFAULT_PLATFORM_PARAMS = {
|
||||
platformClientId: "",
|
||||
platformCancelUrl: "",
|
||||
platformRescheduleUrl: "",
|
||||
platformBookingUrl: "",
|
||||
areEmailsEnabled: true,
|
||||
};
|
||||
|
||||
@Controller({
|
||||
path: "ee/bookings",
|
||||
version: "2",
|
||||
@@ -56,7 +78,8 @@ export class BookingsController {
|
||||
|
||||
constructor(
|
||||
private readonly oAuthFlowService: OAuthFlowService,
|
||||
private readonly prismaReadService: PrismaReadService
|
||||
private readonly prismaReadService: PrismaReadService,
|
||||
private readonly oAuthClientRepository: OAuthClientRepository
|
||||
) {}
|
||||
|
||||
@Get("/")
|
||||
@@ -117,13 +140,13 @@ export class BookingsController {
|
||||
|
||||
@Post("/")
|
||||
async createBooking(
|
||||
@Req() req: Request & { userId?: number },
|
||||
@Body() _: CreateBookingInput
|
||||
@Req() req: BookingRequest,
|
||||
@Body() _: CreateBookingInput,
|
||||
@Headers(X_CAL_CLIENT_ID) clientId?: string
|
||||
): Promise<ApiResponse<unknown>> {
|
||||
req.userId = (await this.getOwnerId(req)) ?? -1;
|
||||
req.body = { ...req.body, noEmail: true };
|
||||
const oAuthClientId = clientId?.toString();
|
||||
try {
|
||||
const booking = await handleNewBooking(req as unknown as NextApiRequest & { userId?: number });
|
||||
const booking = await handleNewBooking(await this.createNextApiBookingRequest(req, oAuthClientId));
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: booking,
|
||||
@@ -136,15 +159,15 @@ export class BookingsController {
|
||||
|
||||
@Post("/:bookingId/cancel")
|
||||
async cancelBooking(
|
||||
@Req() req: Request & { userId?: number },
|
||||
@Req() req: BookingRequest,
|
||||
@Param("bookingId") bookingId: string,
|
||||
@Body() body: CancelBookingInput
|
||||
@Body() _: CancelBookingInput,
|
||||
@Headers(X_CAL_CLIENT_ID) clientId?: string
|
||||
): Promise<ApiResponse> {
|
||||
const oAuthClientId = clientId?.toString();
|
||||
if (bookingId) {
|
||||
req.userId = (await this.getOwnerId(req)) ?? -1;
|
||||
req.body = { ...body, id: parseInt(bookingId) };
|
||||
try {
|
||||
await handleCancelBooking(req as unknown as NextApiRequest & { userId?: number });
|
||||
await handleCancelBooking(await this.createNextApiBookingRequest(req, oAuthClientId));
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
};
|
||||
@@ -159,14 +182,14 @@ export class BookingsController {
|
||||
|
||||
@Post("/reccuring")
|
||||
async createReccuringBooking(
|
||||
@Req() req: Request & { userId?: number },
|
||||
@Body() _: CreateReccuringBookingInput[]
|
||||
@Req() req: BookingRequest,
|
||||
@Body() _: CreateReccuringBookingInput[],
|
||||
@Headers(X_CAL_CLIENT_ID) clientId?: string
|
||||
): Promise<ApiResponse<BookingResponse[]>> {
|
||||
req.userId = (await this.getOwnerId(req)) ?? -1;
|
||||
req.body = { ...req.body, noEmail: true };
|
||||
const oAuthClientId = clientId?.toString();
|
||||
try {
|
||||
const createdBookings: BookingResponse[] = await handleNewRecurringBooking(
|
||||
req as unknown as NextApiRequest & { userId?: number }
|
||||
await this.createNextApiBookingRequest(req, oAuthClientId)
|
||||
);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
@@ -180,14 +203,15 @@ export class BookingsController {
|
||||
|
||||
@Post("/instant")
|
||||
async createInstantBooking(
|
||||
@Req() req: Request & { userId?: number },
|
||||
@Body() _: CreateBookingInput
|
||||
@Req() req: BookingRequest,
|
||||
@Body() _: CreateBookingInput,
|
||||
@Headers(X_CAL_CLIENT_ID) clientId?: string
|
||||
): Promise<ApiResponse<Awaited<ReturnType<typeof handleInstantMeeting>>>> {
|
||||
const oAuthClientId = clientId?.toString();
|
||||
req.userId = (await this.getOwnerId(req)) ?? -1;
|
||||
req.body = { ...req.body, noEmail: true };
|
||||
try {
|
||||
const instantMeeting = await handleInstantMeeting(
|
||||
req as unknown as NextApiRequest & { userId?: number }
|
||||
await this.createNextApiBookingRequest(req, oAuthClientId)
|
||||
);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
@@ -209,6 +233,41 @@ export class BookingsController {
|
||||
this.logger.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async getOAuthClientsParams(
|
||||
req: BookingRequest,
|
||||
clientId: string
|
||||
): Promise<OAuthRequestParams & { areEmailsEnabled: boolean }> {
|
||||
const res = DEFAULT_PLATFORM_PARAMS;
|
||||
try {
|
||||
const client = await this.oAuthClientRepository.getOAuthClient(clientId);
|
||||
// fetch oAuthClient from db and use data stored in db to set these values
|
||||
if (client) {
|
||||
res.platformClientId = clientId;
|
||||
res.platformCancelUrl = client.bookingCancelRedirectUri ?? "";
|
||||
res.platformRescheduleUrl = client.bookingRescheduleRedirectUri ?? "";
|
||||
res.platformBookingUrl = client.bookingRedirectUri ?? "";
|
||||
res.areEmailsEnabled = client.areEmailsEnabled;
|
||||
}
|
||||
return res;
|
||||
} catch (err) {
|
||||
this.logger.error(err);
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
async createNextApiBookingRequest(
|
||||
req: BookingRequest,
|
||||
oAuthClientId?: string
|
||||
): Promise<NextApiRequest & { userId?: number } & OAuthRequestParams> {
|
||||
const userId = (await this.getOwnerId(req)) ?? -1;
|
||||
const oAuthParams = oAuthClientId
|
||||
? await this.getOAuthClientsParams(req, oAuthClientId)
|
||||
: DEFAULT_PLATFORM_PARAMS;
|
||||
Object.assign(req, { userId, ...oAuthParams });
|
||||
req.body = { ...req.body, areEmailsEnabled: oAuthParams.areEmailsEnabled };
|
||||
return req as unknown as NextApiRequest & { userId?: number } & OAuthRequestParams;
|
||||
}
|
||||
}
|
||||
|
||||
function handleBookingErrors(err: Error | HttpError | unknown, type?: "recurring" | `instant`): void {
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
CredentialsWithUserEmail,
|
||||
} from "@/modules/credentials/credentials.repository";
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import {
|
||||
Injectable,
|
||||
@@ -23,7 +24,8 @@ export class CalendarsService {
|
||||
constructor(
|
||||
private readonly usersRepository: UsersRepository,
|
||||
private readonly credentialsRepository: CredentialsRepository,
|
||||
private readonly dbRead: PrismaReadService
|
||||
private readonly dbRead: PrismaReadService,
|
||||
private readonly dbWrite: PrismaWriteService
|
||||
) {}
|
||||
|
||||
async getCalendars(userId: number) {
|
||||
@@ -35,7 +37,7 @@ export class CalendarsService {
|
||||
return getConnectedDestinationCalendars(
|
||||
userWithCalendars,
|
||||
false,
|
||||
this.dbRead.prisma as unknown as PrismaClient
|
||||
this.dbWrite.prisma as unknown as PrismaClient
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ export class CredentialsRepository {
|
||||
invalid: false,
|
||||
},
|
||||
where: {
|
||||
id: credential?.id,
|
||||
id: credential?.id ?? 0,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1235,7 +1235,16 @@
|
||||
},
|
||||
"post": {
|
||||
"operationId": "BookingsController_createBooking",
|
||||
"parameters": [],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "x-cal-client-id",
|
||||
"required": true,
|
||||
"in": "header",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
@@ -1334,6 +1343,14 @@
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "x-cal-client-id",
|
||||
"required": true,
|
||||
"in": "header",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
@@ -1366,7 +1383,16 @@
|
||||
"/api/v2/ee/bookings/reccuring": {
|
||||
"post": {
|
||||
"operationId": "BookingsController_createReccuringBooking",
|
||||
"parameters": [],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "x-cal-client-id",
|
||||
"required": true,
|
||||
"in": "header",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
@@ -1400,7 +1426,16 @@
|
||||
"/api/v2/ee/bookings/instant": {
|
||||
"post": {
|
||||
"operationId": "BookingsController_createInstantBooking",
|
||||
"parameters": [],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "x-cal-client-id",
|
||||
"required": true,
|
||||
"in": "header",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
@@ -1912,6 +1947,18 @@
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"bookingRedirectUri": {
|
||||
"type": "string"
|
||||
},
|
||||
"bookingCancelRedirectUri": {
|
||||
"type": "string"
|
||||
},
|
||||
"bookingRescheduleRedirectUri": {
|
||||
"type": "string"
|
||||
},
|
||||
"areEmailsEnabled": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -9,6 +9,7 @@ import dayjs from "@calcom/dayjs";
|
||||
import { getFeatureFlag } from "@calcom/features/flags/server/utils";
|
||||
import { getLocation, getRichDescription } from "@calcom/lib/CalEventParser";
|
||||
import type CalendarService from "@calcom/lib/CalendarService";
|
||||
import { formatCalEvent } from "@calcom/lib/formatCalendarEvent";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import prisma from "@calcom/prisma";
|
||||
@@ -187,61 +188,65 @@ export default class GoogleCalendarService implements Calendar {
|
||||
};
|
||||
|
||||
async createEvent(calEventRaw: CalendarEvent, credentialId: number): Promise<NewCalendarEventType> {
|
||||
const formattedCalEvent = formatCalEvent(calEventRaw);
|
||||
|
||||
const payload: calendar_v3.Schema$Event = {
|
||||
summary: calEventRaw.title,
|
||||
description: getRichDescription(calEventRaw),
|
||||
summary: formattedCalEvent.title,
|
||||
description: getRichDescription(formattedCalEvent),
|
||||
start: {
|
||||
dateTime: calEventRaw.startTime,
|
||||
timeZone: calEventRaw.organizer.timeZone,
|
||||
dateTime: formattedCalEvent.startTime,
|
||||
timeZone: formattedCalEvent.organizer.timeZone,
|
||||
},
|
||||
end: {
|
||||
dateTime: calEventRaw.endTime,
|
||||
timeZone: calEventRaw.organizer.timeZone,
|
||||
dateTime: formattedCalEvent.endTime,
|
||||
timeZone: formattedCalEvent.organizer.timeZone,
|
||||
},
|
||||
attendees: this.getAttendees(calEventRaw),
|
||||
attendees: this.getAttendees(formattedCalEvent),
|
||||
reminders: {
|
||||
useDefault: true,
|
||||
},
|
||||
guestsCanSeeOtherGuests: !!calEventRaw.seatsPerTimeSlot ? calEventRaw.seatsShowAttendees : true,
|
||||
iCalUID: calEventRaw.iCalUID,
|
||||
guestsCanSeeOtherGuests: !!formattedCalEvent.seatsPerTimeSlot
|
||||
? formattedCalEvent.seatsShowAttendees
|
||||
: true,
|
||||
iCalUID: formattedCalEvent.iCalUID,
|
||||
};
|
||||
|
||||
if (calEventRaw.location) {
|
||||
payload["location"] = getLocation(calEventRaw);
|
||||
if (formattedCalEvent.location) {
|
||||
payload["location"] = getLocation(formattedCalEvent);
|
||||
}
|
||||
|
||||
if (calEventRaw.recurringEvent) {
|
||||
if (formattedCalEvent.recurringEvent) {
|
||||
const rule = new RRule({
|
||||
freq: calEventRaw.recurringEvent.freq,
|
||||
interval: calEventRaw.recurringEvent.interval,
|
||||
count: calEventRaw.recurringEvent.count,
|
||||
freq: formattedCalEvent.recurringEvent.freq,
|
||||
interval: formattedCalEvent.recurringEvent.interval,
|
||||
count: formattedCalEvent.recurringEvent.count,
|
||||
});
|
||||
|
||||
payload["recurrence"] = [rule.toString()];
|
||||
}
|
||||
|
||||
if (calEventRaw.conferenceData && calEventRaw.location === MeetLocationType) {
|
||||
payload["conferenceData"] = calEventRaw.conferenceData;
|
||||
if (formattedCalEvent.conferenceData && formattedCalEvent.location === MeetLocationType) {
|
||||
payload["conferenceData"] = formattedCalEvent.conferenceData;
|
||||
}
|
||||
const calendar = await this.authedCalendar();
|
||||
// Find in calEventRaw.destinationCalendar the one with the same credentialId
|
||||
// Find in formattedCalEvent.destinationCalendar the one with the same credentialId
|
||||
|
||||
const selectedCalendar =
|
||||
calEventRaw.destinationCalendar?.find((cal) => cal.credentialId === credentialId)?.externalId ||
|
||||
formattedCalEvent.destinationCalendar?.find((cal) => cal.credentialId === credentialId)?.externalId ||
|
||||
"primary";
|
||||
|
||||
try {
|
||||
let event;
|
||||
let recurringEventId = null;
|
||||
if (calEventRaw.existingRecurringEvent) {
|
||||
recurringEventId = calEventRaw.existingRecurringEvent.recurringEventId;
|
||||
if (formattedCalEvent.existingRecurringEvent) {
|
||||
recurringEventId = formattedCalEvent.existingRecurringEvent.recurringEventId;
|
||||
const recurringEventInstances = await calendar.events.instances({
|
||||
calendarId: selectedCalendar,
|
||||
eventId: calEventRaw.existingRecurringEvent.recurringEventId,
|
||||
eventId: formattedCalEvent.existingRecurringEvent.recurringEventId,
|
||||
});
|
||||
if (recurringEventInstances.data.items) {
|
||||
const calComEventStartTime = dayjs(calEventRaw.startTime)
|
||||
.tz(calEventRaw.organizer.timeZone)
|
||||
const calComEventStartTime = dayjs(formattedCalEvent.startTime)
|
||||
.tz(formattedCalEvent.organizer.timeZone)
|
||||
.format();
|
||||
for (let i = 0; i < recurringEventInstances.data.items.length; i++) {
|
||||
const instance = recurringEventInstances.data.items[i];
|
||||
@@ -266,9 +271,9 @@ export default class GoogleCalendarService implements Calendar {
|
||||
calendarId: selectedCalendar,
|
||||
eventId: event.id || "",
|
||||
requestBody: {
|
||||
location: getLocation(calEventRaw),
|
||||
location: getLocation(formattedCalEvent),
|
||||
description: getRichDescription({
|
||||
...calEventRaw,
|
||||
...formattedCalEvent,
|
||||
}),
|
||||
},
|
||||
});
|
||||
@@ -296,7 +301,7 @@ export default class GoogleCalendarService implements Calendar {
|
||||
eventId: event.id || "",
|
||||
requestBody: {
|
||||
description: getRichDescription({
|
||||
...calEventRaw,
|
||||
...formattedCalEvent,
|
||||
additionalInformation: { hangoutLink: event.hangoutLink },
|
||||
}),
|
||||
},
|
||||
@@ -342,37 +347,42 @@ export default class GoogleCalendarService implements Calendar {
|
||||
}
|
||||
|
||||
async updateEvent(uid: string, event: CalendarEvent, externalCalendarId: string): Promise<any> {
|
||||
const formattedCalEvent = formatCalEvent(event);
|
||||
|
||||
const payload: calendar_v3.Schema$Event = {
|
||||
summary: event.title,
|
||||
description: getRichDescription(event),
|
||||
summary: formattedCalEvent.title,
|
||||
description: getRichDescription(formattedCalEvent),
|
||||
start: {
|
||||
dateTime: event.startTime,
|
||||
timeZone: event.organizer.timeZone,
|
||||
dateTime: formattedCalEvent.startTime,
|
||||
timeZone: formattedCalEvent.organizer.timeZone,
|
||||
},
|
||||
end: {
|
||||
dateTime: event.endTime,
|
||||
timeZone: event.organizer.timeZone,
|
||||
dateTime: formattedCalEvent.endTime,
|
||||
timeZone: formattedCalEvent.organizer.timeZone,
|
||||
},
|
||||
attendees: this.getAttendees(event),
|
||||
attendees: this.getAttendees(formattedCalEvent),
|
||||
reminders: {
|
||||
useDefault: true,
|
||||
},
|
||||
guestsCanSeeOtherGuests: !!event.seatsPerTimeSlot ? event.seatsShowAttendees : true,
|
||||
guestsCanSeeOtherGuests: !!formattedCalEvent.seatsPerTimeSlot
|
||||
? formattedCalEvent.seatsShowAttendees
|
||||
: true,
|
||||
};
|
||||
|
||||
if (event.location) {
|
||||
payload["location"] = getLocation(event);
|
||||
if (formattedCalEvent.location) {
|
||||
payload["location"] = getLocation(formattedCalEvent);
|
||||
}
|
||||
|
||||
if (event.conferenceData && event.location === MeetLocationType) {
|
||||
payload["conferenceData"] = event.conferenceData;
|
||||
if (formattedCalEvent.conferenceData && formattedCalEvent.location === MeetLocationType) {
|
||||
payload["conferenceData"] = formattedCalEvent.conferenceData;
|
||||
}
|
||||
|
||||
const calendar = await this.authedCalendar();
|
||||
|
||||
const selectedCalendar =
|
||||
(externalCalendarId
|
||||
? event.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId)?.externalId
|
||||
? formattedCalEvent.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId)
|
||||
?.externalId
|
||||
: undefined) || "primary";
|
||||
|
||||
try {
|
||||
@@ -397,7 +407,7 @@ export default class GoogleCalendarService implements Calendar {
|
||||
eventId: evt.data.id || "",
|
||||
requestBody: {
|
||||
description: getRichDescription({
|
||||
...event,
|
||||
...formattedCalEvent,
|
||||
additionalInformation: { hangoutLink: evt.data.hangoutLink },
|
||||
}),
|
||||
},
|
||||
@@ -419,7 +429,7 @@ export default class GoogleCalendarService implements Calendar {
|
||||
} catch (error) {
|
||||
this.log.error(
|
||||
"There was an error updating event in google calendar: ",
|
||||
safeStringify({ error, event, uid })
|
||||
safeStringify({ error, event: formattedCalEvent, uid })
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { TFunction } from "next-i18next";
|
||||
import type { EventNameObjectType } from "@calcom/core/event";
|
||||
import { getEventName } from "@calcom/core/event";
|
||||
import type BaseEmail from "@calcom/emails/templates/_base-email";
|
||||
import { formatCalEvent } from "@calcom/lib/formatCalendarEvent";
|
||||
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
||||
|
||||
import type { MonthlyDigestEmailData } from "./src/templates/MonthlyDigestEmail";
|
||||
@@ -73,27 +74,30 @@ export const sendScheduledEmails = async (
|
||||
hostEmailDisabled?: boolean,
|
||||
attendeeEmailDisabled?: boolean
|
||||
) => {
|
||||
const formattedCalEvent = formatCalEvent(calEvent);
|
||||
const emailsToSend: Promise<unknown>[] = [];
|
||||
|
||||
if (!hostEmailDisabled) {
|
||||
emailsToSend.push(sendEmail(() => new OrganizerScheduledEmail({ calEvent })));
|
||||
emailsToSend.push(sendEmail(() => new OrganizerScheduledEmail({ calEvent: formattedCalEvent })));
|
||||
|
||||
if (calEvent.team) {
|
||||
for (const teamMember of calEvent.team.members) {
|
||||
emailsToSend.push(sendEmail(() => new OrganizerScheduledEmail({ calEvent, teamMember })));
|
||||
if (formattedCalEvent.team) {
|
||||
for (const teamMember of formattedCalEvent.team.members) {
|
||||
emailsToSend.push(
|
||||
sendEmail(() => new OrganizerScheduledEmail({ calEvent: formattedCalEvent, teamMember }))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!attendeeEmailDisabled) {
|
||||
emailsToSend.push(
|
||||
...calEvent.attendees.map((attendee) => {
|
||||
...formattedCalEvent.attendees.map((attendee) => {
|
||||
return sendEmail(
|
||||
() =>
|
||||
new AttendeeScheduledEmail(
|
||||
{
|
||||
...calEvent,
|
||||
...(calEvent.hideCalendarNotes && { additionalNotes: undefined }),
|
||||
...formattedCalEvent,
|
||||
...(formattedCalEvent.hideCalendarNotes && { additionalNotes: undefined }),
|
||||
...(eventNameObject && {
|
||||
title: getEventName({ ...eventNameObject, t: attendee.language.translate }),
|
||||
}),
|
||||
@@ -110,49 +114,59 @@ export const sendScheduledEmails = async (
|
||||
|
||||
// for rescheduled round robin booking that assigned new members
|
||||
export const sendRoundRobinScheduledEmails = async (calEvent: CalendarEvent, members: Person[]) => {
|
||||
const formattedCalEvent = formatCalEvent(calEvent);
|
||||
const emailsToSend: Promise<unknown>[] = [];
|
||||
|
||||
for (const teamMember of members) {
|
||||
emailsToSend.push(sendEmail(() => new OrganizerScheduledEmail({ calEvent, teamMember })));
|
||||
emailsToSend.push(
|
||||
sendEmail(() => new OrganizerScheduledEmail({ calEvent: formattedCalEvent, teamMember }))
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(emailsToSend);
|
||||
};
|
||||
|
||||
export const sendRoundRobinRescheduledEmails = async (calEvent: CalendarEvent, members: Person[]) => {
|
||||
const calendarEvent = formatCalEvent(calEvent);
|
||||
const emailsToSend: Promise<unknown>[] = [];
|
||||
|
||||
for (const teamMember of members) {
|
||||
emailsToSend.push(sendEmail(() => new OrganizerRescheduledEmail({ calEvent, teamMember })));
|
||||
emailsToSend.push(
|
||||
sendEmail(() => new OrganizerRescheduledEmail({ calEvent: calendarEvent, teamMember }))
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(emailsToSend);
|
||||
};
|
||||
|
||||
export const sendRoundRobinCancelledEmails = async (calEvent: CalendarEvent, members: Person[]) => {
|
||||
const calendarEvent = formatCalEvent(calEvent);
|
||||
const emailsToSend: Promise<unknown>[] = [];
|
||||
|
||||
for (const teamMember of members) {
|
||||
emailsToSend.push(sendEmail(() => new OrganizerCancelledEmail({ calEvent, teamMember })));
|
||||
emailsToSend.push(sendEmail(() => new OrganizerCancelledEmail({ calEvent: calendarEvent, teamMember })));
|
||||
}
|
||||
|
||||
await Promise.all(emailsToSend);
|
||||
};
|
||||
|
||||
export const sendRescheduledEmails = async (calEvent: CalendarEvent) => {
|
||||
const calendarEvent = formatCalEvent(calEvent);
|
||||
const emailsToSend: Promise<unknown>[] = [];
|
||||
|
||||
emailsToSend.push(sendEmail(() => new OrganizerRescheduledEmail({ calEvent })));
|
||||
emailsToSend.push(sendEmail(() => new OrganizerRescheduledEmail({ calEvent: calendarEvent })));
|
||||
|
||||
if (calEvent.team) {
|
||||
for (const teamMember of calEvent.team.members) {
|
||||
emailsToSend.push(sendEmail(() => new OrganizerRescheduledEmail({ calEvent, teamMember })));
|
||||
if (calendarEvent.team) {
|
||||
for (const teamMember of calendarEvent.team.members) {
|
||||
emailsToSend.push(
|
||||
sendEmail(() => new OrganizerRescheduledEmail({ calEvent: calendarEvent, teamMember }))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
emailsToSend.push(
|
||||
...calEvent.attendees.map((attendee) => {
|
||||
return sendEmail(() => new AttendeeRescheduledEmail(calEvent, attendee));
|
||||
...calendarEvent.attendees.map((attendee) => {
|
||||
return sendEmail(() => new AttendeeRescheduledEmail(calendarEvent, attendee));
|
||||
})
|
||||
);
|
||||
|
||||
@@ -160,10 +174,12 @@ export const sendRescheduledEmails = async (calEvent: CalendarEvent) => {
|
||||
};
|
||||
|
||||
export const sendRescheduledSeatEmail = async (calEvent: CalendarEvent, attendee: Person) => {
|
||||
const clonedCalEvent = cloneDeep(calEvent);
|
||||
const calendarEvent = formatCalEvent(calEvent);
|
||||
|
||||
const clonedCalEvent = cloneDeep(calendarEvent);
|
||||
const emailsToSend: Promise<unknown>[] = [
|
||||
sendEmail(() => new AttendeeRescheduledEmail(clonedCalEvent, attendee)),
|
||||
sendEmail(() => new OrganizerRescheduledEmail({ calEvent })),
|
||||
sendEmail(() => new OrganizerRescheduledEmail({ calEvent: calendarEvent })),
|
||||
];
|
||||
|
||||
await Promise.all(emailsToSend);
|
||||
@@ -177,14 +193,18 @@ export const sendScheduledSeatsEmails = async (
|
||||
hostEmailDisabled?: boolean,
|
||||
attendeeEmailDisabled?: boolean
|
||||
) => {
|
||||
const calendarEvent = formatCalEvent(calEvent);
|
||||
|
||||
const emailsToSend: Promise<unknown>[] = [];
|
||||
|
||||
if (!hostEmailDisabled) {
|
||||
emailsToSend.push(sendEmail(() => new OrganizerScheduledEmail({ calEvent, newSeat })));
|
||||
emailsToSend.push(sendEmail(() => new OrganizerScheduledEmail({ calEvent: calendarEvent, newSeat })));
|
||||
|
||||
if (calEvent.team) {
|
||||
for (const teamMember of calEvent.team.members) {
|
||||
emailsToSend.push(sendEmail(() => new OrganizerScheduledEmail({ calEvent, newSeat, teamMember })));
|
||||
if (calendarEvent.team) {
|
||||
for (const teamMember of calendarEvent.team.members) {
|
||||
emailsToSend.push(
|
||||
sendEmail(() => new OrganizerScheduledEmail({ calEvent: calendarEvent, newSeat, teamMember }))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -195,8 +215,8 @@ export const sendScheduledSeatsEmails = async (
|
||||
() =>
|
||||
new AttendeeScheduledEmail(
|
||||
{
|
||||
...calEvent,
|
||||
...(calEvent.hideCalendarNotes && { additionalNotes: undefined }),
|
||||
...calendarEvent,
|
||||
...(calendarEvent.hideCalendarNotes && { additionalNotes: undefined }),
|
||||
},
|
||||
invitee,
|
||||
showAttendees
|
||||
@@ -208,21 +228,24 @@ export const sendScheduledSeatsEmails = async (
|
||||
};
|
||||
|
||||
export const sendCancelledSeatEmails = async (calEvent: CalendarEvent, cancelledAttendee: Person) => {
|
||||
const clonedCalEvent = cloneDeep(calEvent);
|
||||
const formattedCalEvent = formatCalEvent(calEvent);
|
||||
const clonedCalEvent = cloneDeep(formattedCalEvent);
|
||||
await Promise.all([
|
||||
sendEmail(() => new AttendeeCancelledSeatEmail(clonedCalEvent, cancelledAttendee)),
|
||||
sendEmail(() => new OrganizerAttendeeCancelledSeatEmail({ calEvent })),
|
||||
sendEmail(() => new OrganizerAttendeeCancelledSeatEmail({ calEvent: formattedCalEvent })),
|
||||
]);
|
||||
};
|
||||
|
||||
export const sendOrganizerRequestEmail = async (calEvent: CalendarEvent) => {
|
||||
const calendarEvent = formatCalEvent(calEvent);
|
||||
|
||||
const emailsToSend: Promise<unknown>[] = [];
|
||||
|
||||
emailsToSend.push(sendEmail(() => new OrganizerRequestEmail({ calEvent })));
|
||||
emailsToSend.push(sendEmail(() => new OrganizerRequestEmail({ calEvent: calendarEvent })));
|
||||
|
||||
if (calEvent.team?.members) {
|
||||
for (const teamMember of calEvent.team.members) {
|
||||
emailsToSend.push(sendEmail(() => new OrganizerRequestEmail({ calEvent, teamMember })));
|
||||
if (calendarEvent.team?.members) {
|
||||
for (const teamMember of calendarEvent.team.members) {
|
||||
emailsToSend.push(sendEmail(() => new OrganizerRequestEmail({ calEvent: calendarEvent, teamMember })));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,15 +253,17 @@ export const sendOrganizerRequestEmail = async (calEvent: CalendarEvent) => {
|
||||
};
|
||||
|
||||
export const sendAttendeeRequestEmail = async (calEvent: CalendarEvent, attendee: Person) => {
|
||||
await sendEmail(() => new AttendeeRequestEmail(calEvent, attendee));
|
||||
const calendarEvent = formatCalEvent(calEvent);
|
||||
await sendEmail(() => new AttendeeRequestEmail(calendarEvent, attendee));
|
||||
};
|
||||
|
||||
export const sendDeclinedEmails = async (calEvent: CalendarEvent) => {
|
||||
const calendarEvent = formatCalEvent(calEvent);
|
||||
const emailsToSend: Promise<unknown>[] = [];
|
||||
|
||||
emailsToSend.push(
|
||||
...calEvent.attendees.map((attendee) => {
|
||||
return sendEmail(() => new AttendeeDeclinedEmail(calEvent, attendee));
|
||||
...calendarEvent.attendees.map((attendee) => {
|
||||
return sendEmail(() => new AttendeeDeclinedEmail(calendarEvent, attendee));
|
||||
})
|
||||
);
|
||||
|
||||
@@ -249,31 +274,34 @@ export const sendCancelledEmails = async (
|
||||
calEvent: CalendarEvent,
|
||||
eventNameObject: Pick<EventNameObjectType, "eventName">
|
||||
) => {
|
||||
const calendarEvent = formatCalEvent(calEvent);
|
||||
const emailsToSend: Promise<unknown>[] = [];
|
||||
|
||||
emailsToSend.push(sendEmail(() => new OrganizerCancelledEmail({ calEvent })));
|
||||
emailsToSend.push(sendEmail(() => new OrganizerCancelledEmail({ calEvent: calendarEvent })));
|
||||
|
||||
if (calEvent.team?.members) {
|
||||
for (const teamMember of calEvent.team.members) {
|
||||
emailsToSend.push(sendEmail(() => new OrganizerCancelledEmail({ calEvent, teamMember })));
|
||||
if (calendarEvent.team?.members) {
|
||||
for (const teamMember of calendarEvent.team.members) {
|
||||
emailsToSend.push(
|
||||
sendEmail(() => new OrganizerCancelledEmail({ calEvent: calendarEvent, teamMember }))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
emailsToSend.push(
|
||||
...calEvent.attendees.map((attendee) => {
|
||||
...calendarEvent.attendees.map((attendee) => {
|
||||
return sendEmail(
|
||||
() =>
|
||||
new AttendeeCancelledEmail(
|
||||
{
|
||||
...calEvent,
|
||||
...calendarEvent,
|
||||
title: getEventName({
|
||||
...eventNameObject,
|
||||
t: attendee.language.translate,
|
||||
attendeeName: attendee.name,
|
||||
host: calEvent.organizer.name,
|
||||
eventType: calEvent.type,
|
||||
...(calEvent.responses && { bookingFields: calEvent.responses }),
|
||||
...(calEvent.location && { location: calEvent.location }),
|
||||
host: calendarEvent.organizer.name,
|
||||
eventType: calendarEvent.type,
|
||||
...(calendarEvent.responses && { bookingFields: calendarEvent.responses }),
|
||||
...(calendarEvent.location && { location: calendarEvent.location }),
|
||||
}),
|
||||
},
|
||||
attendee
|
||||
@@ -286,13 +314,17 @@ export const sendCancelledEmails = async (
|
||||
};
|
||||
|
||||
export const sendOrganizerRequestReminderEmail = async (calEvent: CalendarEvent) => {
|
||||
const calendarEvent = formatCalEvent(calEvent);
|
||||
|
||||
const emailsToSend: Promise<unknown>[] = [];
|
||||
|
||||
emailsToSend.push(sendEmail(() => new OrganizerRequestReminderEmail({ calEvent })));
|
||||
emailsToSend.push(sendEmail(() => new OrganizerRequestReminderEmail({ calEvent: calendarEvent })));
|
||||
|
||||
if (calEvent.team?.members) {
|
||||
for (const teamMember of calEvent.team.members) {
|
||||
emailsToSend.push(sendEmail(() => new OrganizerRequestReminderEmail({ calEvent, teamMember })));
|
||||
if (calendarEvent.team?.members) {
|
||||
for (const teamMember of calendarEvent.team.members) {
|
||||
emailsToSend.push(
|
||||
sendEmail(() => new OrganizerRequestReminderEmail({ calEvent: calendarEvent, teamMember }))
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -354,28 +386,33 @@ export const sendRequestRescheduleEmail = async (
|
||||
metadata: { rescheduleLink: string }
|
||||
) => {
|
||||
const emailsToSend: Promise<unknown>[] = [];
|
||||
const calendarEvent = formatCalEvent(calEvent);
|
||||
|
||||
emailsToSend.push(sendEmail(() => new OrganizerRequestedToRescheduleEmail(calEvent, metadata)));
|
||||
emailsToSend.push(sendEmail(() => new OrganizerRequestedToRescheduleEmail(calendarEvent, metadata)));
|
||||
|
||||
emailsToSend.push(sendEmail(() => new AttendeeWasRequestedToRescheduleEmail(calEvent, metadata)));
|
||||
emailsToSend.push(sendEmail(() => new AttendeeWasRequestedToRescheduleEmail(calendarEvent, metadata)));
|
||||
|
||||
await Promise.all(emailsToSend);
|
||||
};
|
||||
|
||||
export const sendLocationChangeEmails = async (calEvent: CalendarEvent) => {
|
||||
const calendarEvent = formatCalEvent(calEvent);
|
||||
|
||||
const emailsToSend: Promise<unknown>[] = [];
|
||||
|
||||
emailsToSend.push(sendEmail(() => new OrganizerLocationChangeEmail({ calEvent })));
|
||||
emailsToSend.push(sendEmail(() => new OrganizerLocationChangeEmail({ calEvent: calendarEvent })));
|
||||
|
||||
if (calEvent.team?.members) {
|
||||
for (const teamMember of calEvent.team.members) {
|
||||
emailsToSend.push(sendEmail(() => new OrganizerLocationChangeEmail({ calEvent, teamMember })));
|
||||
if (calendarEvent.team?.members) {
|
||||
for (const teamMember of calendarEvent.team.members) {
|
||||
emailsToSend.push(
|
||||
sendEmail(() => new OrganizerLocationChangeEmail({ calEvent: calendarEvent, teamMember }))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
emailsToSend.push(
|
||||
...calEvent.attendees.map((attendee) => {
|
||||
return sendEmail(() => new AttendeeLocationChangeEmail(calEvent, attendee));
|
||||
...calendarEvent.attendees.map((attendee) => {
|
||||
return sendEmail(() => new AttendeeLocationChangeEmail(calendarEvent, attendee));
|
||||
})
|
||||
);
|
||||
|
||||
@@ -386,7 +423,8 @@ export const sendFeedbackEmail = async (feedback: Feedback) => {
|
||||
};
|
||||
|
||||
export const sendBrokenIntegrationEmail = async (evt: CalendarEvent, type: "video" | "calendar") => {
|
||||
await sendEmail(() => new BrokenIntegrationEmail(evt, type));
|
||||
const calendarEvent = formatCalEvent(evt);
|
||||
await sendEmail(() => new BrokenIntegrationEmail(calendarEvent, type));
|
||||
};
|
||||
|
||||
export const sendDisabledAppEmail = async ({
|
||||
@@ -428,13 +466,16 @@ export const sendNoShowFeeChargedEmail = async (attendee: Person, evt: CalendarE
|
||||
};
|
||||
|
||||
export const sendDailyVideoRecordingEmails = async (calEvent: CalendarEvent, downloadLink: string) => {
|
||||
const calendarEvent = formatCalEvent(calEvent);
|
||||
const emailsToSend: Promise<unknown>[] = [];
|
||||
|
||||
emailsToSend.push(sendEmail(() => new OrganizerDailyVideoDownloadRecordingEmail(calEvent, downloadLink)));
|
||||
emailsToSend.push(
|
||||
sendEmail(() => new OrganizerDailyVideoDownloadRecordingEmail(calendarEvent, downloadLink))
|
||||
);
|
||||
|
||||
for (const attendee of calEvent.attendees) {
|
||||
for (const attendee of calendarEvent.attendees) {
|
||||
emailsToSend.push(
|
||||
sendEmail(() => new AttendeeDailyVideoDownloadRecordingEmail(calEvent, attendee, downloadLink))
|
||||
sendEmail(() => new AttendeeDailyVideoDownloadRecordingEmail(calendarEvent, attendee, downloadLink))
|
||||
);
|
||||
}
|
||||
await Promise.all(emailsToSend);
|
||||
|
||||
@@ -23,6 +23,7 @@ export const BaseEmailHtml = (props: {
|
||||
title?: string;
|
||||
subtitle?: React.ReactNode | string;
|
||||
headerType?: BodyHeadType;
|
||||
hideLogo?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<Html>
|
||||
@@ -196,7 +197,7 @@ export const BaseEmailHtml = (props: {
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
<EmailBodyLogo />
|
||||
{!Boolean(props.hideLogo) && <EmailBodyLogo />}
|
||||
<RawHtml html="<!--[if mso | IE]></td></tr></table><![endif]-->" />
|
||||
</div>
|
||||
</body>
|
||||
|
||||
@@ -50,6 +50,7 @@ export const BaseScheduledEmail = (
|
||||
|
||||
return (
|
||||
<BaseEmailHtml
|
||||
hideLogo={Boolean(props.calEvent.platformClientId)}
|
||||
headerType={props.headerType || "checkCircle"}
|
||||
subject={props.subject || subject}
|
||||
title={t(
|
||||
|
||||
@@ -125,13 +125,24 @@ async function getBookingToDelete(id: number | undefined, uid: string | undefine
|
||||
export type CustomRequest = NextApiRequest & {
|
||||
userId?: number;
|
||||
bookingToDelete?: Awaited<ReturnType<typeof getBookingToDelete>>;
|
||||
platformClientId?: string;
|
||||
platformRescheduleUrl?: string;
|
||||
platformCancelUrl?: string;
|
||||
platformBookingUrl?: string;
|
||||
};
|
||||
|
||||
async function handler(req: CustomRequest) {
|
||||
const { id, uid, allRemainingBookings, cancellationReason, seatReferenceUid } =
|
||||
schemaBookingCancelParams.parse(req.body);
|
||||
req.bookingToDelete = await getBookingToDelete(id, uid);
|
||||
const { bookingToDelete, userId } = req;
|
||||
const {
|
||||
bookingToDelete,
|
||||
userId,
|
||||
platformBookingUrl,
|
||||
platformCancelUrl,
|
||||
platformClientId,
|
||||
platformRescheduleUrl,
|
||||
} = req;
|
||||
|
||||
if (!bookingToDelete || !bookingToDelete.user) {
|
||||
throw new HttpError({ statusCode: 400, message: "Booking not found" });
|
||||
@@ -269,6 +280,10 @@ async function handler(req: CustomRequest) {
|
||||
seatsShowAttendees: bookingToDelete.eventType?.seatsShowAttendees,
|
||||
iCalUID: bookingToDelete.iCalUID,
|
||||
iCalSequence: bookingToDelete.iCalSequence + 1,
|
||||
platformClientId,
|
||||
platformRescheduleUrl,
|
||||
platformCancelUrl,
|
||||
platformBookingUrl,
|
||||
};
|
||||
|
||||
const dataForWebhooks = { evt, webhooks, eventTypeInfo };
|
||||
|
||||
@@ -902,10 +902,16 @@ type BookingDataSchemaGetter =
|
||||
| typeof import("@calcom/features/bookings/lib/getBookingDataSchemaForApi").default;
|
||||
|
||||
async function handler(
|
||||
req: NextApiRequest & { userId?: number | undefined },
|
||||
req: NextApiRequest & {
|
||||
userId?: number | undefined;
|
||||
platformClientId?: string;
|
||||
platformRescheduleUrl?: string;
|
||||
platformCancelUrl?: string;
|
||||
platformBookingUrl?: string;
|
||||
},
|
||||
bookingDataSchemaGetter: BookingDataSchemaGetter = getBookingDataSchema
|
||||
) {
|
||||
const { userId } = req;
|
||||
const { userId, platformClientId, platformCancelUrl, platformBookingUrl, platformRescheduleUrl } = req;
|
||||
|
||||
// handle dynamic user
|
||||
let eventType =
|
||||
@@ -1503,6 +1509,10 @@ async function handler(
|
||||
schedulingType: eventType.schedulingType,
|
||||
iCalUID,
|
||||
iCalSequence,
|
||||
platformClientId,
|
||||
platformRescheduleUrl,
|
||||
platformCancelUrl,
|
||||
platformBookingUrl,
|
||||
};
|
||||
|
||||
if (req.body.thirdPartyRecurringEventId) {
|
||||
|
||||
@@ -154,25 +154,94 @@ const getSeatReferenceId = (calEvent: CalendarEvent): string => {
|
||||
return calEvent.attendeeSeatId ? calEvent.attendeeSeatId : "";
|
||||
};
|
||||
|
||||
export const getPlatformManageLink = (calEvent: CalendarEvent, t: TFunction) => {
|
||||
if (calEvent.platformBookingUrl) {
|
||||
return `${t("need_to_reschedule_or_cancel")} ${calEvent.platformBookingUrl}/${getUid(calEvent)}?slug=${
|
||||
calEvent.type
|
||||
}&username=${calEvent.organizer.username}&changes=true`;
|
||||
}
|
||||
let res = "";
|
||||
if (calEvent.platformCancelUrl) {
|
||||
res += `${t("cancel")}: ${getCancelLink(calEvent)}`;
|
||||
}
|
||||
|
||||
if (calEvent.platformRescheduleUrl) {
|
||||
res += `${calEvent.platformCancelUrl ? ` ${t("or_lowercase")} ` : ""}${t(
|
||||
"reschedule"
|
||||
)}: ${getRescheduleLink(calEvent)}`;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getManageLink = (calEvent: CalendarEvent, t: TFunction) => {
|
||||
return `${t("need_to_reschedule_or_cancel")}${calEvent.bookerUrl ?? WEBAPP_URL}/booking/${getUid(
|
||||
if (calEvent.platformClientId) {
|
||||
return getPlatformManageLink(calEvent, t);
|
||||
}
|
||||
|
||||
return `${t("need_to_reschedule_or_cancel")} ${calEvent.bookerUrl ?? WEBAPP_URL}/booking/${getUid(
|
||||
calEvent
|
||||
)}?changes=true`;
|
||||
};
|
||||
|
||||
export const getPlatformCancelLink = (
|
||||
calEvent: CalendarEvent,
|
||||
bookingUid: string,
|
||||
seatUid?: string
|
||||
): string => {
|
||||
if (calEvent.platformCancelUrl) {
|
||||
const platformCancelLink = new URL(`${calEvent.platformCancelUrl}/${bookingUid}`);
|
||||
platformCancelLink.searchParams.append("slug", calEvent.type);
|
||||
calEvent.organizer.username &&
|
||||
platformCancelLink.searchParams.append("username", calEvent.organizer.username);
|
||||
platformCancelLink.searchParams.append("cancel", "true");
|
||||
platformCancelLink.searchParams.append("allRemainingBookings", String(!!calEvent.recurringEvent));
|
||||
if (seatUid) platformCancelLink.searchParams.append("seatReferenceUid", seatUid);
|
||||
return platformCancelLink.toString();
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
export const getCancelLink = (calEvent: CalendarEvent): string => {
|
||||
const cancelLink = new URL(`${calEvent.bookerUrl ?? WEBAPP_URL}/booking/${getUid(calEvent)}`);
|
||||
const Uid = getUid(calEvent);
|
||||
const seatReferenceUid = getSeatReferenceId(calEvent);
|
||||
if (calEvent.platformClientId) {
|
||||
return getPlatformCancelLink(calEvent, Uid, seatReferenceUid);
|
||||
}
|
||||
|
||||
const cancelLink = new URL(`${calEvent.bookerUrl ?? WEBAPP_URL}/booking/${Uid}`);
|
||||
cancelLink.searchParams.append("cancel", "true");
|
||||
cancelLink.searchParams.append("allRemainingBookings", String(!!calEvent.recurringEvent));
|
||||
const seatReferenceUid = getSeatReferenceId(calEvent);
|
||||
if (seatReferenceUid) cancelLink.searchParams.append("seatReferenceUid", seatReferenceUid);
|
||||
return cancelLink.toString();
|
||||
};
|
||||
|
||||
export const getPlatformRescheduleLink = (
|
||||
calEvent: CalendarEvent,
|
||||
bookingUid: string,
|
||||
seatUid?: string
|
||||
): string => {
|
||||
if (calEvent.platformRescheduleUrl) {
|
||||
const platformRescheduleLink = new URL(
|
||||
`${calEvent.platformRescheduleUrl}/${seatUid ? seatUid : bookingUid}`
|
||||
);
|
||||
platformRescheduleLink.searchParams.append("slug", calEvent.type);
|
||||
calEvent.organizer.username &&
|
||||
platformRescheduleLink.searchParams.append("username", calEvent.organizer.username);
|
||||
platformRescheduleLink.searchParams.append("reschedule", "true");
|
||||
return platformRescheduleLink.toString();
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
export const getRescheduleLink = (calEvent: CalendarEvent): string => {
|
||||
const Uid = getUid(calEvent);
|
||||
const seatUid = getSeatReferenceId(calEvent);
|
||||
|
||||
if (calEvent.platformClientId) {
|
||||
return getPlatformRescheduleLink(calEvent, Uid, seatUid);
|
||||
}
|
||||
|
||||
return `${calEvent.bookerUrl ?? WEBAPP_URL}/reschedule/${seatUid ? seatUid : Uid}`;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { cloneDeep } from "lodash";
|
||||
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
|
||||
// format CalEvent to remove platformClientId from email addresses
|
||||
const formatClientIdFromEmails = (calEvent: CalendarEvent, clientId: string) => {
|
||||
const attendees = calEvent.attendees.map((attendee) => ({
|
||||
...attendee,
|
||||
email: attendee.email.replace(`+${clientId}`, ""),
|
||||
}));
|
||||
const organizer = {
|
||||
...calEvent.organizer,
|
||||
email: calEvent.organizer.email.replace(`+${clientId}`, ""),
|
||||
};
|
||||
return [attendees, organizer];
|
||||
};
|
||||
|
||||
export const formatCalEvent = (calEvent: CalendarEvent) => {
|
||||
const clonedEvent = cloneDeep(calEvent);
|
||||
if (clonedEvent.platformClientId) {
|
||||
const [attendees, organizer] = formatClientIdFromEmails(clonedEvent, clonedEvent.platformClientId);
|
||||
Object.assign(clonedEvent, { attendees, organizer });
|
||||
}
|
||||
|
||||
return clonedEvent;
|
||||
};
|
||||
@@ -30,13 +30,17 @@ export const useOAuthClient = ({ clientId, apiUrl, refreshUrl, onError, onSucces
|
||||
useEffect(() => {
|
||||
if (clientId && http.getUrl() && prevClientId !== clientId) {
|
||||
try {
|
||||
http.get<ApiResponse>(`/ee/provider/${clientId}`).catch((err: AxiosError) => {
|
||||
if (err.response?.status === 401) {
|
||||
onError("Invalid oAuth Client.");
|
||||
} else {
|
||||
http
|
||||
.get<ApiResponse>(`/ee/provider/${clientId}`)
|
||||
.then(() => {
|
||||
onSuccess();
|
||||
}
|
||||
});
|
||||
http.setClientIdHeader(clientId);
|
||||
})
|
||||
.catch((err: AxiosError) => {
|
||||
if (err.response?.status === 401) {
|
||||
onError("Invalid oAuth Client.");
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
@@ -34,6 +34,12 @@ const http = (function () {
|
||||
getAuthorizationHeader: () => {
|
||||
return instance.defaults.headers.common?.["Authorization"]?.toString() ?? "";
|
||||
},
|
||||
setClientIdHeader: (clientId: string) => {
|
||||
instance.defaults.headers.common["x-cal-client-id"] = clientId;
|
||||
},
|
||||
getClientIdHeader: () => {
|
||||
return instance.defaults.headers.common?.["x-cal-client-id"]?.toString() ?? "";
|
||||
},
|
||||
refreshTokens: async (refreshUrl: string): Promise<string> => {
|
||||
const response = await fetch(`${refreshUrl}`, {
|
||||
method: "GET",
|
||||
|
||||
@@ -46,6 +46,7 @@ export const API_ERROR_CODES = [
|
||||
|
||||
// Request headers
|
||||
export const X_CAL_SECRET_KEY = "x-cal-secret-key";
|
||||
export const X_CAL_CLIENT_ID = "x-cal-client-id";
|
||||
|
||||
// HTTP status codes
|
||||
export const HTTP_CODE_TOKEN_EXPIRED = 498;
|
||||
|
||||
Vendored
+4
@@ -200,6 +200,10 @@ export interface CalendarEvent {
|
||||
|
||||
// It just has responses to only the user fields. It allows to easily iterate over to show only user fields
|
||||
userFieldsResponses?: CalEventResponses | null;
|
||||
platformClientId?: string | null;
|
||||
platformRescheduleUrl?: string | null;
|
||||
platformCancelUrl?: string | null;
|
||||
platformBookingUrl?: string | null;
|
||||
}
|
||||
|
||||
export interface EntryPoint {
|
||||
|
||||
Reference in New Issue
Block a user