* Revert e5ba8616

* Revert "Reverted Sentry for debug purposes (#5425)"

This reverts commit 4466c4ad4b.

Co-authored-by: Omar López <zomars@me.com>
pull/5375/head^2
Alex van Andel 2022-11-11 09:47:11 +00:00 committed by GitHub
parent 0b2250caf9
commit acb6f292ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 959 additions and 1251 deletions

View File

@ -77,7 +77,7 @@ NEXT_PUBLIC_HELPSCOUT_KEY=
SEND_FEEDBACK_EMAIL=
# Sengrid
# Used for email reminders in workflows and internal sync services
# Used for email reminders in workflows
SENDGRID_API_KEY=
SENDGRID_EMAIL=
@ -134,8 +134,9 @@ NEXT_PUBLIC_TEAM_IMPERSONATION=false
# Close.com internal CRM
CLOSECOM_API_KEY=
# Sendgrid internal sync service
SENDGRID_SYNC_API_KEY=
# Sendgrid internal email sender
SENDGRID_API_KEY=
# Sentry
NEXT_PUBLIC_SENTRY_DSN=
SENTRY_IGNORE_API_RESOLUTION_ERROR=

View File

@ -50,10 +50,6 @@
"description": "Sendgrid api key. Used for email reminders in workflows",
"value": ""
},
"SENDGRID_SYNC_API_KEY": {
"description": "Sendgrid internal sync service",
"value": ""
},
"SENDGRID_EMAIL": {
"description": "Sendgrid email. Used for email reminders in workflows",
"value": ""

4
apps/web/.gitignore vendored
View File

@ -67,4 +67,6 @@ tsconfig.tsbuildinfo
public/embed
# Copied app-store images
public/app-store
public/app-store
# Sentry
.sentryclirc

View File

@ -1,5 +1,6 @@
require("dotenv").config({ path: "../../.env" });
const CopyWebpackPlugin = require("copy-webpack-plugin");
const { withSentryConfig } = require("@sentry/nextjs");
const withTM = require("next-transpile-modules")([
"@calcom/app-store",
@ -229,6 +230,18 @@ const nextConfig = {
return redirects;
},
sentry: {
hideSourceMaps: true,
},
};
module.exports = () => plugins.reduce((acc, next) => next(acc), nextConfig);
const sentryWebpackPluginOptions = {
silent: true, // Suppresses all logs
};
const moduleExports = () => plugins.reduce((acc, next) => next(acc), nextConfig);
// Sentry should be the last thing to export to catch everything right
module.exports = process.env.NEXT_PUBLIC_SENTRY_DSN
? withSentryConfig(moduleExports, sentryWebpackPluginOptions)
: moduleExports;

View File

@ -56,6 +56,7 @@
"@radix-ui/react-switch": "^1.0.0",
"@radix-ui/react-toggle-group": "^1.0.0",
"@radix-ui/react-tooltip": "^1.0.0",
"@sentry/nextjs": "^7.17.3",
"@stripe/react-stripe-js": "^1.10.0",
"@stripe/stripe-js": "^1.35.0",
"@tanstack/react-query": "^4.3.9",

View File

@ -2,6 +2,7 @@
* Typescript class based component for custom-error
* @link https://nextjs.org/docs/advanced-features/custom-error-page
*/
import * as Sentry from "@sentry/nextjs";
import { NextPage, NextPageContext } from "next";
import NextError, { ErrorProps } from "next/error";
import React from "react";
@ -46,7 +47,9 @@ const CustomError: NextPage<CustomErrorProps> = (props) => {
* Partially adapted from the example in
* https://github.com/vercel/next.js/tree/canary/examples/with-sentry
*/
CustomError.getInitialProps = async ({ res, err, asPath }: AugmentedNextPageContext) => {
CustomError.getInitialProps = async (ctx: AugmentedNextPageContext) => {
const { res, err, asPath } = ctx;
await Sentry.captureUnderscoreErrorException(ctx);
const errorInitialProps = (await NextError.getInitialProps({
res,
err,

View File

@ -179,9 +179,7 @@ const IntegrationsContainer = ({ variant, exclude }: IntegrationsContainerProps)
})}
description={t(`no_category_apps_description_${variant || "other"}`)}
buttonRaw={
<Button
color="secondary"
href={variant ? `/apps/categories/${variant}` : "/apps/categories/other"}>
<Button color="secondary" href={variant ? `/apps/categories/${variant}` : "/apps"}>
{t(`connect_${variant || "other"}_apps`)}
</Button>
}

View File

@ -0,0 +1,16 @@
// This file configures the initialization of Sentry on the browser.
// The config you add here will be used whenever a page is visited.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from "@sentry/nextjs";
const SENTRY_DSN = process.env.NEXT_PUBLIC_SENTRY_DSN;
Sentry.init({
dsn: SENTRY_DSN,
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1.0,
// ...
// Note: if you want to override the automatic release value, do not set a
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
// that it will also get attached to your source maps
});

View File

@ -0,0 +1,3 @@
defaults.url=https://sentry.io/
defaults.org=calcom
defaults.project=cal

View File

@ -0,0 +1,16 @@
// This file configures the initialization of Sentry on the server.
// The config you add here will be used whenever the server handles a request.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from "@sentry/nextjs";
const SENTRY_DSN = process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN;
Sentry.init({
dsn: SENTRY_DSN,
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1.0,
// ...
// Note: if you want to override the automatic release value, do not set a
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
// that it will also get attached to your source maps
});

View File

@ -10,7 +10,6 @@ export const AppSetupMap = {
"caldav-calendar": dynamic(() => import("../../caldavcalendar/pages/setup")),
zapier: dynamic(() => import("../../zapier/pages/setup")),
closecom: dynamic(() => import("../../closecomothercalendar/pages/setup")),
sendgrid: dynamic(() => import("../../sendgridothercalendar/pages/setup")),
};
export const AppSetupPage = (props: { slug: string }) => {

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 sendgridothercalendar_meta } from "./sendgridothercalendar/_metadata";
import { metadata as sirius_video_meta } from "./sirius_video/_metadata";
import { metadata as stripepayment_meta } from "./stripepayment/_metadata";
import { metadata as tandemvideo_meta } from "./tandemvideo/_metadata";
@ -71,7 +70,6 @@ export const appStoreMetadata = {
rainbow: rainbow_meta,
raycast: raycast_meta,
riverside: riverside_meta,
sendgridothercalendar: sendgridothercalendar_meta,
sirius_video: sirius_video_meta,
stripepayment: stripepayment_meta,
tandemvideo: tandemvideo_meta,

View File

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

View File

@ -1,6 +1,7 @@
import z from "zod";
import CloseCom, { CloseComFieldOptions } from "@calcom/lib/CloseCom";
import { getCustomActivityTypeInstanceData } from "@calcom/lib/CloseComeUtils";
import { symmetricDecrypt } from "@calcom/lib/crypto";
import logger from "@calcom/lib/logger";
import type {
@ -73,9 +74,10 @@ export default class CloseComCalendarService implements Calendar {
}
closeComUpdateCustomActivity = async (uid: string, event: CalendarEvent) => {
const customActivityTypeInstanceData = await this.closeCom.getCustomActivityTypeInstanceData(
const customActivityTypeInstanceData = await getCustomActivityTypeInstanceData(
event,
calComCustomActivityFields
calComCustomActivityFields,
this.closeCom
);
// Create Custom Activity type instance
const customActivityTypeInstance = await this.closeCom.activity.custom.create(
@ -89,9 +91,10 @@ export default class CloseComCalendarService implements Calendar {
};
async createEvent(event: CalendarEvent): Promise<NewCalendarEventType> {
const customActivityTypeInstanceData = await this.closeCom.getCustomActivityTypeInstanceData(
const customActivityTypeInstanceData = await getCustomActivityTypeInstanceData(
event,
calComCustomActivityFields
calComCustomActivityFields,
this.closeCom
);
// Create Custom Activity type instance
const customActivityTypeInstance = await this.closeCom.activity.custom.create(

View File

@ -1,11 +1,8 @@
jest.mock("@calcom/lib/logger", () => ({
default: {
getChildLogger: () => ({
debug: jest.fn(),
error: jest.fn(),
log: jest.fn(),
}),
},
debug: jest.fn(),
error: jest.fn(),
log: jest.fn(),
getChildLogger: jest.fn(),
}));
jest.mock("@calcom/lib/crypto", () => ({

View File

@ -1,6 +1,20 @@
import CloseCom from "@calcom/lib/CloseCom";
import {
getCloseComContactIds,
getCustomActivityTypeInstanceData,
getCloseComCustomActivityTypeFieldsIds,
getCloseComLeadId,
} from "@calcom/lib/CloseComeUtils";
import type { CalendarEvent } from "@calcom/types/Calendar";
jest.mock("@calcom/lib/CloseCom", () => ({
default: class {
constructor() {
/* Mock */
}
},
}));
afterEach(() => {
jest.resetAllMocks();
});
@ -12,8 +26,9 @@ test("check generic lead generator: already exists", async () => {
data: [{ name: "From Cal.com", id: "abc" }],
}),
} as any;
debugger;
const id = await CloseCom.prototype.getCloseComLeadId();
const closeCom = new CloseCom("someKey");
const id = await getCloseComLeadId(closeCom);
expect(id).toEqual("abc");
});
@ -26,7 +41,8 @@ test("check generic lead generator: doesn't exist", async () => {
create: () => ({ id: "def" }),
} as any;
const id = await CloseCom.prototype.getCloseComLeadId();
const closeCom = new CloseCom("someKey");
const id = await getCloseComLeadId(closeCom);
expect(id).toEqual("def");
});
@ -45,7 +61,8 @@ test("retrieve contact IDs: all exist", async () => {
search: () => ({ data: attendees }),
} as any;
const contactIds = await CloseCom.prototype.getCloseComContactIds(event.attendees, "leadId");
const closeCom = new CloseCom("someKey");
const contactIds = await getCloseComContactIds(event.attendees, closeCom, "leadId");
expect(contactIds).toEqual(["test1", "test2"]);
});
@ -62,7 +79,8 @@ test("retrieve contact IDs: some don't exist", async () => {
create: () => ({ id: "test3" }),
} as any;
const contactIds = await CloseCom.prototype.getCloseComContactIds(event.attendees, "leadId");
const closeCom = new CloseCom("someKey");
const contactIds = await getCloseComContactIds(event.attendees, closeCom, "leadId");
expect(contactIds).toEqual(["test1", "test3"]);
});
@ -87,11 +105,15 @@ test("retrieve custom fields for custom activity type: type doesn't exist, no fi
},
} as any;
const contactIds = await CloseCom.prototype.getCloseComCustomActivityTypeFieldsIds([
["Attendees", "", true, true],
["Date & Time", "", true, true],
["Time Zone", "", true, true],
]);
const closeCom = new CloseCom("someKey");
const contactIds = await getCloseComCustomActivityTypeFieldsIds(
[
["Attendees", "", true, true],
["Date & Time", "", true, true],
["Time Zone", "", true, true],
],
closeCom
);
expect(contactIds).toEqual({
activityType: "type1",
fields: ["field9A", "field11D", "field9T"],
@ -119,11 +141,15 @@ test("retrieve custom fields for custom activity type: type exists, no field cre
},
} as any;
const contactIds = await CloseCom.prototype.getCloseComCustomActivityTypeFieldsIds([
["Attendees", "", true, true],
["Date & Time", "", true, true],
["Time Zone", "", true, true],
]);
const closeCom = new CloseCom("someKey");
const contactIds = await getCloseComCustomActivityTypeFieldsIds(
[
["Attendees", "", true, true],
["Date & Time", "", true, true],
["Time Zone", "", true, true],
],
closeCom
);
expect(contactIds).toEqual({
activityType: "typeX",
fields: ["fieldY", "field11D", "field9T"],
@ -142,7 +168,7 @@ test("prepare data to create custom activity type instance: two attendees, no ad
const event = {
attendees,
startTime: now.toISOString(),
} as unknown as CalendarEvent;
} as CalendarEvent;
CloseCom.prototype.activity = {
type: {
@ -170,11 +196,16 @@ test("prepare data to create custom activity type instance: two attendees, no ad
create: () => ({ id: "def" }),
} as any;
const data = await CloseCom.prototype.getCustomActivityTypeInstanceData(event, [
["Attendees", "", true, true],
["Date & Time", "", true, true],
["Time Zone", "", true, true],
]);
const closeCom = new CloseCom("someKey");
const data = await getCustomActivityTypeInstanceData(
event,
[
["Attendees", "", true, true],
["Date & Time", "", true, true],
["Time Zone", "", true, true],
],
closeCom
);
expect(data).toEqual({
custom_activity_type_id: "type1",
lead_id: "def",
@ -221,11 +252,16 @@ test("prepare data to create custom activity type instance: one attendees, with
}),
} as any;
const data = await CloseCom.prototype.getCustomActivityTypeInstanceData(event, [
["Attendees", "", true, true],
["Date & Time", "", true, true],
["Time Zone", "", true, true],
]);
const closeCom = new CloseCom("someKey");
const data = await getCustomActivityTypeInstanceData(
event,
[
["Attendees", "", true, true],
["Date & Time", "", true, true],
["Time Zone", "", true, true],
],
closeCom
);
expect(data).toEqual({
custom_activity_type_id: "type1",
lead_id: "abc",

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 sendgridothercalendar from "./sendgridothercalendar";
import * as stripepayment from "./stripepayment";
import * as tandemvideo from "./tandemvideo";
import * as vital from "./vital";
@ -37,7 +36,6 @@ const appStore = {
larkcalendar,
office365calendar,
office365video,
sendgridothercalendar,
stripepayment,
tandemvideo,
vital,

View File

@ -1,10 +0,0 @@
---
description: SendGrid delivers your transactional and marketing emails through the world's largest cloud-based email delivery platform.
items:
- /api/app-store/sendgridothercalendar/1.png
---
{description}
Features:
- Creates event attendees as contacts in Sendgrid

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,10 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import checkSession from "../../_utils/auth";
import { checkInstalled } from "../../_utils/installation";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = checkSession(req);
await checkInstalled("sendgrid", session.user?.id);
return res.status(200).json({ url: "/apps/sendgrid/setup" });
}

View File

@ -1,39 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { symmetricEncrypt } from "@calcom/lib/crypto";
import { HttpError } from "@calcom/lib/http-error";
import logger from "@calcom/lib/logger";
import { defaultResponder } from "@calcom/lib/server";
import prisma from "@calcom/prisma";
import checkSession from "../../_utils/auth";
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
export async function getHandler(req: NextApiRequest, res: NextApiResponse) {
const session = checkSession(req);
const { api_key } = req.body;
if (!api_key) throw new HttpError({ statusCode: 400, message: "No Api Key provided to check" });
const encrypted = symmetricEncrypt(JSON.stringify({ api_key }), process.env.CALENDSO_ENCRYPTION_KEY || "");
const data = {
type: "sendgrid_other_calendar",
key: { encrypted },
userId: session.user?.id,
appId: "sendgrid",
};
try {
await prisma.credential.create({
data,
});
} catch (reason) {
logger.error("Could not add Sendgrid app", reason);
return res.status(500).json({ message: "Could not add Sendgrid app" });
}
return res.status(200).json({ url: getInstalledAppPath({ variant: "other", slug: "sendgrid" }) });
}
export default defaultResponder(getHandler);

View File

@ -1,29 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import Sendgrid from "@calcom/lib/Sendgrid";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server";
import checkSession from "../../_utils/auth";
export async function getHandler(req: NextApiRequest, res: NextApiResponse) {
const { api_key } = req.body;
if (!api_key) throw new HttpError({ statusCode: 400, message: "No Api Key provoided to check" });
checkSession(req);
const sendgrid: Sendgrid = new Sendgrid(api_key);
try {
const usernameInfo = await sendgrid.username();
if (usernameInfo.username) {
return res.status(200).end();
} else {
return res.status(404).end();
}
} catch (e) {
return res.status(500).json({ message: e });
}
}
export default defaultResponder(getHandler);

View File

@ -1,6 +0,0 @@
import { defaultHandler } from "@calcom/lib/server";
export default defaultHandler({
GET: import("./_getAdd"),
POST: import("./_postAdd"),
});

View File

@ -1,5 +0,0 @@
import { defaultHandler } from "@calcom/lib/server";
export default defaultHandler({
POST: import("./_postCheck"),
});

View File

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

View File

@ -1,16 +0,0 @@
{
"/*": "Don't modify slug - If required, do it using cli edit command",
"name": "Sendgrid",
"slug": "sendgrid",
"type": "sendgrid_other_calendar",
"imageSrc": "/api/app-store/sendgridothercalendar/icon.png",
"logo": "/api/app-store/sendgridothercalendar/icon.png",
"url": "https://cal.com/apps/sendgrid",
"variant": "other_calendar",
"categories": ["other"],
"publisher": "Cal.com",
"email": "help@cal.com",
"description": "SendGrid delivers your transactional and marketing emails through the world's largest cloud-based email delivery platform.",
"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,99 +0,0 @@
import z from "zod";
import Sendgrid, { SendgridNewContact } from "@calcom/lib/Sendgrid";
import { symmetricDecrypt } from "@calcom/lib/crypto";
import logger from "@calcom/lib/logger";
import type {
Calendar,
CalendarEvent,
EventBusyDate,
IntegrationCalendar,
NewCalendarEventType,
} from "@calcom/types/Calendar";
import { CredentialPayload } from "@calcom/types/Credential";
const apiKeySchema = z.object({
encrypted: z.string(),
});
const CALENDSO_ENCRYPTION_KEY = process.env.CALENDSO_ENCRYPTION_KEY || "";
/**
* Authentication
* Sendgrid requires Basic Auth for any request to their APIs, which is far from
* ideal considering that such a strategy requires generating an API Key by the
* user and input it in our system. A Setup page was created when trying to install
* Sendgrid in order to instruct how to create such resource and to obtain it.
*/
export default class CloseComCalendarService implements Calendar {
private integrationName = "";
private sendgrid: Sendgrid;
private log: typeof logger;
constructor(credential: CredentialPayload) {
this.integrationName = "sendgrid_other_calendar";
this.log = logger.getChildLogger({ prefix: [`[[lib] ${this.integrationName}`] });
const parsedCredentialKey = apiKeySchema.safeParse(credential.key);
let decrypted;
if (parsedCredentialKey.success) {
decrypted = symmetricDecrypt(parsedCredentialKey.data.encrypted, CALENDSO_ENCRYPTION_KEY);
const { api_key } = JSON.parse(decrypted);
this.sendgrid = new Sendgrid(api_key);
} else {
throw Error(
`No API Key found for userId ${credential.userId} and appId ${credential.appId}: ${parsedCredentialKey.error}`
);
}
}
async createEvent(event: CalendarEvent): Promise<NewCalendarEventType> {
// Proceeding to just creating the user in Sendgrid, no event entity exists in Sendgrid
const contactsData = event.attendees.map((attendee) => ({
first_name: attendee.name,
email: attendee.email,
}));
const result = await this.sendgrid.sendgridRequest<SendgridNewContact>({
url: `/v3/marketing/contacts`,
method: "PUT",
body: {
contacts: contactsData,
},
});
return Promise.resolve({
id: "",
uid: result.job_id,
password: "",
url: "",
type: this.integrationName,
additionalInfo: {
result,
},
});
}
async updateEvent(uid: string, event: CalendarEvent): Promise<any> {
// Unless we want to be able to support modifying an event to add more attendees
// to have them created in Sendgrid, ingoring this use case for now
return Promise.resolve();
}
async deleteEvent(uid: string): Promise<void> {
// Unless we want to delete the contact in Sendgrid once the event
// is deleted just ingoring this use case for now
return Promise.resolve();
}
async getAvailability(
dateFrom: string,
dateTo: string,
selectedCalendars: IntegrationCalendar[]
): Promise<EventBusyDate[]> {
return Promise.resolve([]);
}
async listCalendars(event?: CalendarEvent): Promise<IntegrationCalendar[]> {
return Promise.resolve([]);
}
}

View File

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

View File

@ -1,16 +0,0 @@
{
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"name": "@calcom/sendgridothercalendar",
"version": "0.0.0",
"main": "./index.ts",
"description": "SendGrid delivers your transactional and marketing emails through the world's largest cloud-based email delivery platform.",
"dependencies": {
"@calcom/lib": "*",
"@calcom/prisma": "*",
"@sendgrid/client": "^7.7.0"
},
"devDependencies": {
"@calcom/types": "*"
}
}

View File

@ -1,157 +0,0 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useRouter } from "next/router";
import { useState, useEffect } from "react";
import { useForm, Controller } from "react-hook-form";
import { Toaster } from "react-hot-toast";
import z from "zod";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button } from "@calcom/ui";
import { Icon } from "@calcom/ui/Icon";
import { Form, TextField } from "@calcom/ui/components/form";
import { showToast } from "@calcom/ui/v2";
const formSchema = z.object({
api_key: z.string(),
});
export default function SendgridSetup() {
const { t } = useLocale();
const router = useRouter();
const [testPassed, setTestPassed] = useState<boolean | undefined>(undefined);
const [testLoading, setTestLoading] = useState<boolean>(false);
const form = useForm<{
api_key: string;
}>({
resolver: zodResolver(formSchema),
});
useEffect(() => {
const timer = setTimeout(() => {
if (testPassed === false) {
setTestPassed(undefined);
}
}, 3000);
return () => clearTimeout(timer);
}, [testPassed]);
return (
<div className="flex h-screen bg-gray-200">
<div className="m-auto rounded bg-white p-5 md:w-[520px] md:p-10">
<div className="flex flex-col space-y-5 md:flex-row md:space-y-0 md:space-x-5">
<div>
{/* eslint-disable @next/next/no-img-element */}
<img
src="/api/app-store/sendgridothercalendar/icon.png"
alt="Apple Calendar"
className="h-12 w-12 max-w-2xl"
/>
</div>
<div>
<h1 className="text-gray-600">{t("provide_api_key")}</h1>
<div className="mt-1 text-sm">
{t("generate_api_key_description")}{" "}
<a
className="text-indigo-400"
href="https://app.sendgrid.com/settings/api_keys"
target="_blank"
rel="noopener noreferrer">
Sendgrid
</a>
. {t("it_stored_encrypted")}
</div>
<div className="my-2 mt-3">
<Form
form={form}
handleSubmit={async (values) => {
const res = await fetch("/api/integrations/sendgridothercalendar/add", {
method: "POST",
body: JSON.stringify(values),
headers: {
"Content-Type": "application/json",
},
});
const json = await res.json();
if (res.ok) {
router.push(json.url);
} else {
showToast(json.message, "error");
}
}}>
<fieldset className="space-y-2" disabled={form.formState.isSubmitting}>
<Controller
name="api_key"
control={form.control}
render={({ field: { onBlur, onChange } }) => (
<TextField
className="my-0"
onBlur={onBlur}
disabled={testPassed === true}
name="api_key"
placeholder="api_xyz..."
onChange={async (e) => {
onChange(e.target.value);
form.setValue("api_key", e.target.value);
await form.trigger("api_key");
}}
/>
)}
/>
</fieldset>
<div className="mt-5 justify-end space-x-2 sm:mt-4 sm:flex">
<Button type="button" color="secondary" onClick={() => router.back()}>
{t("cancel")}
</Button>
<Button
type="submit"
loading={testLoading}
disabled={testPassed === true}
StartIcon={testPassed !== undefined ? (testPassed ? Icon.FiCheck : Icon.FiX) : undefined}
className={
testPassed !== undefined
? testPassed
? " !bg-green-100 !text-green-700 hover:bg-green-100"
: "!border-red-700 bg-red-100 !text-red-700 hover:bg-red-100"
: "secondary"
}
color={testPassed === true ? "minimal" : "secondary"}
onClick={async () => {
const check = await form.trigger("api_key");
if (!check) return;
const api_key = form.getValues("api_key");
setTestLoading(true);
const res = await fetch("/api/integrations/sendgridothercalendar/check", {
method: "POST",
body: JSON.stringify({ api_key }),
headers: {
"Content-Type": "application/json",
},
});
if (res.status === 200) {
setTestPassed(true);
} else {
setTestPassed(false);
}
setTestLoading(false);
}}>
{t(
testPassed !== undefined ? (testPassed ? "test_passed" : "test_failed") : "test_api_key"
)}
</Button>
<Button type="submit" loading={form.formState.isSubmitting}>
{t("save")}
</Button>
</div>
</Form>
</div>
</div>
</div>
</div>
<Toaster position="bottom-right" />
</div>
);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

View File

@ -1,5 +1,4 @@
import logger from "@calcom/lib/logger";
import { CalendarEvent } from "@calcom/types/Calendar";
export type CloseComLead = {
companyName?: string | null | undefined;
@ -300,7 +299,6 @@ export default class CloseCom {
private _delete = async ({ urlPath }: { urlPath: string }) => {
return this._request({ urlPath, method: "delete" });
};
private _request = async ({
urlPath,
data,
@ -332,197 +330,6 @@ export default class CloseCom {
return await response.json();
});
};
public async getCloseComContactIds(
persons: { email: string; name: string | null }[],
leadFromCalComId?: string
): Promise<string[]> {
// Check if persons exist or to see if any should be created
const closeComContacts = await this.contact.search({
emails: persons.map((att) => att.email),
});
// NOTE: If contact is duplicated in Close.com we will get more results
// messing around with the expected number of contacts retrieved
if (closeComContacts.data.length < persons.length && leadFromCalComId) {
// Create missing contacts
const personsEmails = persons.map((att) => att.email);
// Existing contacts based on persons emails: contacts may have more
// than one email, we just need the one used by the event.
const existingContactsEmails = closeComContacts.data.flatMap((cont) =>
cont.emails.filter((em) => personsEmails.includes(em.email)).map((ems) => ems.email)
);
const nonExistingContacts = persons.filter((person) => !existingContactsEmails.includes(person.email));
const createdContacts = await Promise.all(
nonExistingContacts.map(
async (per) =>
await this.contact.create({
person: per,
leadId: leadFromCalComId,
})
)
);
if (createdContacts.length === nonExistingContacts.length) {
// All non existent contacts where created
return Promise.resolve(
closeComContacts.data.map((cont) => cont.id).concat(createdContacts.map((cont) => cont.id))
);
} else {
return Promise.reject("Some contacts were not possible to create in Close.com");
}
} else {
return Promise.resolve(closeComContacts.data.map((cont) => cont.id));
}
}
public async getCustomActivityTypeInstanceData(
event: CalendarEvent,
customFields: CloseComFieldOptions
): Promise<CloseComCustomActivityCreate> {
// Get Cal.com generic Lead
const leadFromCalComId = await this.getCloseComLeadId();
// Get Contacts ids
const contactsIds = await this.getCloseComContactIds(event.attendees, leadFromCalComId);
// Get Custom Activity Type id
const customActivityTypeAndFieldsIds = await this.getCloseComCustomActivityTypeFieldsIds(customFields);
// Prepare values for each Custom Activity Fields
const customActivityFieldsValues = [
contactsIds.length > 1 ? contactsIds.slice(1) : null, // Attendee
event.startTime, // Date & Time
event.attendees[0].timeZone, // Time Zone
contactsIds[0], // Organizer
event.additionalNotes ?? null, // Additional Notes
];
// Preparing Custom Activity Instance data for Close.com
return Object.assign(
{},
{
custom_activity_type_id: customActivityTypeAndFieldsIds.activityType,
lead_id: leadFromCalComId,
}, // This is to add each field as `"custom.FIELD_ID": "value"` in the object
...customActivityTypeAndFieldsIds.fields.map((fieldId: string, index: number) => {
return {
[`custom.${fieldId}`]: customActivityFieldsValues[index],
};
})
);
}
public async getCustomFieldsIds(
entity: keyof CloseCom["customField"],
customFields: CloseComFieldOptions,
custom_activity_type_id?: string
): Promise<string[]> {
// Get Custom Activity Fields
const allFields: CloseComCustomActivityFieldGet | CloseComCustomContactFieldGet = await this.customField[
entity
].get({
query: { _fields: ["name", "id"].concat(entity === "activity" ? ["custom_activity_type_id"] : []) },
});
let relevantFields: { [key: string]: any }[];
if (entity === "activity") {
relevantFields = (allFields as CloseComCustomActivityFieldGet).data.filter(
(fie) => fie.custom_activity_type_id === custom_activity_type_id
);
} else {
relevantFields = allFields.data as CloseComCustomActivityFieldGet["data"];
}
const customFieldsNames = relevantFields.map((fie) => fie.name);
const customFieldsExist = customFields.map((cusFie) => customFieldsNames.includes(cusFie[0]));
return await Promise.all(
customFieldsExist.map(async (exist, idx) => {
if (!exist && entity !== "shared") {
const [name, type, required, multiple] = customFields[idx];
let created: { [key: string]: any };
if (entity === "activity" && custom_activity_type_id) {
created = await this.customField[entity].create({
name,
type,
required,
accepts_multiple_values: multiple,
editable_with_roles: [],
custom_activity_type_id,
});
return created.id;
} else {
if (entity === "contact") {
created = await this.customField[entity].create({
name,
type,
required,
accepts_multiple_values: multiple,
editable_with_roles: [],
});
return created.id;
}
}
} else {
const index = customFieldsNames.findIndex((val) => val === customFields[idx][0]);
if (index >= 0) {
return relevantFields[index].id;
} else {
throw Error("Couldn't find the field index");
}
}
})
);
}
public async getCloseComCustomActivityTypeFieldsIds(customFields: CloseComFieldOptions) {
// Check if Custom Activity Type exists
const customActivities = await this.customActivity.type.get();
const calComCustomActivity = customActivities.data.filter((act) => act.name === "Cal.com Activity");
if (calComCustomActivity.length > 0) {
// Cal.com Custom Activity type exist
// Get Custom Activity Type fields ids
const fields = await this.getCustomFieldsIds("activity", customFields, calComCustomActivity[0].id);
return {
activityType: calComCustomActivity[0].id,
fields,
};
} else {
// Cal.com Custom Activity type doesn't exist
// Create Custom Activity Type
const { id: activityType } = await this.customActivity.type.create({
name: "Cal.com Activity",
description: "Bookings in your Cal.com account",
});
// Create Custom Activity Fields
const fields = await Promise.all(
customFields.map(async ([name, type, required, multiple]) => {
const creation = await this.customField.activity.create({
custom_activity_type_id: activityType,
name,
type,
required,
accepts_multiple_values: multiple,
editable_with_roles: [],
});
return creation.id;
})
);
return {
activityType,
fields,
};
}
}
public async getCloseComLeadId(
leadInfo: CloseComLead = {
companyName: "From Cal.com",
description: "Generic Lead for Contacts created by Cal.com",
}
): Promise<string> {
const closeComLeadNames = await this.lead.list({ query: { _fields: ["name", "id"] } });
const searchLeadFromCalCom = closeComLeadNames.data.filter((lead) => lead.name === leadInfo.companyName);
if (searchLeadFromCalCom.length === 0) {
// No Lead exists, create it
const createdLeadFromCalCom = await this.lead.create(leadInfo);
return createdLeadFromCalCom.id;
} else {
return searchLeadFromCalCom[0].id;
}
}
}
export const closeComQueries = {

View File

@ -0,0 +1,206 @@
import type { CalendarEvent } from "@calcom/types/Calendar";
import CloseCom, {
CloseComCustomActivityCreate,
CloseComCustomActivityFieldGet,
CloseComCustomContactFieldGet,
CloseComFieldOptions,
CloseComLead,
} from "./CloseCom";
export async function getCloseComContactIds(
persons: { email: string; name: string | null }[],
closeCom: CloseCom,
leadFromCalComId?: string
): Promise<string[]> {
// Check if persons exist or to see if any should be created
const closeComContacts = await closeCom.contact.search({
emails: persons.map((att) => att.email),
});
// NOTE: If contact is duplicated in Close.com we will get more results
// messing around with the expected number of contacts retrieved
if (closeComContacts.data.length < persons.length && leadFromCalComId) {
// Create missing contacts
const personsEmails = persons.map((att) => att.email);
// Existing contacts based on persons emails: contacts may have more
// than one email, we just need the one used by the event.
const existingContactsEmails = closeComContacts.data.flatMap((cont) =>
cont.emails.filter((em) => personsEmails.includes(em.email)).map((ems) => ems.email)
);
const nonExistingContacts = persons.filter((person) => !existingContactsEmails.includes(person.email));
const createdContacts = await Promise.all(
nonExistingContacts.map(
async (per) =>
await closeCom.contact.create({
person: per,
leadId: leadFromCalComId,
})
)
);
if (createdContacts.length === nonExistingContacts.length) {
// All non existent contacts where created
return Promise.resolve(
closeComContacts.data.map((cont) => cont.id).concat(createdContacts.map((cont) => cont.id))
);
} else {
return Promise.reject("Some contacts were not possible to create in Close.com");
}
} else {
return Promise.resolve(closeComContacts.data.map((cont) => cont.id));
}
}
export async function getCustomActivityTypeInstanceData(
event: CalendarEvent,
customFields: CloseComFieldOptions,
closeCom: CloseCom
): Promise<CloseComCustomActivityCreate> {
// Get Cal.com generic Lead
const leadFromCalComId = await getCloseComLeadId(closeCom);
// Get Contacts ids
const contactsIds = await getCloseComContactIds(event.attendees, closeCom, leadFromCalComId);
// Get Custom Activity Type id
const customActivityTypeAndFieldsIds = await getCloseComCustomActivityTypeFieldsIds(customFields, closeCom);
// Prepare values for each Custom Activity Fields
const customActivityFieldsValues = [
contactsIds.length > 1 ? contactsIds.slice(1) : null, // Attendee
event.startTime, // Date & Time
event.attendees[0].timeZone, // Time Zone
contactsIds[0], // Organizer
event.additionalNotes ?? null, // Additional Notes
];
// Preparing Custom Activity Instance data for Close.com
return Object.assign(
{},
{
custom_activity_type_id: customActivityTypeAndFieldsIds.activityType,
lead_id: leadFromCalComId,
}, // This is to add each field as `"custom.FIELD_ID": "value"` in the object
...customActivityTypeAndFieldsIds.fields.map((fieldId: string, index: number) => {
return {
[`custom.${fieldId}`]: customActivityFieldsValues[index],
};
})
);
}
export async function getCustomFieldsIds(
entity: keyof CloseCom["customField"],
customFields: CloseComFieldOptions,
closeCom: CloseCom,
custom_activity_type_id?: string
): Promise<string[]> {
// Get Custom Activity Fields
const allFields: CloseComCustomActivityFieldGet | CloseComCustomContactFieldGet =
await closeCom.customField[entity].get({
query: { _fields: ["name", "id"].concat(entity === "activity" ? ["custom_activity_type_id"] : []) },
});
let relevantFields: { [key: string]: any }[];
if (entity === "activity") {
relevantFields = (allFields as CloseComCustomActivityFieldGet).data.filter(
(fie) => fie.custom_activity_type_id === custom_activity_type_id
);
} else {
relevantFields = allFields.data as CloseComCustomActivityFieldGet["data"];
}
const customFieldsNames = relevantFields.map((fie) => fie.name);
const customFieldsExist = customFields.map((cusFie) => customFieldsNames.includes(cusFie[0]));
return await Promise.all(
customFieldsExist.map(async (exist, idx) => {
if (!exist && entity !== "shared") {
const [name, type, required, multiple] = customFields[idx];
let created: { [key: string]: any };
if (entity === "activity" && custom_activity_type_id) {
created = await closeCom.customField[entity].create({
name,
type,
required,
accepts_multiple_values: multiple,
editable_with_roles: [],
custom_activity_type_id,
});
return created.id;
} else {
if (entity === "contact") {
created = await closeCom.customField[entity].create({
name,
type,
required,
accepts_multiple_values: multiple,
editable_with_roles: [],
});
return created.id;
}
}
} else {
const index = customFieldsNames.findIndex((val) => val === customFields[idx][0]);
if (index >= 0) {
return relevantFields[index].id;
} else {
throw Error("Couldn't find the field index");
}
}
})
);
}
export async function getCloseComCustomActivityTypeFieldsIds(
customFields: CloseComFieldOptions,
closeCom: CloseCom
) {
// Check if Custom Activity Type exists
const customActivities = await closeCom.customActivity.type.get();
const calComCustomActivity = customActivities.data.filter((act) => act.name === "Cal.com Activity");
if (calComCustomActivity.length > 0) {
// Cal.com Custom Activity type exist
// Get Custom Activity Type fields ids
const fields = await getCustomFieldsIds("activity", customFields, closeCom, calComCustomActivity[0].id);
return {
activityType: calComCustomActivity[0].id,
fields,
};
} else {
// Cal.com Custom Activity type doesn't exist
// Create Custom Activity Type
const { id: activityType } = await closeCom.customActivity.type.create({
name: "Cal.com Activity",
description: "Bookings in your Cal.com account",
});
// Create Custom Activity Fields
const fields = await Promise.all(
customFields.map(async ([name, type, required, multiple]) => {
const creation = await closeCom.customField.activity.create({
custom_activity_type_id: activityType,
name,
type,
required,
accepts_multiple_values: multiple,
editable_with_roles: [],
});
return creation.id;
})
);
return {
activityType,
fields,
};
}
}
export async function getCloseComLeadId(
closeCom: CloseCom,
leadInfo: CloseComLead = {
companyName: "From Cal.com",
description: "Generic Lead for Contacts created by Cal.com",
}
): Promise<string> {
const closeComLeadNames = await closeCom.lead.list({ query: { _fields: ["name", "id"] } });
const searchLeadFromCalCom = closeComLeadNames.data.filter((lead) => lead.name === leadInfo.companyName);
if (searchLeadFromCalCom.length === 0) {
// No Lead exists, create it
const createdLeadFromCalCom = await closeCom.lead.create(leadInfo);
return createdLeadFromCalCom.id;
} else {
return searchLeadFromCalCom[0].id;
}
}

View File

@ -1,131 +0,0 @@
import sendgrid from "@sendgrid/client";
import { ClientRequest } from "@sendgrid/client/src/request";
import { ClientResponse } from "@sendgrid/client/src/response";
import logger from "@calcom/lib/logger";
export type SendgridFieldOptions = [string, string][];
type SendgridUsernameResult = {
username: string;
user_id: number;
};
export type SendgridCustomField = {
id: string;
name: string;
field_type: string;
_metadata: {
self: string;
};
};
export type SendgridContact = {
id: string;
first_name: string;
last_name: string;
email: string;
};
export type SendgridSearchResult = {
result: SendgridContact[];
};
export type SendgridFieldDefinitions = {
custom_fields: SendgridCustomField[];
};
export type SendgridNewContact = {
job_id: string;
};
const environmentApiKey = process.env.SENDGRID_SYNC_API_KEY || "";
/**
* This class to instance communicating to Sendgrid APIs requires an API Key.
*
* You can either pass to the constructor an API Key or have one defined as an
* environment variable in case the communication to Sendgrid is just for
* one account only, not configurable by any user at any moment.
*/
export default class Sendgrid {
private sendgrid: typeof sendgrid;
private log: typeof logger;
constructor(providedApiKey = "") {
this.log = logger.getChildLogger({ prefix: [`[[lib] sendgrid`] });
if (!providedApiKey && !environmentApiKey) throw Error("Sendgrid Api Key not present");
this.sendgrid = sendgrid;
this.sendgrid.setApiKey(providedApiKey || environmentApiKey);
}
public username = async () => {
const username = await this.sendgridRequest<SendgridUsernameResult>({
url: `/v3/user/username`,
method: "GET",
});
return username;
};
public async sendgridRequest<R = ClientResponse>(data: ClientRequest): Promise<R> {
this.log.debug("sendgridRequest:request", data);
const results = await this.sendgrid.request(data);
this.log.debug("sendgridRequest:results", results);
if (results[1].errors) throw Error(`Sendgrid request error: ${results[1].errors}`);
return results[1];
}
public async getSendgridContactId(email: string) {
const search = await this.sendgridRequest<SendgridSearchResult>({
url: `/v3/marketing/contacts/search`,
method: "POST",
body: {
query: `email LIKE '${email}'`,
},
});
this.log.debug("sync:sendgrid:getSendgridContactId:search", search);
return search.result || [];
}
public async getSendgridCustomFieldsIds(customFields: SendgridFieldOptions) {
// Get Custom Activity Fields
const allFields = await this.sendgridRequest<SendgridFieldDefinitions>({
url: `/v3/marketing/field_definitions`,
method: "GET",
});
allFields.custom_fields = allFields.custom_fields ?? [];
this.log.debug("sync:sendgrid:getCustomFieldsIds:allFields", allFields);
const customFieldsNames = allFields.custom_fields.map((fie) => fie.name);
this.log.debug("sync:sendgrid:getCustomFieldsIds:customFieldsNames", customFieldsNames);
const customFieldsExist = customFields.map((cusFie) => customFieldsNames.includes(cusFie[0]));
this.log.debug("sync:sendgrid:getCustomFieldsIds:customFieldsExist", customFieldsExist);
return await Promise.all(
customFieldsExist.map(async (exist, idx) => {
if (!exist) {
const [name, field_type] = customFields[idx];
const created = await this.sendgridRequest<SendgridCustomField>({
url: `/v3/marketing/field_definitions`,
method: "POST",
body: {
name,
field_type,
},
});
this.log.debug("sync:sendgrid:getCustomFieldsIds:customField:created", created);
return created.id;
} else {
const index = customFieldsNames.findIndex((val) => val === customFields[idx][0]);
if (index >= 0) {
this.log.debug(
"sync:sendgrid:getCustomFieldsIds:customField:existed",
allFields.custom_fields[index].id
);
return allFields.custom_fields[index].id;
} else {
throw Error("Couldn't find the field index");
}
}
})
);
}
}

View File

@ -1,6 +1,7 @@
import { MembershipRole } from "@prisma/client";
import CloseCom, { CloseComFieldOptions, CloseComLead } from "@calcom/lib/CloseCom";
import { getCloseComContactIds, getCloseComLeadId, getCustomFieldsIds } from "@calcom/lib/CloseComeUtils";
import logger from "@calcom/lib/logger";
import SyncServiceCore, { TeamInfoType } from "@calcom/lib/sync/ISyncService";
import ISyncService, { ConsoleUserInfoType, WebUserInfoType } from "@calcom/lib/sync/ISyncService";
@ -19,8 +20,6 @@ const calComSharedFields: CloseComFieldOptions = [["Contact Role", "text", false
const serviceName = "closecom_service";
export default class CloseComService extends SyncServiceCore implements ISyncService {
protected declare service: CloseCom;
constructor() {
super(serviceName, CloseCom, logger.getChildLogger({ prefix: [`[[sync] ${serviceName}`] }));
}
@ -32,16 +31,16 @@ export default class CloseComService extends SyncServiceCore implements ISyncSer
) => {
this.log.debug("sync:closecom:user", { user });
// Get Cal.com Lead
const leadId = await this.service.getCloseComLeadId(leadInfo);
const leadId = await getCloseComLeadId(this.service, leadInfo);
this.log.debug("sync:closecom:user:leadId", { leadId });
// Get Contacts ids: already creates contacts
const [contactId] = await this.service.getCloseComContactIds([user], leadId);
const [contactId] = await getCloseComContactIds([user], this.service, leadId);
this.log.debug("sync:closecom:user:contactsIds", { contactId });
// Get Custom Contact fields ids
const customFieldsIds = await this.service.getCustomFieldsIds("contact", calComCustomContactFields);
const customFieldsIds = await getCustomFieldsIds("contact", calComCustomContactFields, this.service);
this.log.debug("sync:closecom:user:customFieldsIds", { customFieldsIds });
// Get shared fields ids
const sharedFieldsIds = await this.service.getCustomFieldsIds("shared", calComSharedFields);
const sharedFieldsIds = await getCustomFieldsIds("shared", calComSharedFields, this.service);
this.log.debug("sync:closecom:user:sharedFieldsIds", { sharedFieldsIds });
const allFields = customFieldsIds.concat(sharedFieldsIds);
this.log.debug("sync:closecom:user:allFields", { allFields });
@ -92,7 +91,7 @@ export default class CloseComService extends SyncServiceCore implements ISyncSer
},
delete: async (webUser: WebUserInfoType) => {
this.log.debug("sync:closecom:web:user:delete", { webUser });
const [contactId] = await this.service.getCloseComContactIds([webUser]);
const [contactId] = await getCloseComContactIds([webUser], this.service);
this.log.debug("sync:closecom:web:user:delete:contactId", { contactId });
if (contactId) {
return this.service.contact.delete(contactId);
@ -113,21 +112,21 @@ export default class CloseComService extends SyncServiceCore implements ISyncSer
},
delete: async (team: TeamInfoType) => {
this.log.debug("sync:closecom:web:team:delete", { team });
const leadId = await this.service.getCloseComLeadId({ companyName: team.name });
const leadId = await getCloseComLeadId(this.service, { companyName: team.name });
this.log.debug("sync:closecom:web:team:delete:leadId", { leadId });
this.service.lead.delete(leadId);
},
update: async (prevTeam: TeamInfoType, updatedTeam: TeamInfoType) => {
this.log.debug("sync:closecom:web:team:update", { prevTeam, updatedTeam });
const leadId = await this.service.getCloseComLeadId({ companyName: prevTeam.name });
const leadId = await getCloseComLeadId(this.service, { companyName: prevTeam.name });
this.log.debug("sync:closecom:web:team:update:leadId", { leadId });
this.service.lead.update(leadId, { companyName: updatedTeam.name });
this.service.lead.update(leadId, updatedTeam);
},
},
membership: {
delete: async (webUser: WebUserInfoType) => {
this.log.debug("sync:closecom:web:membership:delete", { webUser });
const [contactId] = await this.service.getCloseComContactIds([webUser]);
const [contactId] = await getCloseComContactIds([webUser], this.service);
this.log.debug("sync:closecom:web:membership:delete:contactId", { contactId });
if (contactId) {
return this.service.contact.delete(contactId);

View File

@ -1,11 +1,41 @@
import logger from "@calcom/lib/logger";
import sendgrid from "@sendgrid/client";
import { ClientRequest } from "@sendgrid/client/src/request";
import { ClientResponse } from "@sendgrid/client/src/response";
import Sendgrid, { SendgridFieldOptions, SendgridNewContact } from "../../Sendgrid";
import ISyncService, { ConsoleUserInfoType, WebUserInfoType } from "../ISyncService";
import SyncServiceCore from "../ISyncService";
import logger from "@calcom/lib/logger";
import ISyncService, { ConsoleUserInfoType, WebUserInfoType } from "@calcom/lib/sync/ISyncService";
import SyncServiceCore from "@calcom/lib/sync/ISyncService";
type SendgridCustomField = {
id: string;
name: string;
field_type: string;
_metadata: {
self: string;
};
};
type SendgridContact = {
id: string;
first_name: string;
last_name: string;
email: string;
};
type SendgridSearchResult = {
result: SendgridContact[];
};
type SendgridFieldDefinitions = {
custom_fields: SendgridCustomField[];
};
type SendgridNewContact = {
job_id: string;
};
// Cal.com Custom Contact Fields
const calComCustomContactFields: SendgridFieldOptions = [
const calComCustomContactFields: [string, string][] = [
// Field name, field type
["username", "Text"],
["plan", "Text"],
@ -13,18 +43,92 @@ const calComCustomContactFields: SendgridFieldOptions = [
["createdAt", "Date"],
];
type SendgridRequest = <R = ClientResponse>(data: ClientRequest) => Promise<R>;
// TODO: When creating Sendgrid app, move this to the corresponding file
class Sendgrid {
constructor() {
if (!process.env.SENDGRID_API_KEY) throw Error("Sendgrid Api Key not present");
sendgrid.setApiKey(process.env.SENDGRID_API_KEY);
return sendgrid;
}
}
const serviceName = "sendgrid_service";
export default class SendgridService extends SyncServiceCore implements ISyncService {
protected declare service: Sendgrid;
constructor() {
super(serviceName, Sendgrid, logger.getChildLogger({ prefix: [`[[sync] ${serviceName}`] }));
}
sendgridRequest: SendgridRequest = async (data: ClientRequest) => {
this.log.debug("sendgridRequest:request", data);
const results = await this.service.request(data);
this.log.debug("sendgridRequest:results", results);
if (results[1].errors) throw Error(`Sendgrid request error: ${results[1].errors}`);
return results[1];
};
getSendgridContactId = async (email: string) => {
const search = await this.sendgridRequest<SendgridSearchResult>({
url: `/v3/marketing/contacts/search`,
method: "POST",
body: {
query: `email LIKE '${email}'`,
},
});
this.log.debug("sync:sendgrid:getSendgridContactId:search", search);
return search.result || [];
};
getSendgridCustomFieldsIds = async () => {
// Get Custom Activity Fields
const allFields = await this.sendgridRequest<SendgridFieldDefinitions>({
url: `/v3/marketing/field_definitions`,
method: "GET",
});
allFields.custom_fields = allFields.custom_fields ?? [];
this.log.debug("sync:sendgrid:getCustomFieldsIds:allFields", allFields);
const customFieldsNames = allFields.custom_fields.map((fie) => fie.name);
this.log.debug("sync:sendgrid:getCustomFieldsIds:customFieldsNames", customFieldsNames);
const customFieldsExist = calComCustomContactFields.map((cusFie) =>
customFieldsNames.includes(cusFie[0])
);
this.log.debug("sync:sendgrid:getCustomFieldsIds:customFieldsExist", customFieldsExist);
return await Promise.all(
customFieldsExist.map(async (exist, idx) => {
if (!exist) {
const [name, field_type] = calComCustomContactFields[idx];
const created = await this.sendgridRequest<SendgridCustomField>({
url: `/v3/marketing/field_definitions`,
method: "POST",
body: {
name,
field_type,
},
});
this.log.debug("sync:sendgrid:getCustomFieldsIds:customField:created", created);
return created.id;
} else {
const index = customFieldsNames.findIndex((val) => val === calComCustomContactFields[idx][0]);
if (index >= 0) {
this.log.debug(
"sync:sendgrid:getCustomFieldsIds:customField:existed",
allFields.custom_fields[index].id
);
return allFields.custom_fields[index].id;
} else {
throw Error("Couldn't find the field index");
}
}
})
);
};
upsert = async (user: WebUserInfoType | ConsoleUserInfoType) => {
this.log.debug("sync:sendgrid:user", user);
// Get Custom Contact fields ids
const customFieldsIds = await this.service.getSendgridCustomFieldsIds(calComCustomContactFields);
const customFieldsIds = await this.getSendgridCustomFieldsIds();
this.log.debug("sync:sendgrid:user:customFieldsIds", customFieldsIds);
const lastBooking = "email" in user ? await this.getUserLastBooking(user) : null;
this.log.debug("sync:sendgrid:user:lastBooking", lastBooking);
@ -55,7 +159,7 @@ export default class SendgridService extends SyncServiceCore implements ISyncSer
),
};
this.log.debug("sync:sendgrid:contact:contactData", contactData);
const newContact = await this.service.sendgridRequest<SendgridNewContact>({
const newContact = await this.sendgridRequest<SendgridNewContact>({
url: `/v3/marketing/contacts`,
method: "PUT",
body: {
@ -81,9 +185,9 @@ export default class SendgridService extends SyncServiceCore implements ISyncSer
return this.upsert(webUser);
},
delete: async (webUser: WebUserInfoType) => {
const [contactId] = await this.service.getSendgridContactId(webUser.email);
const [contactId] = await this.getSendgridContactId(webUser.email);
if (contactId) {
return this.service.sendgridRequest({
return this.sendgridRequest({
url: `/v3/marketing/contacts`,
method: "DELETE",
qs: {

View File

@ -101,11 +101,5 @@
"categories": ["video"],
"slug": "sirius_video",
"type": "sirius_video_video"
},
{
"dirName": "sendgrid",
"categories": ["other"],
"slug": "sendgrid",
"type": "sendgrid_other_calendar"
}
]

View File

@ -240,8 +240,6 @@ export default async function main() {
}
// No need to check if environment variable is present, the API Key is set up by the user, not the system
await createApp("closecom", "closecomothercalendar", ["other"], "closecom_other_calendar");
// No need to check if environment variable is present, the API Key is set up by the user, not the system
await createApp("sendgrid", "sendgridothercalendar", ["other"], "sendgrid_other_calendar");
await createApp("wipe-my-cal", "wipemycalother", ["other"], "wipemycal_other");
if (process.env.GIPHY_API_KEY) {
await createApp("giphy", "giphy", ["other"], "giphy_other", {

View File

@ -182,7 +182,6 @@
"$CI",
"$CLOSECOM_API_KEY",
"$SENDGRID_API_KEY",
"$SENDGRID_SYNC_API_KEY",
"$SENDGRID_EMAIL",
"$CRON_API_KEY",
"$DAILY_API_KEY",
@ -230,6 +229,8 @@
"$SAML_ADMINS",
"$SAML_DATABASE_URL",
"$SEND_FEEDBACK_EMAIL",
"$SENTRY_DSN",
"$NEXT_PUBLIC_SENTRY_DSN",
"$SLACK_CLIENT_ID",
"$SLACK_CLIENT_SECRET",
"$SLACK_SIGNING_SECRET",

928
yarn.lock

File diff suppressed because it is too large Load Diff