297 lines
8.8 KiB
TypeScript
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" };
|
|
}
|