diff --git a/.env.appStore.example b/.env.appStore.example index ca90618a5343b6..2719175e4abfe7 100644 --- a/.env.appStore.example +++ b/.env.appStore.example @@ -125,4 +125,5 @@ SALESFORCE_CONSUMER_SECRET="" ZOHOCRM_CLIENT_ID="" ZOHOCRM_CLIENT_SECRET="" + # ********************************************************************************************************* diff --git a/README.md b/README.md index 2a551a04cb228c..944c4c676443eb 100644 --- a/README.md +++ b/README.md @@ -504,6 +504,9 @@ For example, `Cal.com (support@cal.com)`. 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 b3b679d8edd689..8d889090f083b5 100644 --- a/packages/app-store/apps.keys-schemas.generated.ts +++ b/packages/app-store/apps.keys-schemas.generated.ts @@ -30,6 +30,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"; @@ -62,6 +63,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 87b7ae5de3055a..c5124873684d94 100644 --- a/packages/app-store/apps.metadata.generated.ts +++ b/packages/app-store/apps.metadata.generated.ts @@ -70,6 +70,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"; @@ -142,6 +143,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 1801493d855997..d4b15a93be6465 100644 --- a/packages/app-store/apps.schemas.generated.ts +++ b/packages/app-store/apps.schemas.generated.ts @@ -30,6 +30,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"; @@ -62,6 +63,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 4f17a0f13b020c..1b4d268f6b9ccc 100644 --- a/packages/app-store/apps.server.generated.ts +++ b/packages/app-store/apps.server.generated.ts @@ -70,6 +70,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 10046d53931144..70a0db78170237 100644 --- a/packages/app-store/index.ts +++ b/packages/app-store/index.ts @@ -32,6 +32,7 @@ const appStore = { exchangecalendar: () => import("./exchangecalendar"), facetime: () => import("./facetime"), sylapsvideo: () => import("./sylapsvideo"), + zohocalendar: () => import("./zohocalendar"), "zoho-bigin": () => import("./zoho-bigin"), basecamp3: () => import("./basecamp3"), 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..153614e82ede9f --- /dev/null +++ b/packages/app-store/zohocalendar/DESCRIPTION.md @@ -0,0 +1,9 @@ +--- +items: + - ZCal1.jpg + - ZCal2.jpg + - ZCal3.jpg + - ZCal4.jpg +--- + +Zoho Calendar is an online business calendar that makes scheduling easy for you. Use this app to sync your Cal bookings with your Zoho Calendar. \ 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..5b22a522b9af4a --- /dev/null +++ b/packages/app-store/zohocalendar/README.md @@ -0,0 +1,16 @@ +## 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. Create a "Server-based Applications", set the Redirect URL for OAuth `/api/integrations/zohocalendar/callback` replacing Cal.com URL with the URI at which your application runs. +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, +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 at `/settings/my-account/calendars`. +11. You can access your Zoho calendar at [https://calendar.zoho.com/](https://calendar.zoho.com/) + +NOTE: If you use multiple calendars with Cal, make sure you enable the toggle to prevent double-bookings across calendar. This is in `/settings/my-account/calendars`. \ No newline at end of file diff --git a/packages/app-store/zohocalendar/api/add.ts b/packages/app-store/zohocalendar/api/add.ts new file mode 100644 index 00000000000000..59a9868ff757bd --- /dev/null +++ b/packages/app-store/zohocalendar/api/add.ts @@ -0,0 +1,42 @@ +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"; + +const OAUTH_BASE_URL = "https://accounts.zoho.com/oauth/v2"; + +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: `${OAUTH_BASE_URL}/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..e46cd413eaa8f9 --- /dev/null +++ b/packages/app-store/zohocalendar/api/callback.ts @@ -0,0 +1,76 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import { stringify } from "querystring"; + +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 createOAuthAppCredential from "../../_utils/createOAuthAppCredential"; +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"; +import { appKeysSchema as zohoKeysSchema } from "../zod"; + +const log = logger.getChildLogger({ prefix: [`[[zohocalendar/api/callback]`] }); + +const OAUTH_BASE_URL = "https://accounts.zoho.com/oauth/v2"; + +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(`${OAUTH_BASE_URL}/token?${query}`, { + method: "POST", + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }); + + const responseBody = await response.json(); + + 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 createOAuthAppCredential({ appId: config.slug, type: config.type }, key, req); + + 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..82367f6443660f --- /dev/null +++ b/packages/app-store/zohocalendar/lib/CalendarService.ts @@ -0,0 +1,412 @@ +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", + ...init, + headers: { + Authorization: "Bearer " + credentials.access_token, + "Content-Type": "application/json", + ...init?.headers, + }, + }); + }; + + 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 [mainHostDestinationCalendar] = event.destinationCalendar ?? []; + const calendarId = mainHostDestinationCalendar?.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].uid 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 [mainHostDestinationCalendar] = event.destinationCalendar ?? []; + const calendarId = externalCalendarId || mainHostDestinationCalendar?.externalId; + if (!calendarId) { + this.log.error("no calendar id provided in updateEvent"); + throw new Error("no calendar id provided in updateEvent"); + } + try { + // needed to fetch etag + const existingEventResponse = await this.fetcher(`/calendars/${calendarId}/events/${uid}`); + const existingEventData = await this.handleData(existingEventResponse, this.log); + + const query = stringify({ + eventdata: JSON.stringify({ + ...this.translateEvent(event), + etag: existingEventData.events[0].etag, + }), + }); + + const eventResponse = await this.fetcher(`/calendars/${calendarId}/events/${uid}?${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].uid 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 [mainHostDestinationCalendar] = event.destinationCalendar ?? []; + const calendarId = externalCalendarId || mainHostDestinationCalendar?.externalId; + if (!calendarId) { + this.log.error("no calendar id provided in deleteEvent"); + throw new Error("no calendar id provided in deleteEvent"); + } + try { + // needed to fetch etag + const existingEventResponse = await this.fetcher(`/calendars/${calendarId}/events/${uid}`); + const existingEventData = await this.handleData(existingEventResponse, this.log); + + const response = await this.fetcher(`/calendars/${calendarId}/events/${uid}`, { + method: "DELETE", + headers: { + etag: existingEventData.events[0].etag, + }, + }); + await this.handleData(response, this.log); + } catch (error) { + this.log.error(error); + throw error; + } + } + + private async getBusyData(dateFrom: string, dateTo: string, userEmail: string) { + const query = stringify({ + sdate: dateFrom, + edate: dateTo, + ftype: "eventbased", + uemail: userEmail, + }); + + 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 []; + + return ( + data.freebusy + .filter((freebusy: FreeBusy) => freebusy.fbtype === "busy") + .map((freebusy: FreeBusy) => ({ + // using dayjs utc plugin because by default, dayjs parses and displays in local time, which causes a mismatch + start: dayjs.utc(freebusy.startTime, "YYYYMMDD[T]HHmmss[Z]").toISOString(), + end: dayjs.utc(freebusy.endTime, "YYYYMMDD[T]HHmmss[Z]").toISOString(), + })) || [] + ); + } + + 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(); + const originalStartDate = dayjs(dateFrom); + const originalEndDate = dayjs(dateTo); + const diff = originalEndDate.diff(originalStartDate, "days"); + + if (diff <= 30) { + const busyData = await this.getBusyData( + originalStartDate.format("YYYYMMDD[T]HHmmss[Z]"), + originalEndDate.format("YYYYMMDD[T]HHmmss[Z]"), + userInfo.Email + ); + return busyData; + } else { + // Zoho only supports 31 days of freebusy data + const busyData = []; + + const loopsNumber = Math.ceil(diff / 30); + + let startDate = originalStartDate; + let endDate = originalStartDate.add(30, "days"); + + for (let i = 0; i < loopsNumber; i++) { + if (endDate.isAfter(originalEndDate)) endDate = originalEndDate; + + busyData.push( + ...(await this.getBusyData( + startDate.format("YYYYMMDD[T]HHmmss[Z]"), + endDate.format("YYYYMMDD[T]HHmmss[Z]"), + userInfo.Email + )) + ); + + startDate = endDate.add(1, "minutes"); + endDate = startDate.add(30, "days"); + } + + 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..cc9e6c5460b67a Binary files /dev/null and b/packages/app-store/zohocalendar/static/ZCal1.jpg differ diff --git a/packages/app-store/zohocalendar/static/ZCal2.jpg b/packages/app-store/zohocalendar/static/ZCal2.jpg new file mode 100644 index 00000000000000..b0cdfe135c0730 Binary files /dev/null and b/packages/app-store/zohocalendar/static/ZCal2.jpg differ diff --git a/packages/app-store/zohocalendar/static/ZCal3.jpg b/packages/app-store/zohocalendar/static/ZCal3.jpg new file mode 100644 index 00000000000000..dfc33ba4ce8872 Binary files /dev/null and b/packages/app-store/zohocalendar/static/ZCal3.jpg differ diff --git a/packages/app-store/zohocalendar/static/ZCal4.jpg b/packages/app-store/zohocalendar/static/ZCal4.jpg new file mode 100644 index 00000000000000..d4bf92f38a82ee Binary files /dev/null and b/packages/app-store/zohocalendar/static/ZCal4.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/prisma/seed-app-store.ts b/packages/prisma/seed-app-store.ts index d4bff45b7067e1..eaf642a04e36f9 100644 --- a/packages/prisma/seed-app-store.ts +++ b/packages/prisma/seed-app-store.ts @@ -316,6 +316,7 @@ export default async function main() { client_secret: process.env.ZOHOCRM_CLIENT_SECRET, }); } + await createApp("wipe-my-cal", "wipemycalother", ["automation"], "wipemycal_other"); if (process.env.GIPHY_API_KEY) { await createApp("giphy", "giphy", ["other"], "giphy_other", { diff --git a/yarn.lock b/yarn.lock index 49762c29fad61d..15633373a967d5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4487,6 +4487,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"