2
0

first commit

This commit is contained in:
2024-08-09 00:39:27 +02:00
commit 79688abe2e
5698 changed files with 497838 additions and 0 deletions

View File

@@ -0,0 +1,94 @@
import type logger from "@calcom/lib/logger";
import { default as webPrisma } from "@calcom/prisma";
export type UserInfo = {
email: string;
name: string | null;
id: number;
username: string | null;
createdDate: Date;
};
export type TeamInfoType = {
name: string | undefined | null;
};
export type WebUserInfoType = UserInfo & {
/** All users are PRO now */
plan?: "PRO";
};
export type ConsoleUserInfoType = UserInfo & {
plan: "CLOUD" | "SELFHOSTED"; // DeploymentType;
};
export interface IUserDeletion<T> {
delete(info: T): Promise<WebUserInfoType>;
}
export interface IUserCreation<T> {
create(info: T): Promise<WebUserInfoType>;
update(info: T): Promise<WebUserInfoType>;
upsert?: never;
}
export interface IUserUpsertion<T> {
create?: never;
update?: never;
upsert(info: T): Promise<WebUserInfoType>;
}
export interface ISyncService {
ready(): boolean;
web: {
user: (IUserCreation<WebUserInfoType> | IUserUpsertion<WebUserInfoType>) & IUserDeletion<WebUserInfoType>;
};
console: {
user: IUserCreation<ConsoleUserInfoType> | IUserUpsertion<ConsoleUserInfoType>;
};
}
export default class SyncServiceCore {
protected serviceName: string;
protected service: unknown;
protected log: typeof logger;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
constructor(serviceName: string, service: any, log: typeof logger) {
this.serviceName = serviceName;
this.log = log;
try {
this.service = new service();
} catch (e) {
this.log.warn("Couldn't instantiate sync service:", (e as Error).message);
}
}
ready() {
return this.service !== undefined;
}
async getUserLastBooking(user: { email: string }): Promise<{ booking: { createdAt: Date } | null } | null> {
return await webPrisma.attendee.findFirst({
where: {
email: user.email,
},
select: {
booking: {
select: {
createdAt: true,
},
},
},
orderBy: {
booking: {
createdAt: "desc",
},
},
});
}
}
export interface ISyncServices {
new (): ISyncService;
}

View File

@@ -0,0 +1,168 @@
import logger from "@calcom/lib/logger";
import type { MembershipRole } from "@calcom/prisma/enums";
import { safeStringify } from "../safeStringify";
import type { ConsoleUserInfoType, TeamInfoType, WebUserInfoType } from "./ISyncService";
import services from "./services";
import CloseComService from "./services/CloseComService";
const log = logger.getSubLogger({ prefix: [`[[SyncServiceManager] `] });
export const createConsoleUser = async (user: ConsoleUserInfoType | null | undefined) => {
if (user) {
log.debug("createConsoleUser", safeStringify({ user }));
try {
Promise.all(
services.map(async (serviceClass) => {
const service = new serviceClass();
if (service.ready()) {
if (service.console.user.upsert) {
await service.console.user.upsert(user);
} else {
await service.console.user.create(user);
}
}
})
);
} catch (e) {
log.warn("createConsoleUser", safeStringify({ error: e }));
}
} else {
log.warn("createConsoleUser:noUser", safeStringify({ user }));
}
};
export const createWebUser = async (user: WebUserInfoType | null | undefined) => {
if (user) {
log.debug("createWebUser", safeStringify({ user }));
try {
Promise.all(
services.map(async (serviceClass) => {
const service = new serviceClass();
if (service.ready()) {
if (service.web.user.upsert) {
await service.web.user.upsert(user);
} else {
await service.web.user.create(user);
}
}
})
);
} catch (e) {
log.warn("createWebUser", safeStringify({ error: e }));
}
} else {
log.warn("createWebUser:noUser", safeStringify({ user }));
}
};
export const updateWebUser = async (user: WebUserInfoType | null | undefined) => {
if (user) {
log.debug("updateWebUser", safeStringify({ user }));
try {
Promise.all(
services.map(async (serviceClass) => {
const service = new serviceClass();
if (service.ready()) {
if (service.web.user.upsert) {
await service.web.user.upsert(user);
} else {
await service.web.user.update(user);
}
}
})
);
} catch (e) {
log.warn("updateWebUser", safeStringify({ error: e }));
}
} else {
log.warn("updateWebUser:noUser", safeStringify({ user }));
}
};
export const deleteWebUser = async (user: WebUserInfoType | null | undefined) => {
if (user) {
log.debug("deleteWebUser", { user });
try {
Promise.all(
services.map(async (serviceClass) => {
const service = new serviceClass();
if (service.ready()) {
await service.web.user.delete(user);
}
})
);
} catch (e) {
log.warn("deleteWebUser", e);
}
} else {
log.warn("deleteWebUser:noUser", { user });
}
};
export const closeComUpsertTeamUser = async (
team: TeamInfoType,
user: WebUserInfoType | null | undefined,
role: MembershipRole
) => {
if (user && team && role) {
log.debug("closeComUpsertTeamUser", { team, user, role });
try {
const closeComService = new CloseComService();
if (closeComService.ready()) {
await closeComService.web.team.create(team, user, role);
}
} catch (e) {
log.warn("closeComUpsertTeamUser", e);
}
} else {
log.warn("closeComUpsertTeamUser:noTeamOrUserOrRole", { team, user, role });
}
};
export const closeComDeleteTeam = async (team: TeamInfoType) => {
if (team) {
log.debug("closeComDeleteTeamUser", { team });
try {
const closeComService = new CloseComService();
if (closeComService.ready()) {
await closeComService.web.team.delete(team);
}
} catch (e) {
log.warn("closeComDeleteTeamUser", e);
}
} else {
log.warn("closeComDeleteTeamUser:noTeam");
}
};
export const closeComDeleteTeamMembership = async (user: WebUserInfoType | null | undefined) => {
if (user) {
log.debug("closeComDeleteTeamMembership", { user });
try {
const closeComService = new CloseComService();
if (closeComService.ready()) {
await closeComService.web.membership.delete(user);
}
} catch (e) {
log.warn("closeComDeleteTeamMembership", e);
}
} else {
log.warn("closeComDeleteTeamMembership:noUser");
}
};
export const closeComUpdateTeam = async (prevTeam: TeamInfoType, updatedTeam: TeamInfoType) => {
if (prevTeam && updatedTeam) {
try {
const closeComService = new CloseComService();
if (closeComService.ready()) {
await closeComService.web.team.update(prevTeam, updatedTeam);
}
} catch (e) {
log.warn("closeComUpdateTeam", e);
}
} else {
log.warn("closeComUpdateTeam:noPrevTeamOrUpdatedTeam");
}
};

View File

@@ -0,0 +1,143 @@
import type { CloseComFieldOptions, CloseComLead } from "@calcom/lib/CloseCom";
import CloseCom from "@calcom/lib/CloseCom";
import { getCloseComContactIds, getCloseComLeadId, getCustomFieldsIds } from "@calcom/lib/CloseComeUtils";
import logger from "@calcom/lib/logger";
import type { TeamInfoType } from "@calcom/lib/sync/ISyncService";
import SyncServiceCore from "@calcom/lib/sync/ISyncService";
import type { ConsoleUserInfoType, WebUserInfoType } from "@calcom/lib/sync/ISyncService";
import type ISyncService from "@calcom/lib/sync/ISyncService";
import { MembershipRole } from "@calcom/prisma/enums";
// Cal.com Custom Contact Fields
const calComCustomContactFields: CloseComFieldOptions = [
// Field name, field type, required?, multiple values?
["Username", "text", false, false],
["Plan", "text", true, false],
["Last booking", "date", false, false],
["Created at", "date", true, false],
];
const calComSharedFields: CloseComFieldOptions = [["Contact Role", "text", false, false]];
const serviceName = "closecom_service";
export default class CloseComService extends SyncServiceCore implements ISyncService {
protected declare service: CloseCom;
constructor() {
super(serviceName, CloseCom, logger.getSubLogger({ prefix: [`[[sync] ${serviceName}`] }));
}
upsertAnyUser = async (
user: WebUserInfoType | ConsoleUserInfoType,
leadInfo?: CloseComLead,
role?: string
) => {
this.log.debug("sync:closecom:user", { user });
// Get Cal.com Lead
const leadId = await getCloseComLeadId(this.service, leadInfo);
this.log.debug("sync:closecom:user:leadId", { leadId });
// Get Contacts ids: already creates contacts
const [contactId] = await getCloseComContactIds([user], this.service, leadId);
this.log.debug("sync:closecom:user:contactsIds", { contactId });
// Get Custom Contact fields ids
const customFieldsIds = await getCustomFieldsIds("contact", calComCustomContactFields, this.service);
this.log.debug("sync:closecom:user:customFieldsIds", { customFieldsIds });
// Get shared fields ids
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 });
const lastBooking = "email" in user ? await this.getUserLastBooking(user) : null;
this.log.debug("sync:closecom:user:lastBooking", { lastBooking });
const username = "username" in user ? user.username : null;
// Prepare values for each Custom Contact Fields
const allFieldsValues = [
username, // Username
user.plan, // Plan
lastBooking && lastBooking.booking
? new Date(lastBooking.booking.createdAt).toLocaleDateString("en-US")
: null, // Last Booking
user.createdDate,
role === MembershipRole.OWNER ? "Point of Contact" : "",
];
this.log.debug("sync:closecom:contact:allFieldsValues", { allFieldsValues });
// Preparing Custom Activity Instance data for Close.com
const person = Object.assign(
{},
{
person: user,
lead_id: leadId,
contactId,
},
...allFields.map((fieldId: string, index: number) => {
return {
[`custom.${fieldId}`]: allFieldsValues[index],
};
})
);
// Create Custom Activity type instance
return await this.service.contact.update(person);
};
public console = {
user: {
upsert: async (consoleUser: ConsoleUserInfoType) => {
return this.upsertAnyUser(consoleUser);
},
},
};
public web = {
user: {
upsert: async (webUser: WebUserInfoType) => {
return this.upsertAnyUser(webUser);
},
delete: async (webUser: WebUserInfoType) => {
this.log.debug("sync:closecom:web:user:delete", { 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);
} else {
throw Error("Web user not found in service");
}
},
},
team: {
create: async (team: TeamInfoType, webUser: WebUserInfoType, role: MembershipRole) => {
return this.upsertAnyUser(
webUser,
{
companyName: team.name,
},
role
);
},
delete: async (team: TeamInfoType) => {
this.log.debug("sync:closecom:web:team:delete", { team });
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 getCloseComLeadId(this.service, { companyName: prevTeam.name });
this.log.debug("sync:closecom:web:team:update:leadId", { leadId });
this.service.lead.update(leadId, { companyName: updatedTeam.name });
},
},
membership: {
delete: async (webUser: WebUserInfoType) => {
this.log.debug("sync:closecom:web:membership:delete", { 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);
} else {
throw Error("Web user not found in service");
}
},
},
};
}

View File

@@ -0,0 +1,101 @@
import logger from "@calcom/lib/logger";
import type { SendgridFieldOptions, SendgridNewContact } from "../../Sendgrid";
import Sendgrid from "../../Sendgrid";
import type { ConsoleUserInfoType, WebUserInfoType } from "../ISyncService";
import type ISyncService from "../ISyncService";
import SyncServiceCore from "../ISyncService";
// Cal.com Custom Contact Fields
const calComCustomContactFields: SendgridFieldOptions = [
// Field name, field type
["username", "Text"],
["plan", "Text"],
["last_booking", "Date"], // Sendgrid custom fields only allow alphanumeric characters (letters A-Z, numbers 0-9) and underscores.
["createdAt", "Date"],
];
const serviceName = "sendgrid_service";
export default class SendgridService extends SyncServiceCore implements ISyncService {
protected declare service: Sendgrid;
constructor() {
super(serviceName, Sendgrid, logger.getSubLogger({ prefix: [`[[sync] ${serviceName}`] }));
}
upsert = async (user: WebUserInfoType | ConsoleUserInfoType) => {
this.log.debug("sync:sendgrid:user", user);
// Get Custom Contact fields ids
const customFieldsIds = await this.service.getSendgridCustomFieldsIds(calComCustomContactFields);
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);
const username = "username" in user ? user.username : null;
// Prepare values for each Custom Contact Fields
const customContactFieldsValues = [
username, // Username
user.plan, // Plan
lastBooking && lastBooking.booking
? new Date(lastBooking.booking.createdAt).toLocaleDateString("en-US")
: null, // Last Booking
user.createdDate,
];
this.log.debug("sync:sendgrid:contact:customContactFieldsValues", customContactFieldsValues);
// Preparing Custom Activity Instance data for Sendgrid
const contactData = {
first_name: user.name,
email: user.email,
custom_fields: Object.assign(
{},
...customFieldsIds.map((fieldId: string, index: number) => {
if (customContactFieldsValues[index] !== null) {
return {
[fieldId]: customContactFieldsValues[index],
};
}
})
),
};
this.log.debug("sync:sendgrid:contact:contactData", contactData);
const newContact = await this.service.sendgridRequest<SendgridNewContact>({
url: `/v3/marketing/contacts`,
method: "PUT",
body: {
contacts: [contactData],
},
});
// Create contact
this.log.debug("sync:sendgrid:contact:newContact", newContact);
return newContact;
};
public console = {
user: {
upsert: async (consoleUser: ConsoleUserInfoType) => {
return this.upsert(consoleUser);
},
},
};
public web = {
user: {
upsert: async (webUser: WebUserInfoType) => {
return this.upsert(webUser);
},
delete: async (webUser: WebUserInfoType) => {
const [contactId] = await this.service.getSendgridContactId(webUser.email);
if (contactId) {
return this.service.sendgridRequest({
url: `/v3/marketing/contacts`,
method: "DELETE",
qs: {
ids: contactId.id,
},
});
} else {
throw Error("Web user not found in service");
}
},
},
};
}

View File

@@ -0,0 +1,10 @@
import type { ISyncServices } from "../ISyncService";
import SendgridService from "./SendgridService";
const services: ISyncServices[] = [
//CloseComService, This service gets a special treatment after deciding it shouldn't get the same treatment as Sendgrid
// eslint-disable-next-line @typescript-eslint/no-explicit-any
SendgridService as any,
];
export default services;