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>
This commit is contained in:
Murtaja Ziad 2023-09-25 11:34:37 +03:00 committed by GitHub
parent efb04d04ab
commit 70461b2718
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 663 additions and 0 deletions

View File

@ -125,4 +125,5 @@ SALESFORCE_CONSUMER_SECRET=""
ZOHOCRM_CLIENT_ID=""
ZOHOCRM_CLIENT_SECRET=""
# *********************************************************************************************************

View File

@ -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/)

View File

@ -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,
};

View File

@ -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,
};

View File

@ -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,
};

View File

@ -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"),
};

View File

@ -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"),

View File

@ -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.

View File

@ -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`.

View File

@ -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) }),
});

View File

@ -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) }),
});

View File

@ -0,0 +1,2 @@
export { default as add } from "./add";
export { default as callback } from "./callback";

View File

@ -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"
}

View File

@ -0,0 +1,2 @@
export * as api from "./api";
export * as lib from "./lib";

View File

@ -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;
};
}

View File

@ -0,0 +1 @@
export { default as CalendarService } from "./CalendarService";

View File

@ -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

View File

@ -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

View File

@ -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;
}[];
}[];
};

View File

@ -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),
});

View File

@ -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", {

View File

@ -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"