2023-02-16 22:39:57 +00:00
|
|
|
import type { Calendar as OfficeCalendar, User } from "@microsoft/microsoft-graph-types-beta";
|
2023-06-06 11:59:57 +00:00
|
|
|
import type { DefaultBodyType } from "msw";
|
2023-03-16 14:04:30 +00:00
|
|
|
import { z } from "zod";
|
2022-01-06 17:28:31 +00:00
|
|
|
|
2023-02-24 00:18:49 +00:00
|
|
|
import dayjs from "@calcom/dayjs";
|
2022-03-23 22:00:30 +00:00
|
|
|
import { getLocation, getRichDescription } from "@calcom/lib/CalEventParser";
|
2022-03-16 23:36:43 +00:00
|
|
|
import { handleErrorsJson, handleErrorsRaw } from "@calcom/lib/errors";
|
2022-03-23 22:00:30 +00:00
|
|
|
import logger from "@calcom/lib/logger";
|
|
|
|
import prisma from "@calcom/prisma";
|
|
|
|
import type { BufferedBusyTime } from "@calcom/types/BufferedBusyTime";
|
|
|
|
import type {
|
|
|
|
Calendar,
|
|
|
|
CalendarEvent,
|
|
|
|
EventBusyDate,
|
2022-05-02 23:13:34 +00:00
|
|
|
IntegrationCalendar,
|
2022-03-23 22:00:30 +00:00
|
|
|
NewCalendarEventType,
|
|
|
|
} from "@calcom/types/Calendar";
|
2023-02-16 22:39:57 +00:00
|
|
|
import type { CredentialPayload } from "@calcom/types/Credential";
|
2022-03-23 22:00:30 +00:00
|
|
|
|
2023-02-16 22:39:57 +00:00
|
|
|
import type { O365AuthCredentials } from "../types/Office365Calendar";
|
2022-07-27 19:12:42 +00:00
|
|
|
import { getOfficeAppKeys } from "./getOfficeAppKeys";
|
2022-01-06 17:28:31 +00:00
|
|
|
|
2022-07-27 19:12:42 +00:00
|
|
|
interface IRequest {
|
|
|
|
method: string;
|
|
|
|
url: string;
|
|
|
|
id: number;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface ISettledResponse {
|
|
|
|
id: string;
|
|
|
|
status: number;
|
|
|
|
headers: {
|
|
|
|
"Retry-After": string;
|
|
|
|
"Content-Type": string;
|
|
|
|
};
|
2023-06-06 11:59:57 +00:00
|
|
|
body: Record<string, DefaultBodyType>;
|
2022-07-27 19:12:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
interface IBatchResponse {
|
|
|
|
responses: ISettledResponse[];
|
|
|
|
}
|
2023-06-06 11:59:57 +00:00
|
|
|
interface BodyValue {
|
|
|
|
showAs: string;
|
|
|
|
end: { dateTime: string };
|
|
|
|
evt: { showAs: string };
|
|
|
|
start: { dateTime: string };
|
|
|
|
}
|
2022-01-06 17:28:31 +00:00
|
|
|
|
2023-03-16 14:04:30 +00:00
|
|
|
const refreshTokenResponseSchema = z.object({
|
|
|
|
access_token: z.string(),
|
|
|
|
expires_in: z
|
|
|
|
.number()
|
|
|
|
.transform((currentTimeOffsetInSeconds) => Math.round(+new Date() / 1000 + currentTimeOffsetInSeconds)),
|
|
|
|
refresh_token: z.string().optional(),
|
|
|
|
});
|
|
|
|
|
2022-01-06 17:28:31 +00:00
|
|
|
export default class Office365CalendarService implements Calendar {
|
|
|
|
private url = "";
|
|
|
|
private integrationName = "";
|
2022-01-06 22:06:31 +00:00
|
|
|
private log: typeof logger;
|
2022-07-27 19:12:42 +00:00
|
|
|
private accessToken: string | null = null;
|
|
|
|
auth: { getToken: () => Promise<string> };
|
|
|
|
private apiGraphUrl = "https://graph.microsoft.com/v1.0";
|
2022-01-06 17:28:31 +00:00
|
|
|
|
2022-10-31 22:06:03 +00:00
|
|
|
constructor(credential: CredentialPayload) {
|
2022-03-23 22:00:30 +00:00
|
|
|
this.integrationName = "office365_calendar";
|
2022-07-27 19:12:42 +00:00
|
|
|
this.auth = this.o365Auth(credential);
|
2022-01-06 22:06:31 +00:00
|
|
|
|
|
|
|
this.log = logger.getChildLogger({ prefix: [`[[lib] ${this.integrationName}`] });
|
2022-01-06 17:28:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async createEvent(event: CalendarEvent): Promise<NewCalendarEventType> {
|
|
|
|
try {
|
2023-03-15 20:04:35 +00:00
|
|
|
const eventsUrl = event.destinationCalendar?.externalId
|
|
|
|
? `/me/calendars/${event.destinationCalendar?.externalId}/events`
|
|
|
|
: "/me/calendar/events";
|
2022-01-06 17:28:31 +00:00
|
|
|
|
2023-03-15 20:04:35 +00:00
|
|
|
const response = await this.fetcher(eventsUrl, {
|
2022-01-06 17:28:31 +00:00
|
|
|
method: "POST",
|
|
|
|
body: JSON.stringify(this.translateEvent(event)),
|
|
|
|
});
|
|
|
|
|
2023-04-03 17:13:57 +00:00
|
|
|
const responseJson = await handleErrorsJson<NewCalendarEventType & { iCalUId: string }>(response);
|
|
|
|
|
|
|
|
return { ...responseJson, iCalUID: responseJson.iCalUId };
|
2022-01-06 17:28:31 +00:00
|
|
|
} catch (error) {
|
|
|
|
this.log.error(error);
|
|
|
|
|
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-06-06 11:59:57 +00:00
|
|
|
async updateEvent(uid: string, event: CalendarEvent): Promise<NewCalendarEventType> {
|
2022-01-06 17:28:31 +00:00
|
|
|
try {
|
2022-07-27 19:12:42 +00:00
|
|
|
const response = await this.fetcher(`/me/calendar/events/${uid}`, {
|
2022-01-06 17:28:31 +00:00
|
|
|
method: "PATCH",
|
|
|
|
body: JSON.stringify(this.translateEvent(event)),
|
|
|
|
});
|
|
|
|
|
2023-04-03 17:13:57 +00:00
|
|
|
const responseJson = await handleErrorsJson<NewCalendarEventType & { iCalUId: string }>(response);
|
|
|
|
|
|
|
|
return { ...responseJson, iCalUID: responseJson.iCalUId };
|
2022-01-06 17:28:31 +00:00
|
|
|
} catch (error) {
|
|
|
|
this.log.error(error);
|
|
|
|
|
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async deleteEvent(uid: string): Promise<void> {
|
|
|
|
try {
|
2022-07-27 19:12:42 +00:00
|
|
|
const response = await this.fetcher(`/me/calendar/events/${uid}`, {
|
2022-01-06 17:28:31 +00:00
|
|
|
method: "DELETE",
|
|
|
|
});
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
2022-05-11 20:47:42 +00:00
|
|
|
const filter = `?startDateTime=${encodeURIComponent(
|
2022-01-06 17:28:31 +00:00
|
|
|
dateFromParsed.toISOString()
|
2022-05-11 20:47:42 +00:00
|
|
|
)}&endDateTime=${encodeURIComponent(dateToParsed.toISOString())}`;
|
2022-01-06 17:28:31 +00:00
|
|
|
|
2023-08-10 17:28:56 +00:00
|
|
|
const calendarSelectParams = "$select=showAs,start,end";
|
|
|
|
|
2022-07-27 19:12:42 +00:00
|
|
|
try {
|
|
|
|
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([]);
|
|
|
|
}
|
|
|
|
|
|
|
|
const ids = await (selectedCalendarIds.length === 0
|
|
|
|
? this.listCalendars().then((cals) => cals.map((e_2) => e_2.externalId).filter(Boolean) || [])
|
|
|
|
: Promise.resolve(selectedCalendarIds));
|
|
|
|
const requests = ids.map((calendarId, id) => ({
|
|
|
|
id,
|
|
|
|
method: "GET",
|
2023-08-10 17:28:56 +00:00
|
|
|
url: `/me/calendars/${calendarId}/calendarView${filter}&${calendarSelectParams}`,
|
2022-07-27 19:12:42 +00:00
|
|
|
}));
|
|
|
|
const response = await this.apiGraphBatchCall(requests);
|
2022-10-31 22:06:03 +00:00
|
|
|
const responseBody = await this.handleErrorJsonOffice365Calendar(response);
|
2022-07-27 19:12:42 +00:00
|
|
|
let responseBatchApi: IBatchResponse = { responses: [] };
|
|
|
|
if (typeof responseBody === "string") {
|
|
|
|
responseBatchApi = this.handleTextJsonResponseWithHtmlInBody(responseBody);
|
|
|
|
}
|
|
|
|
let alreadySuccessResponse = [] as ISettledResponse[];
|
|
|
|
|
|
|
|
// Validate if any 429 status Retry-After is present
|
|
|
|
const retryAfter =
|
|
|
|
!!responseBatchApi?.responses && this.findRetryAfterResponse(responseBatchApi.responses);
|
|
|
|
|
|
|
|
if (retryAfter && responseBatchApi.responses) {
|
|
|
|
responseBatchApi = await this.fetchRequestWithRetryAfter(requests, responseBatchApi.responses, 2);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Recursively fetch nextLink responses
|
|
|
|
alreadySuccessResponse = await this.fetchResponsesWithNextLink(responseBatchApi.responses);
|
|
|
|
|
|
|
|
return alreadySuccessResponse ? this.processBusyTimes(alreadySuccessResponse) : [];
|
|
|
|
} catch (err) {
|
|
|
|
console.log(err);
|
|
|
|
return Promise.reject([]);
|
|
|
|
}
|
2022-01-06 17:28:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async listCalendars(): Promise<IntegrationCalendar[]> {
|
2022-12-14 11:04:32 +00:00
|
|
|
const officeCalendars: OfficeCalendar[] = [];
|
|
|
|
// List calendars from MS are paginated
|
|
|
|
let finishedParsingCalendars = false;
|
2023-08-10 17:28:56 +00:00
|
|
|
const calendarFilterParam = "$select=id,name,isDefaultCalendar,canEdit";
|
|
|
|
|
2022-12-14 11:04:32 +00:00
|
|
|
// Store @odata.nextLink if in response
|
2023-08-10 17:28:56 +00:00
|
|
|
let requestLink = `/me/calendars?${calendarFilterParam}`;
|
2022-12-14 11:04:32 +00:00
|
|
|
|
|
|
|
while (!finishedParsingCalendars) {
|
|
|
|
const response = await this.fetcher(requestLink);
|
|
|
|
let responseBody = await handleErrorsJson<{ value: OfficeCalendar[]; "@odata.nextLink"?: string }>(
|
|
|
|
response
|
|
|
|
);
|
|
|
|
// If responseBody is valid then parse the JSON text
|
|
|
|
if (typeof responseBody === "string") {
|
|
|
|
responseBody = JSON.parse(responseBody) as { value: OfficeCalendar[] };
|
|
|
|
}
|
|
|
|
|
|
|
|
officeCalendars.push(...responseBody.value);
|
|
|
|
|
|
|
|
if (responseBody["@odata.nextLink"]) {
|
|
|
|
requestLink = responseBody["@odata.nextLink"].replace(this.apiGraphUrl, "");
|
|
|
|
} else {
|
|
|
|
finishedParsingCalendars = true;
|
|
|
|
}
|
2022-08-04 15:49:51 +00:00
|
|
|
}
|
2022-12-14 11:04:32 +00:00
|
|
|
|
2022-12-20 21:50:20 +00:00
|
|
|
const user = await this.fetcher("/me");
|
|
|
|
const userResponseBody = await handleErrorsJson<User>(user);
|
|
|
|
const email = userResponseBody.mail ?? userResponseBody.userPrincipalName;
|
|
|
|
|
2022-12-14 11:04:32 +00:00
|
|
|
return officeCalendars.map((cal: OfficeCalendar) => {
|
2022-07-27 19:12:42 +00:00
|
|
|
const calendar: IntegrationCalendar = {
|
|
|
|
externalId: cal.id ?? "No Id",
|
|
|
|
integration: this.integrationName,
|
|
|
|
name: cal.name ?? "No calendar name",
|
|
|
|
primary: cal.isDefaultCalendar ?? false,
|
|
|
|
readOnly: !cal.canEdit && true,
|
2022-12-20 21:50:20 +00:00
|
|
|
email: email ?? "",
|
2022-07-27 19:12:42 +00:00
|
|
|
};
|
|
|
|
return calendar;
|
|
|
|
});
|
2022-01-06 17:28:31 +00:00
|
|
|
}
|
|
|
|
|
2022-10-31 22:06:03 +00:00
|
|
|
private o365Auth = (credential: CredentialPayload) => {
|
2023-03-17 17:20:41 +00:00
|
|
|
const isExpired = (expiryDate: number) => {
|
|
|
|
if (!expiryDate) {
|
|
|
|
return true;
|
|
|
|
} else {
|
|
|
|
return expiryDate < Math.round(+new Date() / 1000);
|
|
|
|
}
|
|
|
|
};
|
2022-01-06 17:28:31 +00:00
|
|
|
const o365AuthCredentials = credential.key as O365AuthCredentials;
|
|
|
|
|
2023-03-25 21:19:33 +00:00
|
|
|
const refreshAccessToken = async (o365AuthCredentials: O365AuthCredentials) => {
|
2022-07-27 19:12:42 +00:00
|
|
|
const { client_id, client_secret } = await getOfficeAppKeys();
|
2022-05-02 23:13:34 +00:00
|
|
|
const response = await fetch("https://login.microsoftonline.com/common/oauth2/v2.0/token", {
|
2022-01-06 17:28:31 +00:00
|
|
|
method: "POST",
|
|
|
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
|
|
body: new URLSearchParams({
|
|
|
|
scope: "User.Read Calendars.Read Calendars.ReadWrite",
|
2022-05-02 23:13:34 +00:00
|
|
|
client_id,
|
2023-03-25 21:19:33 +00:00
|
|
|
refresh_token: o365AuthCredentials.refresh_token,
|
2022-01-06 17:28:31 +00:00
|
|
|
grant_type: "refresh_token",
|
2022-05-02 23:13:34 +00:00
|
|
|
client_secret,
|
2022-01-06 17:28:31 +00:00
|
|
|
}),
|
2022-05-02 23:13:34 +00:00
|
|
|
});
|
2023-03-25 21:19:33 +00:00
|
|
|
const responseJson = await handleErrorsJson(response);
|
|
|
|
const tokenResponse = refreshTokenResponseSchema.safeParse(responseJson);
|
|
|
|
o365AuthCredentials = { ...o365AuthCredentials, ...(tokenResponse.success && tokenResponse.data) };
|
|
|
|
if (!tokenResponse.success) {
|
|
|
|
console.error(
|
|
|
|
"Outlook error grabbing new tokens ~ zodError:",
|
|
|
|
tokenResponse.error,
|
|
|
|
"MS response:",
|
|
|
|
responseJson
|
|
|
|
);
|
|
|
|
}
|
2022-05-02 23:13:34 +00:00
|
|
|
await prisma.credential.update({
|
|
|
|
where: {
|
|
|
|
id: credential.id,
|
|
|
|
},
|
|
|
|
data: {
|
|
|
|
key: o365AuthCredentials,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
return o365AuthCredentials.access_token;
|
2022-01-06 17:28:31 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
return {
|
|
|
|
getToken: () =>
|
2023-03-25 21:19:33 +00:00
|
|
|
refreshTokenResponseSchema.safeParse(o365AuthCredentials).success &&
|
2023-03-17 17:20:41 +00:00
|
|
|
!isExpired(o365AuthCredentials.expires_in)
|
2022-01-06 17:28:31 +00:00
|
|
|
? Promise.resolve(o365AuthCredentials.access_token)
|
2023-03-25 21:19:33 +00:00
|
|
|
: refreshAccessToken(o365AuthCredentials),
|
2022-01-06 17:28:31 +00:00
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
private translateEvent = (event: CalendarEvent) => {
|
|
|
|
return {
|
|
|
|
subject: event.title,
|
|
|
|
body: {
|
|
|
|
contentType: "HTML",
|
|
|
|
content: getRichDescription(event),
|
|
|
|
},
|
|
|
|
start: {
|
2023-03-18 21:47:15 +00:00
|
|
|
dateTime: dayjs(event.startTime).tz(event.organizer.timeZone).format("YYYY-MM-DDTHH:mm:ss"),
|
2022-01-06 17:28:31 +00:00
|
|
|
timeZone: event.organizer.timeZone,
|
|
|
|
},
|
|
|
|
end: {
|
2023-03-18 21:47:15 +00:00
|
|
|
dateTime: dayjs(event.endTime).tz(event.organizer.timeZone).format("YYYY-MM-DDTHH:mm:ss"),
|
2022-01-06 17:28:31 +00:00
|
|
|
timeZone: event.organizer.timeZone,
|
|
|
|
},
|
2023-03-21 03:15:59 +00:00
|
|
|
attendees: [
|
|
|
|
...event.attendees.map((attendee) => ({
|
|
|
|
emailAddress: {
|
|
|
|
address: attendee.email,
|
|
|
|
name: attendee.name,
|
|
|
|
},
|
|
|
|
type: "required",
|
|
|
|
})),
|
|
|
|
...(event.team?.members
|
|
|
|
? event.team?.members.map((member) => ({
|
|
|
|
emailAddress: {
|
|
|
|
address: member.email,
|
|
|
|
name: member.name,
|
|
|
|
},
|
|
|
|
type: "required",
|
|
|
|
}))
|
|
|
|
: []),
|
|
|
|
],
|
2022-01-06 17:28:31 +00:00
|
|
|
location: event.location ? { displayName: getLocation(event) } : undefined,
|
|
|
|
};
|
|
|
|
};
|
2022-07-27 19:12:42 +00:00
|
|
|
|
|
|
|
private fetcher = async (endpoint: string, init?: RequestInit | undefined) => {
|
|
|
|
this.accessToken = await this.auth.getToken();
|
|
|
|
return fetch(`${this.apiGraphUrl}${endpoint}`, {
|
|
|
|
method: "get",
|
|
|
|
headers: {
|
|
|
|
Authorization: "Bearer " + this.accessToken,
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
},
|
|
|
|
...init,
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
private fetchResponsesWithNextLink = async (
|
|
|
|
settledResponses: ISettledResponse[]
|
|
|
|
): Promise<ISettledResponse[]> => {
|
|
|
|
const alreadySuccess = [] as ISettledResponse[];
|
|
|
|
const newLinkRequest = [] as IRequest[];
|
|
|
|
settledResponses?.forEach((response) => {
|
|
|
|
if (response.status === 200 && response.body["@odata.nextLink"] === undefined) {
|
|
|
|
alreadySuccess.push(response);
|
|
|
|
} else {
|
|
|
|
const nextLinkUrl = response.body["@odata.nextLink"]
|
2023-06-06 11:59:57 +00:00
|
|
|
? String(response.body["@odata.nextLink"]).replace(this.apiGraphUrl, "")
|
2022-07-27 19:12:42 +00:00
|
|
|
: "";
|
|
|
|
if (nextLinkUrl) {
|
|
|
|
// Saving link for later use
|
|
|
|
newLinkRequest.push({
|
|
|
|
id: Number(response.id),
|
|
|
|
method: "GET",
|
|
|
|
url: nextLinkUrl,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
delete response.body["@odata.nextLink"];
|
|
|
|
// Pushing success body content
|
|
|
|
alreadySuccess.push(response);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
if (newLinkRequest.length === 0) {
|
|
|
|
return alreadySuccess;
|
|
|
|
}
|
|
|
|
|
|
|
|
const newResponse = await this.apiGraphBatchCall(newLinkRequest);
|
2022-10-31 22:06:03 +00:00
|
|
|
let newResponseBody = await handleErrorsJson<IBatchResponse | string>(newResponse);
|
2022-07-27 19:12:42 +00:00
|
|
|
|
|
|
|
if (typeof newResponseBody === "string") {
|
|
|
|
newResponseBody = this.handleTextJsonResponseWithHtmlInBody(newResponseBody);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Going recursive to fetch next link
|
|
|
|
const newSettledResponses = await this.fetchResponsesWithNextLink(newResponseBody.responses);
|
|
|
|
return [...alreadySuccess, ...newSettledResponses];
|
|
|
|
};
|
|
|
|
|
|
|
|
private fetchRequestWithRetryAfter = async (
|
|
|
|
originalRequests: IRequest[],
|
|
|
|
settledPromises: ISettledResponse[],
|
|
|
|
maxRetries: number,
|
|
|
|
retryCount = 0
|
|
|
|
): Promise<IBatchResponse> => {
|
|
|
|
let retryAfterTimeout = 0;
|
|
|
|
if (retryCount >= maxRetries) {
|
|
|
|
return { responses: settledPromises };
|
|
|
|
}
|
|
|
|
const alreadySuccessRequest = [] as ISettledResponse[];
|
|
|
|
const failedRequest = [] as IRequest[];
|
|
|
|
settledPromises.forEach((item) => {
|
|
|
|
if (item.status === 200) {
|
|
|
|
alreadySuccessRequest.push(item);
|
|
|
|
} else if (item.status === 429) {
|
|
|
|
const newTimeout = Number(item.headers["Retry-After"]) * 1000 || 0;
|
|
|
|
retryAfterTimeout = newTimeout > retryAfterTimeout ? newTimeout : retryAfterTimeout;
|
|
|
|
failedRequest.push(originalRequests[Number(item.id)]);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
if (failedRequest.length === 0) {
|
|
|
|
return { responses: alreadySuccessRequest };
|
|
|
|
}
|
|
|
|
|
|
|
|
// Await certain time from retry-after header
|
|
|
|
await new Promise((r) => setTimeout(r, retryAfterTimeout));
|
|
|
|
|
|
|
|
const newResponses = await this.apiGraphBatchCall(failedRequest);
|
2022-10-31 22:06:03 +00:00
|
|
|
let newResponseBody = await handleErrorsJson<IBatchResponse | string>(newResponses);
|
2022-07-27 19:12:42 +00:00
|
|
|
if (typeof newResponseBody === "string") {
|
|
|
|
newResponseBody = this.handleTextJsonResponseWithHtmlInBody(newResponseBody);
|
|
|
|
}
|
|
|
|
const retryAfter = !!newResponseBody?.responses && this.findRetryAfterResponse(newResponseBody.responses);
|
|
|
|
|
|
|
|
if (retryAfter && newResponseBody.responses) {
|
|
|
|
newResponseBody = await this.fetchRequestWithRetryAfter(
|
|
|
|
failedRequest,
|
|
|
|
newResponseBody.responses,
|
|
|
|
maxRetries,
|
|
|
|
retryCount + 1
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return { responses: [...alreadySuccessRequest, ...(newResponseBody?.responses || [])] };
|
|
|
|
};
|
|
|
|
|
|
|
|
private apiGraphBatchCall = async (requests: IRequest[]): Promise<Response> => {
|
2023-06-06 11:59:57 +00:00
|
|
|
const response = await this.fetcher(`/$batch`, {
|
|
|
|
method: "POST",
|
|
|
|
body: JSON.stringify({ requests }),
|
|
|
|
});
|
2022-07-27 19:12:42 +00:00
|
|
|
|
2023-06-06 11:59:57 +00:00
|
|
|
return response;
|
2022-07-27 19:12:42 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
private handleTextJsonResponseWithHtmlInBody = (response: string): IBatchResponse => {
|
|
|
|
try {
|
|
|
|
const parsedJson = JSON.parse(response);
|
|
|
|
return parsedJson;
|
|
|
|
} catch (error) {
|
|
|
|
// Looking for html in body
|
|
|
|
const openTag = '"body":<';
|
|
|
|
const closeTag = "</html>";
|
|
|
|
const htmlBeginning = response.indexOf(openTag) + openTag.length - 1;
|
|
|
|
const htmlEnding = response.indexOf(closeTag) + closeTag.length + 2;
|
|
|
|
const resultString = `${response.repeat(1).substring(0, htmlBeginning)} ""${response
|
|
|
|
.repeat(1)
|
|
|
|
.substring(htmlEnding, response.length)}`;
|
|
|
|
|
|
|
|
return JSON.parse(resultString);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
private findRetryAfterResponse = (response: ISettledResponse[]) => {
|
|
|
|
const foundRetry = response.find((request: ISettledResponse) => request.status === 429);
|
|
|
|
return !!foundRetry;
|
|
|
|
};
|
|
|
|
|
|
|
|
private processBusyTimes = (responses: ISettledResponse[]) => {
|
|
|
|
return responses.reduce(
|
2023-06-06 11:59:57 +00:00
|
|
|
(acc: BufferedBusyTime[], subResponse: { body: { value?: BodyValue[]; error?: Error[] } }) => {
|
2022-07-27 19:12:42 +00:00
|
|
|
if (!subResponse.body?.value) return acc;
|
|
|
|
return acc.concat(
|
|
|
|
subResponse.body.value
|
|
|
|
.filter((evt) => evt.showAs !== "free" && evt.showAs !== "workingElsewhere")
|
|
|
|
.map((evt) => ({
|
|
|
|
start: evt.start.dateTime + "Z",
|
|
|
|
end: evt.end.dateTime + "Z",
|
|
|
|
}))
|
|
|
|
);
|
|
|
|
},
|
|
|
|
[]
|
|
|
|
);
|
|
|
|
};
|
2022-10-31 22:06:03 +00:00
|
|
|
|
|
|
|
private handleErrorJsonOffice365Calendar = <Type>(response: Response): Promise<Type | string> => {
|
|
|
|
if (response.headers.get("content-encoding") === "gzip") {
|
|
|
|
return response.text();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (response.status === 204) {
|
|
|
|
return new Promise((resolve) => resolve({} as Type));
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!response.ok && response.status < 200 && response.status >= 300) {
|
|
|
|
response.json().then(console.log);
|
|
|
|
throw Error(response.statusText);
|
|
|
|
}
|
|
|
|
|
|
|
|
return response.json();
|
|
|
|
};
|
2022-01-06 17:28:31 +00:00
|
|
|
}
|