Files
calendar/packages/trpc/server/routers/viewer/bookings/editLocation.handler.ts
T

297 lines
8.8 KiB
TypeScript

import type { z } from "zod";
import { getEventLocationType, OrganizerDefaultConferencingAppType } from "@calcom/app-store/locations";
import { getAppFromSlug } from "@calcom/app-store/utils";
import EventManager from "@calcom/core/EventManager";
import { sendLocationChangeEmailsAndSMS } from "@calcom/emails";
import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser";
import { buildCalEventFromBooking } from "@calcom/lib/buildCalEventFromBooking";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import { getTranslation } from "@calcom/lib/server";
import { getUsersCredentials } from "@calcom/lib/server/getUsersCredentials";
import { CredentialRepository } from "@calcom/lib/server/repository/credential";
import { UserRepository } from "@calcom/lib/server/repository/user";
import { prisma } from "@calcom/prisma";
import type { Booking, BookingReference } from "@calcom/prisma/client";
import type { userMetadata } from "@calcom/prisma/zod-utils";
import type { EventTypeMetadata } from "@calcom/prisma/zod-utils";
import type { AdditionalInformation, CalendarEvent } from "@calcom/types/Calendar";
import type { CredentialPayload } from "@calcom/types/Credential";
import type { PartialReference } from "@calcom/types/EventManager";
import type { Ensure } from "@calcom/types/utils";
import { TRPCError } from "@trpc/server";
import type { TrpcSessionUser } from "../../../trpc";
import type { TEditLocationInputSchema } from "./editLocation.schema";
import type { BookingsProcedureContext } from "./util";
// #region EditLocation Types and Helpers
type EditLocationOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
} & BookingsProcedureContext;
input: TEditLocationInputSchema;
};
type UserMetadata = z.infer<typeof userMetadata>;
async function updateLocationInConnectedAppForBooking({
evt,
eventManager,
booking,
}: {
evt: CalendarEvent;
eventManager: EventManager;
booking: Booking & {
references: BookingReference[];
};
}) {
const updatedResult = await eventManager.updateLocation(evt, booking);
const results = updatedResult.results;
if (results.length > 0 && results.every((res) => !res.success)) {
const error = {
errorCode: "BookingUpdateLocationFailed",
message: "Updating location failed",
};
logger.error(`Updating location failed`, safeStringify(error), safeStringify(results));
throw new SystemError("Updating location failed");
}
logger.info(`Got results from updateLocationInConnectedApp`, safeStringify(updatedResult.results));
return updatedResult;
}
function extractAdditionalInformation(result: {
updatedEvent: AdditionalInformation;
}): AdditionalInformation {
const additionalInformation: AdditionalInformation = {};
if (result) {
additionalInformation.hangoutLink = result.updatedEvent?.hangoutLink;
additionalInformation.conferenceData = result.updatedEvent?.conferenceData;
additionalInformation.entryPoints = result.updatedEvent?.entryPoints;
}
return additionalInformation;
}
async function updateBookingLocationInDb({
booking,
evt,
references,
}: {
booking: {
id: number;
metadata: Booking["metadata"];
responses: Booking["responses"];
};
evt: Ensure<CalendarEvent, "location">;
references: PartialReference[];
}) {
const bookingMetadataUpdate = {
videoCallUrl: getVideoCallUrlFromCalEvent(evt),
};
const referencesToCreate = references.map((reference) => {
const { credentialId, ...restReference } = reference;
return {
...restReference,
...(credentialId && credentialId > 0 ? { credentialId } : {}),
};
});
await prisma.booking.update({
where: {
id: booking.id,
},
data: {
location: evt.location,
metadata: {
...(typeof booking.metadata === "object" && booking.metadata),
...bookingMetadataUpdate,
},
references: {
create: referencesToCreate,
},
responses: {
...(typeof booking.responses === "object" && booking.responses),
location: {
value: evt.location,
optionValue: "",
},
},
},
});
}
async function getAllCredentials({
user,
conferenceCredentialId,
}: {
user: { id: number };
conferenceCredentialId: number | null;
}) {
const credentials = await getUsersCredentials(user);
let conferenceCredential: CredentialPayload | null = null;
if (conferenceCredentialId) {
conferenceCredential = await CredentialRepository.findFirstByIdWithKeyAndUser({
id: conferenceCredentialId,
});
}
return [...(credentials ? credentials : []), ...(conferenceCredential ? [conferenceCredential] : [])];
}
async function getLocationInEvtFormatOrThrow({
location,
organizer,
loggedInUserTranslate,
}: {
location: string;
organizer: {
name: string | null;
metadata: UserMetadata;
};
loggedInUserTranslate: Awaited<ReturnType<typeof getTranslation>>;
}) {
if (location !== OrganizerDefaultConferencingAppType) {
return location;
}
try {
return getLocationForOrganizerDefaultConferencingAppInEvtFormat({
organizer: {
name: organizer.name ?? "Organizer",
metadata: organizer.metadata,
},
loggedInUserTranslate,
});
} catch (e) {
if (e instanceof UserError) {
throw new TRPCError({ code: "BAD_REQUEST", message: e.message });
}
logger.error(safeStringify(e));
throw e;
}
}
// #endregion
/**
* An error that should be shown to the user
*/
export class UserError extends Error {
constructor(message: string) {
super(message);
this.name = "LocationError";
}
}
/**
* An error that should not be shown to the user
*/
export class SystemError extends Error {
constructor(message: string) {
super(message);
this.name = "SystemError";
}
}
export function getLocationForOrganizerDefaultConferencingAppInEvtFormat({
organizer,
loggedInUserTranslate: translate,
}: {
organizer: {
name: string;
metadata: {
defaultConferencingApp?: NonNullable<UserMetadata>["defaultConferencingApp"];
} | null;
};
/**
* translate is used to translate if any error is thrown
*/
loggedInUserTranslate: Awaited<ReturnType<typeof getTranslation>>;
}) {
const organizerMetadata = organizer.metadata;
const defaultConferencingApp = organizerMetadata?.defaultConferencingApp;
if (!defaultConferencingApp) {
throw new UserError(
translate("organizer_default_conferencing_app_not_found", { organizer: organizer.name })
);
}
const defaultConferencingAppSlug = defaultConferencingApp.appSlug;
const app = getAppFromSlug(defaultConferencingAppSlug);
if (!app) {
throw new SystemError(`Default conferencing app ${defaultConferencingAppSlug} not found`);
}
const defaultConferencingAppLocationType = app.appData?.location?.type;
if (!defaultConferencingAppLocationType) {
throw new SystemError("Default conferencing app has no location type");
}
const location = defaultConferencingAppLocationType;
const locationType = getEventLocationType(location);
if (!locationType) {
throw new SystemError(`Location type not found: ${location}`);
}
if (locationType.linkType === "dynamic") {
// Dynamic location type need to return the location as it is e.g. integrations:zoom_video
return location;
}
const appLink = defaultConferencingApp.appLink;
if (!appLink) {
throw new SystemError(`Default conferencing app ${defaultConferencingAppSlug} has no app link`);
}
return appLink;
}
export async function editLocationHandler({ ctx, input }: EditLocationOptions) {
const { newLocation, credentialId: conferenceCredentialId } = input;
const { booking, user: loggedInUser } = ctx;
const organizer = await UserRepository.findByIdOrThrow({ id: booking.userId || 0 });
const newLocationInEvtFormat = await getLocationInEvtFormatOrThrow({
location: newLocation,
organizer,
loggedInUserTranslate: await getTranslation(loggedInUser.locale ?? "en", "common"),
});
const evt = await buildCalEventFromBooking({
booking,
organizer,
location: newLocationInEvtFormat,
conferenceCredentialId,
});
const eventManager = new EventManager({
...ctx.user,
credentials: await getAllCredentials({ user: ctx.user, conferenceCredentialId }),
});
const updatedResult = await updateLocationInConnectedAppForBooking({
booking,
eventManager,
evt,
});
const additionalInformation = extractAdditionalInformation(updatedResult.results[0]);
await updateBookingLocationInDb({
booking,
evt: { ...evt, additionalInformation },
references: updatedResult.referencesToCreate,
});
try {
await sendLocationChangeEmailsAndSMS(
{ ...evt, additionalInformation },
booking?.eventType?.metadata as EventTypeMetadata
);
} catch (error) {
console.log("Error sending LocationChangeEmails", safeStringify(error));
}
return { message: "Location updated" };
}