diff --git a/.env.appStore.example b/.env.appStore.example index 94943a2fa68cd4..18057e0132b3ab 100644 --- a/.env.appStore.example +++ b/.env.appStore.example @@ -111,8 +111,8 @@ LARK_OPEN_VERIFICATION_TOKEN="" SALESFORCE_CONSUMER_KEY="" SALESFORCE_CONSUMER_SECRET="" -# - ZOHOCRM -# Used for the Zoho CRM integration +# - ZOHOCRM And Calendar +# Used for the Zoho CRM integration and Zoho Calendar integration that uses Zoho API ZOHOCRM_CLIENT_ID="" ZOHOCRM_CLIENT_SECRET="" diff --git a/README.md b/README.md index 4ebb532fa7bdfa..ce8696d215628a 100644 --- a/README.md +++ b/README.md @@ -492,6 +492,9 @@ following 9. Click the "Save"/ "UPDATE" button at the bottom footer. 10. You're good to go. Now you can easily add your ZohoCRM integration in the Cal.com settings. +### Obtaining Zoho Calendar Client ID and Secret + +[Follow these steps](./packages/app-store/zohocalendar/) ### Obtaining Zoho Bigin Client ID and Secret [Follow these steps](./packages/app-store/zoho-bigin/) diff --git a/packages/app-store/apps.keys-schemas.generated.ts b/packages/app-store/apps.keys-schemas.generated.ts index 5e28e70e72571c..fce4f747178f35 100644 --- a/packages/app-store/apps.keys-schemas.generated.ts +++ b/packages/app-store/apps.keys-schemas.generated.ts @@ -27,6 +27,7 @@ import { appKeysSchema as webex_zod_ts } from "./webex/zod"; import { appKeysSchema as wordpress_zod_ts } from "./wordpress/zod"; import { appKeysSchema as zapier_zod_ts } from "./zapier/zod"; import { appKeysSchema as zoho_bigin_zod_ts } from "./zoho-bigin/zod"; +import { appKeysSchema as zohocalendar_zod_ts } from "./zohocalendar/zod"; import { appKeysSchema as zohocrm_zod_ts } from "./zohocrm/zod"; import { appKeysSchema as zoomvideo_zod_ts } from "./zoomvideo/zod"; @@ -56,6 +57,7 @@ export const appKeysSchemas = { wordpress: wordpress_zod_ts, zapier: zapier_zod_ts, "zoho-bigin": zoho_bigin_zod_ts, + zohocalendar: zohocalendar_zod_ts, zohocrm: zohocrm_zod_ts, zoomvideo: zoomvideo_zod_ts, }; diff --git a/packages/app-store/apps.metadata.generated.ts b/packages/app-store/apps.metadata.generated.ts index 3e036aa4f3f8e7..0d91a8a394d592 100644 --- a/packages/app-store/apps.metadata.generated.ts +++ b/packages/app-store/apps.metadata.generated.ts @@ -64,6 +64,7 @@ import { metadata as wipemycalother__metadata_ts } from "./wipemycalother/_metad import wordpress_config_json from "./wordpress/config.json"; import { metadata as zapier__metadata_ts } from "./zapier/_metadata"; import zoho_bigin_config_json from "./zoho-bigin/config.json"; +import zohocalendar_config_json from "./zohocalendar/config.json"; import zohocrm_config_json from "./zohocrm/config.json"; import { metadata as zoomvideo__metadata_ts } from "./zoomvideo/_metadata"; @@ -130,6 +131,7 @@ export const appStoreMetadata = { wordpress: wordpress_config_json, zapier: zapier__metadata_ts, "zoho-bigin": zoho_bigin_config_json, + zohocalendar: zohocalendar_config_json, zohocrm: zohocrm_config_json, zoomvideo: zoomvideo__metadata_ts, }; diff --git a/packages/app-store/apps.schemas.generated.ts b/packages/app-store/apps.schemas.generated.ts index 98c57b824f834c..cf17596d95cdc3 100644 --- a/packages/app-store/apps.schemas.generated.ts +++ b/packages/app-store/apps.schemas.generated.ts @@ -27,6 +27,7 @@ import { appDataSchema as webex_zod_ts } from "./webex/zod"; import { appDataSchema as wordpress_zod_ts } from "./wordpress/zod"; import { appDataSchema as zapier_zod_ts } from "./zapier/zod"; import { appDataSchema as zoho_bigin_zod_ts } from "./zoho-bigin/zod"; +import { appDataSchema as zohocalendar_zod_ts } from "./zohocalendar/zod"; import { appDataSchema as zohocrm_zod_ts } from "./zohocrm/zod"; import { appDataSchema as zoomvideo_zod_ts } from "./zoomvideo/zod"; @@ -56,6 +57,7 @@ export const appDataSchemas = { wordpress: wordpress_zod_ts, zapier: zapier_zod_ts, "zoho-bigin": zoho_bigin_zod_ts, + zohocalendar: zohocalendar_zod_ts, zohocrm: zohocrm_zod_ts, zoomvideo: zoomvideo_zod_ts, }; diff --git a/packages/app-store/apps.server.generated.ts b/packages/app-store/apps.server.generated.ts index 9d2c8a32b068da..a1bdbce1c1bc5c 100644 --- a/packages/app-store/apps.server.generated.ts +++ b/packages/app-store/apps.server.generated.ts @@ -64,6 +64,7 @@ export const apiHandlers = { wordpress: import("./wordpress/api"), zapier: import("./zapier/api"), "zoho-bigin": import("./zoho-bigin/api"), + zohocalendar: import("./zohocalendar/api"), zohocrm: import("./zohocrm/api"), zoomvideo: import("./zoomvideo/api"), }; diff --git a/packages/app-store/index.ts b/packages/app-store/index.ts index 54168d2e8d0a83..dd840342d1783b 100644 --- a/packages/app-store/index.ts +++ b/packages/app-store/index.ts @@ -30,6 +30,7 @@ const appStore = { exchangecalendar: () => import("./exchangecalendar"), facetime: () => import("./facetime"), sylapsvideo: () => import("./sylapsvideo"), + zohocalendar: () => import("./zohocalendar"), "zoho-bigin": () => import("./zoho-bigin"), telegramvideo: () => import("./telegram"), }; diff --git a/packages/app-store/zohocalendar/DESCRIPTION.md b/packages/app-store/zohocalendar/DESCRIPTION.md new file mode 100644 index 00000000000000..54324ab4879ad7 --- /dev/null +++ b/packages/app-store/zohocalendar/DESCRIPTION.md @@ -0,0 +1,6 @@ +--- +items: + - ZCal1.jpg +--- + +Zoho Calendar is an online business calendar that makes scheduling easy for you. You can use it to stay on top of your schedule and also share calendars with your team to keep everyone on the same page. \ No newline at end of file diff --git a/packages/app-store/zohocalendar/README.md b/packages/app-store/zohocalendar/README.md new file mode 100644 index 00000000000000..515088953e79c1 --- /dev/null +++ b/packages/app-store/zohocalendar/README.md @@ -0,0 +1,14 @@ +## Zoho Calendar + +### Obtaining Zoho Calendar Client ID and Secret + +1. Open [Zoho API Console](https://api-console.zoho.com/) and sign into your account, or create a new one. +2. From within the API console page, go to "Applications". +3. Click "ADD CLIENT" button top right and select "Server-based Applications". +4. Fill in any information you want in the "Client Details" tab +5. Go to tab "Client Secret" tab. +6. Now copy the Client ID and Client Secret into your app keys in the Cal.com admin panel (`/settings/admin/apps`). +7. Back in Zoho API Console, set the Redirect URL for OAuth `/api/integrations/zohocalendar/callback` replacing Cal.com URL with the URI at which your application runs. +8. In the "Settings" section check the "Multi-DC" option if you wish to use the same OAuth credentials for all data centers. +9. Click the "Save"/ "UPDATE" button at the bottom footer. +10. You're good to go. Now you can easily add your Zoho Calendar integration in the Cal.com settings. diff --git a/packages/app-store/zohocalendar/api/add.ts b/packages/app-store/zohocalendar/api/add.ts new file mode 100644 index 00000000000000..3e0076611d85b5 --- /dev/null +++ b/packages/app-store/zohocalendar/api/add.ts @@ -0,0 +1,40 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import { stringify } from "querystring"; + +import { WEBAPP_URL } from "@calcom/lib/constants"; +import { defaultHandler, defaultResponder } from "@calcom/lib/server"; + +import { encodeOAuthState } from "../../_utils/encodeOAuthState"; +import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug"; +import config from "../config.json"; +import { appKeysSchema as zohoKeysSchema } from "../zod"; + +async function getHandler(req: NextApiRequest, res: NextApiResponse) { + const appKeys = await getAppKeysFromSlug(config.slug); + const { client_id } = zohoKeysSchema.parse(appKeys); + + const state = encodeOAuthState(req); + + const params = { + client_id, + response_type: "code", + redirect_uri: WEBAPP_URL + "/api/integrations/zohocalendar/callback", + scope: [ + "ZohoCalendar.calendar.ALL", + "ZohoCalendar.event.ALL", + "ZohoCalendar.freebusy.READ", + "AaaServer.profile.READ", + ], + access_type: "offline", + state, + prompt: "consent", + }; + + const query = stringify(params); + + res.status(200).json({ url: `https://accounts.zoho.com/oauth/v2/auth?${query}` }); +} + +export default defaultHandler({ + GET: Promise.resolve({ default: defaultResponder(getHandler) }), +}); diff --git a/packages/app-store/zohocalendar/api/callback.ts b/packages/app-store/zohocalendar/api/callback.ts new file mode 100644 index 00000000000000..2419c373ef9659 --- /dev/null +++ b/packages/app-store/zohocalendar/api/callback.ts @@ -0,0 +1,87 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import { stringify } from "querystring"; +import { z } from "zod"; + +import { WEBAPP_URL } from "@calcom/lib/constants"; +import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl"; +import logger from "@calcom/lib/logger"; +import { defaultHandler, defaultResponder } from "@calcom/lib/server"; +import prisma from "@calcom/prisma"; + +import { decodeOAuthState } from "../../_utils/decodeOAuthState"; +import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug"; +import getInstalledAppPath from "../../_utils/getInstalledAppPath"; +import config from "../config.json"; +import type { ZohoAuthCredentials } from "../types/ZohoCalendar"; + +const log = logger.getChildLogger({ prefix: [`[[zohocalendar/api/callback]`] }); + +const zohoKeysSchema = z.object({ + client_id: z.string(), + client_secret: z.string(), +}); + +async function getHandler(req: NextApiRequest, res: NextApiResponse) { + const { code } = req.query; + const state = decodeOAuthState(req); + + if (code && typeof code !== "string") { + res.status(400).json({ message: "`code` must be a string" }); + return; + } + + if (!req.session?.user?.id) { + return res.status(401).json({ message: "You must be logged in to do this" }); + } + + const appKeys = await getAppKeysFromSlug(config.slug); + const { client_id, client_secret } = zohoKeysSchema.parse(appKeys); + + const params = { + client_id, + grant_type: "authorization_code", + client_secret, + redirect_uri: `${WEBAPP_URL}/api/integrations/${config.slug}/callback`, + code, + }; + + const query = stringify(params); + + const response = await fetch(`https://accounts.zoho.com/oauth/v2/token?${query}`, { + method: "POST", + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }); + + const responseBody = await response.json(); + console.log(responseBody); + + if (!response.ok || responseBody.error) { + log.error("get access_token failed", responseBody); + return res.redirect("/apps/installed?error=" + JSON.stringify(responseBody)); + } + + const key: ZohoAuthCredentials = { + access_token: responseBody.access_token, + refresh_token: responseBody.refresh_token, + expires_in: Math.round(+new Date() / 1000 + responseBody.expires_in), + }; + + await prisma.credential.create({ + data: { + type: config.type, + key, + userId: req.session.user.id, + appId: config.slug, + }, + }); + + res.redirect( + getSafeRedirectUrl(state?.returnTo) ?? getInstalledAppPath({ variant: config.variant, slug: config.slug }) + ); +} + +export default defaultHandler({ + GET: Promise.resolve({ default: defaultResponder(getHandler) }), +}); diff --git a/packages/app-store/zohocalendar/api/index.ts b/packages/app-store/zohocalendar/api/index.ts new file mode 100644 index 00000000000000..eb12c1b4ed2c4f --- /dev/null +++ b/packages/app-store/zohocalendar/api/index.ts @@ -0,0 +1,2 @@ +export { default as add } from "./add"; +export { default as callback } from "./callback"; diff --git a/packages/app-store/zohocalendar/config.json b/packages/app-store/zohocalendar/config.json new file mode 100644 index 00000000000000..918050d9718958 --- /dev/null +++ b/packages/app-store/zohocalendar/config.json @@ -0,0 +1,16 @@ +{ + "name": "Zoho Calendar", + "description": "Zoho Calendar is an online business calendar that makes scheduling easy for you. You can use it to stay on top of your schedule and also share calendars with your team to keep everyone on the same page.", + "slug": "zohocalendar", + "type": "zoho_calendar", + "title": "Zoho Calendar", + "variant": "calendar", + "category": "calendar", + "categories": [ + "calendar" + ], + "logo": "icon.svg", + "publisher": "Cal.com", + "url": "https://cal.com/", + "email": "help@cal.com" +} \ No newline at end of file diff --git a/packages/app-store/zohocalendar/index.ts b/packages/app-store/zohocalendar/index.ts new file mode 100644 index 00000000000000..e2e9d7b029c031 --- /dev/null +++ b/packages/app-store/zohocalendar/index.ts @@ -0,0 +1,2 @@ +export * as api from "./api"; +export * as lib from "./lib"; diff --git a/packages/app-store/zohocalendar/lib/CalendarService.ts b/packages/app-store/zohocalendar/lib/CalendarService.ts new file mode 100644 index 00000000000000..b85fce9d554ef5 --- /dev/null +++ b/packages/app-store/zohocalendar/lib/CalendarService.ts @@ -0,0 +1,356 @@ +import { stringify } from "querystring"; +import { z } from "zod"; + +import dayjs from "@calcom/dayjs"; +import { getLocation, getRichDescription } from "@calcom/lib/CalEventParser"; +import logger from "@calcom/lib/logger"; +import prisma from "@calcom/prisma"; +import type { + Calendar, + CalendarEvent, + EventBusyDate, + IntegrationCalendar, + NewCalendarEventType, +} from "@calcom/types/Calendar"; +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(), +}); + +export default class ZohoCalendarService implements Calendar { + private integrationName = ""; + private log: typeof logger; + auth: { getToken: () => Promise }; + + constructor(credential: CredentialPayload) { + this.integrationName = "zoho_calendar"; + this.auth = this.zohoAuth(credential); + this.log = logger.getChildLogger({ prefix: [`[[lib] ${this.integrationName}`] }); + } + + private zohoAuth = (credential: CredentialPayload) => { + let zohoCredentials = credential.key as ZohoAuthCredentials; + + const refreshAccessToken = async () => { + try { + const appKeys = await getAppKeysFromSlug("zohocalendar"); + const { client_id, client_secret } = zohoKeysSchema.parse(appKeys); + + const params = { + client_id, + grant_type: "refresh_token", + client_secret, + refresh_token: zohoCredentials.refresh_token, + }; + + const query = stringify(params); + + const res = await fetch(`https://accounts.zoho.com/oauth/v2/token?${query}`, { + method: "POST", + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }); + + const token = await res.json(); + + const key: ZohoAuthCredentials = { + access_token: token.access_token, + refresh_token: zohoCredentials.refresh_token, + expires_in: Math.round(+new Date() / 1000 + token.expires_in), + }; + await prisma.credential.update({ + where: { id: credential.id }, + data: { key }, + }); + zohoCredentials = key; + } catch (err) { + this.log.error("Error refreshing zoho token", err); + } + return zohoCredentials; + }; + + return { + getToken: async () => { + const isExpired = () => new Date(zohoCredentials.expires_in * 1000).getTime() <= new Date().getTime(); + return !isExpired() ? Promise.resolve(zohoCredentials) : refreshAccessToken(); + }, + }; + }; + + private fetcher = async (endpoint: string, init?: RequestInit | undefined) => { + const credentials = await this.auth.getToken(); + + return fetch(`https://calendar.zoho.com/api/v1${endpoint}`, { + method: "GET", + headers: { + Authorization: "Bearer " + credentials.access_token, + "Content-Type": "application/json", + ...init?.headers, + }, + ...init, + }); + }; + + private getUserInfo = async () => { + const credentials = await this.auth.getToken(); + + const response = await fetch(`https://accounts.zoho.com/oauth/user/info`, { + method: "GET", + headers: { + Authorization: "Bearer " + credentials.access_token, + "Content-Type": "application/json", + }, + }); + + return this.handleData(response, this.log); + }; + + async createEvent(event: CalendarEvent): Promise { + let eventId = ""; + let eventRespData; + const calendarId = event.destinationCalendar?.externalId; + if (!calendarId) { + throw new Error("no calendar id"); + } + + try { + const query = stringify({ eventdata: JSON.stringify(this.translateEvent(event)) }); + + const eventResponse = await this.fetcher(`/calendars/${calendarId}/events?${query}`, { + method: "POST", + }); + eventRespData = await this.handleData(eventResponse, this.log); + eventId = eventRespData.events[0].uid as string; + } catch (error) { + this.log.error(error); + throw error; + } + + try { + return { + ...eventRespData.events[0], + uid: eventRespData.events[0].uid as string, + id: eventRespData.events[0].id as string, + type: "zoho_calendar", + password: "", + url: "", + additionalInfo: {}, + }; + } catch (error) { + this.log.error(error); + await this.deleteEvent(eventId, event, calendarId); + throw error; + } + } + + /** + * @param uid + * @param event + * @returns + */ + async updateEvent(uid: string, event: CalendarEvent, externalCalendarId?: string) { + const eventId = uid; + let eventRespData; + const calendarId = externalCalendarId || event.destinationCalendar?.externalId; + if (!calendarId) { + this.log.error("no calendar id provided in updateEvent"); + throw new Error("no calendar id provided in updateEvent"); + } + try { + const query = stringify({ eventdata: JSON.stringify(this.translateEvent(event)) }); + + const eventResponse = await this.fetcher(`/calendars/${calendarId}/events/${eventId}?${query}`, { + method: "PUT", + }); + eventRespData = await this.handleData(eventResponse, this.log); + } catch (error) { + this.log.error(error); + throw error; + } + + try { + return { + ...eventRespData.events[0], + uid: eventRespData.events[0].uid as string, + id: eventRespData.events[0].id as string, + type: "zoho_calendar", + password: "", + url: "", + additionalInfo: {}, + }; + } catch (error) { + this.log.error(error); + await this.deleteEvent(eventId, event); + throw error; + } + } + + /** + * @param uid + * @param event + * @returns + */ + async deleteEvent(uid: string, event: CalendarEvent, externalCalendarId?: string) { + const calendarId = externalCalendarId || event.destinationCalendar?.externalId; + if (!calendarId) { + this.log.error("no calendar id provided in deleteEvent"); + throw new Error("no calendar id provided in deleteEvent"); + } + try { + const response = await this.fetcher(`/calendars/${calendarId}/events/${uid}`, { + method: "DELETE", + }); + await this.handleData(response, this.log); + } catch (error) { + this.log.error(error); + throw error; + } + } + + async getAvailability( + dateFrom: string, + dateTo: string, + selectedCalendars: IntegrationCalendar[] + ): Promise { + const selectedCalendarIds = selectedCalendars + .filter((e) => e.integration === this.integrationName) + .map((e) => e.externalId); + if (selectedCalendarIds.length === 0 && selectedCalendars.length > 0) { + // Only calendars of other integrations selected + return Promise.resolve([]); + } + + try { + let queryIds = selectedCalendarIds; + + if (queryIds.length === 0) { + queryIds = (await this.listCalendars()).map((e) => e.externalId) || []; + if (queryIds.length === 0) { + return Promise.resolve([]); + } + } + + if (!selectedCalendars[0]) return []; + + const userInfo = await this.getUserInfo(); + let startDate = dayjs(dateFrom); + const endDate = dayjs(dateTo); + const diff = endDate.diff(startDate, "days"); + + if (diff > 30) { + this.log.error("Zoho only supports 31 days of freebusy data"); + startDate = endDate.subtract(31, "days"); + } + const query = stringify({ + sdate: startDate.format("YYYYMMDD[T]HHmmss[Z]"), + edate: endDate.format("YYYYMMDD[T]HHmmss[Z]"), + ftype: "eventbased", + uemail: userInfo.Email, + }); + + const response = await this.fetcher(`/calendars/freebusy?${query}`, { + method: "GET", + }); + + const data = await this.handleData(response, this.log); + + if (data.fb_not_enabled || data.NODATA) return []; + + const busyData = + data.freebusy + .filter((freebusy: FreeBusy) => freebusy.fbtype === "busy") + .map((freebusy: FreeBusy) => ({ + start: dayjs(freebusy.startTime, "YYYYMMDD[T]HHmmss[Z]").toISOString(), + end: dayjs(freebusy.endTime, "YYYYMMDD[T]HHmmss[Z]").toISOString(), + })) || []; + return busyData; + } catch (error) { + this.log.error(error); + return []; + } + } + + async listCalendars(): Promise { + try { + const resp = await this.fetcher(`/calendars`); + const data = (await this.handleData(resp, this.log)) as ZohoCalendarListResp; + const result = data.calendars + .filter((cal) => { + if (cal.privilege === "owner") { + return true; + } + return false; + }) + .map((cal) => { + const calendar: IntegrationCalendar = { + externalId: cal.uid ?? "No Id", + integration: this.integrationName, + name: cal.name || "No calendar name", + primary: cal.isdefault, + email: cal.uid ?? "", + }; + return calendar; + }); + + if (result.some((cal) => !!cal.primary)) { + return result; + } + + // No primary calendar found, get primary calendar directly + const respPrimary = await this.fetcher(`/calendars?category=own`); + const dataPrimary = (await this.handleData(respPrimary, this.log)) as ZohoCalendarListResp; + return dataPrimary.calendars.map((cal) => { + const calendar: IntegrationCalendar = { + externalId: cal.uid ?? "No Id", + integration: this.integrationName, + name: cal.name || "No calendar name", + primary: cal.isdefault, + email: cal.uid ?? "", + }; + return calendar; + }); + } catch (err) { + this.log.error("There was an error contacting zoho calendar service: ", err); + throw err; + } + } + + async handleData(response: Response, log: typeof logger) { + const data = await response.json(); + if (!response.ok) { + log.debug("zoho request with data", data); + throw data; + } + log.debug("zoho request with data", data); + return data; + } + + private translateEvent = (event: CalendarEvent) => { + const zohoEvent = { + title: event.title, + description: getRichDescription(event), + dateandtime: { + start: dayjs(event.startTime).format("YYYYMMDDTHHmmssZZ"), + end: dayjs(event.endTime).format("YYYYMMDDTHHmmssZZ"), + timezone: event.organizer.timeZone, + }, + attendees: event.attendees.map((attendee) => ({ email: attendee.email })), + isprivate: event.seatsShowAttendees, + reminders: [ + { + minutes: "-15", + action: "popup", + }, + ], + location: event.location ? getLocation(event) : undefined, + }; + + return zohoEvent; + }; +} diff --git a/packages/app-store/zohocalendar/lib/index.ts b/packages/app-store/zohocalendar/lib/index.ts new file mode 100644 index 00000000000000..e168c149df8531 --- /dev/null +++ b/packages/app-store/zohocalendar/lib/index.ts @@ -0,0 +1 @@ +export { default as CalendarService } from "./CalendarService"; diff --git a/packages/app-store/zohocalendar/package.json b/packages/app-store/zohocalendar/package.json new file mode 100644 index 00000000000000..066cd3b8bc0094 --- /dev/null +++ b/packages/app-store/zohocalendar/package.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "private": true, + "name": "@calcom/zohocalendar", + "version": "0.0.0", + "main": "./index.ts", + "description": "Zoho Calendar is an online business calendar that makes scheduling easy for you. You can use it to stay on top of your schedule and also share calendars with your team to keep everyone on the same page.", + "dependencies": { + "@calcom/prisma": "*" + }, + "devDependencies": { + "@calcom/types": "*" + } +} diff --git a/packages/app-store/zohocalendar/static/ZCal1.jpg b/packages/app-store/zohocalendar/static/ZCal1.jpg new file mode 100644 index 00000000000000..bd8bf3960d84a9 Binary files /dev/null and b/packages/app-store/zohocalendar/static/ZCal1.jpg differ diff --git a/packages/app-store/zohocalendar/static/icon.svg b/packages/app-store/zohocalendar/static/icon.svg new file mode 100644 index 00000000000000..3b1fb61c5db3f2 --- /dev/null +++ b/packages/app-store/zohocalendar/static/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app-store/zohocalendar/types/ZohoCalendar.ts b/packages/app-store/zohocalendar/types/ZohoCalendar.ts new file mode 100644 index 00000000000000..0ebd9ea2c19745 --- /dev/null +++ b/packages/app-store/zohocalendar/types/ZohoCalendar.ts @@ -0,0 +1,42 @@ +export type ZohoAuthCredentials = { + access_token: string; + refresh_token: string; + expires_in: number; +}; + +export type FreeBusy = { + fbtype: string; + startTime: string; + endTime: string; +}; + +export type ZohoCalendarListResp = { + calendars: { + name: string; + include_infreebusy: boolean; + textcolor: string; + isdefault: boolean; + status: boolean; + visibility: boolean; + timezone: string; + lastmodifiedtime: string; + color: string; + uid: string; + description: string; + privilege: string; + private: { + status: string; + icalurl: string; + htmlurl: string; + }; + public: { + icalurl: string; + privilege: string; + htmlurl: string; + }; + reminders: { + minutes: string; + action: string; + }[]; + }[]; +}; diff --git a/packages/app-store/zohocalendar/zod.ts b/packages/app-store/zohocalendar/zod.ts new file mode 100644 index 00000000000000..0a84054ebef3f9 --- /dev/null +++ b/packages/app-store/zohocalendar/zod.ts @@ -0,0 +1,8 @@ +import { z } from "zod"; + +export const appDataSchema = z.object({}); + +export const appKeysSchema = z.object({ + client_id: z.string().min(1), + client_secret: z.string().min(1), +}); diff --git a/packages/core/CalendarManager.ts b/packages/core/CalendarManager.ts index 1ae89be41af43b..9c91b1ee06bea7 100644 --- a/packages/core/CalendarManager.ts +++ b/packages/core/CalendarManager.ts @@ -207,6 +207,7 @@ export const getBusyCalendarTimes = async ( // Subtract 11 hours from the start date to avoid problems in UTC- time zones. const startDate = dayjs(dateFrom).subtract(11, "hours").format(); // Add 14 hours from the start date to avoid problems in UTC+ time zones. + // NOTE: some APIs restrict to 31 days for fetch availability e.g. zoho calendar const endDate = dayjs(dateTo).endOf("month").add(14, "hours").format(); results = await getCalendarsEvents(withCredentials, startDate, endDate, selectedCalendars); } catch (e) { diff --git a/packages/prisma/seed-app-store.ts b/packages/prisma/seed-app-store.ts index 069c17891c89e6..08b6442a2e387a 100644 --- a/packages/prisma/seed-app-store.ts +++ b/packages/prisma/seed-app-store.ts @@ -304,6 +304,10 @@ export default async function main() { client_id: process.env.ZOHOCRM_CLIENT_ID, client_secret: process.env.ZOHOCRM_CLIENT_SECRET, }); + await createApp("zohocalendar", "zohocalendar", ["calendar"], "zohocalendar_calendar", { + client_id: process.env.ZOHOCRM_CLIENT_ID, + client_secret: process.env.ZOHOCRM_CLIENT_SECRET, + }); } await createApp("wipe-my-cal", "wipemycalother", ["automation"], "wipemycal_other"); if (process.env.GIPHY_API_KEY) { diff --git a/yarn.lock b/yarn.lock index 208f7a95737821..fe9c68b7878330 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4881,6 +4881,15 @@ __metadata: languageName: unknown linkType: soft +"@calcom/zohocalendar@workspace:packages/app-store/zohocalendar": + version: 0.0.0-use.local + resolution: "@calcom/zohocalendar@workspace:packages/app-store/zohocalendar" + dependencies: + "@calcom/prisma": "*" + "@calcom/types": "*" + languageName: unknown + linkType: soft + "@calcom/zohocrm@workspace:packages/app-store/zohocrm": version: 0.0.0-use.local resolution: "@calcom/zohocrm@workspace:packages/app-store/zohocrm"