mirror of
https://github.com/hanzoai/calendar.git
synced 2026-10-30 14:46:43 +00:00
* wip * update layoutHOC * wip * remove app router related code no longer used * remove more * await cookies, headers, params, serachparams * update yarn.lock * await cookies, headers, params, serachparams more * update yarn lock again * downgrade next-auth to 4.22.1 * update yarn lock * fix * update yarn lock * fix type checks * update yarn lock * await headers and cookies * restore pages folder * restore yarn.lock * update yarn.lock * await headers and cookies * remove * await params in API routes * updates * restore next.config.js * remove i18n from next.config.js * Fixed tests * Fixed types * Removed duplicate favicon.ico * Fixing more types * ImageResponse moved to next/og * Fixed prisma import issues * dynamic import for @ewsjs/xhr * remove deasync dep * dynamic import for @tryvital/vital-node * fix type checks * add back turbopack command * Type fix * Removed unneeded file * fix turbopack relative path errors * add comments * remove unneeded code * Fixed build errors * await apis * use Promise<Params> type in defaultResponderForAppDir util * refactor scim api route * fix type checks * separate app-routing.config into client-config and server-config * wip * refactor routing forms components * revert unneeded changes for easier review * fix * fix * use CustomTrans * fix type * fix unit tests * fix type error * fix build error * fix build error * fix build error * fix warnings * fix warnings * upgrade @tremor/react and tailwindcss * numCols -> numItems * fix forgot-password e2e test * fix 1 more e2e test * fix login e2e test * fix e2e tests * fix e2e tests * clean up * remove error * use tremor/react 2.11.0 * fix * rename CustomTrans to ServerTrans * address comment * fix test * fix ServerTrans * fix * fix type * fix ServerTrans usages 1 * fix translations * fix type checks * fix type checks * link styling * fix --------- Co-authored-by: Keith Williams <[email protected]> Co-authored-by: Anik Dhabal Babu <[email protected]>
213 lines
7.2 KiB
TypeScript
213 lines
7.2 KiB
TypeScript
import type { FindFoldersResults, FindItemsResults } from "ews-javascript-api";
|
|
import {
|
|
Appointment,
|
|
Attendee,
|
|
BasePropertySet,
|
|
CalendarView,
|
|
ConflictResolutionMode,
|
|
DateTime,
|
|
DeleteMode,
|
|
ExchangeService,
|
|
Folder,
|
|
FolderId,
|
|
FolderSchema,
|
|
FolderTraversal,
|
|
FolderView,
|
|
ItemId,
|
|
LegacyFreeBusyStatus,
|
|
LogicalOperator,
|
|
MessageBody,
|
|
PropertySet,
|
|
SearchFilter,
|
|
SendInvitationsMode,
|
|
SendInvitationsOrCancellationsMode,
|
|
Uri,
|
|
WebCredentials,
|
|
WellKnownFolderName,
|
|
} from "ews-javascript-api";
|
|
|
|
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
|
import logger from "@calcom/lib/logger";
|
|
import type {
|
|
Calendar,
|
|
CalendarEvent,
|
|
EventBusyDate,
|
|
IntegrationCalendar,
|
|
NewCalendarEventType,
|
|
Person,
|
|
} from "@calcom/types/Calendar";
|
|
import type { CredentialPayload } from "@calcom/types/Credential";
|
|
|
|
import { ExchangeAuthentication } from "../enums";
|
|
|
|
export default class ExchangeCalendarService implements Calendar {
|
|
private integrationName = "";
|
|
private log: typeof logger;
|
|
private payload;
|
|
|
|
constructor(credential: CredentialPayload) {
|
|
this.integrationName = "exchange_calendar";
|
|
this.log = logger.getSubLogger({ prefix: [`[[lib] ${this.integrationName}`] });
|
|
this.payload = JSON.parse(
|
|
symmetricDecrypt(credential.key?.toString() || "", process.env.CALENDSO_ENCRYPTION_KEY || "")
|
|
);
|
|
}
|
|
|
|
async createEvent(event: CalendarEvent): Promise<NewCalendarEventType> {
|
|
const appointment: Appointment = new Appointment(await this.getExchangeService());
|
|
appointment.Subject = event.title;
|
|
appointment.Start = DateTime.Parse(event.startTime);
|
|
appointment.End = DateTime.Parse(event.endTime);
|
|
appointment.Location = event.location || "";
|
|
appointment.Body = new MessageBody(event.description || "");
|
|
event.attendees.forEach((attendee: Person) => {
|
|
appointment.RequiredAttendees.Add(new Attendee(attendee.email));
|
|
});
|
|
if (event.team?.members) {
|
|
event.team.members.forEach((member: Person) => {
|
|
appointment.RequiredAttendees.Add(new Attendee(member.email));
|
|
});
|
|
}
|
|
return appointment
|
|
.Save(SendInvitationsMode.SendToAllAndSaveCopy)
|
|
.then(() => {
|
|
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
|
|
): Promise<NewCalendarEventType | NewCalendarEventType[]> {
|
|
const appointment: Appointment = await Appointment.Bind(await this.getExchangeService(), new ItemId(uid));
|
|
appointment.Subject = event.title;
|
|
appointment.Start = DateTime.Parse(event.startTime);
|
|
appointment.End = DateTime.Parse(event.endTime);
|
|
appointment.Location = event.location || "";
|
|
appointment.Body = new MessageBody(event.description || "");
|
|
event.attendees.forEach((attendee: Person) => {
|
|
appointment.RequiredAttendees.Add(new Attendee(attendee.email));
|
|
});
|
|
if (event.team?.members) {
|
|
event.team.members.forEach((member) => {
|
|
appointment.RequiredAttendees.Add(new Attendee(member.email));
|
|
});
|
|
}
|
|
return appointment
|
|
.Update(
|
|
ConflictResolutionMode.AlwaysOverwrite,
|
|
SendInvitationsOrCancellationsMode.SendToChangedAndSaveCopy
|
|
)
|
|
.then(() => {
|
|
return {
|
|
uid: appointment.Id.UniqueId,
|
|
id: appointment.Id.UniqueId,
|
|
password: "",
|
|
type: "",
|
|
url: "",
|
|
additionalInfo: {},
|
|
};
|
|
})
|
|
.catch((reason) => {
|
|
this.log.error(reason);
|
|
throw reason;
|
|
});
|
|
}
|
|
|
|
async deleteEvent(uid: string): Promise<void> {
|
|
const appointment: Appointment = await Appointment.Bind(await this.getExchangeService(), new ItemId(uid));
|
|
return appointment.Delete(DeleteMode.MoveToDeletedItems).catch((reason) => {
|
|
this.log.error(reason);
|
|
throw reason;
|
|
});
|
|
}
|
|
|
|
async getAvailability(
|
|
dateFrom: string,
|
|
dateTo: string,
|
|
selectedCalendars: IntegrationCalendar[]
|
|
): Promise<EventBusyDate[]> {
|
|
const calendars: IntegrationCalendar[] = await this.listCalendars();
|
|
const promises: Promise<EventBusyDate[]>[] = calendars
|
|
.filter((lcal) => selectedCalendars.some((rcal) => lcal.externalId == rcal.externalId))
|
|
.map(async (calendar) => {
|
|
return (await this.getExchangeService())
|
|
.FindAppointments(
|
|
new FolderId(calendar.externalId),
|
|
new CalendarView(DateTime.Parse(dateFrom), DateTime.Parse(dateTo))
|
|
)
|
|
.then((results: FindItemsResults<Appointment>) => {
|
|
return results.Items.filter((appointment: Appointment) => {
|
|
return appointment.LegacyFreeBusyStatus != LegacyFreeBusyStatus.Free;
|
|
}).map((appointment: Appointment) => {
|
|
return {
|
|
start: new Date(appointment.Start.ToISOString()),
|
|
end: new Date(appointment.End.ToISOString()),
|
|
};
|
|
});
|
|
})
|
|
.catch((reason) => {
|
|
this.log.error(reason);
|
|
throw reason;
|
|
});
|
|
});
|
|
return Promise.all(promises).then((x) => x.flat());
|
|
}
|
|
|
|
async listCalendars(): Promise<IntegrationCalendar[]> {
|
|
const service: ExchangeService = await this.getExchangeService();
|
|
const view: FolderView = new FolderView(1000);
|
|
view.PropertySet = new PropertySet(BasePropertySet.IdOnly);
|
|
view.PropertySet.Add(FolderSchema.ParentFolderId);
|
|
view.PropertySet.Add(FolderSchema.DisplayName);
|
|
view.PropertySet.Add(FolderSchema.ChildFolderCount);
|
|
view.Traversal = FolderTraversal.Deep;
|
|
const deletedItemsFolder: Folder = await Folder.Bind(service, WellKnownFolderName.DeletedItems);
|
|
const searchFilterCollection = new SearchFilter.SearchFilterCollection(LogicalOperator.And);
|
|
searchFilterCollection.Add(new SearchFilter.IsEqualTo(FolderSchema.FolderClass, "IPF.Appointment"));
|
|
return service
|
|
.FindFolders(WellKnownFolderName.MsgFolderRoot, searchFilterCollection, view)
|
|
.then((res: FindFoldersResults) => {
|
|
return res.Folders.filter((folder: Folder) => {
|
|
return folder.ParentFolderId.UniqueId != deletedItemsFolder.Id.UniqueId;
|
|
}).map((folder: Folder) => {
|
|
return {
|
|
externalId: folder.Id.UniqueId,
|
|
name: folder.DisplayName ?? "",
|
|
primary: folder.ChildFolderCount > 0,
|
|
integration: this.integrationName,
|
|
};
|
|
});
|
|
})
|
|
.catch((reason) => {
|
|
this.log.error(reason);
|
|
throw reason;
|
|
});
|
|
}
|
|
|
|
private async getExchangeService(): Promise<ExchangeService> {
|
|
const service: ExchangeService = new ExchangeService(this.payload.exchangeVersion);
|
|
service.Credentials = new WebCredentials(this.payload.username, this.payload.password);
|
|
service.Url = new Uri(this.payload.url);
|
|
if (this.payload.authenticationMethod === ExchangeAuthentication.NTLM) {
|
|
const { XhrApi } = await import("@ewsjs/xhr");
|
|
const xhr = new XhrApi({
|
|
rejectUnauthorized: false,
|
|
}).useNtlmAuthentication(this.payload.username, this.payload.password);
|
|
service.XHRApi = xhr;
|
|
}
|
|
return service;
|
|
}
|
|
}
|