Omit salesforce app in this PR

This commit is contained in:
zomars 2022-11-22 13:32:00 -07:00
parent 1582efd929
commit 22c09fe535
19 changed files with 0 additions and 433 deletions

View File

@ -30,7 +30,6 @@ import { metadata as qr_code_meta } from "./qr_code/_metadata";
import { metadata as rainbow_meta } from "./rainbow/_metadata";
import { metadata as raycast_meta } from "./raycast/_metadata";
import { metadata as riverside_meta } from "./riverside/_metadata";
import { metadata as salesforce_meta } from "./salesforce/_metadata";
import { metadata as sendgrid_meta } from "./sendgrid/_metadata";
import { metadata as signal_meta } from "./signal/_metadata";
import { metadata as sirius_video_meta } from "./sirius_video/_metadata";
@ -74,7 +73,6 @@ export const appStoreMetadata = {
rainbow: rainbow_meta,
raycast: raycast_meta,
riverside: riverside_meta,
salesforce: salesforce_meta,
sendgrid: sendgrid_meta,
signal: signal_meta,
sirius_video: sirius_video_meta,

View File

@ -29,7 +29,6 @@ export const apiHandlers = {
rainbow: import("./rainbow/api"),
raycast: import("./raycast/api"),
riverside: import("./riverside/api"),
salesforce: import("./salesforce/api"),
sendgrid: import("./sendgrid/api"),
signal: import("./signal/api"),
sirius_video: import("./sirius_video/api"),

View File

@ -15,7 +15,6 @@ import * as jitsivideo from "./jitsivideo";
import * as larkcalendar from "./larkcalendar";
import * as office365calendar from "./office365calendar";
import * as office365video from "./office365video";
import * as salesforce from "./salesforce";
import * as sendgrid from "./sendgrid";
import * as stripepayment from "./stripepayment";
import * as tandemvideo from "./tandemvideo";
@ -38,7 +37,6 @@ const appStore = {
larkcalendar,
office365calendar,
office365video,
salesforce,
sendgrid,
stripepayment,
tandemvideo,

View File

@ -1,12 +0,0 @@
---
description: Salesforce (Sales Cloud) is a cloud-based application designed to help your salespeople sell smarter and faster by centralizing customer information, logging their interactions with your company, and automating many of the tasks salespeople do every day.
items:
- /api/app-store/salesforce/1.png
---
{description}
<br/>
<p class="mb-0">Features:</p>
<ul class="mt-0">
<li>Creates event attendees as contacts in Salesforce (Sales Cloud)</li>
</ul>

View File

@ -1,10 +0,0 @@
import type { AppMeta } from "@calcom/types/App";
import config from "./config.json";
export const metadata = {
category: "other",
...config,
} as AppMeta;
export default metadata;

View File

@ -1,24 +0,0 @@
import jsforce from "jsforce";
import type { NextApiRequest, NextApiResponse } from "next";
import { WEBAPP_URL } from "@calcom/lib/constants";
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
let consumer_key = "";
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("salesforce");
if (typeof appKeys.consumer_key === "string") consumer_key = appKeys.consumer_key;
if (!consumer_key) return res.status(400).json({ message: "Salesforce client id missing." });
const salesforceClient = new jsforce.Connection({
clientId: consumer_key,
redirectUri: `${WEBAPP_URL}/api/integrations/salesforce/callback`,
});
const url = salesforceClient.oauth2.getAuthorizationUrl({ scope: "refresh_token full" });
res.status(200).json({ url });
}

View File

@ -1,55 +0,0 @@
import jsforce from "jsforce";
import type { NextApiRequest, NextApiResponse } from "next";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
import prisma from "@calcom/prisma";
import { decodeOAuthState } from "../../_utils/decodeOAuthState";
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
let consumer_key = "";
let consumer_secret = "";
const instance_url = "";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { code } = req.query;
if (code === undefined && 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("salesforce");
if (typeof appKeys.consumer_key === "string") consumer_key = appKeys.consumer_key;
if (typeof appKeys.consumer_secret === "string") consumer_secret = appKeys.consumer_secret;
if (!consumer_key) return res.status(400).json({ message: "Salesforce consumer key missing." });
if (!consumer_secret) return res.status(400).json({ message: "Salesforce consumer secret missing." });
const conn = new jsforce.Connection({
clientId: consumer_key,
clientSecret: consumer_secret,
redirectUri: WEBAPP_URL + "/api/integrations/salesforce/callback",
});
const salesforceTokenInfo = await conn.oauth2.requestToken(code as string);
await prisma.credential.create({
data: {
type: "salesforce_other_calendar",
key: salesforceTokenInfo as any,
userId: req.session.user.id,
appId: "salesforce",
},
});
const state = decodeOAuthState(req);
res.redirect(
getSafeRedirectUrl(state?.returnTo) ?? getInstalledAppPath({ variant: "other", slug: "salesforce" })
);
}

View File

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

View File

@ -1,16 +0,0 @@
{
"/*": "Don't modify slug - If required, do it using cli edit command",
"name": "Salesforce",
"slug": "salesforce",
"type": "salesforce_other_calendar",
"imageSrc": "/api/app-store/salesforce/icon.png",
"logo": "/api/app-store/salesforce/icon.png",
"url": "https://cal.com/apps/salesforce",
"variant": "other_calendar",
"categories": ["other"],
"publisher": "Cal.com",
"email": "help@cal.com",
"description": "Salesforce (Sales Cloud) is a cloud-based application designed to help your salespeople sell smarter and faster by centralizing customer information, logging their interactions with your company, and automating many of the tasks salespeople do every day.",
"extendsFeature": "User",
"__createdUsingCli": true
}

View File

@ -1,3 +0,0 @@
export * as api from "./api";
export * as lib from "./lib";
export { metadata } from "./_metadata";

View File

@ -1,275 +0,0 @@
import jsforce, { TokenResponse } from "jsforce";
import { RRule } from "rrule";
import { getLocation } from "@calcom/lib/CalEventParser";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { HttpError } from "@calcom/lib/http-error";
import logger from "@calcom/lib/logger";
import prisma from "@calcom/prisma";
import type {
Calendar,
CalendarEvent,
IntegrationCalendar,
NewCalendarEventType,
Person,
} from "@calcom/types/Calendar";
import { CredentialPayload } from "@calcom/types/Credential";
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
type ExtendedTokenResponse = TokenResponse & {
instance_url: string;
};
type ContactSearchResult = {
attributes: {
type: string;
url: string;
};
Id: string;
Email: string;
};
const sfApiErrors = {
INVALID_EVENTWHOIDS: "INVALID_FIELD: No such column 'EventWhoIds' on sobject of type Event",
};
export default class SalesforceCalendarService implements Calendar {
private integrationName = "";
private conn: Promise<jsforce.Connection>;
private log: typeof logger;
private calWarnings: string[] = [];
constructor(credential: CredentialPayload) {
this.integrationName = "salesforce_other_calendar";
this.conn = this.getClient(credential).then((c) => c);
this.log = logger.getChildLogger({ prefix: [`[[lib] ${this.integrationName}`] });
}
private getClient = async (credential: CredentialPayload) => {
let consumer_key = "";
let consumer_secret = "";
const appKeys = await getAppKeysFromSlug("salesforce");
if (typeof appKeys.consumer_key === "string") consumer_key = appKeys.consumer_key;
if (typeof appKeys.consumer_secret === "string") consumer_secret = appKeys.consumer_secret;
if (!consumer_key)
throw new HttpError({ statusCode: 400, message: "Salesforce consumer key is missing." });
if (!consumer_secret)
throw new HttpError({ statusCode: 400, message: "Salesforce consumer secret missing." });
const credentialKey = credential.key as unknown as ExtendedTokenResponse;
return new jsforce.Connection({
clientId: consumer_key,
clientSecret: consumer_secret,
redirectUri: WEBAPP_URL + "/api/integrations/salesforce/callback",
instanceUrl: credentialKey.instance_url,
accessToken: credentialKey.access_token,
refreshToken: credentialKey.refresh_token,
});
};
private salesforceContactCreate = async (attendees: Person[]) => {
const conn = await this.conn;
const createdContacts = await Promise.all(
attendees.map(async (attendee) => {
const [FirstName, LastName] = attendee.name ? attendee.name.split(" ") : [attendee.email, ""];
return await conn
.sobject("Contact")
.create({
FirstName,
LastName,
Email: attendee.email,
})
.then((result) => {
if (result.success) {
return { Id: result.id, Email: attendee.email };
}
});
})
);
return createdContacts.filter(
(contact): contact is Omit<ContactSearchResult, "attributes"> => contact !== undefined
);
};
private salesforceContactSearch = async (event: CalendarEvent) => {
const conn = await this.conn;
const search: ContactSearchResult[] = await conn.sobject("Contact").find(
event.attendees.map((att) => ({ Email: att.email })),
["Id", "Email"]
);
return search;
};
private getSalesforceEventBody = (event: CalendarEvent): string => {
return `${event.organizer.language.translate("invitee_timezone")}: ${
event.attendees[0].timeZone
} \r\n\r\n ${event.organizer.language.translate("share_additional_notes")}\r\n${
event.additionalNotes || "-"
}`;
};
private salesforceCreateEventApiCall = async (
event: CalendarEvent,
options: { [key: string]: unknown }
) => {
const conn = await this.conn;
return await conn.sobject("Event").create({
StartDateTime: new Date(event.startTime).toISOString(),
EndDateTime: new Date(event.endTime).toISOString(),
Subject: event.title,
Description: this.getSalesforceEventBody(event),
Location: getLocation(event),
...options,
...(event.recurringEvent && {
IsRecurrence2: true,
Recurrence2PatternText: new RRule(event.recurringEvent).toString(),
}),
});
};
private salesforceCreateEvent = async (
event: CalendarEvent,
contacts: Omit<ContactSearchResult, "attributes">[]
) => {
const createdEvent = await this.salesforceCreateEventApiCall(event, {
EventWhoIds: contacts.map((contact) => contact.Id),
}).catch(async (reason) => {
if (reason === sfApiErrors.INVALID_EVENTWHOIDS) {
this.calWarnings.push(
`Please enable option "Allow Users to Relate Multiple Contacts to Tasks and Events" under
"Setup > Feature Settings > Sales > Activity Settings" to be able to create events with
multiple contact attendees.`
);
// User has not configured "Allow Users to Relate Multiple Contacts to Tasks and Events"
// proceeding to create the event using just the first attendee as the primary WhoId
return await this.salesforceCreateEventApiCall(event, {
WhoId: contacts[0],
});
} else {
return Promise.reject();
}
});
return createdEvent;
};
private salesforceUpdateEvent = async (uid: string, event: CalendarEvent) => {
const conn = await this.conn;
return await conn.sobject("Event").update({
Id: uid,
StartDateTime: new Date(event.startTime).toISOString(),
EndDateTime: new Date(event.endTime).toISOString(),
Subject: event.title,
Description: this.getSalesforceEventBody(event),
Location: getLocation(event),
...(event.recurringEvent && {
IsRecurrence2: true,
Recurrence2PatternText: new RRule(event.recurringEvent).toString(),
}),
});
};
private salesforceDeleteEvent = async (uid: string) => {
const conn = await this.conn;
return await conn.sobject("Event").delete(uid);
};
async handleEventCreation(event: CalendarEvent, contacts: Omit<ContactSearchResult, "attributes">[]) {
const sfEvent = await this.salesforceCreateEvent(event, contacts);
if (sfEvent.success) {
this.log.debug("event:creation:ok", { sfEvent });
return Promise.resolve({
uid: sfEvent.id,
id: sfEvent.id,
type: "salesforce_other_calendar",
password: "",
url: "",
additionalInfo: { contacts, sfEvent, calWarnings: this.calWarnings },
});
}
this.log.debug("event:creation:notOk", { event, sfEvent, contacts });
return Promise.reject({
calError: "Something went wrong when creating an event in Salesforce",
});
}
async createEvent(event: CalendarEvent): Promise<NewCalendarEventType> {
debugger;
const contacts = await this.salesforceContactSearch(event);
if (contacts.length) {
if (contacts.length == event.attendees.length) {
// All attendees do exist in Salesforce
this.log.debug("contact:search:all", { event, contacts });
return await this.handleEventCreation(event, contacts);
} else {
// Some attendees don't exist in Salesforce
// Get the existing contacts' email to filter out
this.log.debug("contact:search:notAll", { event, contacts });
const existingContacts = contacts.map((contact) => contact.Email);
this.log.debug("contact:filter:existing", { existingContacts });
// Get non existing contacts filtering out existing from attendees
const nonExistingContacts = event.attendees.filter(
(attendee) => !existingContacts.includes(attendee.email)
);
this.log.debug("contact:filter:nonExisting", { nonExistingContacts });
// Only create contacts in Salesforce that were not present in the previous contact search
const createContacts = await this.salesforceContactCreate(nonExistingContacts);
this.log.debug("contact:created", { createContacts });
// Continue with event creation and association only when all contacts are present in Salesforce
if (createContacts.length) {
this.log.debug("contact:creation:ok");
return await this.handleEventCreation(event, createContacts.concat(contacts));
}
return Promise.reject({
calError: "Something went wrong when creating non-existing attendees in Salesforce",
});
}
} else {
this.log.debug("contact:search:none", { event, contacts });
const createContacts = await this.salesforceContactCreate(event.attendees);
this.log.debug("contact:created", { createContacts });
if (createContacts.length) {
this.log.debug("contact:creation:ok");
return await this.handleEventCreation(event, createContacts);
}
}
return Promise.reject({
calError: "Something went wrong when searching/creating the attendees in Salesforce",
});
}
async updateEvent(uid: string, event: CalendarEvent): Promise<NewCalendarEventType> {
const updatedEvent = await this.salesforceUpdateEvent(uid, event);
if (updatedEvent.success) {
return Promise.resolve({
uid: updatedEvent.id,
id: updatedEvent.id,
type: "salesforce_other_calendar",
password: "",
url: "",
additionalInfo: { calWarnings: this.calWarnings },
});
} else {
return Promise.reject({ calError: "Something went wrong when updating the event in Salesforce" });
}
}
async deleteEvent(uid: string) {
const deletedEvent = await this.salesforceDeleteEvent(uid);
if (deletedEvent.success) {
Promise.resolve();
} else {
Promise.reject({ calError: "Something went wrong when deleting the event in Salesforce" });
}
}
async getAvailability(dateFrom: string, dateTo: string, selectedCalendars: IntegrationCalendar[]) {
return Promise.resolve([]);
}
async listCalendars(event?: CalendarEvent) {
return Promise.resolve([]);
}
}

View File

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

View File

@ -1,16 +0,0 @@
{
"private": true,
"name": "@calcom/salesforce",
"version": "0.0.0",
"main": "./index.ts",
"description": "Salesforce (Sales Cloud) is a cloud-based application designed to help your salespeople sell smarter and faster by centralizing customer information, logging their interactions with your company, and automating many of the tasks salespeople do every day.",
"dependencies": {
"@calcom/lib": "*",
"@calcom/prisma": "*",
"jsforce": "^1.11.0"
},
"devDependencies": {
"@calcom/types": "*",
"@types/jsforce": "^1.11.0"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

View File

@ -20,12 +20,6 @@ const templates: Template[] = [
text: "Create Google Sheets rows for scheduled events",
link: "https://workflows.zapier.com/?attempt_id=a086d136-b084-45bc-a6aa-c9f460b05b7d&template=1082047",
},
{
icon: "salesforce.svg",
app: "Salesforce",
text: "Create Salesforce leads from new bookings",
link: "https://zapier.com/editor/170116721?attempt_id=9f939229-ee89-4391-b9c7-f4645c60dbd4",
},
{
icon: "todoist.svg",
app: "Todoist",

View File

@ -238,12 +238,6 @@ export default async function main() {
client_secret: process.env.HUBSPOT_CLIENT_SECRET,
});
}
if (process.env.SALESFORCE_CONSUMER_KEY && process.env.SALESFORCE_CONSUMER_SECRET) {
await createApp("salesforce", "salesforce", ["other"], "salesforce_other_calendar", {
consumer_key: process.env.SALESFORCE_CONSUMER_KEY,
consumer_secret: process.env.SALESFORCE_CONSUMER_SECRET,
});
}
await createApp("wipe-my-cal", "wipemycalother", ["other"], "wipemycal_other");
if (process.env.GIPHY_API_KEY) {
await createApp("giphy", "giphy", ["other"], "giphy_other", {

View File

@ -230,8 +230,6 @@
"$PLAYWRIGHT_TEST_BASE_URL",
"$QUICK",
"$RAILWAY_STATIC_URL",
"$SALESFORCE_CONSUMER_KEY",
"$SALESFORCE_CONSUMER_SECRET",
"$SAML_ADMINS",
"$SAML_DATABASE_URL",
"$SEND_FEEDBACK_EMAIL",