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,292 @@
import axios from "axios";
import qs from "qs";
import { getLocation } from "@calcom/lib/CalEventParser";
import { HttpError } from "@calcom/lib/http-error";
import logger from "@calcom/lib/logger";
import prisma from "@calcom/prisma";
import type { CalendarEvent, NewCalendarEventType } from "@calcom/types/Calendar";
import type { CredentialPayload } from "@calcom/types/Credential";
import type { CRM, Contact, ContactCreateInput } from "@calcom/types/CrmService";
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
import refreshOAuthTokens from "../../_utils/oauth/refreshOAuthTokens";
export type ZohoToken = {
scope: string;
api_domain: string;
expires_in: number;
expiryDate: number;
token_type: string;
access_token: string;
accountServer: string;
refresh_token: string;
};
export type ZohoContact = {
id: string;
email: string;
};
/**
* Converts to the date Format as required by zoho: 2020-08-02T15:30:00+05:30
* https://www.zoho.com/crm/developer/docs/api/v2/events-response.html
*/
const toISO8601String = (date: Date) => {
const tzo = -date.getTimezoneOffset(),
dif = tzo >= 0 ? "+" : "-",
pad = function (num: number) {
return (num < 10 ? "0" : "") + num;
};
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(
date.getHours()
)}:${pad(date.getMinutes())}:${pad(date.getSeconds())}${dif}${pad(Math.floor(Math.abs(tzo) / 60))}:${pad(
Math.abs(tzo) % 60
)}`;
};
export default class ZohoCrmCrmService implements CRM {
private integrationName = "";
private auth: Promise<{ getToken: () => Promise<void> }>;
private log: typeof logger;
private client_id = "";
private client_secret = "";
private accessToken = "";
constructor(credential: CredentialPayload) {
this.integrationName = "zohocrm_crm";
this.auth = this.zohoCrmAuth(credential).then((r) => r);
this.log = logger.getSubLogger({ prefix: [`[[lib] ${this.integrationName}`] });
}
async createContacts(contactsToCreate: ContactCreateInput[]) {
const auth = await this.auth;
await auth.getToken();
const contacts = contactsToCreate.map((contactToCreate) => {
const [firstname, lastname] = !!contactToCreate.name
? contactToCreate.name.split(" ")
: [contactToCreate.email, "-"];
return {
First_Name: firstname,
Last_Name: lastname || "-",
Email: contactToCreate.email,
};
});
const response = await axios({
method: "post",
url: `https://www.zohoapis.com/crm/v3/Contacts`,
headers: {
"content-type": "application/json",
authorization: `Zoho-oauthtoken ${this.accessToken}`,
},
data: JSON.stringify({ data: contacts }),
});
const { data } = response;
return data.data.map((contact: ZohoContact) => {
return {
id: contact.id,
email: contact.email,
};
});
}
async getContacts(emails: string | string[]) {
const auth = await this.auth;
await auth.getToken();
const emailsArray = Array.isArray(emails) ? emails : [emails];
const searchCriteria = `(${emailsArray.map((email) => `(Email:equals:${encodeURI(email)})`).join("or")})`;
const response = await axios({
method: "get",
url: `https://www.zohoapis.com/crm/v3/Contacts/search?criteria=${searchCriteria}`,
headers: {
authorization: `Zoho-oauthtoken ${this.accessToken}`,
},
})
.then((data) => data.data)
.catch((e) => {
this.log.error(e, e.response?.data);
});
return response
? response.data.map((contact: ZohoContact) => {
return {
id: contact.id,
email: contact.email,
};
})
: [];
}
private getMeetingBody = (event: CalendarEvent): string => {
return `<b>${event.organizer.language.translate("invitee_timezone")}:</b> ${
event.attendees[0].timeZone
}<br><br><b>${event.organizer.language.translate("share_additional_notes")}</b><br>${
event.additionalNotes || "-"
}`;
};
private createZohoEvent = async (event: CalendarEvent, contacts: Contact[]) => {
const zohoEvent = {
Event_Title: event.title,
Start_DateTime: toISO8601String(new Date(event.startTime)),
End_DateTime: toISO8601String(new Date(event.endTime)),
Description: this.getMeetingBody(event),
Venue: getLocation(event),
Who_Id: contacts[0].id, // Link the first attendee as the primary Who_Id
};
return axios({
method: "post",
url: `https://www.zohoapis.com/crm/v3/Events`,
headers: {
"content-type": "application/json",
authorization: `Zoho-oauthtoken ${this.accessToken}`,
},
data: JSON.stringify({ data: [zohoEvent] }),
})
.then((data) => data.data)
.catch((e) => this.log.error(e, e.response?.data));
};
private updateMeeting = async (uid: string, event: CalendarEvent) => {
const zohoEvent = {
id: uid,
Event_Title: event.title,
Start_DateTime: toISO8601String(new Date(event.startTime)),
End_DateTime: toISO8601String(new Date(event.endTime)),
Description: this.getMeetingBody(event),
Venue: getLocation(event),
};
return axios({
method: "put",
url: `https://www.zohoapis.com/crm/v3/Events`,
headers: {
"content-type": "application/json",
authorization: `Zoho-oauthtoken ${this.accessToken}`,
},
data: JSON.stringify({ data: [zohoEvent] }),
})
.then((data) => data.data)
.catch((e) => this.log.error(e, e.response?.data));
};
private deleteMeeting = async (uid: string) => {
return axios({
method: "delete",
url: `https://www.zohoapis.com/crm/v3/Events?ids=${uid}`,
headers: {
"content-type": "application/json",
authorization: `Zoho-oauthtoken ${this.accessToken}`,
},
})
.then((data) => data.data)
.catch((e) => this.log.error(e, e.response?.data));
};
private zohoCrmAuth = async (credential: CredentialPayload) => {
const appKeys = await getAppKeysFromSlug("zohocrm");
if (typeof appKeys.client_id === "string") this.client_id = appKeys.client_id;
if (typeof appKeys.client_secret === "string") this.client_secret = appKeys.client_secret;
if (!this.client_id) throw new HttpError({ statusCode: 400, message: "Zoho CRM client_id missing." });
if (!this.client_secret)
throw new HttpError({ statusCode: 400, message: "Zoho CRM client_secret missing." });
const credentialKey = credential.key as unknown as ZohoToken;
const isTokenValid = (token: ZohoToken) => {
const isValid = token && token.access_token && token.expiryDate && token.expiryDate > Date.now();
if (isValid) {
this.accessToken = token.access_token;
}
return isValid;
};
const refreshAccessToken = async (credentialKey: ZohoToken) => {
try {
const url = `${credentialKey.accountServer}/oauth/v2/token`;
const formData = {
grant_type: "refresh_token",
client_id: this.client_id,
client_secret: this.client_secret,
refresh_token: credentialKey.refresh_token,
};
const zohoCrmTokenInfo = await refreshOAuthTokens(
async () =>
await axios({
method: "post",
url: url,
data: qs.stringify(formData),
headers: {
"Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
},
}),
"zohocrm",
credential.userId
);
if (!zohoCrmTokenInfo.data.error) {
// set expiry date as offset from current time.
zohoCrmTokenInfo.data.expiryDate = Math.round(Date.now() + 60 * 60);
await prisma.credential.update({
where: {
id: credential.id,
},
data: {
key: {
...(zohoCrmTokenInfo.data as ZohoToken),
refresh_token: credentialKey.refresh_token,
accountServer: credentialKey.accountServer,
},
},
});
this.accessToken = zohoCrmTokenInfo.data.access_token;
this.log.debug("Fetched token", this.accessToken);
} else {
this.log.error(zohoCrmTokenInfo.data);
}
} catch (e: unknown) {
this.log.error(e);
}
};
return {
getToken: () => (isTokenValid(credentialKey) ? Promise.resolve() : refreshAccessToken(credentialKey)),
};
};
async handleEventCreation(event: CalendarEvent, contacts: Contact[]) {
const meetingEvent = await this.createZohoEvent(event, contacts);
if (meetingEvent.data && meetingEvent.data.length && meetingEvent.data[0].status === "success") {
this.log.debug("event:creation:ok", { meetingEvent });
return Promise.resolve({
uid: meetingEvent.data[0].details.id,
id: meetingEvent.data[0].details.id,
type: this.integrationName,
password: "",
url: "",
additionalInfo: { contacts, meetingEvent },
});
}
this.log.debug("meeting:creation:notOk", { meetingEvent, event, contacts });
return Promise.reject("Something went wrong when creating a meeting in ZohoCRM");
}
async createEvent(event: CalendarEvent, contacts: Contact[]): Promise<NewCalendarEventType> {
const auth = await this.auth;
await auth.getToken();
return await this.handleEventCreation(event, contacts);
}
async updateEvent(uid: string, event: CalendarEvent): Promise<NewCalendarEventType> {
const auth = await this.auth;
await auth.getToken();
return await this.updateMeeting(uid, event);
}
async deleteEvent(uid: string): Promise<void> {
const auth = await this.auth;
await auth.getToken();
return await this.deleteMeeting(uid);
}
}