feat(editor): ✨ Add send email integration
This commit is contained in:
@@ -22,8 +22,7 @@ export const ImageBubble = ({ step, onTransitionEnd }: Props) => {
|
||||
const [isTyping, setIsTyping] = useState(true)
|
||||
|
||||
const url = useMemo(
|
||||
() =>
|
||||
parseVariables({ text: step.content?.url, variables: typebot.variables }),
|
||||
() => parseVariables(typebot.variables)(step.content?.url),
|
||||
[step.content?.url, typebot.variables]
|
||||
)
|
||||
|
||||
|
||||
@@ -26,8 +26,7 @@ export const TextBubble = ({ step, onTransitionEnd }: Props) => {
|
||||
const [isTyping, setIsTyping] = useState(true)
|
||||
|
||||
const content = useMemo(
|
||||
() =>
|
||||
parseVariables({ text: step.content.html, variables: typebot.variables }),
|
||||
() => parseVariables(typebot.variables)(step.content.html),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[typebot.variables]
|
||||
)
|
||||
|
||||
@@ -85,7 +85,7 @@ const VideoContent = ({
|
||||
variables: Variable[]
|
||||
}) => {
|
||||
const url = useMemo(
|
||||
() => parseVariables({ text: content?.url, variables: variables }),
|
||||
() => parseVariables(variables)(content?.url),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[variables]
|
||||
)
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
GoogleSheetsGetOptions,
|
||||
GoogleAnalyticsStep,
|
||||
WebhookStep,
|
||||
SendEmailStep,
|
||||
} from 'models'
|
||||
import { stringify } from 'qs'
|
||||
import { sendRequest } from 'utils'
|
||||
@@ -25,7 +26,7 @@ export const executeIntegration = (
|
||||
variables: Variable[],
|
||||
indices: Indices,
|
||||
updateVariableValue: (variableId: string, value: string) => void
|
||||
) => {
|
||||
): Promise<string | undefined> => {
|
||||
switch (step.type) {
|
||||
case IntegrationStepType.GOOGLE_SHEETS:
|
||||
return executeGoogleSheetIntegration(step, variables, updateVariableValue)
|
||||
@@ -39,6 +40,8 @@ export const executeIntegration = (
|
||||
indices,
|
||||
updateVariableValue
|
||||
)
|
||||
case IntegrationStepType.EMAIL:
|
||||
return sendEmail(step, variables)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,10 +49,11 @@ export const executeGoogleAnalyticsIntegration = async (
|
||||
step: GoogleAnalyticsStep,
|
||||
variables: Variable[]
|
||||
) => {
|
||||
if (!step.options?.trackingId) return
|
||||
if (!step.options?.trackingId) return step.outgoingEdgeId
|
||||
const { default: initGoogleAnalytics } = await import('../../lib/gtag')
|
||||
await initGoogleAnalytics(step.options.trackingId)
|
||||
sendGaEvent(parseVariablesInObject(step.options, variables))
|
||||
return step.outgoingEdgeId
|
||||
}
|
||||
|
||||
const executeGoogleSheetIntegration = async (
|
||||
@@ -100,10 +104,7 @@ const updateRowInGoogleSheets = async (
|
||||
values: parseCellValues(options.cellsToUpsert, variables),
|
||||
referenceCell: {
|
||||
column: options.referenceCell.column,
|
||||
value: parseVariables({
|
||||
text: options.referenceCell.value ?? '',
|
||||
variables,
|
||||
}),
|
||||
value: parseVariables(variables)(options.referenceCell.value ?? ''),
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -120,10 +121,7 @@ const getRowFromGoogleSheets = async (
|
||||
credentialsId: options.credentialsId,
|
||||
referenceCell: {
|
||||
column: options.referenceCell.column,
|
||||
value: parseVariables({
|
||||
text: options.referenceCell.value ?? '',
|
||||
variables,
|
||||
}),
|
||||
value: parseVariables(variables)(options.referenceCell.value ?? ''),
|
||||
},
|
||||
columns: options.cellsToExtract.map((cell) => cell.column),
|
||||
},
|
||||
@@ -147,7 +145,7 @@ const parseCellValues = (
|
||||
? row
|
||||
: {
|
||||
...row,
|
||||
[cell.column]: parseVariables({ text: cell.value, variables }),
|
||||
[cell.column]: parseVariables(variables)(cell.value),
|
||||
}
|
||||
}, {})
|
||||
|
||||
@@ -174,3 +172,19 @@ const executeWebhook = async (
|
||||
updateVariableValue(varMapping.variableId, value)
|
||||
})
|
||||
}
|
||||
|
||||
const sendEmail = async (step: SendEmailStep, variables: Variable[]) => {
|
||||
const { options } = step
|
||||
const { error } = await sendRequest({
|
||||
url: `http://localhost:3001/api/integrations/email`,
|
||||
method: 'POST',
|
||||
body: {
|
||||
credentialsId: options.credentialsId,
|
||||
recipients: options.recipients.map(parseVariables(variables)),
|
||||
subject: parseVariables(variables)(options.subject ?? ''),
|
||||
body: parseVariables(variables)(options.body ?? ''),
|
||||
},
|
||||
})
|
||||
console.error(error)
|
||||
return step.outgoingEdgeId
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ const executeSetVariable = (
|
||||
return step.outgoingEdgeId
|
||||
const expression = step.options.expressionToEvaluate
|
||||
const evaluatedExpression = isMathFormula(expression)
|
||||
? evaluateExpression(parseVariables({ text: expression, variables }))
|
||||
? evaluateExpression(parseVariables(variables)(expression))
|
||||
: expression
|
||||
updateVariableValue(step.options.variableId, evaluatedExpression)
|
||||
return step.outgoingEdgeId
|
||||
@@ -92,7 +92,7 @@ const executeRedirect = (
|
||||
): EdgeId | undefined => {
|
||||
if (!step.options?.url) return step.outgoingEdgeId
|
||||
window.open(
|
||||
sanitizeUrl(parseVariables({ text: step.options?.url, variables })),
|
||||
sanitizeUrl(parseVariables(variables)(step.options?.url)),
|
||||
step.options.isNewTab ? '_blank' : '_self'
|
||||
)
|
||||
return step.outgoingEdgeId
|
||||
|
||||
@@ -6,23 +6,19 @@ const safeEval = eval
|
||||
export const stringContainsVariable = (str: string): boolean =>
|
||||
/\{\{(.*?)\}\}/g.test(str)
|
||||
|
||||
export const parseVariables = ({
|
||||
text,
|
||||
variables,
|
||||
}: {
|
||||
text?: string
|
||||
variables: Variable[]
|
||||
}): string => {
|
||||
if (!text || text === '') return ''
|
||||
return text.replace(/\{\{(.*?)\}\}/g, (_, fullVariableString) => {
|
||||
const matchedVarName = fullVariableString.replace(/{{|}}/g, '')
|
||||
return (
|
||||
variables.find((v) => {
|
||||
return matchedVarName === v.name && isDefined(v.value)
|
||||
})?.value ?? ''
|
||||
)
|
||||
})
|
||||
}
|
||||
export const parseVariables =
|
||||
(variables: Variable[]) =>
|
||||
(text?: string): string => {
|
||||
if (!text || text === '') return ''
|
||||
return text.replace(/\{\{(.*?)\}\}/g, (_, fullVariableString) => {
|
||||
const matchedVarName = fullVariableString.replace(/{{|}}/g, '')
|
||||
return (
|
||||
variables.find((v) => {
|
||||
return matchedVarName === v.name && isDefined(v.value)
|
||||
})?.value ?? ''
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export const isMathFormula = (str?: string) =>
|
||||
['*', '/', '+', '-'].some((val) => str && str.includes(val))
|
||||
@@ -58,7 +54,7 @@ export const parseVariablesInObject = (
|
||||
...newObj,
|
||||
[key]:
|
||||
typeof currentValue === 'string'
|
||||
? parseVariables({ text: currentValue, variables })
|
||||
? parseVariables(variables)(currentValue)
|
||||
: currentValue,
|
||||
}
|
||||
}, {})
|
||||
|
||||
@@ -56,17 +56,14 @@ model Credentials {
|
||||
id String @id @default(cuid())
|
||||
ownerId String
|
||||
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
|
||||
data Json
|
||||
data String // Encrypted data
|
||||
name String
|
||||
type CredentialsType
|
||||
type String
|
||||
iv String
|
||||
|
||||
@@unique([name, type, ownerId])
|
||||
}
|
||||
|
||||
enum CredentialsType {
|
||||
GOOGLE_SHEETS
|
||||
}
|
||||
|
||||
enum Plan {
|
||||
FREE
|
||||
PRO
|
||||
|
||||
38
packages/models/src/credentials.ts
Normal file
38
packages/models/src/credentials.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Credentials as CredentialsFromPrisma } from 'db'
|
||||
|
||||
export type Credentials = SmtpCredentials | GoogleSheetsCredentials
|
||||
|
||||
export type CredentialsBase = Omit<CredentialsFromPrisma, 'data' | 'type'>
|
||||
|
||||
export enum CredentialsType {
|
||||
GOOGLE_SHEETS = 'google sheets',
|
||||
SMTP = 'smtp',
|
||||
}
|
||||
|
||||
export type SmtpCredentials = CredentialsBase & {
|
||||
type: CredentialsType.SMTP
|
||||
data: SmtpCredentialsData
|
||||
}
|
||||
|
||||
export type GoogleSheetsCredentials = CredentialsBase & {
|
||||
type: CredentialsType.GOOGLE_SHEETS
|
||||
data: GoogleSheetsCredentialsData
|
||||
}
|
||||
|
||||
export type GoogleSheetsCredentialsData = {
|
||||
refresh_token?: string | null
|
||||
expiry_date?: number | null
|
||||
access_token?: string | null
|
||||
token_type?: string | null
|
||||
id_token?: string | null
|
||||
scope?: string
|
||||
}
|
||||
|
||||
export type SmtpCredentialsData = {
|
||||
host?: string
|
||||
username?: string
|
||||
password?: string
|
||||
isTlsEnabled?: boolean
|
||||
port: number
|
||||
from: { email?: string; name?: string }
|
||||
}
|
||||
@@ -3,3 +3,4 @@ export * from './publicTypebot'
|
||||
export * from './result'
|
||||
export * from './answer'
|
||||
export * from './utils'
|
||||
export * from './credentials'
|
||||
|
||||
@@ -4,16 +4,19 @@ export type IntegrationStep =
|
||||
| GoogleSheetsStep
|
||||
| GoogleAnalyticsStep
|
||||
| WebhookStep
|
||||
| SendEmailStep
|
||||
|
||||
export type IntegrationStepOptions =
|
||||
| GoogleSheetsOptions
|
||||
| GoogleAnalyticsOptions
|
||||
| WebhookOptions
|
||||
| SendEmailOptions
|
||||
|
||||
export enum IntegrationStepType {
|
||||
GOOGLE_SHEETS = 'Google Sheets',
|
||||
GOOGLE_ANALYTICS = 'Google Analytics',
|
||||
WEBHOOK = 'Webhook',
|
||||
EMAIL = 'Email',
|
||||
}
|
||||
|
||||
export type GoogleSheetsStep = StepBase & {
|
||||
@@ -32,6 +35,18 @@ export type WebhookStep = StepBase & {
|
||||
webhook: Webhook
|
||||
}
|
||||
|
||||
export type SendEmailStep = StepBase & {
|
||||
type: IntegrationStepType.EMAIL
|
||||
options: SendEmailOptions
|
||||
}
|
||||
|
||||
export type SendEmailOptions = {
|
||||
credentialsId: string | 'default'
|
||||
recipients: string[]
|
||||
subject?: string
|
||||
body?: string
|
||||
}
|
||||
|
||||
export type GoogleAnalyticsOptions = {
|
||||
trackingId?: string
|
||||
category?: string
|
||||
@@ -142,3 +157,8 @@ export const defaultWebhookAttributes: Omit<Webhook, 'id'> = {
|
||||
headers: [],
|
||||
queryParams: [],
|
||||
}
|
||||
|
||||
export const defaultSendEmailOptions: SendEmailOptions = {
|
||||
credentialsId: 'default',
|
||||
recipients: [],
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
RedirectStep,
|
||||
SetVariableStep,
|
||||
} from '.'
|
||||
import { Edge } from '..'
|
||||
import { BubbleStep, BubbleStepType } from './bubble'
|
||||
import { InputStep, InputStepType } from './inputs'
|
||||
import { IntegrationStep } from './integration'
|
||||
|
||||
@@ -25,7 +25,7 @@ export default [
|
||||
],
|
||||
plugins: [
|
||||
peerDepsExternal(),
|
||||
resolve(),
|
||||
resolve({ preferBuiltins: true }),
|
||||
commonjs(),
|
||||
typescript({ tsconfig: './tsconfig.json' }),
|
||||
],
|
||||
|
||||
36
packages/utils/src/encryption.ts
Normal file
36
packages/utils/src/encryption.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { randomBytes, createCipheriv, createDecipheriv } from 'crypto'
|
||||
|
||||
const algorithm = 'aes-256-gcm'
|
||||
const secretKey = process.env.SECRET
|
||||
|
||||
export const encrypt = (
|
||||
data: object
|
||||
): { encryptedData: string; iv: string } => {
|
||||
if (!secretKey) throw new Error(`SECRET is not in environment`)
|
||||
const iv = randomBytes(16)
|
||||
const cipher = createCipheriv(algorithm, secretKey, iv)
|
||||
const dataString = JSON.stringify(data)
|
||||
const encryptedData =
|
||||
cipher.update(dataString, 'utf8', 'hex') + cipher.final('hex')
|
||||
const tag = cipher.getAuthTag()
|
||||
return {
|
||||
encryptedData,
|
||||
iv: iv.toString('hex') + '.' + tag.toString('hex'),
|
||||
}
|
||||
}
|
||||
|
||||
export const decrypt = (encryptedData: string, auth: string): object => {
|
||||
if (!secretKey) throw new Error(`SECRET is not in environment`)
|
||||
const [iv, tag] = auth.split('.')
|
||||
const decipher = createDecipheriv(
|
||||
algorithm,
|
||||
secretKey,
|
||||
Buffer.from(iv, 'hex')
|
||||
)
|
||||
decipher.setAuthTag(Buffer.from(tag, 'hex'))
|
||||
return JSON.parse(
|
||||
(
|
||||
decipher.update(Buffer.from(encryptedData, 'hex')) + decipher.final('hex')
|
||||
).toString()
|
||||
)
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './utils'
|
||||
export * from './apiUtils'
|
||||
export * from './encryption'
|
||||
|
||||
Reference in New Issue
Block a user