feat: Zoho Calendar (#10429)
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com> Co-authored-by: alannnc <alannnc@gmail.com> Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: aar2dee2 <85004512+aar2dee2@users.noreply.github.com>pull/11507/head
parent
efb04d04ab
commit
70461b2718
|
@ -125,4 +125,5 @@ SALESFORCE_CONSUMER_SECRET=""
|
|||
ZOHOCRM_CLIENT_ID=""
|
||||
ZOHOCRM_CLIENT_SECRET=""
|
||||
|
||||
|
||||
# *********************************************************************************************************
|
||||
|
|
|
@ -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/)
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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"),
|
||||
};
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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.
|
|
@ -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 `<Cal.com URL>/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 (`<Cal.com>/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`.
|
|
@ -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) }),
|
||||
});
|
|
@ -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) }),
|
||||
});
|
|
@ -0,0 +1,2 @@
|
|||
export { default as add } from "./add";
|
||||
export { default as callback } from "./callback";
|
|
@ -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"
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export * as api from "./api";
|
||||
export * as lib from "./lib";
|
|
@ -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<ZohoAuthCredentials> };
|
||||
|
||||
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<NewCalendarEventType> {
|
||||
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<EventBusyDate[]> {
|
||||
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<IntegrationCalendar[]> {
|
||||
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;
|
||||
};
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export { default as CalendarService } from "./CalendarService";
|
|
@ -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": "*"
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 59 KiB |
Binary file not shown.
After Width: | Height: | Size: 52 KiB |
Binary file not shown.
After Width: | Height: | Size: 57 KiB |
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?><svg enable-background="new 0 0 1024 1024" version="1.1" viewBox="0 0 1024 1024" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><style type="text/css">.st1{fill:#F4B01C;}.st2{fill:#256FB2;}</style><g class="st0"><g class="st0"><path class="st1" d="m733.3 796.1c-65.5 0-118.8-53.3-118.8-118.8 0-33.2 10.2-60 30.4-79.6 5.5-5.4 11.5-9.9 17.9-13.8-5.5-6-8.5-14.3-7.6-23 1.6-16.5 16.2-28.6 32.7-27 26 2.5 56.7 13.3 83.6 29 4.4 0.1 8.6 0.2 12.5 0.3 16.6 0.5 29.6 14.3 29.1 30.8v0.5c24 24.1 37.6 52.2 39 81.2 1.6 32.8-8.7 61.8-29.8 84-22 23.2-54.4 36.4-89 36.4zm20.5-173.2c-25 0.5-52.8 3.9-67.2 17.9-8.2 8-12.2 19.9-12.2 36.5 0 32.4 26.4 58.8 58.8 58.8 18.2 0 34.8-6.4 45.5-17.7 9.7-10.1 14.1-23.5 13.3-39.7-1.1-25.5-24.1-45.8-38.2-55.8z"/></g></g><g class="st0"><path class="st2" d="m267.9 955.2c-32.4 0-63.4-12-87.4-33.8s-38.9-51.5-42-83.7l-4-41.4-26.9-4.7c-68.4-11.9-115.3-75.3-106.6-144.3l28.5-225.9c2.2-17.4 10.6-32.6 23.7-43 11.6-9.2 26.5-14.1 41.8-13.9s30 5.6 41.3 15.1c12.8 10.7 20.7 26.2 22.4 43.6l39.6 408.8c3.5 36.1 33.4 63.3 69.7 63.3h626c20 0 38.4-8.2 51.8-23s19.8-33.9 17.8-53.8l-60.6-626.5c-3.5-36.1-33.4-63.3-69.7-63.3h-626c-20 0-38.4 8.1-51.8 22.9s-19.8 33.9-17.9 53.8l3.9 40.8c0.2 2.6 2.4 4.5 5 4.5h636.1c16.6 0 30 13.4 30 30s-13.4 30-30 30h-636c-33.6 0-61.5-25.2-64.7-58.7l-3.9-40.8c-3.5-36.4 8.6-72.8 33.2-99.9s59.7-42.6 96.2-42.6h626.1c32.4 0 63.4 12 87.4 33.8s38.9 51.5 42 83.7l60.6 626.4c3.5 36.4-8.6 72.8-33.2 99.9s-59.7 42.6-96.2 42.6h-626.2zm-174-530.6c-4.3 0-4.6 2.5-4.9 4.4l-28.4 225.8c-4.7 37.1 20.5 71.3 57.4 77.7l10.6 1.8-29.6-305.2c-0.2-1.9-0.4-4.5-4.9-4.5h-0.2z"/></g></svg>
|
After Width: | Height: | Size: 1.6 KiB |
|
@ -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;
|
||||
}[];
|
||||
}[];
|
||||
};
|
|
@ -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),
|
||||
});
|
|
@ -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", {
|
||||
|
|
|
@ -4488,6 +4488,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"
|
||||
|
|
Loading…
Reference in New Issue