[CAL-770] add new integration architecture revamp (#1369)
* [CAL-770] add new integration architecture revamp * Type fixes * Type fixes * [CAL-770] Remove tsconfig.tsbuildinfo * [CAL-770] add integration test * Improve google calendar test integration * Remove console.log * Change response any to void in the deleteEvent method * Remove unnecesary const * Add tsconfig.tsbuildinfo to the .gitignore * Remove process env variables as const Co-authored-by: Edward Fernández <edwardfernandez@Edwards-Mac-mini.local> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: zomars <zomars@me.com> Co-authored-by: Edward Fernandez <edward.fernandez@rappi.com>
This commit is contained in:
parent
8a70ea66e9
commit
bd2a795d7a
48 changed files with 1098 additions and 945 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -56,3 +56,5 @@ yarn-error.log*
|
|||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
# Typescript
|
||||
tsconfig.tsbuildinfo
|
|
@ -270,7 +270,7 @@ Contributions are what make the open source community such an amazing place to b
|
|||
3. Set **Who can use this application or access this API?** to **Accounts in any organizational directory (Any Azure AD directory - Multitenant)**
|
||||
4. Set the **Web** redirect URI to `<Cal.com URL>/api/integrations/office365calendar/callback` replacing Cal.com URL with the URI at which your application runs.
|
||||
5. Use **Application (client) ID** as the **MS_GRAPH_CLIENT_ID** attribute value in .env
|
||||
6. Click **Certificates & secrets** create a new client secret and use the value as the **MS_GRAPH_CLIENT_SECRET** attriubte
|
||||
6. Click **Certificates & secrets** create a new client secret and use the value as the **MS_GRAPH_CLIENT_SECRET** attribute
|
||||
|
||||
## Obtaining Zoom Client ID and Secret
|
||||
|
||||
|
|
|
@ -2,8 +2,8 @@ import type { IntegrationOAuthCallbackState } from "pages/api/integrations/types
|
|||
import { useState } from "react";
|
||||
import { useMutation } from "react-query";
|
||||
|
||||
import { AddAppleIntegrationModal } from "@lib/integrations/Apple/components/AddAppleIntegration";
|
||||
import { AddCalDavIntegrationModal } from "@lib/integrations/CalDav/components/AddCalDavIntegration";
|
||||
import { AddAppleIntegrationModal } from "@lib/integrations/calendar/components/AddAppleIntegration";
|
||||
import { AddCalDavIntegrationModal } from "@lib/integrations/calendar/components/AddCalDavIntegration";
|
||||
|
||||
import { ButtonBaseProps } from "@components/ui/Button";
|
||||
|
||||
|
|
|
@ -2,9 +2,9 @@ import { PaymentType, Prisma } from "@prisma/client";
|
|||
import Stripe from "stripe";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import { CalendarEvent } from "@lib/calendarClient";
|
||||
import { sendAwaitingPaymentEmail, sendOrganizerPaymentRefundFailedEmail } from "@lib/emails/email-manager";
|
||||
import { getErrorFromUnknown } from "@lib/errors";
|
||||
import { CalendarEvent } from "@lib/integrations/calendar/interfaces/Calendar";
|
||||
import prisma from "@lib/prisma";
|
||||
|
||||
import { createPaymentLink } from "./client";
|
||||
|
@ -77,7 +77,7 @@ export async function handlePayment(
|
|||
data: Object.assign({}, paymentIntent, {
|
||||
stripe_publishable_key,
|
||||
stripeAccount: stripe_user_id,
|
||||
}) as PaymentData as unknown as Prisma.JsonValue,
|
||||
}) /* We should treat this */ as PaymentData /* but Prisma doesn't know how to handle it, so it we treat it */ as unknown /* and then */ as Prisma.InputJsonValue,
|
||||
externalId: paymentIntent.id,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -4,11 +4,11 @@ import Stripe from "stripe";
|
|||
|
||||
import stripe from "@ee/lib/stripe/server";
|
||||
|
||||
import { CalendarEvent } from "@lib/calendarClient";
|
||||
import { IS_PRODUCTION } from "@lib/config/constants";
|
||||
import { HttpError as HttpCode } from "@lib/core/http/error";
|
||||
import { getErrorFromUnknown } from "@lib/errors";
|
||||
import EventManager from "@lib/events/EventManager";
|
||||
import { CalendarEvent } from "@lib/integrations/calendar/interfaces/Calendar";
|
||||
import prisma from "@lib/prisma";
|
||||
import { Ensure } from "@lib/types/utils";
|
||||
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import { Person } from "ics";
|
||||
import short from "short-uuid";
|
||||
import { v5 as uuidv5 } from "uuid";
|
||||
|
||||
import { getIntegrationName } from "@lib/integrations";
|
||||
|
||||
import { CalendarEvent, Person } from "./calendarClient";
|
||||
import { BASE_URL } from "./config/constants";
|
||||
import { CalendarEvent } from "./integrations/calendar/interfaces/Calendar";
|
||||
|
||||
const translator = short();
|
||||
|
||||
|
|
|
@ -1,172 +0,0 @@
|
|||
import { Credential, DestinationCalendar, SelectedCalendar } from "@prisma/client";
|
||||
import { TFunction } from "next-i18next";
|
||||
|
||||
import { PaymentInfo } from "@ee/lib/stripe/server";
|
||||
|
||||
import { getUid } from "@lib/CalEventParser";
|
||||
import { Event, EventResult } from "@lib/events/EventManager";
|
||||
import { AppleCalendar } from "@lib/integrations/Apple/AppleCalendarAdapter";
|
||||
import { CalDavCalendar } from "@lib/integrations/CalDav/CalDavCalendarAdapter";
|
||||
import {
|
||||
ConferenceData,
|
||||
GoogleCalendarApiAdapter,
|
||||
} from "@lib/integrations/GoogleCalendar/GoogleCalendarApiAdapter";
|
||||
import { Office365CalendarApiAdapter } from "@lib/integrations/Office365Calendar/Office365CalendarApiAdapter";
|
||||
import logger from "@lib/logger";
|
||||
import { VideoCallData } from "@lib/videoClient";
|
||||
|
||||
import notEmpty from "./notEmpty";
|
||||
import { Ensure } from "./types/utils";
|
||||
|
||||
const log = logger.getChildLogger({ prefix: ["[lib] calendarClient"] });
|
||||
|
||||
export type Person = { name: string; email: string; timeZone: string };
|
||||
|
||||
export interface EntryPoint {
|
||||
entryPointType?: string;
|
||||
uri?: string;
|
||||
label?: string;
|
||||
pin?: string;
|
||||
accessCode?: string;
|
||||
meetingCode?: string;
|
||||
passcode?: string;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
export interface AdditionInformation {
|
||||
conferenceData?: ConferenceData;
|
||||
entryPoints?: EntryPoint[];
|
||||
hangoutLink?: string;
|
||||
}
|
||||
|
||||
export interface CalendarEvent {
|
||||
type: string;
|
||||
title: string;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
description?: string | null;
|
||||
team?: {
|
||||
name: string;
|
||||
members: string[];
|
||||
};
|
||||
location?: string | null;
|
||||
organizer: Person;
|
||||
attendees: Person[];
|
||||
conferenceData?: ConferenceData;
|
||||
language: TFunction;
|
||||
additionInformation?: AdditionInformation;
|
||||
uid?: string | null;
|
||||
videoCallData?: VideoCallData;
|
||||
paymentInfo?: PaymentInfo | null;
|
||||
destinationCalendar?: DestinationCalendar | null;
|
||||
}
|
||||
|
||||
export interface IntegrationCalendar extends Ensure<Partial<SelectedCalendar>, "externalId"> {
|
||||
primary?: boolean;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
type EventBusyDate = Record<"start" | "end", Date | string>;
|
||||
|
||||
export interface CalendarApiAdapter {
|
||||
createEvent(event: CalendarEvent): Promise<Event>;
|
||||
|
||||
updateEvent(uid: string, event: CalendarEvent): Promise<any>;
|
||||
|
||||
deleteEvent(uid: string): Promise<unknown>;
|
||||
|
||||
getAvailability(
|
||||
dateFrom: string,
|
||||
dateTo: string,
|
||||
selectedCalendars: IntegrationCalendar[]
|
||||
): Promise<EventBusyDate[]>;
|
||||
|
||||
listCalendars(): Promise<IntegrationCalendar[]>;
|
||||
}
|
||||
|
||||
function getCalendarAdapterOrNull(credential: Credential): CalendarApiAdapter | null {
|
||||
switch (credential.type) {
|
||||
case "google_calendar":
|
||||
return GoogleCalendarApiAdapter(credential);
|
||||
case "office365_calendar":
|
||||
return Office365CalendarApiAdapter(credential);
|
||||
case "caldav_calendar":
|
||||
return new CalDavCalendar(credential);
|
||||
case "apple_calendar":
|
||||
return new AppleCalendar(credential);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const getBusyCalendarTimes = async (
|
||||
withCredentials: Credential[],
|
||||
dateFrom: string,
|
||||
dateTo: string,
|
||||
selectedCalendars: SelectedCalendar[]
|
||||
) => {
|
||||
const adapters = withCredentials.map(getCalendarAdapterOrNull).filter(notEmpty);
|
||||
const results = await Promise.all(
|
||||
adapters.map((c) => c.getAvailability(dateFrom, dateTo, selectedCalendars))
|
||||
);
|
||||
return results.reduce((acc, availability) => acc.concat(availability), []);
|
||||
};
|
||||
|
||||
const createEvent = async (credential: Credential, calEvent: CalendarEvent): Promise<EventResult> => {
|
||||
const uid: string = getUid(calEvent);
|
||||
const adapter = getCalendarAdapterOrNull(credential);
|
||||
let success = true;
|
||||
|
||||
const creationResult = adapter
|
||||
? await adapter.createEvent(calEvent).catch((e) => {
|
||||
log.error("createEvent failed", e, calEvent);
|
||||
success = false;
|
||||
return undefined;
|
||||
})
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
type: credential.type,
|
||||
success,
|
||||
uid,
|
||||
createdEvent: creationResult,
|
||||
originalEvent: calEvent,
|
||||
};
|
||||
};
|
||||
|
||||
const updateEvent = async (
|
||||
credential: Credential,
|
||||
calEvent: CalendarEvent,
|
||||
bookingRefUid: string | null
|
||||
): Promise<EventResult> => {
|
||||
const uid = getUid(calEvent);
|
||||
const adapter = getCalendarAdapterOrNull(credential);
|
||||
let success = true;
|
||||
|
||||
const updatedResult =
|
||||
adapter && bookingRefUid
|
||||
? await adapter.updateEvent(bookingRefUid, calEvent).catch((e) => {
|
||||
log.error("updateEvent failed", e, calEvent);
|
||||
success = false;
|
||||
return undefined;
|
||||
})
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
type: credential.type,
|
||||
success,
|
||||
uid,
|
||||
updatedEvent: updatedResult,
|
||||
originalEvent: calEvent,
|
||||
};
|
||||
};
|
||||
|
||||
const deleteEvent = (credential: Credential, uid: string): Promise<unknown> => {
|
||||
const adapter = getCalendarAdapterOrNull(credential);
|
||||
if (adapter) {
|
||||
return adapter.deleteEvent(uid);
|
||||
}
|
||||
|
||||
return Promise.resolve({});
|
||||
};
|
||||
|
||||
export { getBusyCalendarTimes, createEvent, updateEvent, deleteEvent, getCalendarAdapterOrNull };
|
|
@ -1,4 +1,3 @@
|
|||
import { CalendarEvent } from "@lib/calendarClient";
|
||||
import AttendeeAwaitingPaymentEmail from "@lib/emails/templates/attendee-awaiting-payment-email";
|
||||
import AttendeeCancelledEmail from "@lib/emails/templates/attendee-cancelled-email";
|
||||
import AttendeeDeclinedEmail from "@lib/emails/templates/attendee-declined-email";
|
||||
|
@ -12,6 +11,7 @@ import OrganizerRequestReminderEmail from "@lib/emails/templates/organizer-reque
|
|||
import OrganizerRescheduledEmail from "@lib/emails/templates/organizer-rescheduled-email";
|
||||
import OrganizerScheduledEmail from "@lib/emails/templates/organizer-scheduled-email";
|
||||
import TeamInviteEmail, { TeamInvite } from "@lib/emails/templates/team-invite-email";
|
||||
import { CalendarEvent } from "@lib/integrations/calendar/interfaces/Calendar";
|
||||
|
||||
export const sendScheduledEmails = async (calEvent: CalendarEvent) => {
|
||||
const emailsToSend = [];
|
||||
|
|
|
@ -3,13 +3,13 @@ import localizedFormat from "dayjs/plugin/localizedFormat";
|
|||
import timezone from "dayjs/plugin/timezone";
|
||||
import toArray from "dayjs/plugin/toArray";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import { createEvent, DateArray } from "ics";
|
||||
import { createEvent, DateArray, Person } from "ics";
|
||||
import nodemailer from "nodemailer";
|
||||
|
||||
import { getCancelLink, getRichDescription } from "@lib/CalEventParser";
|
||||
import { CalendarEvent, Person } from "@lib/calendarClient";
|
||||
import { getErrorFromUnknown } from "@lib/errors";
|
||||
import { getIntegrationName } from "@lib/integrations";
|
||||
import { CalendarEvent } from "@lib/integrations/calendar/interfaces/Calendar";
|
||||
import { serverConfig } from "@lib/serverConfig";
|
||||
|
||||
import {
|
||||
|
|
|
@ -3,13 +3,13 @@ import localizedFormat from "dayjs/plugin/localizedFormat";
|
|||
import timezone from "dayjs/plugin/timezone";
|
||||
import toArray from "dayjs/plugin/toArray";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import { createEvent, DateArray } from "ics";
|
||||
import { createEvent, DateArray, Person } from "ics";
|
||||
import nodemailer from "nodemailer";
|
||||
|
||||
import { getCancelLink, getRichDescription } from "@lib/CalEventParser";
|
||||
import { CalendarEvent, Person } from "@lib/calendarClient";
|
||||
import { getErrorFromUnknown } from "@lib/errors";
|
||||
import { getIntegrationName } from "@lib/integrations";
|
||||
import { CalendarEvent } from "@lib/integrations/calendar/interfaces/Calendar";
|
||||
import { serverConfig } from "@lib/serverConfig";
|
||||
|
||||
import {
|
||||
|
|
|
@ -3,8 +3,9 @@ import async from "async";
|
|||
import merge from "lodash/merge";
|
||||
import { v5 as uuidv5 } from "uuid";
|
||||
|
||||
import { AdditionInformation, CalendarEvent, createEvent, updateEvent } from "@lib/calendarClient";
|
||||
import { FAKE_DAILY_CREDENTIAL } from "@lib/integrations/Daily/DailyVideoApiAdapter";
|
||||
import { createEvent, updateEvent } from "@lib/integrations/calendar/CalendarManager";
|
||||
import { AdditionInformation, CalendarEvent } from "@lib/integrations/calendar/interfaces/Calendar";
|
||||
import { LocationType } from "@lib/location";
|
||||
import prisma from "@lib/prisma";
|
||||
import { Ensure } from "@lib/types/utils";
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
import { Credential } from "@prisma/client";
|
||||
|
||||
import { BaseCalendarApiAdapter } from "@lib/BaseCalendarApiAdapter";
|
||||
import { CalendarApiAdapter } from "@lib/calendarClient";
|
||||
|
||||
export class AppleCalendar extends BaseCalendarApiAdapter implements CalendarApiAdapter {
|
||||
constructor(credential: Credential) {
|
||||
super(credential, "apple_calendar", "https://caldav.icloud.com");
|
||||
}
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
import { Credential } from "@prisma/client";
|
||||
|
||||
import { BaseCalendarApiAdapter } from "@lib/BaseCalendarApiAdapter";
|
||||
import { CalendarApiAdapter } from "@lib/calendarClient";
|
||||
|
||||
export class CalDavCalendar extends BaseCalendarApiAdapter implements CalendarApiAdapter {
|
||||
constructor(credential: Credential) {
|
||||
super(credential, "caldav_calendar");
|
||||
}
|
||||
}
|
|
@ -1,12 +1,13 @@
|
|||
import { Credential } from "@prisma/client";
|
||||
|
||||
import { CalendarEvent } from "@lib/calendarClient";
|
||||
import { BASE_URL } from "@lib/config/constants";
|
||||
import { handleErrorsJson } from "@lib/errors";
|
||||
import { PartialReference } from "@lib/events/EventManager";
|
||||
import prisma from "@lib/prisma";
|
||||
import { VideoApiAdapter, VideoCallData } from "@lib/videoClient";
|
||||
|
||||
import { CalendarEvent } from "../calendar/interfaces/Calendar";
|
||||
|
||||
export interface DailyReturnType {
|
||||
/** Long UID string ie: 987b5eb5-d116-4a4e-8e2c-14fcb5710966 */
|
||||
id: string;
|
||||
|
|
|
@ -1,290 +0,0 @@
|
|||
import { Credential, Prisma } from "@prisma/client";
|
||||
import { GetTokenResponse } from "google-auth-library/build/src/auth/oauth2client";
|
||||
import { Auth, calendar_v3, google } from "googleapis";
|
||||
|
||||
import { getLocation, getRichDescription } from "@lib/CalEventParser";
|
||||
import { CalendarApiAdapter, CalendarEvent, IntegrationCalendar } from "@lib/calendarClient";
|
||||
import prisma from "@lib/prisma";
|
||||
|
||||
export interface ConferenceData {
|
||||
createRequest?: calendar_v3.Schema$CreateConferenceRequest;
|
||||
}
|
||||
|
||||
class MyGoogleAuth extends google.auth.OAuth2 {
|
||||
constructor(client_id: string, client_secret: string, redirect_uri: string) {
|
||||
super(client_id, client_secret, redirect_uri);
|
||||
}
|
||||
|
||||
isTokenExpiring() {
|
||||
return super.isTokenExpiring();
|
||||
}
|
||||
|
||||
async refreshToken(token: string | null | undefined) {
|
||||
return super.refreshToken(token);
|
||||
}
|
||||
}
|
||||
|
||||
const googleAuth = (credential: Credential) => {
|
||||
const { client_secret, client_id, redirect_uris } = JSON.parse(process.env.GOOGLE_API_CREDENTIALS!).web;
|
||||
const myGoogleAuth = new MyGoogleAuth(client_id, client_secret, redirect_uris[0]);
|
||||
const googleCredentials = credential.key as Auth.Credentials;
|
||||
myGoogleAuth.setCredentials(googleCredentials);
|
||||
|
||||
const isExpired = () => myGoogleAuth.isTokenExpiring();
|
||||
|
||||
const refreshAccessToken = () =>
|
||||
myGoogleAuth
|
||||
.refreshToken(googleCredentials.refresh_token)
|
||||
.then((res: GetTokenResponse) => {
|
||||
const token = res.res?.data;
|
||||
googleCredentials.access_token = token.access_token;
|
||||
googleCredentials.expiry_date = token.expiry_date;
|
||||
return prisma.credential
|
||||
.update({
|
||||
where: {
|
||||
id: credential.id,
|
||||
},
|
||||
data: {
|
||||
key: googleCredentials as Prisma.InputJsonValue,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
myGoogleAuth.setCredentials(googleCredentials);
|
||||
return myGoogleAuth;
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Error refreshing google token", err);
|
||||
return myGoogleAuth;
|
||||
});
|
||||
|
||||
return {
|
||||
getToken: () => (!isExpired() ? Promise.resolve(myGoogleAuth) : refreshAccessToken()),
|
||||
};
|
||||
};
|
||||
|
||||
export const GoogleCalendarApiAdapter = (credential: Credential): CalendarApiAdapter => {
|
||||
const auth = googleAuth(credential);
|
||||
const integrationType = "google_calendar";
|
||||
|
||||
return {
|
||||
getAvailability: (dateFrom, dateTo, selectedCalendars) =>
|
||||
new Promise((resolve, reject) =>
|
||||
auth.getToken().then((myGoogleAuth) => {
|
||||
const calendar = google.calendar({
|
||||
version: "v3",
|
||||
auth: myGoogleAuth,
|
||||
});
|
||||
const selectedCalendarIds = selectedCalendars
|
||||
.filter((e) => e.integration === integrationType)
|
||||
.map((e) => e.externalId);
|
||||
if (selectedCalendarIds.length === 0 && selectedCalendars.length > 0) {
|
||||
// Only calendars of other integrations selected
|
||||
resolve([]);
|
||||
return;
|
||||
}
|
||||
|
||||
(selectedCalendarIds.length === 0
|
||||
? calendar.calendarList
|
||||
.list()
|
||||
.then((cals) => cals.data.items?.map((cal) => cal.id).filter(Boolean) || [])
|
||||
: Promise.resolve(selectedCalendarIds)
|
||||
)
|
||||
.then((calsIds) => {
|
||||
calendar.freebusy.query(
|
||||
{
|
||||
requestBody: {
|
||||
timeMin: dateFrom,
|
||||
timeMax: dateTo,
|
||||
items: calsIds.map((id) => ({ id: id })),
|
||||
},
|
||||
},
|
||||
(err, apires) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
let result: Prisma.PromiseReturnType<CalendarApiAdapter["getAvailability"]> = [];
|
||||
if (apires?.data.calendars) {
|
||||
result = Object.values(apires.data.calendars).reduce((c, i) => {
|
||||
i.busy?.forEach((busyTime) => {
|
||||
c.push({
|
||||
start: busyTime.start || "",
|
||||
end: busyTime.end || "",
|
||||
});
|
||||
});
|
||||
return c;
|
||||
}, [] as typeof result);
|
||||
}
|
||||
resolve(result);
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("There was an error contacting google calendar service: ", err);
|
||||
reject(err);
|
||||
});
|
||||
})
|
||||
),
|
||||
createEvent: (event: CalendarEvent) =>
|
||||
new Promise((resolve, reject) =>
|
||||
auth.getToken().then((myGoogleAuth) => {
|
||||
const payload: calendar_v3.Schema$Event = {
|
||||
summary: event.title,
|
||||
description: getRichDescription(event),
|
||||
start: {
|
||||
dateTime: event.startTime,
|
||||
timeZone: event.organizer.timeZone,
|
||||
},
|
||||
end: {
|
||||
dateTime: event.endTime,
|
||||
timeZone: event.organizer.timeZone,
|
||||
},
|
||||
attendees: event.attendees,
|
||||
reminders: {
|
||||
useDefault: false,
|
||||
overrides: [{ method: "email", minutes: 10 }],
|
||||
},
|
||||
};
|
||||
|
||||
if (event.location) {
|
||||
payload["location"] = getLocation(event);
|
||||
}
|
||||
|
||||
if (event.conferenceData && event.location === "integrations:google:meet") {
|
||||
payload["conferenceData"] = event.conferenceData;
|
||||
}
|
||||
|
||||
const calendar = google.calendar({
|
||||
version: "v3",
|
||||
auth: myGoogleAuth,
|
||||
});
|
||||
calendar.events.insert(
|
||||
{
|
||||
auth: myGoogleAuth,
|
||||
calendarId: event.destinationCalendar?.externalId
|
||||
? event.destinationCalendar.externalId
|
||||
: "primary",
|
||||
requestBody: payload,
|
||||
conferenceDataVersion: 1,
|
||||
},
|
||||
function (err, event) {
|
||||
if (err || !event?.data) {
|
||||
console.error("There was an error contacting google calendar service: ", err);
|
||||
return reject(err);
|
||||
}
|
||||
return resolve({
|
||||
...event.data,
|
||||
id: event.data.id || "",
|
||||
hangoutLink: event.data.hangoutLink || "",
|
||||
type: "google_calendar",
|
||||
password: "",
|
||||
url: "",
|
||||
});
|
||||
}
|
||||
);
|
||||
})
|
||||
),
|
||||
updateEvent: (uid: string, event: CalendarEvent) =>
|
||||
new Promise((resolve, reject) =>
|
||||
auth.getToken().then((myGoogleAuth) => {
|
||||
const payload: calendar_v3.Schema$Event = {
|
||||
summary: event.title,
|
||||
description: getRichDescription(event),
|
||||
start: {
|
||||
dateTime: event.startTime,
|
||||
timeZone: event.organizer.timeZone,
|
||||
},
|
||||
end: {
|
||||
dateTime: event.endTime,
|
||||
timeZone: event.organizer.timeZone,
|
||||
},
|
||||
attendees: event.attendees,
|
||||
reminders: {
|
||||
useDefault: true,
|
||||
},
|
||||
};
|
||||
|
||||
if (event.location) {
|
||||
payload["location"] = getLocation(event);
|
||||
}
|
||||
|
||||
const calendar = google.calendar({
|
||||
version: "v3",
|
||||
auth: myGoogleAuth,
|
||||
});
|
||||
calendar.events.update(
|
||||
{
|
||||
auth: myGoogleAuth,
|
||||
calendarId: event.destinationCalendar?.externalId
|
||||
? event.destinationCalendar.externalId
|
||||
: "primary",
|
||||
eventId: uid,
|
||||
sendNotifications: true,
|
||||
sendUpdates: "all",
|
||||
requestBody: payload,
|
||||
},
|
||||
function (err, event) {
|
||||
if (err) {
|
||||
console.error("There was an error contacting google calendar service: ", err);
|
||||
return reject(err);
|
||||
}
|
||||
return resolve(event?.data);
|
||||
}
|
||||
);
|
||||
})
|
||||
),
|
||||
deleteEvent: (uid: string) =>
|
||||
new Promise((resolve, reject) =>
|
||||
auth.getToken().then((myGoogleAuth) => {
|
||||
const calendar = google.calendar({
|
||||
version: "v3",
|
||||
auth: myGoogleAuth,
|
||||
});
|
||||
calendar.events.delete(
|
||||
{
|
||||
auth: myGoogleAuth,
|
||||
calendarId: "primary",
|
||||
eventId: uid,
|
||||
sendNotifications: true,
|
||||
sendUpdates: "all",
|
||||
},
|
||||
function (err, event) {
|
||||
if (err) {
|
||||
console.error("There was an error contacting google calendar service: ", err);
|
||||
return reject(err);
|
||||
}
|
||||
return resolve(event?.data);
|
||||
}
|
||||
);
|
||||
})
|
||||
),
|
||||
listCalendars: () =>
|
||||
new Promise((resolve, reject) =>
|
||||
auth.getToken().then((myGoogleAuth) => {
|
||||
const calendar = google.calendar({
|
||||
version: "v3",
|
||||
auth: myGoogleAuth,
|
||||
});
|
||||
calendar.calendarList
|
||||
.list()
|
||||
.then((cals) => {
|
||||
resolve(
|
||||
cals.data.items?.map((cal) => {
|
||||
const calendar: IntegrationCalendar = {
|
||||
externalId: cal.id ?? "No id",
|
||||
integration: integrationType,
|
||||
name: cal.summary ?? "No name",
|
||||
primary: cal.primary ?? false,
|
||||
};
|
||||
return calendar;
|
||||
}) || []
|
||||
);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("There was an error contacting google calendar service: ", err);
|
||||
reject(err);
|
||||
});
|
||||
})
|
||||
),
|
||||
};
|
||||
};
|
|
@ -1,220 +0,0 @@
|
|||
import { Calendar as OfficeCalendar } from "@microsoft/microsoft-graph-types-beta";
|
||||
import { Credential } from "@prisma/client";
|
||||
|
||||
import { getLocation, getRichDescription } from "@lib/CalEventParser";
|
||||
import { CalendarApiAdapter, CalendarEvent, IntegrationCalendar } from "@lib/calendarClient";
|
||||
import { handleErrorsJson, handleErrorsRaw } from "@lib/errors";
|
||||
import prisma from "@lib/prisma";
|
||||
|
||||
export type BufferedBusyTime = {
|
||||
start: string;
|
||||
end: string;
|
||||
};
|
||||
|
||||
type O365AuthCredentials = {
|
||||
expiry_date: number;
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
};
|
||||
|
||||
const o365Auth = (credential: Credential) => {
|
||||
const isExpired = (expiryDate: number) => expiryDate < Math.round(+new Date() / 1000);
|
||||
const o365AuthCredentials = credential.key as O365AuthCredentials;
|
||||
|
||||
const refreshAccessToken = (refreshToken: string) => {
|
||||
return fetch("https://login.microsoftonline.com/common/oauth2/v2.0/token", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({
|
||||
scope: "User.Read Calendars.Read Calendars.ReadWrite",
|
||||
client_id: process.env.MS_GRAPH_CLIENT_ID!,
|
||||
refresh_token: refreshToken,
|
||||
grant_type: "refresh_token",
|
||||
client_secret: process.env.MS_GRAPH_CLIENT_SECRET!,
|
||||
}),
|
||||
})
|
||||
.then(handleErrorsJson)
|
||||
.then((responseBody) => {
|
||||
o365AuthCredentials.access_token = responseBody.access_token;
|
||||
o365AuthCredentials.expiry_date = Math.round(+new Date() / 1000 + responseBody.expires_in);
|
||||
return prisma.credential
|
||||
.update({
|
||||
where: {
|
||||
id: credential.id,
|
||||
},
|
||||
data: {
|
||||
key: o365AuthCredentials,
|
||||
},
|
||||
})
|
||||
.then(() => o365AuthCredentials.access_token);
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
getToken: () =>
|
||||
!isExpired(o365AuthCredentials.expiry_date)
|
||||
? Promise.resolve(o365AuthCredentials.access_token)
|
||||
: refreshAccessToken(o365AuthCredentials.refresh_token),
|
||||
};
|
||||
};
|
||||
|
||||
export const Office365CalendarApiAdapter = (credential: Credential): CalendarApiAdapter => {
|
||||
const auth = o365Auth(credential);
|
||||
|
||||
const translateEvent = (event: CalendarEvent) => {
|
||||
return {
|
||||
subject: event.title,
|
||||
body: {
|
||||
contentType: "HTML",
|
||||
content: getRichDescription(event),
|
||||
},
|
||||
start: {
|
||||
dateTime: event.startTime,
|
||||
timeZone: event.organizer.timeZone,
|
||||
},
|
||||
end: {
|
||||
dateTime: event.endTime,
|
||||
timeZone: event.organizer.timeZone,
|
||||
},
|
||||
attendees: event.attendees.map((attendee) => ({
|
||||
emailAddress: {
|
||||
address: attendee.email,
|
||||
name: attendee.name,
|
||||
},
|
||||
type: "required",
|
||||
})),
|
||||
location: event.location ? { displayName: getLocation(event) } : undefined,
|
||||
};
|
||||
};
|
||||
|
||||
const integrationType = "office365_calendar";
|
||||
|
||||
function listCalendars(): Promise<IntegrationCalendar[]> {
|
||||
return auth.getToken().then((accessToken) =>
|
||||
fetch("https://graph.microsoft.com/v1.0/me/calendars", {
|
||||
method: "get",
|
||||
headers: {
|
||||
Authorization: "Bearer " + accessToken,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then(handleErrorsJson)
|
||||
.then((responseBody: { value: OfficeCalendar[] }) => {
|
||||
return responseBody.value.map((cal) => {
|
||||
const calendar: IntegrationCalendar = {
|
||||
externalId: cal.id ?? "No Id",
|
||||
integration: integrationType,
|
||||
name: cal.name ?? "No calendar name",
|
||||
primary: cal.isDefaultCalendar ?? false,
|
||||
};
|
||||
return calendar;
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
getAvailability: (dateFrom, dateTo, selectedCalendars) => {
|
||||
const dateFromParsed = new Date(dateFrom);
|
||||
const dateToParsed = new Date(dateTo);
|
||||
|
||||
const filter = `?startdatetime=${encodeURIComponent(
|
||||
dateFromParsed.toISOString()
|
||||
)}&enddatetime=${encodeURIComponent(dateToParsed.toISOString())}`;
|
||||
return auth
|
||||
.getToken()
|
||||
.then((accessToken) => {
|
||||
const selectedCalendarIds = selectedCalendars
|
||||
.filter((e) => e.integration === integrationType)
|
||||
.map((e) => e.externalId)
|
||||
.filter(Boolean);
|
||||
if (selectedCalendarIds.length === 0 && selectedCalendars.length > 0) {
|
||||
// Only calendars of other integrations selected
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
return (
|
||||
selectedCalendarIds.length === 0
|
||||
? listCalendars().then((cals) => cals.map((e) => e.externalId).filter(Boolean) || [])
|
||||
: Promise.resolve(selectedCalendarIds)
|
||||
).then((ids) => {
|
||||
const requests = ids.map((calendarId, id) => ({
|
||||
id,
|
||||
method: "GET",
|
||||
url: `/me/calendars/${calendarId}/calendarView${filter}`,
|
||||
}));
|
||||
|
||||
type BatchResponse = {
|
||||
responses: SubResponse[];
|
||||
};
|
||||
type SubResponse = {
|
||||
body: { value: { start: { dateTime: string }; end: { dateTime: string } }[] };
|
||||
};
|
||||
|
||||
return fetch("https://graph.microsoft.com/v1.0/$batch", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: "Bearer " + accessToken,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ requests }),
|
||||
})
|
||||
.then(handleErrorsJson)
|
||||
.then((responseBody: BatchResponse) =>
|
||||
responseBody.responses.reduce(
|
||||
(acc: BufferedBusyTime[], subResponse) =>
|
||||
acc.concat(
|
||||
subResponse.body.value.map((evt) => {
|
||||
return {
|
||||
start: evt.start.dateTime + "Z",
|
||||
end: evt.end.dateTime + "Z",
|
||||
};
|
||||
})
|
||||
),
|
||||
[]
|
||||
)
|
||||
);
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
return Promise.reject([]);
|
||||
});
|
||||
},
|
||||
createEvent: (event: CalendarEvent) =>
|
||||
auth.getToken().then((accessToken) => {
|
||||
const calendarId = event.destinationCalendar?.externalId
|
||||
? `${event.destinationCalendar.externalId}/`
|
||||
: "";
|
||||
return fetch(`https://graph.microsoft.com/v1.0/me/calendar/${calendarId}events`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: "Bearer " + accessToken,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(translateEvent(event)),
|
||||
}).then(handleErrorsJson);
|
||||
}),
|
||||
deleteEvent: (uid: string) =>
|
||||
auth.getToken().then((accessToken) =>
|
||||
fetch("https://graph.microsoft.com/v1.0/me/calendar/events/" + uid, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
Authorization: "Bearer " + accessToken,
|
||||
},
|
||||
}).then(handleErrorsRaw)
|
||||
),
|
||||
updateEvent: (uid: string, event: CalendarEvent) =>
|
||||
auth.getToken().then((accessToken) =>
|
||||
fetch("https://graph.microsoft.com/v1.0/me/calendar/events/" + uid, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
Authorization: "Bearer " + accessToken,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(translateEvent(event)),
|
||||
}).then(handleErrorsRaw)
|
||||
),
|
||||
listCalendars,
|
||||
};
|
||||
};
|
|
@ -1,11 +1,12 @@
|
|||
import { Credential } from "@prisma/client";
|
||||
|
||||
import { CalendarEvent } from "@lib/calendarClient";
|
||||
import { handleErrorsJson, handleErrorsRaw } from "@lib/errors";
|
||||
import { PartialReference } from "@lib/events/EventManager";
|
||||
import prisma from "@lib/prisma";
|
||||
import { VideoApiAdapter, VideoCallData } from "@lib/videoClient";
|
||||
|
||||
import { CalendarEvent } from "../calendar/interfaces/Calendar";
|
||||
|
||||
/** @link https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingcreate */
|
||||
export interface ZoomEventResult {
|
||||
password: string;
|
||||
|
|
177
lib/integrations/calendar/CalendarManager.ts
Normal file
177
lib/integrations/calendar/CalendarManager.ts
Normal file
|
@ -0,0 +1,177 @@
|
|||
import { Credential, SelectedCalendar } from "@prisma/client";
|
||||
import _ from "lodash";
|
||||
|
||||
import { getUid } from "@lib/CalEventParser";
|
||||
import { getErrorFromUnknown } from "@lib/errors";
|
||||
import { EventResult } from "@lib/events/EventManager";
|
||||
import logger from "@lib/logger";
|
||||
import notEmpty from "@lib/notEmpty";
|
||||
|
||||
import { ALL_INTEGRATIONS } from "../getIntegrations";
|
||||
import { CALENDAR_INTEGRATIONS_TYPES } from "./constants/generals";
|
||||
import { CalendarServiceType } from "./constants/types";
|
||||
import { Calendar, CalendarEvent } from "./interfaces/Calendar";
|
||||
import AppleCalendarService from "./services/AppleCalendarService";
|
||||
import CalDavCalendarService from "./services/CalDavCalendarService";
|
||||
import GoogleCalendarService from "./services/GoogleCalendarService";
|
||||
import Office365CalendarService from "./services/Office365CalendarService";
|
||||
|
||||
const CALENDARS: Record<string, CalendarServiceType> = {
|
||||
[CALENDAR_INTEGRATIONS_TYPES.apple]: AppleCalendarService,
|
||||
[CALENDAR_INTEGRATIONS_TYPES.caldav]: CalDavCalendarService,
|
||||
[CALENDAR_INTEGRATIONS_TYPES.google]: GoogleCalendarService,
|
||||
[CALENDAR_INTEGRATIONS_TYPES.office365]: Office365CalendarService,
|
||||
};
|
||||
|
||||
const log = logger.getChildLogger({ prefix: ["CalendarManager"] });
|
||||
|
||||
export const getCalendar = (credential: Credential): Calendar | null => {
|
||||
const { type: calendarType } = credential;
|
||||
|
||||
const calendar = CALENDARS[calendarType];
|
||||
if (!calendar) {
|
||||
log.warn(`calendar of type ${calendarType} does not implemented`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return new calendar(credential);
|
||||
};
|
||||
|
||||
export const getCalendarCredentials = (credentials: Array<Omit<Credential, "userId">>, userId: number) => {
|
||||
const calendarCredentials = credentials
|
||||
.filter((credential) => credential.type.endsWith("_calendar"))
|
||||
.flatMap((credential) => {
|
||||
const integration = ALL_INTEGRATIONS.find((integration) => integration.type === credential.type);
|
||||
|
||||
const calendar = getCalendar({
|
||||
...credential,
|
||||
userId,
|
||||
});
|
||||
return integration && calendar && integration.variant === "calendar"
|
||||
? [{ integration, credential, calendar }]
|
||||
: [];
|
||||
});
|
||||
|
||||
return calendarCredentials;
|
||||
};
|
||||
|
||||
export const getConnectedCalendars = async (
|
||||
calendarCredentials: ReturnType<typeof getCalendarCredentials>,
|
||||
selectedCalendars: { externalId: string }[]
|
||||
) => {
|
||||
const connectedCalendars = await Promise.all(
|
||||
calendarCredentials.map(async (item) => {
|
||||
const { calendar, integration, credential } = item;
|
||||
|
||||
const credentialId = credential.id;
|
||||
try {
|
||||
const cals = await calendar.listCalendars();
|
||||
const calendars = _(cals)
|
||||
.map((cal) => ({
|
||||
...cal,
|
||||
primary: cal.primary || null,
|
||||
isSelected: selectedCalendars.some((selected) => selected.externalId === cal.externalId),
|
||||
}))
|
||||
.sortBy(["primary"])
|
||||
.value();
|
||||
const primary = calendars.find((item) => item.primary) ?? calendars[0];
|
||||
if (!primary) {
|
||||
throw new Error("No primary calendar found");
|
||||
}
|
||||
return {
|
||||
integration,
|
||||
credentialId,
|
||||
primary,
|
||||
calendars,
|
||||
};
|
||||
} catch (_error) {
|
||||
const error = getErrorFromUnknown(_error);
|
||||
return {
|
||||
integration,
|
||||
credentialId,
|
||||
error: {
|
||||
message: error.message,
|
||||
},
|
||||
};
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return connectedCalendars;
|
||||
};
|
||||
|
||||
export const getBusyCalendarTimes = async (
|
||||
withCredentials: Credential[],
|
||||
dateFrom: string,
|
||||
dateTo: string,
|
||||
selectedCalendars: SelectedCalendar[]
|
||||
) => {
|
||||
const calendars = withCredentials
|
||||
.filter((credential) => credential.type.endsWith("_calendar"))
|
||||
.map((credential) => getCalendar(credential))
|
||||
.filter(notEmpty);
|
||||
|
||||
const results = await Promise.all(
|
||||
calendars.map((c) => c.getAvailability(dateFrom, dateTo, selectedCalendars))
|
||||
);
|
||||
|
||||
return results.reduce((acc, availability) => acc.concat(availability), []);
|
||||
};
|
||||
|
||||
export const createEvent = async (credential: Credential, calEvent: CalendarEvent): Promise<EventResult> => {
|
||||
const uid: string = getUid(calEvent);
|
||||
const calendar = getCalendar(credential);
|
||||
let success = true;
|
||||
|
||||
const creationResult = calendar
|
||||
? await calendar.createEvent(calEvent).catch((e) => {
|
||||
log.error("createEvent failed", e, calEvent);
|
||||
success = false;
|
||||
return undefined;
|
||||
})
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
type: credential.type,
|
||||
success,
|
||||
uid,
|
||||
createdEvent: creationResult,
|
||||
originalEvent: calEvent,
|
||||
};
|
||||
};
|
||||
|
||||
export const updateEvent = async (
|
||||
credential: Credential,
|
||||
calEvent: CalendarEvent,
|
||||
bookingRefUid: string | null
|
||||
): Promise<EventResult> => {
|
||||
const uid = getUid(calEvent);
|
||||
const calendar = getCalendar(credential);
|
||||
let success = true;
|
||||
|
||||
const updatedResult =
|
||||
calendar && bookingRefUid
|
||||
? await calendar.updateEvent(bookingRefUid, calEvent).catch((e) => {
|
||||
log.error("updateEvent failed", e, calEvent);
|
||||
success = false;
|
||||
return undefined;
|
||||
})
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
type: credential.type,
|
||||
success,
|
||||
uid,
|
||||
updatedEvent: updatedResult,
|
||||
originalEvent: calEvent,
|
||||
};
|
||||
};
|
||||
|
||||
export const deleteEvent = (credential: Credential, uid: string): Promise<unknown> => {
|
||||
const calendar = getCalendar(credential);
|
||||
if (calendar) {
|
||||
return calendar.deleteEvent(uid);
|
||||
}
|
||||
|
||||
return Promise.resolve({});
|
||||
};
|
|
@ -45,7 +45,7 @@ export function AddAppleIntegrationModal(props: DialogProps) {
|
|||
|
||||
<Form
|
||||
form={form}
|
||||
onSubmit={form.handleSubmit(async (values) => {
|
||||
handleSubmit={async (values) => {
|
||||
setErrorMessage("");
|
||||
const res = await fetch("/api/integrations/apple/add", {
|
||||
method: "POST",
|
||||
|
@ -60,7 +60,7 @@ export function AddAppleIntegrationModal(props: DialogProps) {
|
|||
} else {
|
||||
props.onOpenChange?.(false);
|
||||
}
|
||||
})}>
|
||||
}}>
|
||||
<fieldset className="space-y-2" disabled={form.formState.isSubmitting}>
|
||||
<TextField
|
||||
required
|
|
@ -43,7 +43,7 @@ export function AddCalDavIntegrationModal(props: DialogProps) {
|
|||
|
||||
<Form
|
||||
form={form}
|
||||
onSubmit={form.handleSubmit(async (values) => {
|
||||
handleSubmit={async (values) => {
|
||||
setErrorMessage("");
|
||||
const res = await fetch("/api/integrations/caldav/add", {
|
||||
method: "POST",
|
||||
|
@ -58,7 +58,7 @@ export function AddCalDavIntegrationModal(props: DialogProps) {
|
|||
} else {
|
||||
props.onOpenChange?.(false);
|
||||
}
|
||||
})}>
|
||||
}}>
|
||||
<fieldset className="space-y-2" disabled={form.formState.isSubmitting}>
|
||||
<TextField
|
||||
required
|
||||
|
@ -123,14 +123,14 @@ const AddCalDavIntegration = React.forwardRef<HTMLFormElement, Props>((props, re
|
|||
<label htmlFor="url" className="block text-sm font-medium text-gray-700">
|
||||
Calendar URL
|
||||
</label>
|
||||
<div className="mt-1 rounded-md shadow-sm flex">
|
||||
<div className="flex mt-1 rounded-md shadow-sm">
|
||||
<input
|
||||
required
|
||||
type="text"
|
||||
name="url"
|
||||
id="url"
|
||||
placeholder="https://example.com/calendar"
|
||||
className="focus:ring-black focus:border-brand flex-grow block w-full min-w-0 rounded-none rounded-r-sm sm:text-sm border-gray-300 lowercase"
|
||||
className="flex-grow block w-full min-w-0 lowercase border-gray-300 rounded-none rounded-r-sm focus:ring-black focus:border-brand sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -144,7 +144,7 @@ const AddCalDavIntegration = React.forwardRef<HTMLFormElement, Props>((props, re
|
|||
name="username"
|
||||
id="username"
|
||||
placeholder="rickroll"
|
||||
className="mt-1 block w-full border border-gray-300 rounded-sm shadow-sm py-2 px-3 focus:outline-none focus:ring-neutral-500 focus:border-neutral-500 sm:text-sm"
|
||||
className="block w-full px-3 py-2 mt-1 border border-gray-300 rounded-sm shadow-sm focus:outline-none focus:ring-neutral-500 focus:border-neutral-500 sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
|
@ -157,7 +157,7 @@ const AddCalDavIntegration = React.forwardRef<HTMLFormElement, Props>((props, re
|
|||
name="password"
|
||||
id="password"
|
||||
placeholder="•••••••••••••"
|
||||
className="mt-1 block w-full border border-gray-300 rounded-sm shadow-sm py-2 px-3 focus:outline-none focus:ring-neutral-500 focus:border-neutral-500 sm:text-sm"
|
||||
className="block w-full px-3 py-2 mt-1 border border-gray-300 rounded-sm shadow-sm focus:outline-none focus:ring-neutral-500 focus:border-neutral-500 sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
1
lib/integrations/calendar/constants/formats.ts
Normal file
1
lib/integrations/calendar/constants/formats.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export const TIMEZONE_FORMAT = "YYYY-MM-DDTHH:mm:ss[Z]";
|
10
lib/integrations/calendar/constants/generals.ts
Normal file
10
lib/integrations/calendar/constants/generals.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
export const CALDAV_CALENDAR_TYPE = "caldav";
|
||||
|
||||
export const APPLE_CALENDAR_URL = "https://caldav.icloud.com";
|
||||
|
||||
export const CALENDAR_INTEGRATIONS_TYPES = {
|
||||
apple: "apple_calendar",
|
||||
caldav: "caldav_calendar",
|
||||
google: "google_calendar",
|
||||
office365: "office365_calendar",
|
||||
};
|
56
lib/integrations/calendar/constants/types.ts
Normal file
56
lib/integrations/calendar/constants/types.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
import dayjs from "dayjs";
|
||||
import ICAL from "ical.js";
|
||||
|
||||
import AppleCalendarService from "../services/AppleCalendarService";
|
||||
import CalDavCalendarService from "../services/CalDavCalendarService";
|
||||
import GoogleCalendarService from "../services/GoogleCalendarService";
|
||||
import Office365CalendarService from "../services/Office365CalendarService";
|
||||
|
||||
export type EventBusyDate = Record<"start" | "end", Date | string>;
|
||||
|
||||
export type CalendarServiceType =
|
||||
| typeof AppleCalendarService
|
||||
| typeof CalDavCalendarService
|
||||
| typeof GoogleCalendarService
|
||||
| typeof Office365CalendarService;
|
||||
|
||||
export type NewCalendarEventType = {
|
||||
uid: string;
|
||||
id: string;
|
||||
type: string;
|
||||
password: string;
|
||||
url: string;
|
||||
additionalInfo: Record<string, any>;
|
||||
};
|
||||
|
||||
export type CalendarEventType = {
|
||||
uid: string;
|
||||
etag: string;
|
||||
url: string;
|
||||
summary: string;
|
||||
description: string;
|
||||
location: string;
|
||||
sequence: number;
|
||||
startDate: Date | dayjs.Dayjs;
|
||||
endDate: Date | dayjs.Dayjs;
|
||||
duration: {
|
||||
weeks: number;
|
||||
days: number;
|
||||
hours: number;
|
||||
minutes: number;
|
||||
seconds: number;
|
||||
isNegative: boolean;
|
||||
};
|
||||
organizer: string;
|
||||
attendees: any[][];
|
||||
recurrenceId: ICAL.Time;
|
||||
timezone: any;
|
||||
};
|
||||
|
||||
export type BatchResponse = {
|
||||
responses: SubResponse[];
|
||||
};
|
||||
|
||||
export type SubResponse = {
|
||||
body: { value: { start: { dateTime: string }; end: { dateTime: string } }[] };
|
||||
};
|
78
lib/integrations/calendar/interfaces/Calendar.ts
Normal file
78
lib/integrations/calendar/interfaces/Calendar.ts
Normal file
|
@ -0,0 +1,78 @@
|
|||
import { DestinationCalendar, SelectedCalendar } from "@prisma/client";
|
||||
import { TFunction } from "next-i18next";
|
||||
|
||||
import { PaymentInfo } from "@ee/lib/stripe/server";
|
||||
|
||||
import { Ensure } from "@lib/types/utils";
|
||||
import { VideoCallData } from "@lib/videoClient";
|
||||
|
||||
import { NewCalendarEventType } from "../constants/types";
|
||||
import { ConferenceData } from "./GoogleCalendar";
|
||||
|
||||
export type Person = {
|
||||
name: string;
|
||||
email: string;
|
||||
timeZone: string;
|
||||
};
|
||||
|
||||
export interface EntryPoint {
|
||||
entryPointType?: string;
|
||||
uri?: string;
|
||||
label?: string;
|
||||
pin?: string;
|
||||
accessCode?: string;
|
||||
meetingCode?: string;
|
||||
passcode?: string;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
export interface AdditionInformation {
|
||||
conferenceData?: ConferenceData;
|
||||
entryPoints?: EntryPoint[];
|
||||
hangoutLink?: string;
|
||||
}
|
||||
|
||||
export interface CalendarEvent {
|
||||
type: string;
|
||||
title: string;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
description?: string | null;
|
||||
team?: {
|
||||
name: string;
|
||||
members: string[];
|
||||
};
|
||||
location?: string | null;
|
||||
organizer: Person;
|
||||
attendees: Person[];
|
||||
conferenceData?: ConferenceData;
|
||||
language: TFunction;
|
||||
additionInformation?: AdditionInformation;
|
||||
uid?: string | null;
|
||||
videoCallData?: VideoCallData;
|
||||
paymentInfo?: PaymentInfo | null;
|
||||
destinationCalendar?: DestinationCalendar | null;
|
||||
}
|
||||
|
||||
export interface IntegrationCalendar extends Ensure<Partial<SelectedCalendar>, "externalId"> {
|
||||
primary?: boolean;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
type EventBusyDate = Record<"start" | "end", Date | string>;
|
||||
|
||||
export interface Calendar {
|
||||
createEvent(event: CalendarEvent): Promise<NewCalendarEventType>;
|
||||
|
||||
updateEvent(uid: string, event: CalendarEvent): Promise<any>;
|
||||
|
||||
deleteEvent(uid: string): Promise<unknown>;
|
||||
|
||||
getAvailability(
|
||||
dateFrom: string,
|
||||
dateTo: string,
|
||||
selectedCalendars: IntegrationCalendar[]
|
||||
): Promise<EventBusyDate[]>;
|
||||
|
||||
listCalendars(event?: CalendarEvent): Promise<IntegrationCalendar[]>;
|
||||
}
|
5
lib/integrations/calendar/interfaces/GoogleCalendar.ts
Normal file
5
lib/integrations/calendar/interfaces/GoogleCalendar.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { calendar_v3 } from "googleapis";
|
||||
|
||||
export interface ConferenceData {
|
||||
createRequest?: calendar_v3.Schema$CreateConferenceRequest;
|
||||
}
|
10
lib/integrations/calendar/interfaces/Office365Calendar.ts
Normal file
10
lib/integrations/calendar/interfaces/Office365Calendar.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
export type BufferedBusyTime = {
|
||||
start: string;
|
||||
end: string;
|
||||
};
|
||||
|
||||
export type O365AuthCredentials = {
|
||||
expiry_date: number;
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
};
|
10
lib/integrations/calendar/services/AppleCalendarService.ts
Normal file
10
lib/integrations/calendar/services/AppleCalendarService.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { Credential } from "@prisma/client";
|
||||
|
||||
import { APPLE_CALENDAR_URL, CALENDAR_INTEGRATIONS_TYPES } from "../constants/generals";
|
||||
import CalendarService from "./BaseCalendarService";
|
||||
|
||||
export default class AppleCalendarService extends CalendarService {
|
||||
constructor(credential: Credential) {
|
||||
super(credential, CALENDAR_INTEGRATIONS_TYPES.apple, APPLE_CALENDAR_URL);
|
||||
}
|
||||
}
|
|
@ -1,11 +1,11 @@
|
|||
import { Credential } from "@prisma/client";
|
||||
import dayjs from "dayjs";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import ICAL from "ical.js";
|
||||
import { Attendee, createEvent, DateArray, DurationObject } from "ics";
|
||||
import { createEvent } from "ics";
|
||||
import {
|
||||
createAccount,
|
||||
createCalendarObject,
|
||||
DAVAccount,
|
||||
deleteCalendarObject,
|
||||
fetchCalendarObjects,
|
||||
fetchCalendars,
|
||||
|
@ -18,73 +18,62 @@ import { getLocation, getRichDescription } from "@lib/CalEventParser";
|
|||
import { symmetricDecrypt } from "@lib/crypto";
|
||||
import logger from "@lib/logger";
|
||||
|
||||
import { CalendarEvent, IntegrationCalendar } from "./calendarClient";
|
||||
import { TIMEZONE_FORMAT } from "../constants/formats";
|
||||
import { CALDAV_CALENDAR_TYPE } from "../constants/generals";
|
||||
import { CalendarEventType, EventBusyDate, NewCalendarEventType } from "../constants/types";
|
||||
import { Calendar, CalendarEvent, IntegrationCalendar } from "../interfaces/Calendar";
|
||||
import { convertDate, getAttendees, getDuration } from "../utils/CalendarUtils";
|
||||
|
||||
dayjs.extend(utc);
|
||||
const CALENDSO_ENCRYPTION_KEY = process.env.CALENDSO_ENCRYPTION_KEY || "";
|
||||
|
||||
export type Person = { name: string; email: string; timeZone: string };
|
||||
export default abstract class BaseCalendarService implements Calendar {
|
||||
private url = "";
|
||||
private credentials: Record<string, string> = {};
|
||||
private headers: Record<string, string> = {};
|
||||
protected integrationName = "";
|
||||
|
||||
export class BaseCalendarApiAdapter {
|
||||
private url: string;
|
||||
private credentials: Record<string, string>;
|
||||
private headers: Record<string, string>;
|
||||
private integrationName = "";
|
||||
log = logger.getChildLogger({ prefix: [`[[lib] ${this.integrationName}`] });
|
||||
|
||||
constructor(credential: Credential, integrationName: string, url?: string) {
|
||||
const decryptedCredential = JSON.parse(
|
||||
symmetricDecrypt(credential.key as string, process.env.CALENDSO_ENCRYPTION_KEY!)
|
||||
);
|
||||
const username = decryptedCredential.username;
|
||||
const password = decryptedCredential.password;
|
||||
this.url = url || decryptedCredential.url;
|
||||
this.integrationName = integrationName;
|
||||
|
||||
const {
|
||||
username,
|
||||
password,
|
||||
url: credentialURL,
|
||||
} = JSON.parse(symmetricDecrypt(credential.key as string, CALENDSO_ENCRYPTION_KEY));
|
||||
|
||||
this.url = url || credentialURL;
|
||||
|
||||
this.credentials = { username, password };
|
||||
this.headers = getBasicAuthHeaders({ username, password });
|
||||
}
|
||||
|
||||
log = logger.getChildLogger({ prefix: [`[[lib] ${this.integrationName}`] });
|
||||
|
||||
convertDate(date: string): DateArray {
|
||||
return dayjs(date)
|
||||
.utc()
|
||||
.toArray()
|
||||
.slice(0, 6)
|
||||
.map((v, i) => (i === 1 ? v + 1 : v)) as DateArray;
|
||||
}
|
||||
|
||||
getDuration(start: string, end: string): DurationObject {
|
||||
return {
|
||||
minutes: dayjs(end).diff(dayjs(start), "minute"),
|
||||
};
|
||||
}
|
||||
|
||||
getAttendees(attendees: Person[]): Attendee[] {
|
||||
return attendees.map(({ email, name }) => ({ name, email, partstat: "NEEDS-ACTION" }));
|
||||
}
|
||||
|
||||
async createEvent(event: CalendarEvent) {
|
||||
async createEvent(event: CalendarEvent): Promise<NewCalendarEventType> {
|
||||
try {
|
||||
const calendars = await this.listCalendars(event);
|
||||
|
||||
const uid = uuidv4();
|
||||
/** We create local ICS files */
|
||||
|
||||
// We create local ICS files
|
||||
const { error, value: iCalString } = createEvent({
|
||||
uid,
|
||||
startInputType: "utc",
|
||||
start: this.convertDate(event.startTime),
|
||||
duration: this.getDuration(event.startTime, event.endTime),
|
||||
start: convertDate(event.startTime),
|
||||
duration: getDuration(event.startTime, event.endTime),
|
||||
title: event.title,
|
||||
description: getRichDescription(event),
|
||||
location: getLocation(event),
|
||||
organizer: { email: event.organizer.email, name: event.organizer.name },
|
||||
// according to https://datatracker.ietf.org/doc/html/rfc2446#section-3.2.1, in a published iCalendar component. "Attendees" MUST NOT be present
|
||||
// attendees: this.getAttendees(event.attendees),
|
||||
/** according to https://datatracker.ietf.org/doc/html/rfc2446#section-3.2.1, in a published iCalendar component.
|
||||
* "Attendees" MUST NOT be present
|
||||
* `attendees: this.getAttendees(event.attendees),`
|
||||
*/
|
||||
});
|
||||
|
||||
if (error) throw new Error("Error creating iCalString");
|
||||
if (error || !iCalString) throw new Error("Error creating iCalString");
|
||||
|
||||
if (!iCalString) throw new Error("Error creating iCalString");
|
||||
|
||||
/** We create the event directly on iCal */
|
||||
// We create the event directly on iCal
|
||||
const responses = await Promise.all(
|
||||
calendars
|
||||
.filter((c) =>
|
||||
|
@ -117,46 +106,40 @@ export class BaseCalendarApiAdapter {
|
|||
type: this.integrationName,
|
||||
password: "",
|
||||
url: "",
|
||||
additionalInfo: {},
|
||||
};
|
||||
} catch (reason) {
|
||||
console.error(reason);
|
||||
logger.error(reason);
|
||||
|
||||
throw reason;
|
||||
}
|
||||
}
|
||||
|
||||
async updateEvent(uid: string, event: CalendarEvent) {
|
||||
async updateEvent(uid: string, event: CalendarEvent): Promise<any> {
|
||||
try {
|
||||
const calendars = await this.listCalendars();
|
||||
const events = [];
|
||||
|
||||
for (const cal of calendars) {
|
||||
const calEvents = await this.getEvents(cal.externalId, null, null, [`${cal.externalId}${uid}.ics`]);
|
||||
|
||||
for (const ev of calEvents) {
|
||||
events.push(ev);
|
||||
}
|
||||
}
|
||||
const events = await this.getEventsByUID(uid);
|
||||
|
||||
const { error, value: iCalString } = createEvent({
|
||||
uid,
|
||||
startInputType: "utc",
|
||||
start: this.convertDate(event.startTime),
|
||||
duration: this.getDuration(event.startTime, event.endTime),
|
||||
start: convertDate(event.startTime),
|
||||
duration: getDuration(event.startTime, event.endTime),
|
||||
title: event.title,
|
||||
description: getRichDescription(event),
|
||||
location: getLocation(event),
|
||||
organizer: { email: event.organizer.email, name: event.organizer.name },
|
||||
attendees: this.getAttendees(event.attendees),
|
||||
attendees: getAttendees(event.attendees),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
this.log.debug("Error creating iCalString");
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
const eventsToUpdate = events.filter((event) => event.uid === uid);
|
||||
|
||||
return await Promise.all(
|
||||
return Promise.all(
|
||||
eventsToUpdate.map((event) => {
|
||||
return updateCalendarObject({
|
||||
calendarObject: {
|
||||
|
@ -169,28 +152,20 @@ export class BaseCalendarApiAdapter {
|
|||
})
|
||||
);
|
||||
} catch (reason) {
|
||||
console.error(reason);
|
||||
this.log.error(reason);
|
||||
|
||||
throw reason;
|
||||
}
|
||||
}
|
||||
|
||||
async deleteEvent(uid: string): Promise<void> {
|
||||
try {
|
||||
const calendars = await this.listCalendars();
|
||||
const events = [];
|
||||
const events = await this.getEventsByUID(uid);
|
||||
|
||||
for (const cal of calendars) {
|
||||
const calEvents = await this.getEvents(cal.externalId, null, null, [`${cal.externalId}${uid}.ics`]);
|
||||
|
||||
for (const ev of calEvents) {
|
||||
events.push(ev);
|
||||
}
|
||||
}
|
||||
|
||||
const eventsToUpdate = events.filter((event) => event.uid === uid);
|
||||
const eventsToDelete = events.filter((event) => event.uid === uid);
|
||||
|
||||
await Promise.all(
|
||||
eventsToUpdate.map((event) => {
|
||||
eventsToDelete.map((event) => {
|
||||
return deleteCalendarObject({
|
||||
calendarObject: {
|
||||
url: event.url,
|
||||
|
@ -201,52 +176,30 @@ export class BaseCalendarApiAdapter {
|
|||
})
|
||||
);
|
||||
} catch (reason) {
|
||||
console.error(reason);
|
||||
this.log.error(reason);
|
||||
|
||||
throw reason;
|
||||
}
|
||||
}
|
||||
|
||||
async getAvailability(dateFrom: string, dateTo: string, selectedCalendars: IntegrationCalendar[]) {
|
||||
try {
|
||||
const selectedCalendarIds = selectedCalendars
|
||||
.filter((e) => e.integration === this.integrationName)
|
||||
.map((e) => e.externalId);
|
||||
if (selectedCalendarIds.length == 0 && selectedCalendars.length > 0) {
|
||||
// Only calendars of other integrations selected
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
getAvailability(
|
||||
dateFrom: string,
|
||||
dateTo: string,
|
||||
selectedCalendars: IntegrationCalendar[]
|
||||
): Promise<EventBusyDate[]> {
|
||||
this.log.warn(
|
||||
`Method not implemented. dateFrom: ${dateFrom}, dateTo: ${dateTo}, selectedCalendars: ${selectedCalendars}`
|
||||
);
|
||||
|
||||
return (
|
||||
selectedCalendarIds.length === 0
|
||||
? this.listCalendars().then((calendars) => calendars.map((calendar) => calendar.externalId))
|
||||
: Promise.resolve(selectedCalendarIds)
|
||||
).then(async (ids: string[]) => {
|
||||
if (ids.length === 0) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
const eventsBusyDate: EventBusyDate[] = [];
|
||||
|
||||
return (
|
||||
await Promise.all(
|
||||
ids.map(async (calId) => {
|
||||
return (await this.getEvents(calId, dateFrom, dateTo)).map((event) => {
|
||||
return {
|
||||
start: event.startDate.toISOString(),
|
||||
end: event.endDate.toISOString(),
|
||||
};
|
||||
});
|
||||
})
|
||||
)
|
||||
).flatMap((event) => event);
|
||||
});
|
||||
} catch (reason) {
|
||||
this.log.error(reason);
|
||||
throw reason;
|
||||
}
|
||||
return Promise.resolve(eventsBusyDate);
|
||||
}
|
||||
|
||||
async listCalendars(event?: CalendarEvent): Promise<IntegrationCalendar[]> {
|
||||
try {
|
||||
const account = await this.getAccount();
|
||||
|
||||
const calendars = await fetchCalendars({
|
||||
account,
|
||||
headers: this.headers,
|
||||
|
@ -254,6 +207,7 @@ export class BaseCalendarApiAdapter {
|
|||
|
||||
return calendars.reduce<IntegrationCalendar[]>((newCalendars, calendar) => {
|
||||
if (!calendar.components?.includes("VEVENT")) return newCalendars;
|
||||
|
||||
newCalendars.push({
|
||||
externalId: calendar.url,
|
||||
name: calendar.displayName ?? "",
|
||||
|
@ -265,12 +219,13 @@ export class BaseCalendarApiAdapter {
|
|||
return newCalendars;
|
||||
}, []);
|
||||
} catch (reason) {
|
||||
console.error(reason);
|
||||
logger.error(reason);
|
||||
|
||||
throw reason;
|
||||
}
|
||||
}
|
||||
|
||||
async getEvents(
|
||||
private async getEvents(
|
||||
calId: string,
|
||||
dateFrom: string | null,
|
||||
dateTo: string | null,
|
||||
|
@ -285,8 +240,8 @@ export class BaseCalendarApiAdapter {
|
|||
timeRange:
|
||||
dateFrom && dateTo
|
||||
? {
|
||||
start: dayjs(dateFrom).utc().format("YYYY-MM-DDTHH:mm:ss[Z]"),
|
||||
end: dayjs(dateTo).utc().format("YYYY-MM-DDTHH:mm:ss[Z]"),
|
||||
start: dayjs(dateFrom).utc().format(TIMEZONE_FORMAT),
|
||||
end: dayjs(dateTo).utc().format(TIMEZONE_FORMAT),
|
||||
}
|
||||
: undefined,
|
||||
headers: this.headers,
|
||||
|
@ -296,7 +251,9 @@ export class BaseCalendarApiAdapter {
|
|||
.filter((e) => !!e.data)
|
||||
.map((object) => {
|
||||
const jcalData = ICAL.parse(object.data);
|
||||
|
||||
const vcalendar = new ICAL.Component(jcalData);
|
||||
|
||||
const vevent = vcalendar.getFirstSubcomponent("vevent");
|
||||
const event = new ICAL.Event(vevent);
|
||||
|
||||
|
@ -306,6 +263,7 @@ export class BaseCalendarApiAdapter {
|
|||
const startDate = calendarTimezone
|
||||
? dayjs(event.startDate.toJSDate()).tz(calendarTimezone)
|
||||
: new Date(event.startDate.toUnixTime() * 1000);
|
||||
|
||||
const endDate = calendarTimezone
|
||||
? dayjs(event.endDate.toJSDate()).tz(calendarTimezone)
|
||||
: new Date(event.endDate.toUnixTime() * 1000);
|
||||
|
@ -342,16 +300,30 @@ export class BaseCalendarApiAdapter {
|
|||
}
|
||||
}
|
||||
|
||||
private async getAccount() {
|
||||
const account = await createAccount({
|
||||
private async getEventsByUID(uid: string): Promise<CalendarEventType[]> {
|
||||
const events = [];
|
||||
|
||||
const calendars = await this.listCalendars();
|
||||
|
||||
for (const cal of calendars) {
|
||||
const calEvents = await this.getEvents(cal.externalId, null, null, [`${cal.externalId}${uid}.ics`]);
|
||||
|
||||
for (const ev of calEvents) {
|
||||
events.push(ev);
|
||||
}
|
||||
}
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
private async getAccount(): Promise<DAVAccount> {
|
||||
return createAccount({
|
||||
account: {
|
||||
serverUrl: this.url,
|
||||
accountType: "caldav",
|
||||
accountType: CALDAV_CALENDAR_TYPE,
|
||||
credentials: this.credentials,
|
||||
},
|
||||
headers: this.headers,
|
||||
});
|
||||
|
||||
return account;
|
||||
}
|
||||
}
|
10
lib/integrations/calendar/services/CalDavCalendarService.ts
Normal file
10
lib/integrations/calendar/services/CalDavCalendarService.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { Credential } from "@prisma/client";
|
||||
|
||||
import { CALENDAR_INTEGRATIONS_TYPES } from "../constants/generals";
|
||||
import CalendarService from "./BaseCalendarService";
|
||||
|
||||
export default class CalDavCalendarService extends CalendarService {
|
||||
constructor(credential: Credential) {
|
||||
super(credential, CALENDAR_INTEGRATIONS_TYPES.caldav);
|
||||
}
|
||||
}
|
324
lib/integrations/calendar/services/GoogleCalendarService.ts
Normal file
324
lib/integrations/calendar/services/GoogleCalendarService.ts
Normal file
|
@ -0,0 +1,324 @@
|
|||
import { Credential, Prisma } from "@prisma/client";
|
||||
import { GetTokenResponse } from "google-auth-library/build/src/auth/oauth2client";
|
||||
import { Auth, calendar_v3, google } from "googleapis";
|
||||
|
||||
import { getLocation, getRichDescription } from "@lib/CalEventParser";
|
||||
import { CALENDAR_INTEGRATIONS_TYPES } from "@lib/integrations/calendar/constants/generals";
|
||||
import logger from "@lib/logger";
|
||||
import prisma from "@lib/prisma";
|
||||
|
||||
import { EventBusyDate, NewCalendarEventType } from "../constants/types";
|
||||
import { Calendar, CalendarEvent, IntegrationCalendar } from "../interfaces/Calendar";
|
||||
import CalendarService from "./BaseCalendarService";
|
||||
|
||||
const GOOGLE_API_CREDENTIALS = process.env.GOOGLE_API_CREDENTIALS || "";
|
||||
|
||||
export default class GoogleCalendarService implements Calendar {
|
||||
private url = "";
|
||||
private integrationName = "";
|
||||
private auth: { getToken: () => Promise<MyGoogleAuth> };
|
||||
|
||||
log = logger.getChildLogger({ prefix: [`[[lib] ${this.integrationName}`] });
|
||||
|
||||
constructor(credential: Credential) {
|
||||
this.integrationName = CALENDAR_INTEGRATIONS_TYPES.google;
|
||||
|
||||
this.auth = this.googleAuth(credential);
|
||||
}
|
||||
|
||||
private googleAuth = (credential: Credential) => {
|
||||
const { client_secret, client_id, redirect_uris } = JSON.parse(GOOGLE_API_CREDENTIALS).web;
|
||||
|
||||
const myGoogleAuth = new MyGoogleAuth(client_id, client_secret, redirect_uris[0]);
|
||||
|
||||
const googleCredentials = credential.key as Auth.Credentials;
|
||||
myGoogleAuth.setCredentials(googleCredentials);
|
||||
|
||||
const isExpired = () => myGoogleAuth.isTokenExpiring();
|
||||
|
||||
const refreshAccessToken = () =>
|
||||
myGoogleAuth
|
||||
.refreshToken(googleCredentials.refresh_token)
|
||||
.then((res: GetTokenResponse) => {
|
||||
const token = res.res?.data;
|
||||
googleCredentials.access_token = token.access_token;
|
||||
googleCredentials.expiry_date = token.expiry_date;
|
||||
return prisma.credential
|
||||
.update({
|
||||
where: {
|
||||
id: credential.id,
|
||||
},
|
||||
data: {
|
||||
key: googleCredentials as Prisma.InputJsonValue,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
myGoogleAuth.setCredentials(googleCredentials);
|
||||
return myGoogleAuth;
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
this.log.error("Error refreshing google token", err);
|
||||
|
||||
return myGoogleAuth;
|
||||
});
|
||||
|
||||
return {
|
||||
getToken: () => (!isExpired() ? Promise.resolve(myGoogleAuth) : refreshAccessToken()),
|
||||
};
|
||||
};
|
||||
|
||||
async createEvent(event: CalendarEvent): Promise<NewCalendarEventType> {
|
||||
return new Promise((resolve, reject) =>
|
||||
this.auth.getToken().then((myGoogleAuth) => {
|
||||
const payload: calendar_v3.Schema$Event = {
|
||||
summary: event.title,
|
||||
description: getRichDescription(event),
|
||||
start: {
|
||||
dateTime: event.startTime,
|
||||
timeZone: event.organizer.timeZone,
|
||||
},
|
||||
end: {
|
||||
dateTime: event.endTime,
|
||||
timeZone: event.organizer.timeZone,
|
||||
},
|
||||
attendees: event.attendees,
|
||||
reminders: {
|
||||
useDefault: false,
|
||||
overrides: [{ method: "email", minutes: 10 }],
|
||||
},
|
||||
};
|
||||
|
||||
if (event.location) {
|
||||
payload["location"] = getLocation(event);
|
||||
}
|
||||
|
||||
if (event.conferenceData && event.location === "integrations:google:meet") {
|
||||
payload["conferenceData"] = event.conferenceData;
|
||||
}
|
||||
|
||||
const calendar = google.calendar({
|
||||
version: "v3",
|
||||
auth: myGoogleAuth,
|
||||
});
|
||||
calendar.events.insert(
|
||||
{
|
||||
auth: myGoogleAuth,
|
||||
calendarId: event.destinationCalendar?.externalId
|
||||
? event.destinationCalendar.externalId
|
||||
: "primary",
|
||||
requestBody: payload,
|
||||
conferenceDataVersion: 1,
|
||||
},
|
||||
function (err, event) {
|
||||
if (err || !event?.data) {
|
||||
console.error("There was an error contacting google calendar service: ", err);
|
||||
return reject(err);
|
||||
}
|
||||
return resolve({
|
||||
uid: "",
|
||||
...event.data,
|
||||
id: event.data.id || "",
|
||||
additionalInfo: {
|
||||
hangoutLink: event.data.hangoutLink || "",
|
||||
},
|
||||
type: "google_calendar",
|
||||
password: "",
|
||||
url: "",
|
||||
});
|
||||
}
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async updateEvent(uid: string, event: CalendarEvent): Promise<any> {
|
||||
return new Promise((resolve, reject) =>
|
||||
this.auth.getToken().then((myGoogleAuth) => {
|
||||
const payload: calendar_v3.Schema$Event = {
|
||||
summary: event.title,
|
||||
description: getRichDescription(event),
|
||||
start: {
|
||||
dateTime: event.startTime,
|
||||
timeZone: event.organizer.timeZone,
|
||||
},
|
||||
end: {
|
||||
dateTime: event.endTime,
|
||||
timeZone: event.organizer.timeZone,
|
||||
},
|
||||
attendees: event.attendees,
|
||||
reminders: {
|
||||
useDefault: true,
|
||||
},
|
||||
};
|
||||
|
||||
if (event.location) {
|
||||
payload["location"] = getLocation(event);
|
||||
}
|
||||
|
||||
const calendar = google.calendar({
|
||||
version: "v3",
|
||||
auth: myGoogleAuth,
|
||||
});
|
||||
calendar.events.update(
|
||||
{
|
||||
auth: myGoogleAuth,
|
||||
calendarId: event.destinationCalendar?.externalId
|
||||
? event.destinationCalendar.externalId
|
||||
: "primary",
|
||||
eventId: uid,
|
||||
sendNotifications: true,
|
||||
sendUpdates: "all",
|
||||
requestBody: payload,
|
||||
},
|
||||
function (err, event) {
|
||||
if (err) {
|
||||
console.error("There was an error contacting google calendar service: ", err);
|
||||
|
||||
return reject(err);
|
||||
}
|
||||
return resolve(event?.data);
|
||||
}
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async deleteEvent(uid: string): Promise<void> {
|
||||
return new Promise((resolve, reject) =>
|
||||
this.auth.getToken().then((myGoogleAuth) => {
|
||||
const calendar = google.calendar({
|
||||
version: "v3",
|
||||
auth: myGoogleAuth,
|
||||
});
|
||||
calendar.events.delete(
|
||||
{
|
||||
auth: myGoogleAuth,
|
||||
calendarId: "primary",
|
||||
eventId: uid,
|
||||
sendNotifications: true,
|
||||
sendUpdates: "all",
|
||||
},
|
||||
function (err, event) {
|
||||
if (err) {
|
||||
console.error("There was an error contacting google calendar service: ", err);
|
||||
return reject(err);
|
||||
}
|
||||
return resolve(event?.data);
|
||||
}
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async getAvailability(
|
||||
dateFrom: string,
|
||||
dateTo: string,
|
||||
selectedCalendars: IntegrationCalendar[]
|
||||
): Promise<EventBusyDate[]> {
|
||||
return new Promise((resolve, reject) =>
|
||||
this.auth.getToken().then((myGoogleAuth) => {
|
||||
const calendar = google.calendar({
|
||||
version: "v3",
|
||||
auth: myGoogleAuth,
|
||||
});
|
||||
const selectedCalendarIds = selectedCalendars
|
||||
.filter((e) => e.integration === this.integrationName)
|
||||
.map((e) => e.externalId);
|
||||
if (selectedCalendarIds.length === 0 && selectedCalendars.length > 0) {
|
||||
// Only calendars of other integrations selected
|
||||
resolve([]);
|
||||
return;
|
||||
}
|
||||
|
||||
(selectedCalendarIds.length === 0
|
||||
? calendar.calendarList
|
||||
.list()
|
||||
.then((cals) => cals.data.items?.map((cal) => cal.id).filter(Boolean) || [])
|
||||
: Promise.resolve(selectedCalendarIds)
|
||||
)
|
||||
.then((calsIds) => {
|
||||
calendar.freebusy.query(
|
||||
{
|
||||
requestBody: {
|
||||
timeMin: dateFrom,
|
||||
timeMax: dateTo,
|
||||
items: calsIds.map((id) => ({ id: id })),
|
||||
},
|
||||
},
|
||||
(err, apires) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
let result: Prisma.PromiseReturnType<CalendarService["getAvailability"]> = [];
|
||||
|
||||
if (apires?.data.calendars) {
|
||||
result = Object.values(apires.data.calendars).reduce((c, i) => {
|
||||
i.busy?.forEach((busyTime) => {
|
||||
c.push({
|
||||
start: busyTime.start || "",
|
||||
end: busyTime.end || "",
|
||||
});
|
||||
});
|
||||
return c;
|
||||
}, [] as typeof result);
|
||||
}
|
||||
resolve(result);
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch((err) => {
|
||||
this.log.error("There was an error contacting google calendar service: ", err);
|
||||
|
||||
reject(err);
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async listCalendars(): Promise<IntegrationCalendar[]> {
|
||||
return new Promise((resolve, reject) =>
|
||||
this.auth.getToken().then((myGoogleAuth) => {
|
||||
const calendar = google.calendar({
|
||||
version: "v3",
|
||||
auth: myGoogleAuth,
|
||||
});
|
||||
|
||||
calendar.calendarList
|
||||
.list()
|
||||
.then((cals) => {
|
||||
resolve(
|
||||
cals.data.items?.map((cal) => {
|
||||
const calendar: IntegrationCalendar = {
|
||||
externalId: cal.id ?? "No id",
|
||||
integration: this.integrationName,
|
||||
name: cal.summary ?? "No name",
|
||||
primary: cal.primary ?? false,
|
||||
};
|
||||
return calendar;
|
||||
}) || []
|
||||
);
|
||||
})
|
||||
.catch((err) => {
|
||||
this.log.error("There was an error contacting google calendar service: ", err);
|
||||
|
||||
reject(err);
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MyGoogleAuth extends google.auth.OAuth2 {
|
||||
constructor(client_id: string, client_secret: string, redirect_uri: string) {
|
||||
super(client_id, client_secret, redirect_uri);
|
||||
}
|
||||
|
||||
isTokenExpiring() {
|
||||
return super.isTokenExpiring();
|
||||
}
|
||||
|
||||
async refreshToken(token: string | null | undefined) {
|
||||
return super.refreshToken(token);
|
||||
}
|
||||
}
|
250
lib/integrations/calendar/services/Office365CalendarService.ts
Normal file
250
lib/integrations/calendar/services/Office365CalendarService.ts
Normal file
|
@ -0,0 +1,250 @@
|
|||
import { Calendar as OfficeCalendar } from "@microsoft/microsoft-graph-types-beta";
|
||||
import { Credential } from "@prisma/client";
|
||||
|
||||
import { getLocation, getRichDescription } from "@lib/CalEventParser";
|
||||
import { handleErrorsJson, handleErrorsRaw } from "@lib/errors";
|
||||
import { CALENDAR_INTEGRATIONS_TYPES } from "@lib/integrations/calendar/constants/generals";
|
||||
import logger from "@lib/logger";
|
||||
import prisma from "@lib/prisma";
|
||||
|
||||
import { BatchResponse, EventBusyDate, NewCalendarEventType } from "../constants/types";
|
||||
import { Calendar, CalendarEvent, IntegrationCalendar } from "../interfaces/Calendar";
|
||||
import { BufferedBusyTime, O365AuthCredentials } from "../interfaces/Office365Calendar";
|
||||
|
||||
const MS_GRAPH_CLIENT_ID = process.env.MS_GRAPH_CLIENT_ID || "";
|
||||
const MS_GRAPH_CLIENT_SECRET = process.env.MS_GRAPH_CLIENT_SECRET || "";
|
||||
|
||||
export default class Office365CalendarService implements Calendar {
|
||||
private url = "";
|
||||
private integrationName = "";
|
||||
auth: { getToken: () => Promise<string> };
|
||||
|
||||
log = logger.getChildLogger({ prefix: [`[[lib] ${this.integrationName}`] });
|
||||
|
||||
constructor(credential: Credential) {
|
||||
this.integrationName = CALENDAR_INTEGRATIONS_TYPES.office365;
|
||||
this.auth = this.o365Auth(credential);
|
||||
}
|
||||
|
||||
async createEvent(event: CalendarEvent): Promise<NewCalendarEventType> {
|
||||
try {
|
||||
const accessToken = await this.auth.getToken();
|
||||
|
||||
const calendarId = event.destinationCalendar?.externalId
|
||||
? `${event.destinationCalendar.externalId}/`
|
||||
: "";
|
||||
|
||||
const response = await fetch(`https://graph.microsoft.com/v1.0/me/calendar/${calendarId}events`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: "Bearer " + accessToken,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(this.translateEvent(event)),
|
||||
});
|
||||
|
||||
return handleErrorsJson(response);
|
||||
} catch (error) {
|
||||
this.log.error(error);
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async updateEvent(uid: string, event: CalendarEvent): Promise<any> {
|
||||
try {
|
||||
const accessToken = await this.auth.getToken();
|
||||
|
||||
const response = await fetch("https://graph.microsoft.com/v1.0/me/calendar/events/" + uid, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
Authorization: "Bearer " + accessToken,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(this.translateEvent(event)),
|
||||
});
|
||||
|
||||
return handleErrorsRaw(response);
|
||||
} catch (error) {
|
||||
this.log.error(error);
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async deleteEvent(uid: string): Promise<void> {
|
||||
try {
|
||||
const accessToken = await this.auth.getToken();
|
||||
|
||||
const response = await fetch("https://graph.microsoft.com/v1.0/me/calendar/events/" + uid, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
Authorization: "Bearer " + accessToken,
|
||||
},
|
||||
});
|
||||
|
||||
handleErrorsRaw(response);
|
||||
} catch (error) {
|
||||
this.log.error(error);
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getAvailability(
|
||||
dateFrom: string,
|
||||
dateTo: string,
|
||||
selectedCalendars: IntegrationCalendar[]
|
||||
): Promise<EventBusyDate[]> {
|
||||
const dateFromParsed = new Date(dateFrom);
|
||||
const dateToParsed = new Date(dateTo);
|
||||
|
||||
const filter = `?startdatetime=${encodeURIComponent(
|
||||
dateFromParsed.toISOString()
|
||||
)}&enddatetime=${encodeURIComponent(dateToParsed.toISOString())}`;
|
||||
return this.auth
|
||||
.getToken()
|
||||
.then((accessToken) => {
|
||||
const selectedCalendarIds = selectedCalendars
|
||||
.filter((e) => e.integration === this.integrationName)
|
||||
.map((e) => e.externalId)
|
||||
.filter(Boolean);
|
||||
if (selectedCalendarIds.length === 0 && selectedCalendars.length > 0) {
|
||||
// Only calendars of other integrations selected
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
return (
|
||||
selectedCalendarIds.length === 0
|
||||
? this.listCalendars().then((cals) => cals.map((e) => e.externalId).filter(Boolean) || [])
|
||||
: Promise.resolve(selectedCalendarIds)
|
||||
).then((ids) => {
|
||||
const requests = ids.map((calendarId, id) => ({
|
||||
id,
|
||||
method: "GET",
|
||||
url: `/me/calendars/${calendarId}/calendarView${filter}`,
|
||||
}));
|
||||
|
||||
return fetch("https://graph.microsoft.com/v1.0/$batch", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: "Bearer " + accessToken,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ requests }),
|
||||
})
|
||||
.then(handleErrorsJson)
|
||||
.then((responseBody: BatchResponse) =>
|
||||
responseBody.responses.reduce(
|
||||
(acc: BufferedBusyTime[], subResponse) =>
|
||||
acc.concat(
|
||||
subResponse.body.value.map((evt) => {
|
||||
return {
|
||||
start: evt.start.dateTime + "Z",
|
||||
end: evt.end.dateTime + "Z",
|
||||
};
|
||||
})
|
||||
),
|
||||
[]
|
||||
)
|
||||
);
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
return Promise.reject([]);
|
||||
});
|
||||
}
|
||||
|
||||
async listCalendars(): Promise<IntegrationCalendar[]> {
|
||||
return this.auth.getToken().then((accessToken) =>
|
||||
fetch("https://graph.microsoft.com/v1.0/me/calendars", {
|
||||
method: "get",
|
||||
headers: {
|
||||
Authorization: "Bearer " + accessToken,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then(handleErrorsJson)
|
||||
.then((responseBody: { value: OfficeCalendar[] }) => {
|
||||
return responseBody.value.map((cal) => {
|
||||
const calendar: IntegrationCalendar = {
|
||||
externalId: cal.id ?? "No Id",
|
||||
integration: this.integrationName,
|
||||
name: cal.name ?? "No calendar name",
|
||||
primary: cal.isDefaultCalendar ?? false,
|
||||
};
|
||||
return calendar;
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private o365Auth = (credential: Credential) => {
|
||||
const isExpired = (expiryDate: number) => expiryDate < Math.round(+new Date() / 1000);
|
||||
|
||||
const o365AuthCredentials = credential.key as O365AuthCredentials;
|
||||
|
||||
const refreshAccessToken = (refreshToken: string) => {
|
||||
return fetch("https://login.microsoftonline.com/common/oauth2/v2.0/token", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({
|
||||
scope: "User.Read Calendars.Read Calendars.ReadWrite",
|
||||
client_id: MS_GRAPH_CLIENT_ID,
|
||||
refresh_token: refreshToken,
|
||||
grant_type: "refresh_token",
|
||||
client_secret: MS_GRAPH_CLIENT_SECRET,
|
||||
}),
|
||||
})
|
||||
.then(handleErrorsJson)
|
||||
.then((responseBody) => {
|
||||
o365AuthCredentials.access_token = responseBody.access_token;
|
||||
o365AuthCredentials.expiry_date = Math.round(+new Date() / 1000 + responseBody.expires_in);
|
||||
return prisma.credential
|
||||
.update({
|
||||
where: {
|
||||
id: credential.id,
|
||||
},
|
||||
data: {
|
||||
key: o365AuthCredentials,
|
||||
},
|
||||
})
|
||||
.then(() => o365AuthCredentials.access_token);
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
getToken: () =>
|
||||
!isExpired(o365AuthCredentials.expiry_date)
|
||||
? Promise.resolve(o365AuthCredentials.access_token)
|
||||
: refreshAccessToken(o365AuthCredentials.refresh_token),
|
||||
};
|
||||
};
|
||||
|
||||
private translateEvent = (event: CalendarEvent) => {
|
||||
return {
|
||||
subject: event.title,
|
||||
body: {
|
||||
contentType: "HTML",
|
||||
content: getRichDescription(event),
|
||||
},
|
||||
start: {
|
||||
dateTime: event.startTime,
|
||||
timeZone: event.organizer.timeZone,
|
||||
},
|
||||
end: {
|
||||
dateTime: event.endTime,
|
||||
timeZone: event.organizer.timeZone,
|
||||
},
|
||||
attendees: event.attendees.map((attendee) => ({
|
||||
emailAddress: {
|
||||
address: attendee.email,
|
||||
name: attendee.name,
|
||||
},
|
||||
type: "required",
|
||||
})),
|
||||
location: event.location ? { displayName: getLocation(event) } : undefined,
|
||||
};
|
||||
};
|
||||
}
|
16
lib/integrations/calendar/utils/CalendarUtils.ts
Normal file
16
lib/integrations/calendar/utils/CalendarUtils.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import dayjs from "dayjs";
|
||||
import { Attendee, DateArray, DurationObject, Person } from "ics";
|
||||
|
||||
export const convertDate = (date: string): DateArray =>
|
||||
dayjs(date)
|
||||
.utc()
|
||||
.toArray()
|
||||
.slice(0, 6)
|
||||
.map((v, i) => (i === 1 ? v + 1 : v)) as DateArray;
|
||||
|
||||
export const getDuration = (start: string, end: string): DurationObject => ({
|
||||
minutes: dayjs(end).diff(dayjs(start), "minute"),
|
||||
});
|
||||
|
||||
export const getAttendees = (attendees: Person[]): Attendee[] =>
|
||||
attendees.map(({ email, name }) => ({ name, email, partstat: "NEEDS-ACTION" }));
|
|
@ -4,7 +4,7 @@ import dayjs from "dayjs";
|
|||
|
||||
import { asStringOrNull } from "@lib/asStringOrNull";
|
||||
import { getWorkingHours } from "@lib/availability";
|
||||
import { getBusyCalendarTimes } from "@lib/calendarClient";
|
||||
import { getBusyCalendarTimes } from "@lib/integrations/calendar/CalendarManager";
|
||||
import prisma from "@lib/prisma";
|
||||
|
||||
export async function getUserAvailability(query: {
|
||||
|
|
|
@ -7,9 +7,9 @@ import { EventResult } from "@lib/events/EventManager";
|
|||
import { PartialReference } from "@lib/events/EventManager";
|
||||
import logger from "@lib/logger";
|
||||
|
||||
import { CalendarEvent } from "./calendarClient";
|
||||
import DailyVideoApiAdapter from "./integrations/Daily/DailyVideoApiAdapter";
|
||||
import ZoomVideoApiAdapter from "./integrations/Zoom/ZoomVideoApiAdapter";
|
||||
import { CalendarEvent } from "./integrations/calendar/interfaces/Calendar";
|
||||
import { Ensure } from "./types/utils";
|
||||
|
||||
const log = logger.getChildLogger({ prefix: ["[lib] videoClient"] });
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { compile } from "handlebars";
|
||||
|
||||
import { CalendarEvent } from "@lib/calendarClient";
|
||||
import { CalendarEvent } from "@lib/integrations/calendar/interfaces/Calendar";
|
||||
|
||||
type ContentType = "application/json" | "application/x-www-form-urlencoded";
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
|||
|
||||
import { asStringOrNull } from "@lib/asStringOrNull";
|
||||
import { getWorkingHours } from "@lib/availability";
|
||||
import { getBusyCalendarTimes } from "@lib/calendarClient";
|
||||
import { getBusyCalendarTimes } from "@lib/integrations/calendar/CalendarManager";
|
||||
import prisma from "@lib/prisma";
|
||||
|
||||
dayjs.extend(utc);
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
import { getCalendarCredentials, getConnectedCalendars } from "@lib/integrations/calendar/CalendarManager";
|
||||
import notEmpty from "@lib/notEmpty";
|
||||
import prisma from "@lib/prisma";
|
||||
|
||||
import getCalendarCredentials from "@server/integrations/getCalendarCredentials";
|
||||
import getConnectedCalendars from "@server/integrations/getConnectedCalendars";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const session = await getSession({ req });
|
||||
|
||||
|
|
|
@ -4,10 +4,10 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
|||
import { refund } from "@ee/lib/stripe/server";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
import { CalendarEvent, AdditionInformation } from "@lib/calendarClient";
|
||||
import { sendDeclinedEmails } from "@lib/emails/email-manager";
|
||||
import { sendScheduledEmails } from "@lib/emails/email-manager";
|
||||
import EventManager from "@lib/events/EventManager";
|
||||
import { CalendarEvent, AdditionInformation } from "@lib/integrations/calendar/interfaces/Calendar";
|
||||
import logger from "@lib/logger";
|
||||
import prisma from "@lib/prisma";
|
||||
import { BookingConfirmBody } from "@lib/types/booking";
|
||||
|
|
|
@ -11,7 +11,6 @@ import { v5 as uuidv5 } from "uuid";
|
|||
|
||||
import { handlePayment } from "@ee/lib/stripe/server";
|
||||
|
||||
import { CalendarEvent, AdditionInformation, getBusyCalendarTimes } from "@lib/calendarClient";
|
||||
import {
|
||||
sendScheduledEmails,
|
||||
sendRescheduledEmails,
|
||||
|
@ -21,7 +20,9 @@ import { ensureArray } from "@lib/ensureArray";
|
|||
import { getErrorFromUnknown } from "@lib/errors";
|
||||
import { getEventName } from "@lib/event";
|
||||
import EventManager, { EventResult, PartialReference } from "@lib/events/EventManager";
|
||||
import { BufferedBusyTime } from "@lib/integrations/Office365Calendar/Office365CalendarApiAdapter";
|
||||
import { getBusyCalendarTimes } from "@lib/integrations/calendar/CalendarManager";
|
||||
import { CalendarEvent, AdditionInformation } from "@lib/integrations/calendar/interfaces/Calendar";
|
||||
import { BufferedBusyTime } from "@lib/integrations/calendar/interfaces/Office365Calendar";
|
||||
import logger from "@lib/logger";
|
||||
import notEmpty from "@lib/notEmpty";
|
||||
import prisma from "@lib/prisma";
|
||||
|
|
|
@ -6,9 +6,10 @@ import { refund } from "@ee/lib/stripe/server";
|
|||
|
||||
import { asStringOrNull } from "@lib/asStringOrNull";
|
||||
import { getSession } from "@lib/auth";
|
||||
import { CalendarEvent, deleteEvent } from "@lib/calendarClient";
|
||||
import { sendCancelledEmails } from "@lib/emails/email-manager";
|
||||
import { FAKE_DAILY_CREDENTIAL } from "@lib/integrations/Daily/DailyVideoApiAdapter";
|
||||
import { getCalendar } from "@lib/integrations/calendar/CalendarManager";
|
||||
import { CalendarEvent } from "@lib/integrations/calendar/interfaces/Calendar";
|
||||
import prisma from "@lib/prisma";
|
||||
import { deleteMeeting } from "@lib/videoClient";
|
||||
import sendPayload from "@lib/webhooks/sendPayload";
|
||||
|
@ -138,9 +139,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
const bookingRefUid = bookingToDelete.references.filter((ref) => ref.type === credential.type)[0]?.uid;
|
||||
if (bookingRefUid) {
|
||||
if (credential.type.endsWith("_calendar")) {
|
||||
return await deleteEvent(credential, bookingRefUid);
|
||||
const calendar = getCalendar(credential);
|
||||
|
||||
return calendar?.deleteEvent(bookingRefUid);
|
||||
} else if (credential.type.endsWith("_video")) {
|
||||
return await deleteMeeting(credential, bookingRefUid);
|
||||
return deleteMeeting(credential, bookingRefUid);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -2,8 +2,8 @@ import { ReminderType } from "@prisma/client";
|
|||
import dayjs from "dayjs";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { CalendarEvent } from "@lib/calendarClient";
|
||||
import { sendOrganizerRequestReminderEmail } from "@lib/emails/email-manager";
|
||||
import { CalendarEvent } from "@lib/integrations/calendar/interfaces/Calendar";
|
||||
import prisma from "@lib/prisma";
|
||||
|
||||
import { getTranslation } from "@server/lib/i18n";
|
||||
|
|
|
@ -2,7 +2,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
|||
|
||||
import { getSession } from "@lib/auth";
|
||||
import { symmetricEncrypt } from "@lib/crypto";
|
||||
import { AppleCalendar } from "@lib/integrations/Apple/AppleCalendarAdapter";
|
||||
import { getCalendar } from "@lib/integrations/calendar/CalendarManager";
|
||||
import logger from "@lib/logger";
|
||||
import prisma from "@lib/prisma";
|
||||
|
||||
|
@ -35,11 +35,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
};
|
||||
|
||||
try {
|
||||
const dav = new AppleCalendar({
|
||||
const dav = getCalendar({
|
||||
id: 0,
|
||||
...data,
|
||||
});
|
||||
await dav.listCalendars();
|
||||
await dav?.listCalendars();
|
||||
await prisma.credential.create({
|
||||
data,
|
||||
});
|
||||
|
|
|
@ -2,7 +2,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
|||
|
||||
import { getSession } from "@lib/auth";
|
||||
import { symmetricEncrypt } from "@lib/crypto";
|
||||
import { CalDavCalendar } from "@lib/integrations/CalDav/CalDavCalendarAdapter";
|
||||
import { getCalendar } from "@lib/integrations/calendar/CalendarManager";
|
||||
import logger from "@lib/logger";
|
||||
import prisma from "@lib/prisma";
|
||||
|
||||
|
@ -38,11 +38,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
};
|
||||
|
||||
try {
|
||||
const dav = new CalDavCalendar({
|
||||
const dav = getCalendar({
|
||||
id: 0,
|
||||
...data,
|
||||
});
|
||||
await dav.listCalendars();
|
||||
await dav?.listCalendars();
|
||||
await prisma.credential.create({
|
||||
data,
|
||||
});
|
||||
|
|
|
@ -18,6 +18,7 @@ import TimezoneSelect from "react-timezone-select";
|
|||
import { getSession } from "@lib/auth";
|
||||
import { DEFAULT_SCHEDULE } from "@lib/availability";
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
import { getCalendarCredentials, getConnectedCalendars } from "@lib/integrations/calendar/CalendarManager";
|
||||
import getIntegrations from "@lib/integrations/getIntegrations";
|
||||
import prisma from "@lib/prisma";
|
||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
@ -32,9 +33,6 @@ import Button from "@components/ui/Button";
|
|||
import Text from "@components/ui/Text";
|
||||
import Schedule from "@components/ui/form/Schedule";
|
||||
|
||||
import getCalendarCredentials from "@server/integrations/getCalendarCredentials";
|
||||
import getConnectedCalendars from "@server/integrations/getConnectedCalendars";
|
||||
|
||||
import getEventTypes from "../lib/queries/event-types/get-event-types";
|
||||
|
||||
dayjs.extend(utc);
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import { Browser, chromium } from "@playwright/test";
|
||||
import fs from "fs";
|
||||
|
||||
async function loginAsUser(username: string, browser: Browser) {
|
||||
// Skip is file exists
|
||||
if (fs.existsSync(`playwright/artifacts/${username}StorageState.json`)) return;
|
||||
const page = await browser.newPage();
|
||||
await page.goto(`${process.env.PLAYWRIGHT_TEST_BASE_URL}/auth/login`);
|
||||
// Click input[name="email"]
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
import { Credential } from "@prisma/client";
|
||||
|
||||
import { getCalendarAdapterOrNull } from "@lib/calendarClient";
|
||||
import { ALL_INTEGRATIONS } from "@lib/integrations/getIntegrations";
|
||||
|
||||
export default function getCalendarCredentials(
|
||||
credentials: Array<Omit<Credential, "userId">>,
|
||||
userId: number
|
||||
) {
|
||||
const calendarCredentials = credentials
|
||||
.filter((credential) => credential.type.endsWith("_calendar"))
|
||||
.flatMap((credential) => {
|
||||
const integration = ALL_INTEGRATIONS.find((integration) => integration.type === credential.type);
|
||||
|
||||
const adapter = getCalendarAdapterOrNull({
|
||||
...credential,
|
||||
userId,
|
||||
});
|
||||
return integration && adapter && integration.variant === "calendar"
|
||||
? [{ integration, credential, adapter }]
|
||||
: [];
|
||||
});
|
||||
|
||||
return calendarCredentials;
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
import _ from "lodash";
|
||||
|
||||
import { getErrorFromUnknown } from "@lib/errors";
|
||||
|
||||
import getCalendarCredentials from "./getCalendarCredentials";
|
||||
|
||||
export default async function getConnectedCalendars(
|
||||
calendarCredentials: ReturnType<typeof getCalendarCredentials>,
|
||||
selectedCalendars: { externalId: string }[]
|
||||
) {
|
||||
const connectedCalendars = await Promise.all(
|
||||
calendarCredentials.map(async (item) => {
|
||||
const { adapter, integration, credential } = item;
|
||||
|
||||
const credentialId = credential.id;
|
||||
try {
|
||||
const cals = await adapter.listCalendars();
|
||||
const calendars = _(cals)
|
||||
.map((cal) => ({
|
||||
...cal,
|
||||
primary: cal.primary || null,
|
||||
isSelected: selectedCalendars.some((selected) => selected.externalId === cal.externalId),
|
||||
}))
|
||||
.sortBy(["primary"])
|
||||
.value();
|
||||
const primary = calendars.find((item) => item.primary) ?? calendars[0];
|
||||
if (!primary) {
|
||||
throw new Error("No primary calendar found");
|
||||
}
|
||||
return {
|
||||
integration,
|
||||
credentialId,
|
||||
primary,
|
||||
calendars,
|
||||
};
|
||||
} catch (_error) {
|
||||
const error = getErrorFromUnknown(_error);
|
||||
return {
|
||||
integration,
|
||||
credentialId,
|
||||
error: {
|
||||
message: error.message,
|
||||
},
|
||||
};
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return connectedCalendars;
|
||||
}
|
|
@ -5,12 +5,11 @@ import { z } from "zod";
|
|||
import { checkPremiumUsername } from "@ee/lib/core/checkPremiumUsername";
|
||||
|
||||
import { checkRegularUsername } from "@lib/core/checkRegularUsername";
|
||||
import { getCalendarCredentials, getConnectedCalendars } from "@lib/integrations/calendar/CalendarManager";
|
||||
import { ALL_INTEGRATIONS } from "@lib/integrations/getIntegrations";
|
||||
import slugify from "@lib/slugify";
|
||||
import { Schedule } from "@lib/types/schedule";
|
||||
|
||||
import getCalendarCredentials from "@server/integrations/getCalendarCredentials";
|
||||
import getConnectedCalendars from "@server/integrations/getConnectedCalendars";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import { createProtectedRouter, createRouter } from "../createRouter";
|
||||
|
@ -298,7 +297,10 @@ const loggedInViewerRouter = createProtectedRouter()
|
|||
},
|
||||
],
|
||||
};
|
||||
const bookingListingOrderby: Record<typeof bookingListingByStatus, Prisma.BookingOrderByInput> = {
|
||||
const bookingListingOrderby: Record<
|
||||
typeof bookingListingByStatus,
|
||||
Prisma.BookingOrderByWithAggregationInput
|
||||
> = {
|
||||
upcoming: { startTime: "desc" },
|
||||
past: { startTime: "desc" },
|
||||
cancelled: { startTime: "desc" },
|
||||
|
|
Loading…
Reference in a new issue