diff --git a/packages/app-store/zohocalendar/api/callback.ts b/packages/app-store/zohocalendar/api/callback.ts index 2ffa8373cd..ac36514d41 100644 --- a/packages/app-store/zohocalendar/api/callback.ts +++ b/packages/app-store/zohocalendar/api/callback.ts @@ -18,10 +18,13 @@ 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") { @@ -29,6 +32,11 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) { 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" }); } @@ -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); @@ -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}`, diff --git a/packages/app-store/zohocalendar/lib/CalendarService.ts b/packages/app-store/zohocalendar/lib/CalendarService.ts index a245a1e78c..0797732fe7 100644 --- a/packages/app-store/zohocalendar/lib/CalendarService.ts +++ b/packages/app-store/zohocalendar/lib/CalendarService.ts @@ -1,5 +1,4 @@ import { stringify } from "querystring"; -import { z } from "zod"; import dayjs from "@calcom/dayjs"; import { getLocation, getRichDescription } from "@calcom/lib/CalEventParser"; @@ -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 = ""; @@ -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", @@ -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", @@ -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 }, @@ -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: { @@ -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}`, @@ -263,6 +262,37 @@ export default class ZohoCalendarService implements Calendar { ); } + private async getUnavailability( + range: { start: string; end: string }, + calendarId: string + ): Promise> { + 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) + .map((event: any) => { + 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, @@ -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 = []; @@ -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"); } diff --git a/packages/app-store/zohocalendar/types/ZohoCalendar.ts b/packages/app-store/zohocalendar/types/ZohoCalendar.ts index 0ebd9ea2c1..77de55d071 100644 --- a/packages/app-store/zohocalendar/types/ZohoCalendar.ts +++ b/packages/app-store/zohocalendar/types/ZohoCalendar.ts @@ -2,6 +2,7 @@ export type ZohoAuthCredentials = { access_token: string; refresh_token: string; expires_in: number; + server_location: string; }; export type FreeBusy = {