2
0

first commit

This commit is contained in:
2024-08-09 00:39:27 +02:00
commit 79688abe2e
5698 changed files with 497838 additions and 0 deletions

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.
3. Fill in any information you want in the "Client Details" tab
4. Go to tab "Client Secret" tab.
5. Now copy the Client ID and Client Secret into your app keys in the Cal.com admin panel (`<Cal.com>/settings/admin/apps`).
6. Back in Zoho API Console,
7. In the "Settings" section check the "Multi-DC" option if you wish to use the same OAuth credentials for all data centers.
8. Click the "Save"/ "UPDATE" button at the bottom footer.
9. You're good to go. Now you can easily add your Zoho Calendar integration in the Cal.com settings at `/settings/my-account/calendars`.
10. 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 getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
import { encodeOAuthState } from "../../_utils/oauth/encodeOAuthState";
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,151 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { stringify } from "querystring";
import { renewSelectedCalendarCredentialId } from "@calcom/lib/connectedCalendar";
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 { Prisma } from "@calcom/prisma/client";
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
import { decodeOAuthState } from "../../_utils/oauth/decodeOAuthState";
import config from "../config.json";
import type { ZohoAuthCredentials } from "../types/ZohoCalendar";
import { appKeysSchema as zohoKeysSchema } from "../zod";
const log = logger.getSubLogger({ prefix: [`[[zohocalendar/api/callback]`] });
function getOAuthBaseUrl(domain: string): string {
return `https://accounts.zoho.${domain}/oauth/v2`;
}
async function getHandler(req: NextApiRequest, res: NextApiResponse) {
const { code, location } = req.query;
const state = decodeOAuthState(req);
if (code && typeof code !== "string") {
res.status(400).json({ message: "`code` must be a string" });
return;
}
if (location && typeof location !== "string") {
res.status(400).json({ message: "`location` must be a string" });
return;
}
if (!req.session?.user?.id) {
return res.status(401).json({ message: "You must be logged in to do this" });
}
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 server_location = location === "us" ? "com" : location;
const query = stringify(params);
const response = await fetch(`${getOAuthBaseUrl(server_location || "com")}/token?${query}`, {
method: "POST",
headers: {
"Content-Type": "application/json; charset=utf-8",
},
});
const responseBody = await JSON.parse(await response.text());
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),
server_location: server_location || "com",
};
function getCalenderUri(domain: string): string {
return `https://calendar.zoho.${domain}/api/v1/calendars`;
}
const calendarResponse = await fetch(getCalenderUri(server_location || "com"), {
method: "GET",
headers: {
Authorization: `Bearer ${key.access_token}`,
"Content-Type": "application/json",
},
});
const data = await calendarResponse.json();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const primaryCalendar = data.calendars.find((calendar: any) => calendar.isdefault);
if (primaryCalendar.uid) {
const credential = await prisma.credential.create({
data: {
type: config.type,
key,
userId: req.session.user.id,
appId: config.slug,
},
});
const selectedCalendarWhereUnique = {
userId: req.session?.user.id,
integration: config.type,
externalId: primaryCalendar.uid,
};
// Wrapping in a try/catch to reduce chance of race conditions-
// also this improves performance for most of the happy-paths.
try {
await prisma.selectedCalendar.create({
data: {
...selectedCalendarWhereUnique,
credentialId: credential.id,
},
});
} catch (error) {
let errorMessage = "something_went_wrong";
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002") {
// it is possible a selectedCalendar was orphaned, in this situation-
// we want to recover by connecting the existing selectedCalendar to the new Credential.
if (await renewSelectedCalendarCredentialId(selectedCalendarWhereUnique, credential.id)) {
res.redirect(
getSafeRedirectUrl(state?.returnTo) ??
getInstalledAppPath({ variant: "calendar", slug: config.slug })
);
return;
}
// else
errorMessage = "account_already_linked";
}
await prisma.credential.delete({ where: { id: credential.id } });
res.redirect(
`${
getSafeRedirectUrl(state?.onErrorReturnTo) ??
getInstalledAppPath({ variant: config.variant, slug: config.slug })
}?error=${errorMessage}`
);
return;
}
}
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,15 @@
{
"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",
"isAuth": true
}

View File

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

View File

@@ -0,0 +1,473 @@
import { stringify } from "querystring";
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";
import { appKeysSchema as zohoKeysSchema } from "../zod";
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.getSubLogger({
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 server_location = zohoCredentials.server_location;
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.${server_location}/oauth/v2/token?${query}`, {
method: "POST",
headers: {
"Content-Type": "application/json; charset=utf-8",
},
});
const token = await res.json();
// Revert if access_token is not present
if (!token.access_token) {
throw new Error("Invalid token response");
}
const key: ZohoAuthCredentials = {
access_token: token.access_token,
refresh_token: zohoCredentials.refresh_token,
expires_in: Math.round(+new Date() / 1000 + token.expires_in),
server_location,
};
await prisma.credential.update({
where: { id: credential.id },
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.${credentials.server_location}/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.${credentials.server_location}/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(),
})) || []
);
}
private async getUnavailability(
range: { start: string; end: string },
calendarId: string
): Promise<Array<{ start: string; end: string }>> {
const query = stringify({
range: JSON.stringify(range),
});
this.log.debug("getUnavailability query", query);
try {
// List all events within the range
const response = await this.fetcher(`/calendars/${calendarId}/events?${query}`);
const data = await this.handleData(response, this.log);
// Check for no data scenario
if (!data.events || data.events.length === 0) return [];
return (
data.events
.filter((event: any) => event.isprivate === false)
.map((event: any) => {
const start = dayjs(event.dateandtime.start, "YYYYMMDD[T]HHmmssZ").utc().toISOString();
const end = dayjs(event.dateandtime.end, "YYYYMMDD[T]HHmmssZ").utc().toISOString();
return { start, end };
}) || []
);
} catch (error) {
this.log.error(error);
return [];
}
}
async getAvailability(
dateFrom: string,
dateTo: string,
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
);
const unavailabilityData = await Promise.all(
queryIds.map((calendarId) =>
this.getUnavailability(
{
start: originalStartDate.format("YYYYMMDD[T]HHmmss[Z]"),
end: originalEndDate.format("YYYYMMDD[T]HHmmss[Z]"),
},
calendarId
)
)
);
const unavailability = unavailabilityData.flat();
return busyData.concat(unavailability);
} else {
// Zoho only supports 31 days of freebusy data
const busyData = [];
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
))
);
const unavailabilityData = await Promise.all(
queryIds.map((calendarId) =>
this.getUnavailability(
{
start: startDate.format("YYYYMMDD[T]HHmmss[Z]"),
end: endDate.format("YYYYMMDD[T]HHmmss[Z]"),
},
calendarId
)
)
);
const unavailability = unavailabilityData.flat();
busyData.push(...unavailability);
startDate = endDate.add(1, "minutes");
endDate = startDate.add(30, "days");
}
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,43 @@
export type ZohoAuthCredentials = {
access_token: string;
refresh_token: string;
expires_in: number;
server_location: string;
};
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),
});