Skip to content

Commit

Permalink
fix: [CAL-3578] [CAL-2733] Zoho calendar issues (#14905)
Browse files Browse the repository at this point in the history
* fix zohocalender installation

* fix func name

* fix: [CAL-3578] [CAL-2733] check events in the calendar when checking availability

* fix: server location on credentials instead app keys

* refactor: remove console log

Co-authored-by: Joe Au-Yeung <[email protected]>

---------

Co-authored-by: pritam <[email protected]>
Co-authored-by: Keith Williams <[email protected]>
Co-authored-by: Joe Au-Yeung <[email protected]>
  • Loading branch information
4 people committed Jun 6, 2024
1 parent def87ea commit 75edc45
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 18 deletions.
24 changes: 19 additions & 5 deletions packages/app-store/zohocalendar/api/callback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,25 @@ import { appKeysSchema as zohoKeysSchema } from "../zod";

const log = logger.getSubLogger({ prefix: [`[[zohocalendar/api/callback]`] });

const OAUTH_BASE_URL = "https://accounts.zoho.com/oauth/v2";
function getOAuthBaseUrl(domain: string): string {
return `https://accounts.zoho.${domain}/oauth/v2`;
}

async function getHandler(req: NextApiRequest, res: NextApiResponse) {
const { code } = req.query;
const { code, location } = req.query;

const state = decodeOAuthState(req);

if (code && typeof code !== "string") {
res.status(400).json({ message: "`code` must be a string" });
return;
}

if (location && typeof location !== "string") {
res.status(400).json({ message: "`location` must be a string" });
return;
}

if (!req.session?.user?.id) {
return res.status(401).json({ message: "You must be logged in to do this" });
}
Expand All @@ -43,17 +51,18 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
redirect_uri: `${WEBAPP_URL}/api/integrations/${config.slug}/callback`,
code,
};
const server_location = location === "us" ? "com" : location;

const query = stringify(params);

const response = await fetch(`${OAUTH_BASE_URL}/token?${query}`, {
const response = await fetch(`${getOAuthBaseUrl(server_location || "com")}/token?${query}`, {
method: "POST",
headers: {
"Content-Type": "application/json; charset=utf-8",
},
});

const responseBody = await response.json();
const responseBody = await JSON.parse(await response.text());

if (!response.ok || responseBody.error) {
log.error("get access_token failed", responseBody);
Expand All @@ -64,9 +73,14 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
access_token: responseBody.access_token,
refresh_token: responseBody.refresh_token,
expires_in: Math.round(+new Date() / 1000 + responseBody.expires_in),
server_location: server_location || "com",
};

const calendarResponse = await fetch("https://calendar.zoho.com/api/v1/calendars", {
function getCalenderUri(domain: string): string {
return `https://calendar.zoho.${domain}/api/v1/calendars`;
}

const calendarResponse = await fetch(getCalenderUri(server_location || "com"), {
method: "GET",
headers: {
Authorization: `Bearer ${key.access_token}`,
Expand Down
87 changes: 74 additions & 13 deletions packages/app-store/zohocalendar/lib/CalendarService.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { stringify } from "querystring";
import { z } from "zod";

import dayjs from "@calcom/dayjs";
import { getLocation, getRichDescription } from "@calcom/lib/CalEventParser";
Expand All @@ -16,11 +15,7 @@ import type { CredentialPayload } from "@calcom/types/Credential";

import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
import type { ZohoAuthCredentials, FreeBusy, ZohoCalendarListResp } from "../types/ZohoCalendar";

const zohoKeysSchema = z.object({
client_id: z.string(),
client_secret: z.string(),
});
import { appKeysSchema as zohoKeysSchema } from "../zod";

export default class ZohoCalendarService implements Calendar {
private integrationName = "";
Expand All @@ -42,7 +37,7 @@ export default class ZohoCalendarService implements Calendar {
try {
const appKeys = await getAppKeysFromSlug("zohocalendar");
const { client_id, client_secret } = zohoKeysSchema.parse(appKeys);

const server_location = zohoCredentials.server_location;
const params = {
client_id,
grant_type: "refresh_token",
Expand All @@ -52,7 +47,7 @@ export default class ZohoCalendarService implements Calendar {

const query = stringify(params);

const res = await fetch(`https://accounts.zoho.com/oauth/v2/token?${query}`, {
const res = await fetch(`https://accounts.zoho.${server_location}/oauth/v2/token?${query}`, {
method: "POST",
headers: {
"Content-Type": "application/json; charset=utf-8",
Expand All @@ -61,10 +56,16 @@ export default class ZohoCalendarService implements Calendar {

const token = await res.json();

// Revert if access_token is not present
if (!token.access_token) {
throw new Error("Invalid token response");
}

const key: ZohoAuthCredentials = {
access_token: token.access_token,
refresh_token: zohoCredentials.refresh_token,
expires_in: Math.round(+new Date() / 1000 + token.expires_in),
server_location,
};
await prisma.credential.update({
where: { id: credential.id },
Expand All @@ -87,8 +88,7 @@ export default class ZohoCalendarService implements Calendar {

private fetcher = async (endpoint: string, init?: RequestInit | undefined) => {
const credentials = await this.auth.getToken();

return fetch(`https://calendar.zoho.com/api/v1${endpoint}`, {
return fetch(`https://calendar.zoho.${credentials.server_location}/api/v1${endpoint}`, {
method: "GET",
...init,
headers: {
Expand All @@ -101,8 +101,7 @@ export default class ZohoCalendarService implements Calendar {

private getUserInfo = async () => {
const credentials = await this.auth.getToken();

const response = await fetch(`https://accounts.zoho.com/oauth/user/info`, {
const response = await fetch(`https://accounts.zoho.${credentials.server_location}/oauth/user/info`, {
method: "GET",
headers: {
Authorization: `Bearer ${credentials.access_token}`,
Expand Down Expand Up @@ -263,6 +262,37 @@ export default class ZohoCalendarService implements Calendar {
);
}

private async getUnavailability(
range: { start: string; end: string },
calendarId: string
): Promise<Array<{ start: string; end: string }>> {
const query = stringify({
range: JSON.stringify(range),
});
this.log.debug("getUnavailability query", query);
try {
// List all events within the range
const response = await this.fetcher(`/calendars/${calendarId}/events?${query}`);
const data = await this.handleData(response, this.log);

// Check for no data scenario
if (!data.events || data.events.length === 0) return [];

return (
data.events
.filter((event: any) => event.isprivate === false)

Check warning on line 283 in packages/app-store/zohocalendar/lib/CalendarService.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/app-store/zohocalendar/lib/CalendarService.ts#L283

[@typescript-eslint/no-explicit-any] Unexpected any. Specify a different type.
.map((event: any) => {

Check warning on line 284 in packages/app-store/zohocalendar/lib/CalendarService.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/app-store/zohocalendar/lib/CalendarService.ts#L284

[@typescript-eslint/no-explicit-any] Unexpected any. Specify a different type.
const start = dayjs(event.dateandtime.start, "YYYYMMDD[T]HHmmssZ").utc().toISOString();
const end = dayjs(event.dateandtime.end, "YYYYMMDD[T]HHmmssZ").utc().toISOString();
return { start, end };
}) || []
);
} catch (error) {
this.log.error(error);
return [];
}
}

async getAvailability(
dateFrom: string,
dateTo: string,
Expand Down Expand Up @@ -299,7 +329,22 @@ export default class ZohoCalendarService implements Calendar {
originalEndDate.format("YYYYMMDD[T]HHmmss[Z]"),
userInfo.Email
);
return busyData;

const unavailabilityData = await Promise.all(
queryIds.map((calendarId) =>
this.getUnavailability(
{
start: originalStartDate.format("YYYYMMDD[T]HHmmss[Z]"),
end: originalEndDate.format("YYYYMMDD[T]HHmmss[Z]"),
},
calendarId
)
)
);

const unavailability = unavailabilityData.flat();

return busyData.concat(unavailability);
} else {
// Zoho only supports 31 days of freebusy data
const busyData = [];
Expand All @@ -320,6 +365,22 @@ export default class ZohoCalendarService implements Calendar {
))
);

const unavailabilityData = await Promise.all(
queryIds.map((calendarId) =>
this.getUnavailability(
{
start: startDate.format("YYYYMMDD[T]HHmmss[Z]"),
end: endDate.format("YYYYMMDD[T]HHmmss[Z]"),
},
calendarId
)
)
);

const unavailability = unavailabilityData.flat();

busyData.push(...unavailability);

startDate = endDate.add(1, "minutes");
endDate = startDate.add(30, "days");
}
Expand Down
1 change: 1 addition & 0 deletions packages/app-store/zohocalendar/types/ZohoCalendar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export type ZohoAuthCredentials = {
access_token: string;
refresh_token: string;
expires_in: number;
server_location: string;
};

export type FreeBusy = {
Expand Down

0 comments on commit 75edc45

Please sign in to comment.