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,6 @@
---
items:
- pipedrive-banner.jpeg
---
{DESCRIPTION}

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

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

View 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 })
);
}

View File

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

View File

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

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

View File

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

View 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([]);
}
}

View File

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

View 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."
}

View 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

View 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;