2
0

feat(editor): Add send email integration

This commit is contained in:
Baptiste Arnaud
2022-02-07 18:06:37 +01:00
parent f4336b83cc
commit d6238b3474
48 changed files with 2119 additions and 2606 deletions

View File

@@ -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]
)

View File

@@ -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]
)

View File

@@ -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]
)

View File

@@ -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
}

View File

@@ -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

View File

@@ -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,
}
}, {})

View File

@@ -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

View 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 }
}

View File

@@ -3,3 +3,4 @@ export * from './publicTypebot'
export * from './result'
export * from './answer'
export * from './utils'
export * from './credentials'

View File

@@ -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: [],
}

View File

@@ -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'

View File

@@ -25,7 +25,7 @@ export default [
],
plugins: [
peerDepsExternal(),
resolve(),
resolve({ preferBuiltins: true }),
commonjs(),
typescript({ tsconfig: './tsconfig.json' }),
],

View 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()
)
}

View File

@@ -1,2 +1,3 @@
export * from './utils'
export * from './apiUtils'
export * from './encryption'