first commit
This commit is contained in:
6
calcom/packages/app-store/pipedrive-crm/DESCRIPTION.md
Normal file
6
calcom/packages/app-store/pipedrive-crm/DESCRIPTION.md
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
items:
|
||||
- pipedrive-banner.jpeg
|
||||
---
|
||||
|
||||
{DESCRIPTION}
|
19
calcom/packages/app-store/pipedrive-crm/README.md
Normal file
19
calcom/packages/app-store/pipedrive-crm/README.md
Normal file
@ -0,0 +1,19 @@
|
||||
## Pipedrive Integration via Revert
|
||||
|
||||
#### Obtaining Pipedrive Client ID and Secret
|
||||
|
||||
* Open [Pipedrive Developers Corner](https://developers.pipedrive.com/) and sign in to your account, or create a new one
|
||||
* Go to Settings > (company name) Developer Hub
|
||||
* Create a Pipedrive app, using the steps mentioned [here](https://pipedrive.readme.io/docs/marketplace-creating-a-proper-app#create-an-app-in-5-simple-steps)
|
||||
* You can skip this step and use the default revert Pipedrive app
|
||||
* Set `https://app.revert.dev/oauth-callback/pipedrive` as a callback url for your app
|
||||
* **Get your client\_id and client\_secret**:
|
||||
* Go to the "OAuth & access scopes" tab of your app
|
||||
* Copy your client\_id and client\_secret
|
||||
|
||||
#### Obtaining Revert API keys
|
||||
|
||||
* Create an account on Revert if you don't already have one. (https://app.revert.dev/sign-up)
|
||||
* Login to your revert dashboard (https://app.revert.dev/sign-in) and click on `Customize your apps` - `Pipedrive`
|
||||
* Enter the `client_id` and `client_secret` you copied in the previous step
|
||||
* Enter the `client_id` and `client_secret` previously copied to `Settings > Admin > Apps > CRM > Pipedrive` by clicking the `Edit` button on the app settings.
|
37
calcom/packages/app-store/pipedrive-crm/api/add.ts
Normal file
37
calcom/packages/app-store/pipedrive-crm/api/add.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { createDefaultInstallation } from "@calcom/app-store/_utils/installation";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
|
||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||
import appConfig from "../config.json";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method !== "GET") return res.status(405).json({ message: "Method not allowed" });
|
||||
const appKeys = await getAppKeysFromSlug(appConfig.slug);
|
||||
let client_id = "";
|
||||
if (typeof appKeys.client_id === "string") client_id = appKeys.client_id;
|
||||
if (!client_id) return res.status(400).json({ message: "pipedrive client id missing." });
|
||||
// Check that user is authenticated
|
||||
req.session = await getServerSession({ req, res });
|
||||
const { teamId } = req.query;
|
||||
const user = req.session?.user;
|
||||
if (!user) {
|
||||
throw new HttpError({ statusCode: 401, message: "You must be logged in to do this" });
|
||||
}
|
||||
const userId = user.id;
|
||||
await createDefaultInstallation({
|
||||
appType: `${appConfig.slug}_other_calendar`,
|
||||
user,
|
||||
slug: appConfig.slug,
|
||||
key: {},
|
||||
teamId: Number(teamId),
|
||||
});
|
||||
|
||||
const tenantId = teamId ? teamId : userId;
|
||||
res.status(200).json({
|
||||
url: `https://oauth.pipedrive.com/oauth/authorize?client_id=${appKeys.client_id}&redirect_uri=https://app.revert.dev/oauth-callback/pipedrive&state={%22tenantId%22:%22${tenantId}%22,%22revertPublicToken%22:%22${process.env.REVERT_PUBLIC_TOKEN}%22}`,
|
||||
newTab: true,
|
||||
});
|
||||
}
|
19
calcom/packages/app-store/pipedrive-crm/api/callback.ts
Normal file
19
calcom/packages/app-store/pipedrive-crm/api/callback.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
||||
|
||||
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
|
||||
import { decodeOAuthState } from "../../_utils/oauth/decodeOAuthState";
|
||||
import appConfig from "../config.json";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (!req.session?.user?.id) {
|
||||
return res.status(401).json({ message: "You must be logged in to do this" });
|
||||
}
|
||||
|
||||
const state = decodeOAuthState(req);
|
||||
res.redirect(
|
||||
getSafeRedirectUrl(state?.returnTo) ??
|
||||
getInstalledAppPath({ variant: appConfig.variant, slug: appConfig.slug })
|
||||
);
|
||||
}
|
2
calcom/packages/app-store/pipedrive-crm/api/index.ts
Normal file
2
calcom/packages/app-store/pipedrive-crm/api/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { default as add } from "./add";
|
||||
export { default as callback } from "./callback";
|
@ -0,0 +1,27 @@
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
import AppCard from "@calcom/app-store/_components/AppCard";
|
||||
import useIsAppEnabled from "@calcom/app-store/_utils/useIsAppEnabled";
|
||||
import type { EventTypeAppCardComponent } from "@calcom/app-store/types";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
|
||||
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) {
|
||||
const pathname = usePathname();
|
||||
|
||||
const { enabled, updateEnabled } = useIsAppEnabled(app);
|
||||
|
||||
return (
|
||||
<AppCard
|
||||
returnTo={`${WEBAPP_URL}${pathname}?tabName=apps`}
|
||||
app={app}
|
||||
teamId={eventType.team?.id || undefined}
|
||||
switchOnClick={(e) => {
|
||||
updateEnabled(e);
|
||||
}}
|
||||
switchChecked={enabled}
|
||||
hideAppCardOptions
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventTypeAppCard;
|
18
calcom/packages/app-store/pipedrive-crm/config.json
Normal file
18
calcom/packages/app-store/pipedrive-crm/config.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"/*": "Don't modify slug - If required, do it using cli edit command",
|
||||
"name": "Pipedrive CRM",
|
||||
"slug": "pipedrive-crm",
|
||||
"type": "pipedrive-crm_crm",
|
||||
"logo": "icon.svg",
|
||||
"url": "https://revert.dev",
|
||||
"variant": "crm",
|
||||
"categories": ["crm"],
|
||||
"extendsFeature": "EventType",
|
||||
"publisher": "Revert.dev ",
|
||||
"email": "jatin@revert.dev",
|
||||
"description": "Founded in 2010, Pipedrive is an easy and effective sales CRM that drives small business growth.\r\rToday, Pipedrive is used by revenue teams at more than 100,000 companies worldwide. Pipedrive is headquartered in New York and has offices across Europe and the US.\r\rThe company is backed by majority holder Vista Equity Partners, Bessemer Venture Partners, Insight Partners, Atomico, and DTCP.\r\rLearn more at www.pipedrive.com.",
|
||||
"isTemplate": false,
|
||||
"__createdUsingCli": true,
|
||||
"__template": "basic",
|
||||
"dirName": "pipedrive-crm"
|
||||
}
|
2
calcom/packages/app-store/pipedrive-crm/index.ts
Normal file
2
calcom/packages/app-store/pipedrive-crm/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * as api from "./api";
|
||||
export * as lib from "./lib";
|
233
calcom/packages/app-store/pipedrive-crm/lib/CrmService.ts
Normal file
233
calcom/packages/app-store/pipedrive-crm/lib/CrmService.ts
Normal file
@ -0,0 +1,233 @@
|
||||
import { getLocation } from "@calcom/lib/CalEventParser";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import type {
|
||||
CalendarEvent,
|
||||
EventBusyDate,
|
||||
IntegrationCalendar,
|
||||
NewCalendarEventType,
|
||||
} from "@calcom/types/Calendar";
|
||||
import type { CredentialPayload } from "@calcom/types/Credential";
|
||||
import type { ContactCreateInput, CRM, Contact } from "@calcom/types/CrmService";
|
||||
|
||||
import appConfig from "../config.json";
|
||||
|
||||
type ContactSearchResult = {
|
||||
status: string;
|
||||
results: Array<{
|
||||
id: string;
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
name: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
type ContactCreateResult = {
|
||||
status: string;
|
||||
result: {
|
||||
id: string;
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
|
||||
export default class PipedriveCrmService implements CRM {
|
||||
private log: typeof logger;
|
||||
private tenantId: string;
|
||||
private revertApiKey: string;
|
||||
private revertApiUrl: string;
|
||||
constructor(credential: CredentialPayload) {
|
||||
this.revertApiKey = process.env.REVERT_API_KEY || "";
|
||||
this.revertApiUrl = process.env.REVERT_API_URL || "https://api.revert.dev/";
|
||||
this.tenantId = String(credential.teamId ? credential.teamId : credential.userId); // Question: Is this a reasonable assumption to be made? Get confirmation on the exact field to be used here.
|
||||
this.log = logger.getSubLogger({ prefix: [`[[lib] ${appConfig.slug}`] });
|
||||
}
|
||||
|
||||
async createContacts(contactsToCreate: ContactCreateInput[]): Promise<Contact[]> {
|
||||
const result = contactsToCreate.map(async (attendee) => {
|
||||
const headers = new Headers();
|
||||
headers.append("x-revert-api-token", this.revertApiKey);
|
||||
headers.append("x-revert-t-id", this.tenantId);
|
||||
headers.append("Content-Type", "application/json");
|
||||
|
||||
const [firstname, lastname] = !!attendee.name ? attendee.name.split(" ") : [attendee.email, "-"];
|
||||
const bodyRaw = JSON.stringify({
|
||||
firstName: firstname,
|
||||
lastName: lastname || "-",
|
||||
email: attendee.email,
|
||||
});
|
||||
|
||||
const requestOptions = {
|
||||
method: "POST",
|
||||
headers: headers,
|
||||
body: bodyRaw,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.revertApiUrl}crm/contacts`, requestOptions);
|
||||
const result = (await response.json()) as ContactCreateResult;
|
||||
return result;
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
const results = await Promise.all(result);
|
||||
return results.map((result) => result.result);
|
||||
}
|
||||
|
||||
async getContacts(email: string | string[]): Promise<Contact[]> {
|
||||
const emailArray = Array.isArray(email) ? email : [email];
|
||||
|
||||
const result = emailArray.map(async (attendeeEmail) => {
|
||||
const headers = new Headers();
|
||||
headers.append("x-revert-api-token", this.revertApiKey);
|
||||
headers.append("x-revert-t-id", this.tenantId);
|
||||
headers.append("Content-Type", "application/json");
|
||||
|
||||
const bodyRaw = JSON.stringify({ searchCriteria: attendeeEmail });
|
||||
|
||||
const requestOptions = {
|
||||
method: "POST",
|
||||
headers: headers,
|
||||
body: bodyRaw,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.revertApiUrl}crm/contacts/search`, requestOptions);
|
||||
const result = (await response.json()) as ContactSearchResult;
|
||||
return result;
|
||||
} catch (error) {
|
||||
return { status: "error", results: [] };
|
||||
}
|
||||
});
|
||||
const results = await Promise.all(result);
|
||||
return results[0].results;
|
||||
}
|
||||
|
||||
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 createPipedriveEvent = async (event: CalendarEvent, contacts: Contact[]) => {
|
||||
const eventPayload = {
|
||||
subject: event.title,
|
||||
startDateTime: event.startTime,
|
||||
endDateTime: event.endTime,
|
||||
description: this.getMeetingBody(event),
|
||||
location: getLocation(event),
|
||||
associations: {
|
||||
contactId: String(contacts[0].id),
|
||||
},
|
||||
};
|
||||
const headers = new Headers();
|
||||
headers.append("x-revert-api-token", this.revertApiKey);
|
||||
headers.append("x-revert-t-id", this.tenantId);
|
||||
headers.append("Content-Type", "application/json");
|
||||
|
||||
const eventBody = JSON.stringify(eventPayload);
|
||||
const requestOptions = {
|
||||
method: "POST",
|
||||
headers: headers,
|
||||
body: eventBody,
|
||||
};
|
||||
|
||||
return await fetch(`${this.revertApiUrl}crm/events`, requestOptions);
|
||||
};
|
||||
|
||||
private updateMeeting = async (uid: string, event: CalendarEvent) => {
|
||||
const eventPayload = {
|
||||
subject: event.title,
|
||||
startDateTime: event.startTime,
|
||||
endDateTime: event.endTime,
|
||||
description: this.getMeetingBody(event),
|
||||
location: getLocation(event),
|
||||
};
|
||||
const headers = new Headers();
|
||||
headers.append("x-revert-api-token", this.revertApiKey);
|
||||
headers.append("x-revert-t-id", this.tenantId);
|
||||
headers.append("Content-Type", "application/json");
|
||||
|
||||
const eventBody = JSON.stringify(eventPayload);
|
||||
const requestOptions = {
|
||||
method: "PATCH",
|
||||
headers: headers,
|
||||
body: eventBody,
|
||||
};
|
||||
|
||||
return await fetch(`${this.revertApiUrl}crm/events/${uid}`, requestOptions);
|
||||
};
|
||||
|
||||
private deleteMeeting = async (uid: string) => {
|
||||
const headers = new Headers();
|
||||
headers.append("x-revert-api-token", this.revertApiKey);
|
||||
headers.append("x-revert-t-id", this.tenantId);
|
||||
|
||||
const requestOptions = {
|
||||
method: "DELETE",
|
||||
headers: headers,
|
||||
};
|
||||
|
||||
return await fetch(`${this.revertApiUrl}crm/events/${uid}`, requestOptions);
|
||||
};
|
||||
|
||||
async handleEventCreation(event: CalendarEvent, contacts: Contact[]) {
|
||||
const meetingEvent = await (await this.createPipedriveEvent(event, contacts)).json();
|
||||
if (meetingEvent && meetingEvent.status === "ok") {
|
||||
this.log.debug("event:creation:ok", { meetingEvent });
|
||||
return Promise.resolve({
|
||||
uid: meetingEvent.result.id,
|
||||
id: meetingEvent.result.id,
|
||||
type: appConfig.slug,
|
||||
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 PipedriveCRM");
|
||||
}
|
||||
|
||||
async createEvent(event: CalendarEvent, contacts: Contact[]): Promise<NewCalendarEventType> {
|
||||
return await this.handleEventCreation(event, contacts);
|
||||
}
|
||||
|
||||
async updateEvent(uid: string, event: CalendarEvent): Promise<NewCalendarEventType> {
|
||||
const meetingEvent = await (await this.updateMeeting(uid, event)).json();
|
||||
if (meetingEvent && meetingEvent.status === "ok") {
|
||||
this.log.debug("event:updation:ok", { meetingEvent });
|
||||
return Promise.resolve({
|
||||
uid: meetingEvent.result.id,
|
||||
id: meetingEvent.result.id,
|
||||
type: appConfig.slug,
|
||||
password: "",
|
||||
url: "",
|
||||
additionalInfo: { meetingEvent },
|
||||
});
|
||||
}
|
||||
this.log.debug("meeting:updation:notOk", { meetingEvent, event });
|
||||
return Promise.reject("Something went wrong when updating a meeting in PipedriveCRM");
|
||||
}
|
||||
|
||||
async deleteEvent(uid: string): Promise<void> {
|
||||
await this.deleteMeeting(uid);
|
||||
}
|
||||
|
||||
async getAvailability(
|
||||
_dateFrom: string,
|
||||
_dateTo: string,
|
||||
_selectedCalendars: IntegrationCalendar[]
|
||||
): Promise<EventBusyDate[]> {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
async listCalendars(_event?: CalendarEvent): Promise<IntegrationCalendar[]> {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
}
|
1
calcom/packages/app-store/pipedrive-crm/lib/index.ts
Normal file
1
calcom/packages/app-store/pipedrive-crm/lib/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default as CrmService } from "./CrmService";
|
14
calcom/packages/app-store/pipedrive-crm/package.json
Normal file
14
calcom/packages/app-store/pipedrive-crm/package.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"name": "@calcom/pipedrive-crm",
|
||||
"version": "0.0.0",
|
||||
"main": "./index.ts",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
},
|
||||
"description": "Founded in 2010, Pipedrive is an easy and effective sales CRM that drives small business growth.\r\rToday, Pipedrive is used by revenue teams at more than 100,000 companies worldwide. Pipedrive is headquartered in New York and has offices across Europe and the US.\r\rThe company is backed by majority holder Vista Equity Partners, Bessemer Venture Partners, Insight Partners, Atomico, and DTCP.\r\rLearn more at www.pipedrive.com."
|
||||
}
|
23
calcom/packages/app-store/pipedrive-crm/static/icon.svg
Normal file
23
calcom/packages/app-store/pipedrive-crm/static/icon.svg
Normal file
@ -0,0 +1,23 @@
|
||||
<svg width="304px" height="304px" viewBox="0 0 304 304" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 52.4 (67378) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>
|
||||
Pipedrive_letter_logo_light@1,5x
|
||||
</title>
|
||||
<desc>
|
||||
Created with Sketch.
|
||||
</desc>
|
||||
<defs>
|
||||
<path d="M59.6807,81.1772 C59.6807,101.5343 70.0078,123.4949 92.7336,123.4949 C109.5872,123.4949 126.6277,110.3374 126.6277,80.8785 C126.6277,55.0508 113.232,37.7119 93.2944,37.7119 C77.0483,37.7119 59.6807,49.1244 59.6807,81.1772 Z M101.3006,0 C142.0482,0 169.4469,32.2728 169.4469,80.3126 C169.4469,127.5978 140.584,160.60942 99.3224,160.60942 C79.6495,160.60942 67.0483,152.1836 60.4595,146.0843 C60.5063,147.5305 60.5374,149.1497 60.5374,150.8788 L60.5374,215 L18.32565,215 L18.32565,44.157 C18.32565,41.6732 17.53126,40.8873 15.07021,40.8873 L0.5531,40.8873 L0.5531,3.4741 L35.9736,3.4741 C52.282,3.4741 56.4564,11.7741 57.2508,18.1721 C63.8708,10.7524 77.5935,0 101.3006,0 Z" id="path-1">
|
||||
</path>
|
||||
</defs>
|
||||
<g id="Pipedrive_letter_logo_light" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Pipedrive_monogram_logo_light" transform="translate(67.000000, 44.000000)">
|
||||
<mask id="mask-2" fill="white">
|
||||
<use href="#path-1">
|
||||
</use>
|
||||
</mask>
|
||||
<use id="Clip-5" fill="#26292C" xlink:href="#path-1">
|
||||
</use>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
After Width: | Height: | Size: 64 KiB |
10
calcom/packages/app-store/pipedrive-crm/zod.ts
Normal file
10
calcom/packages/app-store/pipedrive-crm/zod.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { eventTypeAppCardZod } from "../eventTypeAppCardZod";
|
||||
|
||||
export const appKeysSchema = z.object({
|
||||
client_id: z.string().min(1),
|
||||
client_secret: z.string().min(1),
|
||||
});
|
||||
|
||||
export const appDataSchema = eventTypeAppCardZod;
|
Reference in New Issue
Block a user