Merge branch 'teams-stripe-checkout-form' of https://github.com/calcom/cal.com into teams-stripe-checkout-form
This commit is contained in:
commit
57b8104630
|
@ -77,7 +77,7 @@ NEXT_PUBLIC_HELPSCOUT_KEY=
|
||||||
SEND_FEEDBACK_EMAIL=
|
SEND_FEEDBACK_EMAIL=
|
||||||
|
|
||||||
# Sengrid
|
# Sengrid
|
||||||
# Used for email reminders in workflows
|
# Used for email reminders in workflows and internal sync services
|
||||||
SENDGRID_API_KEY=
|
SENDGRID_API_KEY=
|
||||||
SENDGRID_EMAIL=
|
SENDGRID_EMAIL=
|
||||||
|
|
||||||
|
@ -131,6 +131,9 @@ NEXT_PUBLIC_TEAM_IMPERSONATION=false
|
||||||
# Close.com internal CRM
|
# Close.com internal CRM
|
||||||
CLOSECOM_API_KEY=
|
CLOSECOM_API_KEY=
|
||||||
|
|
||||||
|
# Sendgrid internal sync service
|
||||||
|
SENDGRID_SYNC_API_KEY=
|
||||||
|
|
||||||
# Sendgrid internal email sender
|
# Sendgrid internal email sender
|
||||||
SENDGRID_API_KEY=
|
SENDGRID_API_KEY=
|
||||||
|
|
||||||
|
|
4
app.json
4
app.json
|
@ -50,6 +50,10 @@
|
||||||
"description": "Sendgrid api key. Used for email reminders in workflows",
|
"description": "Sendgrid api key. Used for email reminders in workflows",
|
||||||
"value": ""
|
"value": ""
|
||||||
},
|
},
|
||||||
|
"SENDGRID_SYNC_API_KEY": {
|
||||||
|
"description": "Sendgrid internal sync service",
|
||||||
|
"value": ""
|
||||||
|
},
|
||||||
"SENDGRID_EMAIL": {
|
"SENDGRID_EMAIL": {
|
||||||
"description": "Sendgrid email. Used for email reminders in workflows",
|
"description": "Sendgrid email. Used for email reminders in workflows",
|
||||||
"value": ""
|
"value": ""
|
||||||
|
|
|
@ -31,7 +31,6 @@ import {
|
||||||
} from "@calcom/embed-core/embed-iframe";
|
} from "@calcom/embed-core/embed-iframe";
|
||||||
import CustomBranding from "@calcom/lib/CustomBranding";
|
import CustomBranding from "@calcom/lib/CustomBranding";
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
import { formatTime } from "@calcom/lib/date-fns";
|
|
||||||
import getStripeAppData from "@calcom/lib/getStripeAppData";
|
import getStripeAppData from "@calcom/lib/getStripeAppData";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||||
|
@ -40,8 +39,8 @@ import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
||||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
import { Icon } from "@calcom/ui/Icon";
|
import { Icon } from "@calcom/ui/Icon";
|
||||||
import { Tooltip } from "@calcom/ui/Tooltip";
|
import { Tooltip } from "@calcom/ui/Tooltip";
|
||||||
import AddressInput from "@calcom/ui/form/AddressInputLazy";
|
|
||||||
import { Button } from "@calcom/ui/components";
|
import { Button } from "@calcom/ui/components";
|
||||||
|
import AddressInput from "@calcom/ui/form/AddressInputLazy";
|
||||||
import PhoneInput from "@calcom/ui/form/PhoneInputLazy";
|
import PhoneInput from "@calcom/ui/form/PhoneInputLazy";
|
||||||
import { EmailInput, Form } from "@calcom/ui/form/fields";
|
import { EmailInput, Form } from "@calcom/ui/form/fields";
|
||||||
|
|
||||||
|
|
|
@ -179,7 +179,9 @@ const IntegrationsContainer = ({ variant, exclude }: IntegrationsContainerProps)
|
||||||
})}
|
})}
|
||||||
description={t(`no_category_apps_description_${variant || "other"}`)}
|
description={t(`no_category_apps_description_${variant || "other"}`)}
|
||||||
buttonRaw={
|
buttonRaw={
|
||||||
<Button color="secondary" href={variant ? `/apps/categories/${variant}` : "/apps"}>
|
<Button
|
||||||
|
color="secondary"
|
||||||
|
href={variant ? `/apps/categories/${variant}` : "/apps/categories/other"}>
|
||||||
{t(`connect_${variant || "other"}_apps`)}
|
{t(`connect_${variant || "other"}_apps`)}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
|
|
|
@ -1158,7 +1158,7 @@
|
||||||
"skip": "Пропустить",
|
"skip": "Пропустить",
|
||||||
"do_this_later": "Настроить позже",
|
"do_this_later": "Настроить позже",
|
||||||
"set_availability_getting_started_subtitle_1": "Укажите время, в которое вы доступны",
|
"set_availability_getting_started_subtitle_1": "Укажите время, в которое вы доступны",
|
||||||
"set_availability_getting_started_subtitle_2": "Эти данные можно изменить позже на странице «Доступность».",
|
"set_availability_getting_started_subtitle_2": "Эти данные можно указать позже на странице «Доступность».",
|
||||||
"connect_calendars_from_app_store": "Вы можете добавить другие календари из App Store",
|
"connect_calendars_from_app_store": "Вы можете добавить другие календари из App Store",
|
||||||
"connect_conference_apps": "Подключить приложения для конференций",
|
"connect_conference_apps": "Подключить приложения для конференций",
|
||||||
"connect_calendar_apps": "Подключить приложения календаря",
|
"connect_calendar_apps": "Подключить приложения календаря",
|
||||||
|
@ -1186,7 +1186,7 @@
|
||||||
"create_your_first_form_description": "Формы маршрутизации позволяют задавать пользователям уточняющие вопросы, чтобы перенаправлять их к соответствующему лицу или типу события.",
|
"create_your_first_form_description": "Формы маршрутизации позволяют задавать пользователям уточняющие вопросы, чтобы перенаправлять их к соответствующему лицу или типу события.",
|
||||||
"create_your_first_webhook": "Создайте свой первый вебхук",
|
"create_your_first_webhook": "Создайте свой первый вебхук",
|
||||||
"create_your_first_webhook_description": "Вебхуки позволяют в реальном времени получать оповещения, когда данные о встрече на Cal.com изменяются.",
|
"create_your_first_webhook_description": "Вебхуки позволяют в реальном времени получать оповещения, когда данные о встрече на Cal.com изменяются.",
|
||||||
"for_a_maximum_of": "Максимум:",
|
"for_a_maximum_of": "Максимальное количество:",
|
||||||
"event_one": "событие",
|
"event_one": "событие",
|
||||||
"event_other": "событий",
|
"event_other": "событий",
|
||||||
"profile_team_description": "Настройка профиля вашей команды",
|
"profile_team_description": "Настройка профиля вашей команды",
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { DynamicComponent } from "./DynamicComponent";
|
||||||
export const AppSettings = (props: { slug: string }) => {
|
export const AppSettings = (props: { slug: string }) => {
|
||||||
return (
|
return (
|
||||||
<DynamicComponent<typeof AppSettingsComponentsMap>
|
<DynamicComponent<typeof AppSettingsComponentsMap>
|
||||||
wrapperClassName="border-t border-gray-200"
|
wrapperClassName="border-t border-gray-200 bg-gray-100"
|
||||||
componentMap={AppSettingsComponentsMap}
|
componentMap={AppSettingsComponentsMap}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -10,6 +10,7 @@ export const AppSetupMap = {
|
||||||
"caldav-calendar": dynamic(() => import("../../caldavcalendar/pages/setup")),
|
"caldav-calendar": dynamic(() => import("../../caldavcalendar/pages/setup")),
|
||||||
zapier: dynamic(() => import("../../zapier/pages/setup")),
|
zapier: dynamic(() => import("../../zapier/pages/setup")),
|
||||||
closecom: dynamic(() => import("../../closecomothercalendar/pages/setup")),
|
closecom: dynamic(() => import("../../closecomothercalendar/pages/setup")),
|
||||||
|
sendgrid: dynamic(() => import("../../sendgridothercalendar/pages/setup")),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AppSetupPage = (props: { slug: string }) => {
|
export const AppSetupPage = (props: { slug: string }) => {
|
||||||
|
|
|
@ -30,6 +30,7 @@ import { metadata as qr_code_meta } from "./qr_code/_metadata";
|
||||||
import { metadata as rainbow_meta } from "./rainbow/_metadata";
|
import { metadata as rainbow_meta } from "./rainbow/_metadata";
|
||||||
import { metadata as raycast_meta } from "./raycast/_metadata";
|
import { metadata as raycast_meta } from "./raycast/_metadata";
|
||||||
import { metadata as riverside_meta } from "./riverside/_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 sirius_video_meta } from "./sirius_video/_metadata";
|
||||||
import { metadata as slackmessaging_meta } from "./slackmessaging/_metadata";
|
import { metadata as slackmessaging_meta } from "./slackmessaging/_metadata";
|
||||||
import { metadata as stripepayment_meta } from "./stripepayment/_metadata";
|
import { metadata as stripepayment_meta } from "./stripepayment/_metadata";
|
||||||
|
@ -71,6 +72,7 @@ export const appStoreMetadata = {
|
||||||
rainbow: rainbow_meta,
|
rainbow: rainbow_meta,
|
||||||
raycast: raycast_meta,
|
raycast: raycast_meta,
|
||||||
riverside: riverside_meta,
|
riverside: riverside_meta,
|
||||||
|
sendgridothercalendar: sendgridothercalendar_meta,
|
||||||
sirius_video: sirius_video_meta,
|
sirius_video: sirius_video_meta,
|
||||||
slackmessaging: slackmessaging_meta,
|
slackmessaging: slackmessaging_meta,
|
||||||
stripepayment: stripepayment_meta,
|
stripepayment: stripepayment_meta,
|
||||||
|
|
|
@ -29,6 +29,7 @@ export const apiHandlers = {
|
||||||
rainbow: import("./rainbow/api"),
|
rainbow: import("./rainbow/api"),
|
||||||
raycast: import("./raycast/api"),
|
raycast: import("./raycast/api"),
|
||||||
riverside: import("./riverside/api"),
|
riverside: import("./riverside/api"),
|
||||||
|
sendgridothercalendar: import("./sendgridothercalendar/api"),
|
||||||
sirius_video: import("./sirius_video/api"),
|
sirius_video: import("./sirius_video/api"),
|
||||||
slackmessaging: import("./slackmessaging/api"),
|
slackmessaging: import("./slackmessaging/api"),
|
||||||
stripepayment: import("./stripepayment/api"),
|
stripepayment: import("./stripepayment/api"),
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
|
|
||||||
import CloseCom, { CloseComFieldOptions } from "@calcom/lib/CloseCom";
|
import CloseCom, { CloseComFieldOptions } from "@calcom/lib/CloseCom";
|
||||||
import { getCustomActivityTypeInstanceData } from "@calcom/lib/CloseComeUtils";
|
|
||||||
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||||
import logger from "@calcom/lib/logger";
|
import logger from "@calcom/lib/logger";
|
||||||
import type {
|
import type {
|
||||||
|
@ -74,10 +73,9 @@ export default class CloseComCalendarService implements Calendar {
|
||||||
}
|
}
|
||||||
|
|
||||||
closeComUpdateCustomActivity = async (uid: string, event: CalendarEvent) => {
|
closeComUpdateCustomActivity = async (uid: string, event: CalendarEvent) => {
|
||||||
const customActivityTypeInstanceData = await getCustomActivityTypeInstanceData(
|
const customActivityTypeInstanceData = await this.closeCom.getCustomActivityTypeInstanceData(
|
||||||
event,
|
event,
|
||||||
calComCustomActivityFields,
|
calComCustomActivityFields
|
||||||
this.closeCom
|
|
||||||
);
|
);
|
||||||
// Create Custom Activity type instance
|
// Create Custom Activity type instance
|
||||||
const customActivityTypeInstance = await this.closeCom.activity.custom.create(
|
const customActivityTypeInstance = await this.closeCom.activity.custom.create(
|
||||||
|
@ -91,10 +89,9 @@ export default class CloseComCalendarService implements Calendar {
|
||||||
};
|
};
|
||||||
|
|
||||||
async createEvent(event: CalendarEvent): Promise<NewCalendarEventType> {
|
async createEvent(event: CalendarEvent): Promise<NewCalendarEventType> {
|
||||||
const customActivityTypeInstanceData = await getCustomActivityTypeInstanceData(
|
const customActivityTypeInstanceData = await this.closeCom.getCustomActivityTypeInstanceData(
|
||||||
event,
|
event,
|
||||||
calComCustomActivityFields,
|
calComCustomActivityFields
|
||||||
this.closeCom
|
|
||||||
);
|
);
|
||||||
// Create Custom Activity type instance
|
// Create Custom Activity type instance
|
||||||
const customActivityTypeInstance = await this.closeCom.activity.custom.create(
|
const customActivityTypeInstance = await this.closeCom.activity.custom.create(
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
jest.mock("@calcom/lib/logger", () => ({
|
jest.mock("@calcom/lib/logger", () => ({
|
||||||
debug: jest.fn(),
|
default: {
|
||||||
error: jest.fn(),
|
getChildLogger: () => ({
|
||||||
log: jest.fn(),
|
debug: jest.fn(),
|
||||||
getChildLogger: jest.fn(),
|
error: jest.fn(),
|
||||||
|
log: jest.fn(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock("@calcom/lib/crypto", () => ({
|
jest.mock("@calcom/lib/crypto", () => ({
|
||||||
|
|
|
@ -1,20 +1,6 @@
|
||||||
import CloseCom from "@calcom/lib/CloseCom";
|
import CloseCom from "@calcom/lib/CloseCom";
|
||||||
import {
|
|
||||||
getCloseComContactIds,
|
|
||||||
getCustomActivityTypeInstanceData,
|
|
||||||
getCloseComCustomActivityTypeFieldsIds,
|
|
||||||
getCloseComLeadId,
|
|
||||||
} from "@calcom/lib/CloseComeUtils";
|
|
||||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||||
|
|
||||||
jest.mock("@calcom/lib/CloseCom", () => ({
|
|
||||||
default: class {
|
|
||||||
constructor() {
|
|
||||||
/* Mock */
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.resetAllMocks();
|
jest.resetAllMocks();
|
||||||
});
|
});
|
||||||
|
@ -26,9 +12,8 @@ test("check generic lead generator: already exists", async () => {
|
||||||
data: [{ name: "From Cal.com", id: "abc" }],
|
data: [{ name: "From Cal.com", id: "abc" }],
|
||||||
}),
|
}),
|
||||||
} as any;
|
} as any;
|
||||||
|
debugger;
|
||||||
const closeCom = new CloseCom("someKey");
|
const id = await CloseCom.prototype.getCloseComLeadId();
|
||||||
const id = await getCloseComLeadId(closeCom);
|
|
||||||
expect(id).toEqual("abc");
|
expect(id).toEqual("abc");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -41,8 +26,7 @@ test("check generic lead generator: doesn't exist", async () => {
|
||||||
create: () => ({ id: "def" }),
|
create: () => ({ id: "def" }),
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
const closeCom = new CloseCom("someKey");
|
const id = await CloseCom.prototype.getCloseComLeadId();
|
||||||
const id = await getCloseComLeadId(closeCom);
|
|
||||||
expect(id).toEqual("def");
|
expect(id).toEqual("def");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -61,8 +45,7 @@ test("retrieve contact IDs: all exist", async () => {
|
||||||
search: () => ({ data: attendees }),
|
search: () => ({ data: attendees }),
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
const closeCom = new CloseCom("someKey");
|
const contactIds = await CloseCom.prototype.getCloseComContactIds(event.attendees, "leadId");
|
||||||
const contactIds = await getCloseComContactIds(event.attendees, closeCom, "leadId");
|
|
||||||
expect(contactIds).toEqual(["test1", "test2"]);
|
expect(contactIds).toEqual(["test1", "test2"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -79,8 +62,7 @@ test("retrieve contact IDs: some don't exist", async () => {
|
||||||
create: () => ({ id: "test3" }),
|
create: () => ({ id: "test3" }),
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
const closeCom = new CloseCom("someKey");
|
const contactIds = await CloseCom.prototype.getCloseComContactIds(event.attendees, "leadId");
|
||||||
const contactIds = await getCloseComContactIds(event.attendees, closeCom, "leadId");
|
|
||||||
expect(contactIds).toEqual(["test1", "test3"]);
|
expect(contactIds).toEqual(["test1", "test3"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -105,15 +87,11 @@ test("retrieve custom fields for custom activity type: type doesn't exist, no fi
|
||||||
},
|
},
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
const closeCom = new CloseCom("someKey");
|
const contactIds = await CloseCom.prototype.getCloseComCustomActivityTypeFieldsIds([
|
||||||
const contactIds = await getCloseComCustomActivityTypeFieldsIds(
|
["Attendees", "", true, true],
|
||||||
[
|
["Date & Time", "", true, true],
|
||||||
["Attendees", "", true, true],
|
["Time Zone", "", true, true],
|
||||||
["Date & Time", "", true, true],
|
]);
|
||||||
["Time Zone", "", true, true],
|
|
||||||
],
|
|
||||||
closeCom
|
|
||||||
);
|
|
||||||
expect(contactIds).toEqual({
|
expect(contactIds).toEqual({
|
||||||
activityType: "type1",
|
activityType: "type1",
|
||||||
fields: ["field9A", "field11D", "field9T"],
|
fields: ["field9A", "field11D", "field9T"],
|
||||||
|
@ -141,15 +119,11 @@ test("retrieve custom fields for custom activity type: type exists, no field cre
|
||||||
},
|
},
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
const closeCom = new CloseCom("someKey");
|
const contactIds = await CloseCom.prototype.getCloseComCustomActivityTypeFieldsIds([
|
||||||
const contactIds = await getCloseComCustomActivityTypeFieldsIds(
|
["Attendees", "", true, true],
|
||||||
[
|
["Date & Time", "", true, true],
|
||||||
["Attendees", "", true, true],
|
["Time Zone", "", true, true],
|
||||||
["Date & Time", "", true, true],
|
]);
|
||||||
["Time Zone", "", true, true],
|
|
||||||
],
|
|
||||||
closeCom
|
|
||||||
);
|
|
||||||
expect(contactIds).toEqual({
|
expect(contactIds).toEqual({
|
||||||
activityType: "typeX",
|
activityType: "typeX",
|
||||||
fields: ["fieldY", "field11D", "field9T"],
|
fields: ["fieldY", "field11D", "field9T"],
|
||||||
|
@ -168,7 +142,7 @@ test("prepare data to create custom activity type instance: two attendees, no ad
|
||||||
const event = {
|
const event = {
|
||||||
attendees,
|
attendees,
|
||||||
startTime: now.toISOString(),
|
startTime: now.toISOString(),
|
||||||
} as CalendarEvent;
|
} as unknown as CalendarEvent;
|
||||||
|
|
||||||
CloseCom.prototype.activity = {
|
CloseCom.prototype.activity = {
|
||||||
type: {
|
type: {
|
||||||
|
@ -196,16 +170,11 @@ test("prepare data to create custom activity type instance: two attendees, no ad
|
||||||
create: () => ({ id: "def" }),
|
create: () => ({ id: "def" }),
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
const closeCom = new CloseCom("someKey");
|
const data = await CloseCom.prototype.getCustomActivityTypeInstanceData(event, [
|
||||||
const data = await getCustomActivityTypeInstanceData(
|
["Attendees", "", true, true],
|
||||||
event,
|
["Date & Time", "", true, true],
|
||||||
[
|
["Time Zone", "", true, true],
|
||||||
["Attendees", "", true, true],
|
]);
|
||||||
["Date & Time", "", true, true],
|
|
||||||
["Time Zone", "", true, true],
|
|
||||||
],
|
|
||||||
closeCom
|
|
||||||
);
|
|
||||||
expect(data).toEqual({
|
expect(data).toEqual({
|
||||||
custom_activity_type_id: "type1",
|
custom_activity_type_id: "type1",
|
||||||
lead_id: "def",
|
lead_id: "def",
|
||||||
|
@ -252,16 +221,11 @@ test("prepare data to create custom activity type instance: one attendees, with
|
||||||
}),
|
}),
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
const closeCom = new CloseCom("someKey");
|
const data = await CloseCom.prototype.getCustomActivityTypeInstanceData(event, [
|
||||||
const data = await getCustomActivityTypeInstanceData(
|
["Attendees", "", true, true],
|
||||||
event,
|
["Date & Time", "", true, true],
|
||||||
[
|
["Time Zone", "", true, true],
|
||||||
["Attendees", "", true, true],
|
]);
|
||||||
["Date & Time", "", true, true],
|
|
||||||
["Time Zone", "", true, true],
|
|
||||||
],
|
|
||||||
closeCom
|
|
||||||
);
|
|
||||||
expect(data).toEqual({
|
expect(data).toEqual({
|
||||||
custom_activity_type_id: "type1",
|
custom_activity_type_id: "type1",
|
||||||
lead_id: "abc",
|
lead_id: "abc",
|
||||||
|
|
|
@ -15,6 +15,7 @@ import * as jitsivideo from "./jitsivideo";
|
||||||
import * as larkcalendar from "./larkcalendar";
|
import * as larkcalendar from "./larkcalendar";
|
||||||
import * as office365calendar from "./office365calendar";
|
import * as office365calendar from "./office365calendar";
|
||||||
import * as office365video from "./office365video";
|
import * as office365video from "./office365video";
|
||||||
|
import * as sendgridothercalendar from "./sendgridothercalendar";
|
||||||
import * as slackmessaging from "./slackmessaging";
|
import * as slackmessaging from "./slackmessaging";
|
||||||
import * as stripepayment from "./stripepayment";
|
import * as stripepayment from "./stripepayment";
|
||||||
import * as tandemvideo from "./tandemvideo";
|
import * as tandemvideo from "./tandemvideo";
|
||||||
|
@ -37,6 +38,7 @@ const appStore = {
|
||||||
larkcalendar,
|
larkcalendar,
|
||||||
office365calendar,
|
office365calendar,
|
||||||
office365video,
|
office365video,
|
||||||
|
sendgridothercalendar,
|
||||||
slackmessaging,
|
slackmessaging,
|
||||||
stripepayment,
|
stripepayment,
|
||||||
tandemvideo,
|
tandemvideo,
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
---
|
||||||
|
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
|
|
@ -0,0 +1,10 @@
|
||||||
|
import type { AppMeta } from "@calcom/types/App";
|
||||||
|
|
||||||
|
import config from "./config.json";
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
category: "other",
|
||||||
|
...config,
|
||||||
|
} as AppMeta;
|
||||||
|
|
||||||
|
export default metadata;
|
|
@ -0,0 +1,10 @@
|
||||||
|
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" });
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
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);
|
|
@ -0,0 +1,29 @@
|
||||||
|
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);
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { defaultHandler } from "@calcom/lib/server";
|
||||||
|
|
||||||
|
export default defaultHandler({
|
||||||
|
GET: import("./_getAdd"),
|
||||||
|
POST: import("./_postAdd"),
|
||||||
|
});
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { defaultHandler } from "@calcom/lib/server";
|
||||||
|
|
||||||
|
export default defaultHandler({
|
||||||
|
POST: import("./_postCheck"),
|
||||||
|
});
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { default as add } from "./add";
|
||||||
|
export { default as check } from "./check";
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"/*": "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
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export * as api from "./api";
|
||||||
|
export * as lib from "./lib";
|
||||||
|
export { metadata } from "./_metadata";
|
|
@ -0,0 +1,99 @@
|
||||||
|
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([]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export { default as CalendarService } from "./CalendarService";
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"$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": "*"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,157 @@
|
||||||
|
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.
After Width: | Height: | Size: 20 KiB |
Binary file not shown.
After Width: | Height: | Size: 4.4 KiB |
|
@ -4,24 +4,28 @@ import { Template } from "./AppSettings";
|
||||||
|
|
||||||
export default function TemplateCard({ template }: { template: Template }) {
|
export default function TemplateCard({ template }: { template: Template }) {
|
||||||
return (
|
return (
|
||||||
<div className="rounded-md border border-gray-200 p-4 ">
|
<div className="rounded-md border border-gray-200 bg-white p-5">
|
||||||
<div className="flex items-center justify-center">
|
<div className="min-h-16 flex items-start justify-start">
|
||||||
<div>
|
<div>
|
||||||
<div className="mr-4 flex h-12 w-12 items-center justify-center rounded-md border p-1 ">
|
<div className="mr-4 flex h-12 w-12 items-center justify-center rounded-md p-1">
|
||||||
<img className="h-8" alt={template.app} src={`/api/app-store/zapier/${template.icon}`} />
|
<img className="h-8" alt={template.app} src={`/api/app-store/zapier/${template.icon}`} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mr-4 ">
|
<div className="mr-4">
|
||||||
<div>
|
<div>
|
||||||
<p className="truncate text-sm font-medium text-neutral-900">{template.app}</p>
|
<p className="truncate text-sm font-medium leading-4 text-neutral-900">{template.app}</p>
|
||||||
<p className="mt-[2px] text-xs text-gray-500 ">{template.text}</p>
|
<p className="mt-[2px] text-sm text-gray-500">{template.text}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-auto hidden sm:block">
|
</div>
|
||||||
<Button color="secondary" className="w-[90px] " target="_blank" href={template.link}>
|
<div className="relative float-right ml-auto mt-4 hidden h-full sm:block">
|
||||||
Use Zap
|
<Button
|
||||||
</Button>
|
color="secondary"
|
||||||
</div>
|
className="absolute bottom-0 right-0 w-[90px]"
|
||||||
|
target="_blank"
|
||||||
|
href={template.link}>
|
||||||
|
Use Zap
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 block w-full sm:hidden">
|
<div className="mt-2 block w-full sm:hidden">
|
||||||
<div className="float-right">
|
<div className="float-right">
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import logger from "@calcom/lib/logger";
|
import logger from "@calcom/lib/logger";
|
||||||
|
import { CalendarEvent } from "@calcom/types/Calendar";
|
||||||
|
|
||||||
export type CloseComLead = {
|
export type CloseComLead = {
|
||||||
companyName?: string | null | undefined;
|
companyName?: string | null | undefined;
|
||||||
|
@ -299,6 +300,7 @@ export default class CloseCom {
|
||||||
private _delete = async ({ urlPath }: { urlPath: string }) => {
|
private _delete = async ({ urlPath }: { urlPath: string }) => {
|
||||||
return this._request({ urlPath, method: "delete" });
|
return this._request({ urlPath, method: "delete" });
|
||||||
};
|
};
|
||||||
|
|
||||||
private _request = async ({
|
private _request = async ({
|
||||||
urlPath,
|
urlPath,
|
||||||
data,
|
data,
|
||||||
|
@ -330,6 +332,198 @@ export default class CloseCom {
|
||||||
return await response.json();
|
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> {
|
||||||
|
debugger;
|
||||||
|
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 = {
|
export const closeComQueries = {
|
||||||
|
|
|
@ -1,206 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,131 @@
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,8 +4,13 @@ import dayjs, { Dayjs } from "@calcom/dayjs";
|
||||||
export const yyyymmdd = (date: Date | Dayjs) =>
|
export const yyyymmdd = (date: Date | Dayjs) =>
|
||||||
date instanceof Date ? dayjs(date).format("YYYY-MM-DD") : date.format("YYYY-MM-DD");
|
date instanceof Date ? dayjs(date).format("YYYY-MM-DD") : date.format("YYYY-MM-DD");
|
||||||
|
|
||||||
export const daysInMonth = (date: Date | Dayjs) =>
|
// @see: https://github.com/iamkun/dayjs/issues/1272 - for the reason we're not using dayjs to do this.
|
||||||
date instanceof Date ? dayjs(date).daysInMonth() : date.daysInMonth();
|
export const daysInMonth = (date: Date | Dayjs) => {
|
||||||
|
const [year, month] =
|
||||||
|
date instanceof Date ? [date.getFullYear(), date.getMonth()] : [date.year(), date.month()];
|
||||||
|
// strange JS quirk: new Date(2022, 12, 0).getMonth() = 11
|
||||||
|
return new Date(year, month + 1, 0).getDate();
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Expects timeFormat to be either 12 or 24, if null or undefined
|
* Expects timeFormat to be either 12 or 24, if null or undefined
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { MembershipRole } from "@prisma/client";
|
import { MembershipRole } from "@prisma/client";
|
||||||
|
|
||||||
import CloseCom, { CloseComFieldOptions, CloseComLead } from "@calcom/lib/CloseCom";
|
import CloseCom, { CloseComFieldOptions, CloseComLead } from "@calcom/lib/CloseCom";
|
||||||
import { getCloseComContactIds, getCloseComLeadId, getCustomFieldsIds } from "@calcom/lib/CloseComeUtils";
|
|
||||||
import logger from "@calcom/lib/logger";
|
import logger from "@calcom/lib/logger";
|
||||||
import SyncServiceCore, { TeamInfoType } from "@calcom/lib/sync/ISyncService";
|
import SyncServiceCore, { TeamInfoType } from "@calcom/lib/sync/ISyncService";
|
||||||
import ISyncService, { ConsoleUserInfoType, WebUserInfoType } from "@calcom/lib/sync/ISyncService";
|
import ISyncService, { ConsoleUserInfoType, WebUserInfoType } from "@calcom/lib/sync/ISyncService";
|
||||||
|
@ -20,6 +19,8 @@ const calComSharedFields: CloseComFieldOptions = [["Contact Role", "text", false
|
||||||
const serviceName = "closecom_service";
|
const serviceName = "closecom_service";
|
||||||
|
|
||||||
export default class CloseComService extends SyncServiceCore implements ISyncService {
|
export default class CloseComService extends SyncServiceCore implements ISyncService {
|
||||||
|
protected declare service: CloseCom;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(serviceName, CloseCom, logger.getChildLogger({ prefix: [`[[sync] ${serviceName}`] }));
|
super(serviceName, CloseCom, logger.getChildLogger({ prefix: [`[[sync] ${serviceName}`] }));
|
||||||
}
|
}
|
||||||
|
@ -31,16 +32,16 @@ export default class CloseComService extends SyncServiceCore implements ISyncSer
|
||||||
) => {
|
) => {
|
||||||
this.log.debug("sync:closecom:user", { user });
|
this.log.debug("sync:closecom:user", { user });
|
||||||
// Get Cal.com Lead
|
// Get Cal.com Lead
|
||||||
const leadId = await getCloseComLeadId(this.service, leadInfo);
|
const leadId = await this.service.getCloseComLeadId(leadInfo);
|
||||||
this.log.debug("sync:closecom:user:leadId", { leadId });
|
this.log.debug("sync:closecom:user:leadId", { leadId });
|
||||||
// Get Contacts ids: already creates contacts
|
// Get Contacts ids: already creates contacts
|
||||||
const [contactId] = await getCloseComContactIds([user], this.service, leadId);
|
const [contactId] = await this.service.getCloseComContactIds([user], leadId);
|
||||||
this.log.debug("sync:closecom:user:contactsIds", { contactId });
|
this.log.debug("sync:closecom:user:contactsIds", { contactId });
|
||||||
// Get Custom Contact fields ids
|
// Get Custom Contact fields ids
|
||||||
const customFieldsIds = await getCustomFieldsIds("contact", calComCustomContactFields, this.service);
|
const customFieldsIds = await this.service.getCustomFieldsIds("contact", calComCustomContactFields);
|
||||||
this.log.debug("sync:closecom:user:customFieldsIds", { customFieldsIds });
|
this.log.debug("sync:closecom:user:customFieldsIds", { customFieldsIds });
|
||||||
// Get shared fields ids
|
// Get shared fields ids
|
||||||
const sharedFieldsIds = await getCustomFieldsIds("shared", calComSharedFields, this.service);
|
const sharedFieldsIds = await this.service.getCustomFieldsIds("shared", calComSharedFields);
|
||||||
this.log.debug("sync:closecom:user:sharedFieldsIds", { sharedFieldsIds });
|
this.log.debug("sync:closecom:user:sharedFieldsIds", { sharedFieldsIds });
|
||||||
const allFields = customFieldsIds.concat(sharedFieldsIds);
|
const allFields = customFieldsIds.concat(sharedFieldsIds);
|
||||||
this.log.debug("sync:closecom:user:allFields", { allFields });
|
this.log.debug("sync:closecom:user:allFields", { allFields });
|
||||||
|
@ -91,7 +92,7 @@ export default class CloseComService extends SyncServiceCore implements ISyncSer
|
||||||
},
|
},
|
||||||
delete: async (webUser: WebUserInfoType) => {
|
delete: async (webUser: WebUserInfoType) => {
|
||||||
this.log.debug("sync:closecom:web:user:delete", { webUser });
|
this.log.debug("sync:closecom:web:user:delete", { webUser });
|
||||||
const [contactId] = await getCloseComContactIds([webUser], this.service);
|
const [contactId] = await this.service.getCloseComContactIds([webUser]);
|
||||||
this.log.debug("sync:closecom:web:user:delete:contactId", { contactId });
|
this.log.debug("sync:closecom:web:user:delete:contactId", { contactId });
|
||||||
if (contactId) {
|
if (contactId) {
|
||||||
return this.service.contact.delete(contactId);
|
return this.service.contact.delete(contactId);
|
||||||
|
@ -112,21 +113,21 @@ export default class CloseComService extends SyncServiceCore implements ISyncSer
|
||||||
},
|
},
|
||||||
delete: async (team: TeamInfoType) => {
|
delete: async (team: TeamInfoType) => {
|
||||||
this.log.debug("sync:closecom:web:team:delete", { team });
|
this.log.debug("sync:closecom:web:team:delete", { team });
|
||||||
const leadId = await getCloseComLeadId(this.service, { companyName: team.name });
|
const leadId = await this.service.getCloseComLeadId({ companyName: team.name });
|
||||||
this.log.debug("sync:closecom:web:team:delete:leadId", { leadId });
|
this.log.debug("sync:closecom:web:team:delete:leadId", { leadId });
|
||||||
this.service.lead.delete(leadId);
|
this.service.lead.delete(leadId);
|
||||||
},
|
},
|
||||||
update: async (prevTeam: TeamInfoType, updatedTeam: TeamInfoType) => {
|
update: async (prevTeam: TeamInfoType, updatedTeam: TeamInfoType) => {
|
||||||
this.log.debug("sync:closecom:web:team:update", { prevTeam, updatedTeam });
|
this.log.debug("sync:closecom:web:team:update", { prevTeam, updatedTeam });
|
||||||
const leadId = await getCloseComLeadId(this.service, { companyName: prevTeam.name });
|
const leadId = await this.service.getCloseComLeadId({ companyName: prevTeam.name });
|
||||||
this.log.debug("sync:closecom:web:team:update:leadId", { leadId });
|
this.log.debug("sync:closecom:web:team:update:leadId", { leadId });
|
||||||
this.service.lead.update(leadId, updatedTeam);
|
this.service.lead.update(leadId, { companyName: updatedTeam.name });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
membership: {
|
membership: {
|
||||||
delete: async (webUser: WebUserInfoType) => {
|
delete: async (webUser: WebUserInfoType) => {
|
||||||
this.log.debug("sync:closecom:web:membership:delete", { webUser });
|
this.log.debug("sync:closecom:web:membership:delete", { webUser });
|
||||||
const [contactId] = await getCloseComContactIds([webUser], this.service);
|
const [contactId] = await this.service.getCloseComContactIds([webUser]);
|
||||||
this.log.debug("sync:closecom:web:membership:delete:contactId", { contactId });
|
this.log.debug("sync:closecom:web:membership:delete:contactId", { contactId });
|
||||||
if (contactId) {
|
if (contactId) {
|
||||||
return this.service.contact.delete(contactId);
|
return this.service.contact.delete(contactId);
|
||||||
|
|
|
@ -1,41 +1,11 @@
|
||||||
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";
|
import logger from "@calcom/lib/logger";
|
||||||
import ISyncService, { ConsoleUserInfoType, WebUserInfoType } from "@calcom/lib/sync/ISyncService";
|
|
||||||
import SyncServiceCore from "@calcom/lib/sync/ISyncService";
|
|
||||||
|
|
||||||
type SendgridCustomField = {
|
import Sendgrid, { SendgridFieldOptions, SendgridNewContact } from "../../Sendgrid";
|
||||||
id: string;
|
import ISyncService, { ConsoleUserInfoType, WebUserInfoType } from "../ISyncService";
|
||||||
name: string;
|
import SyncServiceCore from "../ISyncService";
|
||||||
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
|
// Cal.com Custom Contact Fields
|
||||||
const calComCustomContactFields: [string, string][] = [
|
const calComCustomContactFields: SendgridFieldOptions = [
|
||||||
// Field name, field type
|
// Field name, field type
|
||||||
["username", "Text"],
|
["username", "Text"],
|
||||||
["plan", "Text"],
|
["plan", "Text"],
|
||||||
|
@ -43,92 +13,18 @@ const calComCustomContactFields: [string, string][] = [
|
||||||
["createdAt", "Date"],
|
["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";
|
const serviceName = "sendgrid_service";
|
||||||
|
|
||||||
export default class SendgridService extends SyncServiceCore implements ISyncService {
|
export default class SendgridService extends SyncServiceCore implements ISyncService {
|
||||||
|
protected declare service: Sendgrid;
|
||||||
constructor() {
|
constructor() {
|
||||||
super(serviceName, Sendgrid, logger.getChildLogger({ prefix: [`[[sync] ${serviceName}`] }));
|
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) => {
|
upsert = async (user: WebUserInfoType | ConsoleUserInfoType) => {
|
||||||
this.log.debug("sync:sendgrid:user", user);
|
this.log.debug("sync:sendgrid:user", user);
|
||||||
// Get Custom Contact fields ids
|
// Get Custom Contact fields ids
|
||||||
const customFieldsIds = await this.getSendgridCustomFieldsIds();
|
const customFieldsIds = await this.service.getSendgridCustomFieldsIds(calComCustomContactFields);
|
||||||
this.log.debug("sync:sendgrid:user:customFieldsIds", customFieldsIds);
|
this.log.debug("sync:sendgrid:user:customFieldsIds", customFieldsIds);
|
||||||
const lastBooking = "email" in user ? await this.getUserLastBooking(user) : null;
|
const lastBooking = "email" in user ? await this.getUserLastBooking(user) : null;
|
||||||
this.log.debug("sync:sendgrid:user:lastBooking", lastBooking);
|
this.log.debug("sync:sendgrid:user:lastBooking", lastBooking);
|
||||||
|
@ -159,7 +55,7 @@ export default class SendgridService extends SyncServiceCore implements ISyncSer
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
this.log.debug("sync:sendgrid:contact:contactData", contactData);
|
this.log.debug("sync:sendgrid:contact:contactData", contactData);
|
||||||
const newContact = await this.sendgridRequest<SendgridNewContact>({
|
const newContact = await this.service.sendgridRequest<SendgridNewContact>({
|
||||||
url: `/v3/marketing/contacts`,
|
url: `/v3/marketing/contacts`,
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
body: {
|
body: {
|
||||||
|
@ -185,9 +81,9 @@ export default class SendgridService extends SyncServiceCore implements ISyncSer
|
||||||
return this.upsert(webUser);
|
return this.upsert(webUser);
|
||||||
},
|
},
|
||||||
delete: async (webUser: WebUserInfoType) => {
|
delete: async (webUser: WebUserInfoType) => {
|
||||||
const [contactId] = await this.getSendgridContactId(webUser.email);
|
const [contactId] = await this.service.getSendgridContactId(webUser.email);
|
||||||
if (contactId) {
|
if (contactId) {
|
||||||
return this.sendgridRequest({
|
return this.service.sendgridRequest({
|
||||||
url: `/v3/marketing/contacts`,
|
url: `/v3/marketing/contacts`,
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
qs: {
|
qs: {
|
||||||
|
|
|
@ -101,5 +101,11 @@
|
||||||
"categories": ["video"],
|
"categories": ["video"],
|
||||||
"slug": "sirius_video",
|
"slug": "sirius_video",
|
||||||
"type": "sirius_video_video"
|
"type": "sirius_video_video"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dirName": "sendgrid",
|
||||||
|
"categories": ["other"],
|
||||||
|
"slug": "sendgrid",
|
||||||
|
"type": "sendgrid_other_calendar"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -240,6 +240,8 @@ 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
|
// 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");
|
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");
|
await createApp("wipe-my-cal", "wipemycalother", ["other"], "wipemycal_other");
|
||||||
if (process.env.GIPHY_API_KEY) {
|
if (process.env.GIPHY_API_KEY) {
|
||||||
await createApp("giphy", "giphy", ["other"], "giphy_other", {
|
await createApp("giphy", "giphy", ["other"], "giphy_other", {
|
||||||
|
|
|
@ -185,6 +185,7 @@
|
||||||
"$CI",
|
"$CI",
|
||||||
"$CLOSECOM_API_KEY",
|
"$CLOSECOM_API_KEY",
|
||||||
"$SENDGRID_API_KEY",
|
"$SENDGRID_API_KEY",
|
||||||
|
"$SENDGRID_SYNC_API_KEY",
|
||||||
"$SENDGRID_EMAIL",
|
"$SENDGRID_EMAIL",
|
||||||
"$CRON_API_KEY",
|
"$CRON_API_KEY",
|
||||||
"$DAILY_API_KEY",
|
"$DAILY_API_KEY",
|
||||||
|
|
|
@ -26173,7 +26173,7 @@ zod-prisma@^0.5.4:
|
||||||
parenthesis "^3.1.8"
|
parenthesis "^3.1.8"
|
||||||
ts-morph "^13.0.2"
|
ts-morph "^13.0.2"
|
||||||
|
|
||||||
zod@^3.17.3, zod@^3.19.1:
|
zod@^3.17.3, zod@^3.18.0, zod@^3.19.1:
|
||||||
version "3.19.1"
|
version "3.19.1"
|
||||||
resolved "https://registry.yarnpkg.com/zod/-/zod-3.19.1.tgz#112f074a97b50bfc4772d4ad1576814bd8ac4473"
|
resolved "https://registry.yarnpkg.com/zod/-/zod-3.19.1.tgz#112f074a97b50bfc4772d4ad1576814bd8ac4473"
|
||||||
integrity sha512-LYjZsEDhCdYET9ikFu6dVPGp2YH9DegXjdJToSzD9rO6fy4qiRYFoyEYwps88OseJlPyl2NOe2iJuhEhL7IpEA==
|
integrity sha512-LYjZsEDhCdYET9ikFu6dVPGp2YH9DegXjdJToSzD9rO6fy4qiRYFoyEYwps88OseJlPyl2NOe2iJuhEhL7IpEA==
|
||||||
|
|
Loading…
Reference in New Issue
Block a user