forked from calcom/cal.com
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Create 2013 package * Create 2016 package * Add ews * Update package.json * Translate 2013 app to new structure * Translate 2013 app to new structure * Translate 2016 app to new structure * Add appId * Move setup to a seperate page * RHF dependency version mismatch * Move exchange 2016 setup to new page * Add translations * Relying on AppSetupMap not defined static pages * Console build fixes * Resolved node version to 16 * Prisma errors can't be handled on client * Fixes node version mismatches * Improvements * Endpoint fixes * Revert "Endpoint fixes" This reverts commit c0320e3. * Fixes Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com> Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: Leo Giovanetti <hello@leog.me>
- Loading branch information
1 parent
3c9cf81
commit 1960046
Showing
31 changed files
with
975 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import type { App } from "@calcom/types/App"; | ||
|
||
import _package from "./package.json"; | ||
|
||
export const metadata = { | ||
name: "Microsoft Exchange 2013 Calendar", | ||
description: _package.description, | ||
installed: true, | ||
type: "exchange2013_calendar", | ||
title: "Microsoft Exchange 2013 Calendar", | ||
imageSrc: "/api/app-store/exchange2013calendar/icon.svg", | ||
variant: "calendar", | ||
category: "calendar", | ||
label: "Exchange Calendar", | ||
logo: "/api/app-store/exchange2013calendar/icon.svg", | ||
publisher: "Cal.com", | ||
rating: 5, | ||
reviews: 69, | ||
slug: "exchange2013-calendar", | ||
trending: false, | ||
url: "https://cal.com/", | ||
verified: true, | ||
email: "help@cal.com", | ||
} as App; | ||
|
||
export default metadata; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import type { NextApiRequest, NextApiResponse } from "next"; | ||
import { z } from "zod"; | ||
|
||
import { symmetricEncrypt } from "@calcom/lib/crypto"; | ||
import logger from "@calcom/lib/logger"; | ||
import { defaultHandler, defaultResponder } from "@calcom/lib/server"; | ||
import prisma from "@calcom/prisma"; | ||
|
||
import { CalendarService } from "../lib"; | ||
|
||
const bodySchema = z | ||
.object({ | ||
username: z.string(), | ||
password: z.string(), | ||
url: z.string().url(), | ||
}) | ||
.strict(); | ||
|
||
async function postHandler(req: NextApiRequest, res: NextApiResponse) { | ||
const body = bodySchema.parse(req.body); | ||
// Get user | ||
const user = await prisma.user.findFirst({ | ||
rejectOnNotFound: true, | ||
where: { | ||
id: req.session?.user?.id, | ||
}, | ||
select: { | ||
id: true, | ||
}, | ||
}); | ||
|
||
const data = { | ||
type: "exchange2013_calendar", | ||
key: symmetricEncrypt(JSON.stringify(body), process.env.CALENDSO_ENCRYPTION_KEY!), | ||
userId: user.id, | ||
appId: "exchange2013_calendar", | ||
}; | ||
|
||
try { | ||
const dav = new CalendarService({ | ||
id: 0, | ||
...data, | ||
}); | ||
await dav?.listCalendars(); | ||
await prisma.credential.create({ | ||
data, | ||
}); | ||
} catch (reason) { | ||
logger.error("Could not add this exchange account", reason); | ||
return res.status(500).json({ message: "Could not add this exchange account" }); | ||
} | ||
|
||
return { url: "/apps/installed" }; | ||
} | ||
|
||
async function getHandler() { | ||
return { url: "/apps/exchange2013-calendar/setup" }; | ||
} | ||
|
||
export default defaultHandler({ | ||
GET: Promise.resolve({ default: defaultResponder(getHandler) }), | ||
POST: Promise.resolve({ default: defaultResponder(postHandler) }), | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { default as add } from "./add"; |
18 changes: 18 additions & 0 deletions
18
packages/app-store/exchange2013calendar/components/InstallAppButton.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import type { InstallAppButtonProps } from "@calcom/app-store/types"; | ||
|
||
import useAddAppMutation from "../../_utils/useAddAppMutation"; | ||
|
||
export default function InstallAppButton(props: InstallAppButtonProps) { | ||
const mutation = useAddAppMutation("exchange2013_calendar"); | ||
|
||
return ( | ||
<> | ||
{props.render({ | ||
onClick() { | ||
mutation.mutate(""); | ||
}, | ||
loading: mutation.isLoading, | ||
})} | ||
</> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { default as InstallAppButton } from "./InstallAppButton"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export * as api from "./api"; | ||
export * as lib from "./lib"; | ||
export { metadata } from "./_metadata"; |
218 changes: 218 additions & 0 deletions
218
packages/app-store/exchange2013calendar/lib/CalendarService.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,218 @@ | ||
import { Credential } from "@prisma/client"; | ||
import { | ||
Appointment, | ||
Attendee, | ||
CalendarView, | ||
ConflictResolutionMode, | ||
DateTime, | ||
DeleteMode, | ||
ExchangeService, | ||
ExchangeVersion, | ||
FolderId, | ||
FolderView, | ||
ItemId, | ||
LegacyFreeBusyStatus, | ||
MessageBody, | ||
PropertySet, | ||
SendInvitationsMode, | ||
SendInvitationsOrCancellationsMode, | ||
Uri, | ||
WebCredentials, | ||
WellKnownFolderName, | ||
} from "ews-javascript-api"; | ||
|
||
import { symmetricDecrypt } from "@calcom/lib/crypto"; | ||
// Probably don't need | ||
// import { CALENDAR_INTEGRATIONS_TYPES } from "@calcom/lib/integrations/calendar/constants/generals"; | ||
import logger from "@calcom/lib/logger"; | ||
import { Calendar, CalendarEvent, EventBusyDate, IntegrationCalendar } from "@calcom/types/Calendar"; | ||
|
||
export default class ExchangeCalendarService implements Calendar { | ||
private url = ""; | ||
private integrationName = ""; | ||
private log: typeof logger; | ||
private readonly exchangeVersion: ExchangeVersion; | ||
private credentials: Record<string, string>; | ||
|
||
constructor(credential: Credential) { | ||
this.integrationName = "exchange2013_calendar"; | ||
|
||
this.log = logger.getChildLogger({ prefix: [`[[lib] ${this.integrationName}`] }); | ||
|
||
const decryptedCredential = JSON.parse( | ||
symmetricDecrypt(credential.key?.toString() || "", process.env.CALENDSO_ENCRYPTION_KEY || "") | ||
); | ||
const username = decryptedCredential.username; | ||
const url = decryptedCredential.url; | ||
const password = decryptedCredential.password; | ||
|
||
this.url = url; | ||
|
||
this.credentials = { | ||
username, | ||
password, | ||
}; | ||
this.exchangeVersion = ExchangeVersion.Exchange2013; | ||
} | ||
|
||
async createEvent(event: CalendarEvent) { | ||
try { | ||
const appointment = new Appointment(this.getExchangeService()); // service instance of ExchangeService | ||
appointment.Subject = event.title; | ||
appointment.Start = DateTime.Parse(event.startTime); // moment string | ||
appointment.End = DateTime.Parse(event.endTime); // moment string | ||
appointment.Location = event.location || "Location not defined!"; | ||
appointment.Body = new MessageBody(event.description || ""); // you can not use any special character or escape the content | ||
|
||
for (let i = 0; i < event.attendees.length; i++) { | ||
appointment.RequiredAttendees.Add(new Attendee(event.attendees[i].email)); | ||
} | ||
|
||
await appointment.Save(SendInvitationsMode.SendToAllAndSaveCopy); | ||
|
||
return { | ||
uid: appointment.Id.UniqueId, | ||
id: appointment.Id.UniqueId, | ||
password: "", | ||
type: "", | ||
url: "", | ||
additionalInfo: [], | ||
}; | ||
} catch (reason) { | ||
this.log.error(reason); | ||
throw reason; | ||
} | ||
} | ||
|
||
async updateEvent(uid: string, event: CalendarEvent) { | ||
try { | ||
const appointment = await Appointment.Bind( | ||
this.getExchangeService(), | ||
new ItemId(uid), | ||
new PropertySet() | ||
); | ||
appointment.Subject = event.title; | ||
appointment.Start = DateTime.Parse(event.startTime); // moment string | ||
appointment.End = DateTime.Parse(event.endTime); // moment string | ||
appointment.Location = event.location || "Location not defined!"; | ||
appointment.Body = new MessageBody(event.description || ""); // you can not use any special character or escape the content | ||
for (let i = 0; i < event.attendees.length; i++) { | ||
appointment.RequiredAttendees.Add(new Attendee(event.attendees[i].email)); | ||
} | ||
appointment.Update( | ||
ConflictResolutionMode.AlwaysOverwrite, | ||
SendInvitationsOrCancellationsMode.SendToAllAndSaveCopy | ||
); | ||
} catch (reason) { | ||
this.log.error(reason); | ||
throw reason; | ||
} | ||
} | ||
|
||
async deleteEvent(uid: string) { | ||
try { | ||
const appointment = await Appointment.Bind( | ||
this.getExchangeService(), | ||
new ItemId(uid), | ||
new PropertySet() | ||
); | ||
// Delete the appointment. Note that the item ID will change when the item is moved to the Deleted Items folder. | ||
appointment.Delete(DeleteMode.MoveToDeletedItems); | ||
} catch (reason) { | ||
this.log.error(reason); | ||
throw reason; | ||
} | ||
} | ||
|
||
async getAvailability(dateFrom: string, dateTo: string, selectedCalendars: IntegrationCalendar[]) { | ||
try { | ||
const externalCalendars = await this.listCalendars(); | ||
const calendarsToGetAppointmentsFrom = []; | ||
for (let i = 0; i < selectedCalendars.length; i++) { | ||
//Only select vaild calendars! (We get all all active calendars on the instance! even from different users!) | ||
for (let k = 0; k < externalCalendars.length; k++) { | ||
if (selectedCalendars[i].externalId == externalCalendars[k].externalId) { | ||
calendarsToGetAppointmentsFrom.push(selectedCalendars[i]); | ||
} | ||
} | ||
} | ||
|
||
const finaleRet = []; | ||
for (let i = 0; i < calendarsToGetAppointmentsFrom.length; i++) { | ||
const calendarFolderId = new FolderId(calendarsToGetAppointmentsFrom[i].externalId); | ||
const localReturn = await this.getExchangeService() | ||
.FindAppointments( | ||
calendarFolderId, | ||
new CalendarView(DateTime.Parse(dateFrom), DateTime.Parse(dateTo)) | ||
) | ||
.then(function (params) { | ||
const ret: EventBusyDate[] = []; | ||
|
||
for (let k = 0; k < params.Items.length; k++) { | ||
if (params.Items[k].LegacyFreeBusyStatus != LegacyFreeBusyStatus.Free) { | ||
//Dont use this appointment if "ShowAs" was set to "free" | ||
ret.push({ | ||
start: new Date(params.Items[k].Start.ToISOString()), | ||
end: new Date(params.Items[k].End.ToISOString()), | ||
}); | ||
} | ||
} | ||
return ret; | ||
}); | ||
finaleRet.push(...localReturn); | ||
} | ||
|
||
return finaleRet; | ||
} catch (reason) { | ||
this.log.error(reason); | ||
throw reason; | ||
} | ||
} | ||
|
||
async listCalendars() { | ||
try { | ||
const allFolders: IntegrationCalendar[] = []; | ||
return this.getExchangeService() | ||
.FindFolders(WellKnownFolderName.MsgFolderRoot, new FolderView(1000)) | ||
.then(async (res) => { | ||
for (const k in res.Folders) { | ||
const f = res.Folders[k]; | ||
if (f.FolderClass == "IPF.Appointment") { | ||
//Select parent folder for all calendars | ||
allFolders.push({ | ||
externalId: f.Id.UniqueId, | ||
name: f.DisplayName ?? "", | ||
primary: true, //The first one is prime | ||
integration: this.integrationName, | ||
}); | ||
return await this.getExchangeService() | ||
.FindFolders(f.Id, new FolderView(1000)) | ||
.then((res) => { | ||
//Find all calendars inside calendar folder | ||
res.Folders.forEach((fs) => { | ||
allFolders.push({ | ||
externalId: fs.Id.UniqueId, | ||
name: fs.DisplayName ?? "", | ||
primary: false, | ||
integration: this.integrationName, | ||
}); | ||
}); | ||
return allFolders; | ||
}); | ||
} | ||
} | ||
return allFolders; | ||
}); | ||
} catch (reason) { | ||
this.log.error(reason); | ||
throw reason; | ||
} | ||
} | ||
|
||
private getExchangeService(): ExchangeService { | ||
const exch1 = new ExchangeService(this.exchangeVersion); | ||
exch1.Credentials = new WebCredentials(this.credentials.username, this.credentials.password); | ||
exch1.Url = new Uri(this.url); | ||
return exch1; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { default as CalendarService } from "./CalendarService"; |
Oops, something went wrong.