chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
import { z, ActionDefinition } from '@botpress/sdk'
|
||||
|
||||
export const contactSchema = z.object({
|
||||
id: z.number().describe('Contact ID'),
|
||||
name: z.string().describe('Contact name'),
|
||||
firstName: z.string().describe('First name'),
|
||||
lastName: z.string().describe('Last name'),
|
||||
responsibleUserId: z.number().describe('User responsible for this contact'),
|
||||
groupId: z.number().describe('Group ID'),
|
||||
updatedBy: z.number().describe('User who last updated this contact'),
|
||||
createdAt: z.number().describe('When created (Unix timestamp)'),
|
||||
updatedAt: z.number().describe('When last updated (Unix timestamp)'),
|
||||
closestTaskAt: z.number().optional().describe('Closest task timestamp'),
|
||||
isDeleted: z.boolean().describe('Whether contact is deleted'),
|
||||
accountId: z.number().describe('Account ID'),
|
||||
})
|
||||
|
||||
const createContact: ActionDefinition = {
|
||||
title: 'Create Contact',
|
||||
description: 'Creates a new contact',
|
||||
input: {
|
||||
schema: z.object({
|
||||
name: z.string().optional().title('Full contact name').describe('Full contact name'),
|
||||
firstName: z.string().optional().title('First name').describe('First name'),
|
||||
lastName: z.string().optional().title('Last name').describe('Last name'),
|
||||
responsibleUserId: z.number().title('Responsible User ID').describe('User ID to assign this contact to'),
|
||||
createdBy: z.number().title('Created By').describe('User ID who creates this contact'),
|
||||
updatedBy: z.number().optional().title('Updated By').describe('User ID who updates this contact'),
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
schema: z.object({
|
||||
contact: contactSchema.describe('The created contact'),
|
||||
}),
|
||||
},
|
||||
}
|
||||
const searchContacts: ActionDefinition = {
|
||||
title: 'Search Contacts',
|
||||
description: 'Search for contacts by name, phone number, or email',
|
||||
input: {
|
||||
schema: z.object({
|
||||
query: z.string().describe('Search query (name, phone number, or email)'),
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
schema: z.object({
|
||||
contacts: z.array(contactSchema).describe('Array of matching contacts (empty if none found)'),
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
createContact,
|
||||
searchContacts,
|
||||
} as const
|
||||
@@ -0,0 +1,8 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import { actions as contactActions } from './contact'
|
||||
import { actions as leadActions } from './lead'
|
||||
|
||||
export const actions = {
|
||||
...leadActions,
|
||||
...contactActions,
|
||||
} as const satisfies sdk.IntegrationDefinitionProps['actions']
|
||||
@@ -0,0 +1,74 @@
|
||||
import { z, ActionDefinition } from '@botpress/sdk'
|
||||
|
||||
// Lead Schema: defines what a Kommo lead looks like in Botpress
|
||||
export const leadSchema = z.object({
|
||||
id: z.number().describe('Lead ID'),
|
||||
name: z.string().describe('Lead name'),
|
||||
price: z.number().optional().describe('Lead value in dollars'),
|
||||
responsibleUserId: z.number().optional().describe('User responsible for this lead'),
|
||||
pipelineId: z.number().optional().describe('Which sales pipeline'),
|
||||
statusId: z.number().optional().describe('Which stage in the pipeline'),
|
||||
createdAt: z.number().describe('When created (Unix timestamp)'),
|
||||
updatedAt: z.number().describe('When last updated (Unix timestamp)'),
|
||||
})
|
||||
|
||||
const createLead: ActionDefinition = {
|
||||
title: 'Create Lead',
|
||||
description: 'Creates a new lead in Kommo CRM',
|
||||
input: {
|
||||
schema: z.object({
|
||||
name: z.string().title('Lead name').describe('Lead name (required)'),
|
||||
price: z.number().optional().title('Lead value').describe('Lead value in dollars'),
|
||||
responsibleUserId: z.number().optional().title('Responsible User ID').describe('User ID to assign this lead to'),
|
||||
pipelineId: z.number().optional().title('Pipeline ID').describe('Pipeline ID (defaults to main pipeline)'),
|
||||
statusId: z.number().optional().title('Status ID').describe('Initial status/stage ID'),
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
schema: z.object({
|
||||
lead: leadSchema.describe('The created lead'),
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
const updateLead: ActionDefinition = {
|
||||
title: 'Update Lead',
|
||||
description: 'Updates an existing lead in Kommo',
|
||||
input: {
|
||||
schema: z.object({
|
||||
leadId: z.number().title('Lead ID').describe('Lead ID to update'),
|
||||
name: z.string().optional().title('New name').describe('New name'),
|
||||
price: z.number().optional().title('New price').describe('New price'),
|
||||
responsibleUserId: z.number().optional().title('New responsible user').describe('New responsible user'),
|
||||
pipelineId: z.number().optional().title('New pipeline ID').describe('New pipeline ID'),
|
||||
statusId: z.number().optional().title('New status/stage ID').describe('New status/stage ID'),
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
schema: z.object({
|
||||
lead: leadSchema.describe('The updated lead'),
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
const searchLeads: ActionDefinition = {
|
||||
title: 'Search Leads',
|
||||
description: 'search for leads by name or other fields',
|
||||
input: {
|
||||
schema: z.object({
|
||||
query: z.string().describe('Search query'),
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
schema: z.object({
|
||||
leads: z.array(leadSchema).describe('Array of matching leads (empty if none found)'),
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
// Export all lead actions
|
||||
export const actions = {
|
||||
createLead,
|
||||
searchLeads,
|
||||
updateLead,
|
||||
} as const
|
||||
@@ -0,0 +1,2 @@
|
||||
// Re-export everything from definitions
|
||||
export * from './actions'
|
||||
@@ -0,0 +1,39 @@
|
||||
# Kommo Integration
|
||||
|
||||
The Kommo integration allows you to connect your Botpress chatbot with Kommo. With this integration, your chatbot can create and update leads, manage contacts, and search through your CRM data.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before using this integration, you need:
|
||||
|
||||
1. A **Kommo account** with administrator access
|
||||
2. A **private integration** created in your Kommo account
|
||||
3. A **long-lived access token** from your private integration
|
||||
|
||||
## Configuration
|
||||
|
||||
To set up the Kommo integration in Botpress:
|
||||
|
||||
### Step 1: Create a Private Integration in Kommo
|
||||
|
||||
1. Log in to your Kommo account.
|
||||
2. Navigate to **Settings** → **Integrations marketplace**.
|
||||
3. Click **Create Integration** at the top-right corner.
|
||||
4. Give your integration a name.
|
||||
5. Select all scopes.
|
||||
6. Save the integration.
|
||||
|
||||
### Step 2: Generate an Access Token
|
||||
|
||||
1. In your integration settings, click **Generate long-lived token**
|
||||
2. Select an expiration date.
|
||||
3. Copy the **long-lived token**.
|
||||
|
||||
### Step 3: Configure Botpress Integration
|
||||
|
||||
In your Botpress integration settings, provide:
|
||||
|
||||
- **Base Domain**: Your Kommo subdomain (e.g., `yourcompany.kommo.com`)
|
||||
- Do not include `https://` or trailing slashes
|
||||
- Example: `mycompany.kommo.com`
|
||||
- **Access Token**: Paste the long-lived access token from Step 2
|
||||
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="1200.000000pt" height="1200.000000pt" viewBox="0 0 1200.000000 1200.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.16, written by Peter Selinger 2001-2019
|
||||
</metadata>
|
||||
<rect width="1200" height="1200" rx="150" ry="150" fill="white"/>
|
||||
<g transform="translate(0.000000,1200.000000) scale(0.100000,-0.100000)"
|
||||
fill="#6043D0" stroke="none">
|
||||
<path d="M815 11983 c-201 -43 -364 -132 -515 -283 -118 -119 -183 -216 -236
|
||||
-358 -70 -188 -64 309 -64 -5342 0 -5651 -6 -5154 64 -5342 53 -142 118 -239
|
||||
236 -358 119 -118 216 -183 358 -236 188 -70 -309 -64 5342 -64 5651 0 5154
|
||||
-6 5342 64 142 53 239 118 358 236 118 119 183 216 236 358 70 188 64 -309 64
|
||||
5342 0 5651 6 5154 -64 5342 -53 142 -118 239 -236 358 -119 118 -216 183
|
||||
-358 236 -188 70 310 64 -5349 63 -4495 -1 -5116 -2 -5178 -16z m9115 -2191
|
||||
c19 -9 45 -33 58 -52 51 -75 121 35 -1089 -1712 -765 -1104 -1118 -1622 -1151
|
||||
-1688 -26 -52 -57 -131 -68 -175 -82 -321 -25 -608 180 -900 35 -49 124 -178
|
||||
198 -285 75 -107 151 -217 171 -245 19 -27 167 -241 330 -475 280 -404 402
|
||||
-579 656 -945 61 -88 132 -189 156 -225 25 -36 124 -177 219 -315 96 -137 236
|
||||
-339 311 -448 76 -109 141 -212 144 -228 9 -40 -9 -90 -46 -125 l-30 -29 -867
|
||||
0 -867 0 -115 23 c-356 72 -626 217 -880 471 -121 121 -155 163 -305 381 -93
|
||||
135 -199 288 -235 340 -37 52 -254 365 -482 695 -943 1365 -860 1250 -932
|
||||
1287 -123 63 -280 14 -354 -110 l-27 -47 -5 -1350 -5 -1350 -22 -55 c-51 -124
|
||||
-154 -228 -265 -267 -50 -17 -101 -18 -793 -18 l-740 0 -78 37 c-130 62 -218
|
||||
174 -247 314 -8 36 -10 373 -7 1106 4 1130 2 1072 56 1285 145 566 596 1028
|
||||
1164 1192 185 54 259 63 564 68 244 5 285 8 310 23 16 10 109 133 214 286 103
|
||||
148 352 508 554 799 202 292 612 884 912 1316 300 433 576 822 616 866 290
|
||||
326 685 524 1127 566 19 2 395 5 835 5 744 2 802 1 835 -16z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,25 @@
|
||||
import { z, IntegrationDefinition } from '@botpress/sdk'
|
||||
import { actions } from './definitions'
|
||||
|
||||
export default new IntegrationDefinition({
|
||||
name: 'kommo',
|
||||
title: 'Kommo',
|
||||
description: 'Manage leads and contacts in your Kommo CRM directly from your chatbot.',
|
||||
version: '0.1.1',
|
||||
readme: 'hub.md',
|
||||
icon: 'icon.svg',
|
||||
configuration: {
|
||||
schema: z.object({
|
||||
baseDomain: z.string().title('Subdomain').describe('Your Kommo subdomain (e.g., yourcompany.kommo.com)'),
|
||||
accessToken: z
|
||||
.string()
|
||||
.title('Access token')
|
||||
.describe('Long-lived access token from your Kommo private integration'),
|
||||
}),
|
||||
},
|
||||
actions,
|
||||
attributes: {
|
||||
category: 'CRM & Sales',
|
||||
repo: 'botpress',
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "@botpresshub/kommo",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "bp add -y && bp build",
|
||||
"check:type": "tsc --noEmit",
|
||||
"check:bplint": "bp lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@botpress/client": "workspace:*",
|
||||
"@botpress/sdk": "workspace:*",
|
||||
"axios": "^1.13.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@botpress/cli": "workspace:*"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import { KommoClient, CreateContactRequest, KommoContact, getErrorMessage } from '../kommo-api'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
// mapping kommo to local schema
|
||||
function mapKommoContactToBotpress(contact: KommoContact) {
|
||||
return {
|
||||
id: contact.id,
|
||||
name: contact.name,
|
||||
firstName: contact.first_name,
|
||||
lastName: contact.last_name,
|
||||
responsibleUserId: contact.responsible_user_id,
|
||||
groupId: contact.group_id,
|
||||
updatedBy: contact.updated_by,
|
||||
createdAt: contact.created_at,
|
||||
updatedAt: contact.updated_at,
|
||||
closestTaskAt: contact.closest_task_at ?? undefined,
|
||||
isDeleted: contact.is_deleted,
|
||||
accountId: contact.account_id,
|
||||
}
|
||||
}
|
||||
|
||||
export const createContact: bp.IntegrationProps['actions']['createContact'] = async ({ ctx, input, logger }) => {
|
||||
try {
|
||||
logger.forBot().info('Creating contact with input:', input)
|
||||
|
||||
const { baseDomain, accessToken } = ctx.configuration
|
||||
const kommoClient = new KommoClient(accessToken, baseDomain, logger)
|
||||
|
||||
const contactData: CreateContactRequest = {
|
||||
name: input.name,
|
||||
first_name: input.firstName,
|
||||
last_name: input.lastName,
|
||||
responsible_user_id: input.responsibleUserId,
|
||||
created_by: input.createdBy,
|
||||
updated_by: input.updatedBy,
|
||||
}
|
||||
|
||||
logger.forBot().info('Contact data to send:', contactData)
|
||||
const kommoContact = await kommoClient.createContact(contactData)
|
||||
logger.forBot().info('Contact created successfully:', { contactId: kommoContact.id })
|
||||
return {
|
||||
contact: mapKommoContactToBotpress(kommoContact),
|
||||
}
|
||||
} catch (error) {
|
||||
logger.forBot().error('Failed to create contact', { error })
|
||||
throw new sdk.RuntimeError(getErrorMessage(error))
|
||||
}
|
||||
}
|
||||
|
||||
export const searchContacts: bp.IntegrationProps['actions']['searchContacts'] = async ({ ctx, input, logger }) => {
|
||||
try {
|
||||
logger.forBot().info('Searching contacts:', { query: input.query })
|
||||
const { baseDomain, accessToken } = ctx.configuration
|
||||
|
||||
const kommoClient = new KommoClient(accessToken, baseDomain, logger)
|
||||
const kommoContacts = await kommoClient.searchContacts(input.query)
|
||||
|
||||
const contacts = kommoContacts.map(mapKommoContactToBotpress)
|
||||
|
||||
logger.forBot().info('Search complete:', { count: contacts.length })
|
||||
|
||||
return { contacts }
|
||||
} catch (error) {
|
||||
logger.forBot().error('Failed to search contacts', { error })
|
||||
throw new sdk.RuntimeError(getErrorMessage(error))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './lead'
|
||||
export * from './contact'
|
||||
@@ -0,0 +1,95 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import { KommoClient, CreateLeadRequest, UpdateLeadRequest, KommoLead, getErrorMessage } from '../kommo-api'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
// mapping kommo to local schema
|
||||
function mapKommoLeadToBotpress(lead: KommoLead) {
|
||||
return {
|
||||
id: lead.id,
|
||||
name: lead.name,
|
||||
price: lead.price,
|
||||
responsibleUserId: lead.responsible_user_id,
|
||||
pipelineId: lead.pipeline_id,
|
||||
statusId: lead.status_id,
|
||||
createdAt: lead.created_at,
|
||||
updatedAt: lead.updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
export const createLead: bp.IntegrationProps['actions']['createLead'] = async ({ ctx, input, logger }) => {
|
||||
try {
|
||||
logger.forBot().debug('Creating lead with input:', input)
|
||||
logger.forBot().debug('Configuration:', { baseDomain: ctx.configuration.baseDomain })
|
||||
|
||||
const { baseDomain, accessToken } = ctx.configuration
|
||||
const kommoClient = new KommoClient(accessToken, baseDomain, logger)
|
||||
|
||||
const leadData: CreateLeadRequest = {
|
||||
name: input.name,
|
||||
price: input.price,
|
||||
responsible_user_id: input.responsibleUserId,
|
||||
pipeline_id: input.pipelineId,
|
||||
status_id: input.statusId,
|
||||
}
|
||||
|
||||
logger.forBot().info('Lead data to send:', leadData)
|
||||
const kommoLead = await kommoClient.createLead(leadData)
|
||||
logger.forBot().info('Lead created successfully:', { leadId: kommoLead.id })
|
||||
|
||||
return {
|
||||
lead: mapKommoLeadToBotpress(kommoLead),
|
||||
}
|
||||
} catch (error) {
|
||||
logger.forBot().error('Error creating lead:', { error })
|
||||
throw new sdk.RuntimeError(getErrorMessage(error))
|
||||
}
|
||||
}
|
||||
|
||||
// handler for updating lead
|
||||
export const updateLead: bp.IntegrationProps['actions']['updateLead'] = async ({ ctx, input, logger }) => {
|
||||
try {
|
||||
logger.forBot().info('Updating lead:', input)
|
||||
|
||||
const { baseDomain, accessToken } = ctx.configuration
|
||||
const kommoClient = new KommoClient(accessToken, baseDomain, logger)
|
||||
|
||||
const updateData: UpdateLeadRequest = {
|
||||
name: input.name,
|
||||
price: input.price,
|
||||
responsible_user_id: input.responsibleUserId,
|
||||
pipeline_id: input.pipelineId,
|
||||
status_id: input.statusId,
|
||||
}
|
||||
|
||||
logger.forBot().info('Update data to send:', updateData)
|
||||
const kommoLead = await kommoClient.updateLead(input.leadId, updateData)
|
||||
logger.forBot().info('Lead updated successfully:', { leadId: kommoLead.id })
|
||||
|
||||
return {
|
||||
lead: mapKommoLeadToBotpress(kommoLead),
|
||||
}
|
||||
} catch (error) {
|
||||
logger.forBot().error('Error updating lead:', { error })
|
||||
throw new sdk.RuntimeError(getErrorMessage(error))
|
||||
}
|
||||
}
|
||||
|
||||
// search for lead action
|
||||
export const searchLeads: bp.IntegrationProps['actions']['searchLeads'] = async ({ ctx, input, logger }) => {
|
||||
try {
|
||||
logger.forBot().info('Searching leads:', { query: input.query })
|
||||
|
||||
const { baseDomain, accessToken } = ctx.configuration
|
||||
const kommoClient = new KommoClient(accessToken, baseDomain, logger)
|
||||
const kommoLeads = await kommoClient.searchLeads(input.query)
|
||||
|
||||
const leads = kommoLeads.map(mapKommoLeadToBotpress)
|
||||
|
||||
logger.forBot().info('Search complete:', { count: leads.length })
|
||||
|
||||
return { leads }
|
||||
} catch (error) {
|
||||
logger.forBot().error('Failed to search leads', { error })
|
||||
throw new sdk.RuntimeError(getErrorMessage(error))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { RuntimeError } from '@botpress/sdk'
|
||||
import { createLead, updateLead, createContact, searchContacts, searchLeads } from './actions'
|
||||
import { KommoClient } from './kommo-api'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
export default new bp.Integration({
|
||||
register: async ({ ctx, logger }) => {
|
||||
const { baseDomain, accessToken } = ctx.configuration
|
||||
const kommoClient = new KommoClient(accessToken, baseDomain, logger)
|
||||
|
||||
try {
|
||||
await kommoClient.searchLeads('')
|
||||
logger.forBot().info('Connection to Kommo Successful')
|
||||
} catch (error) {
|
||||
logger.forBot().error('Connection to Kommo Failed', { error })
|
||||
throw new RuntimeError('failed to connect to Kommo, Please check your access token and subdomain are correct. ')
|
||||
}
|
||||
},
|
||||
unregister: async () => {},
|
||||
|
||||
actions: {
|
||||
createLead,
|
||||
updateLead,
|
||||
searchLeads,
|
||||
createContact,
|
||||
searchContacts,
|
||||
},
|
||||
channels: {},
|
||||
handler: async () => {},
|
||||
})
|
||||
@@ -0,0 +1,69 @@
|
||||
import { z } from '@botpress/sdk'
|
||||
import { isAxiosError } from 'axios'
|
||||
import { KommoErrorResponse } from './types'
|
||||
|
||||
const _isZodError = (error: any): error is z.ZodError => {
|
||||
return error && typeof error === 'object' && z.is.zuiError(error) && 'errors' in error
|
||||
}
|
||||
|
||||
const formatZodErrors = (issues: z.ZodIssue[]) =>
|
||||
'Validation Error: ' +
|
||||
issues
|
||||
.map((issue) => {
|
||||
const path = issue.path?.length ? `${issue.path.join('.')}: ` : ''
|
||||
return path ? `${path}${issue.message}` : issue.message
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
export const getErrorMessage = (err: unknown): string => {
|
||||
if (isAxiosError(err)) {
|
||||
// server dependent error
|
||||
const status = err.response?.status
|
||||
const data = err.response?.data
|
||||
// always present
|
||||
const message = err.message
|
||||
|
||||
if (data && typeof data === 'object') {
|
||||
const kommoError = data as KommoErrorResponse
|
||||
|
||||
if (kommoError.detail) {
|
||||
let errorMsg = kommoError.detail
|
||||
|
||||
if (kommoError.validation_errors && kommoError.validation_errors.length > 0) {
|
||||
const validationDetails = kommoError.validation_errors
|
||||
.flatMap((ve) => ve.errors.map((e) => `${e.path}: ${e.detail}`))
|
||||
.join(', ')
|
||||
errorMsg += ` - ${validationDetails}`
|
||||
}
|
||||
|
||||
return status ? `${errorMsg} (Status: ${status})` : errorMsg
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback for generic axios errors
|
||||
if (typeof data === 'string' && data.trim()) {
|
||||
return status ? `${data} (Status: ${status})` : data
|
||||
}
|
||||
return status ? `${message} (Status: ${status})` : message
|
||||
}
|
||||
|
||||
if (_isZodError(err)) {
|
||||
return formatZodErrors(err.errors)
|
||||
}
|
||||
|
||||
if (err instanceof Error) {
|
||||
return err.message
|
||||
}
|
||||
|
||||
if (typeof err === 'string') {
|
||||
return err
|
||||
}
|
||||
|
||||
if (err && typeof err === 'object' && 'message' in err) {
|
||||
const message = (err as { message: unknown }).message
|
||||
if (typeof message === 'string') {
|
||||
return message
|
||||
}
|
||||
}
|
||||
return 'An unexpected error occurred'
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './types'
|
||||
export * from './kommo-client'
|
||||
export * from './error-handler'
|
||||
@@ -0,0 +1,186 @@
|
||||
import axios, { AxiosInstance } from 'axios'
|
||||
import * as bp from '../../.botpress'
|
||||
import { getErrorMessage } from './error-handler'
|
||||
import {
|
||||
CreateLeadRequest,
|
||||
KommoLead,
|
||||
KommoCreateResponse,
|
||||
UpdateLeadRequest,
|
||||
CreateContactRequest,
|
||||
KommoContact,
|
||||
KommoCreateContactResponse,
|
||||
KommoSearchContactsResponse,
|
||||
KommoSearchLeadResponse,
|
||||
} from './types'
|
||||
|
||||
export class KommoClient {
|
||||
private _axios: AxiosInstance
|
||||
private _logger: bp.Logger
|
||||
|
||||
public constructor(accessToken: string, baseDomain: string, logger: bp.Logger) {
|
||||
this._logger = logger
|
||||
|
||||
// Ensure basedomain doesn't have protocol prefix or trailing slash
|
||||
const cleanDomain = baseDomain.replace(/^https?:\/\//, '').replace(/\/$/, '')
|
||||
|
||||
// Create axios instance with Kommo API configuration
|
||||
this._axios = axios.create({
|
||||
baseURL: `https://${cleanDomain}/api/v4`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
this._logger.forBot().debug('KommoClient initialized', {
|
||||
baseURL: `https://${cleanDomain}/api/v4`,
|
||||
})
|
||||
}
|
||||
|
||||
// -----LEADS------
|
||||
public async createLead(data: CreateLeadRequest): Promise<KommoLead> {
|
||||
try {
|
||||
this._logger.forBot().debug('Creating lead in Kommo', { name: data.name })
|
||||
const response = await this._axios.post<KommoCreateResponse>('/leads', [data])
|
||||
const createdLeadId = response.data._embedded.leads[0]?.id
|
||||
|
||||
if (!createdLeadId) {
|
||||
throw new Error('No lead ID returned from Kommo')
|
||||
}
|
||||
|
||||
const lead = await this.getLead(createdLeadId)
|
||||
|
||||
if (!lead) {
|
||||
throw new Error('Failed to fetch created lead')
|
||||
}
|
||||
|
||||
this._logger.forBot().info('Lead created successfully', { leadId: lead.id })
|
||||
return lead
|
||||
} catch (error) {
|
||||
throw new Error(getErrorMessage(error))
|
||||
}
|
||||
}
|
||||
|
||||
// gets a lead by id
|
||||
public async getLead(leadId: number): Promise<KommoLead | undefined> {
|
||||
try {
|
||||
this._logger.forBot().debug('Fetching lead from Kommo', { leadId })
|
||||
|
||||
const response = await this._axios.get<KommoLead>(`/leads/${leadId}`)
|
||||
const lead = response.data
|
||||
|
||||
return lead
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error) && error.response?.status === 404) {
|
||||
this._logger.forBot().info('Lead not found', { leadId })
|
||||
return undefined
|
||||
}
|
||||
|
||||
throw new Error(getErrorMessage(error))
|
||||
}
|
||||
}
|
||||
|
||||
// update a single lead
|
||||
public async updateLead(leadId: number, data: UpdateLeadRequest): Promise<KommoLead> {
|
||||
try {
|
||||
this._logger.forBot().debug('Updating lead in Kommo', { leadId, data })
|
||||
await this._axios.patch(`/leads/${leadId}`, data)
|
||||
const lead = await this.getLead(leadId)
|
||||
|
||||
if (!lead) {
|
||||
throw new Error('Failed to fetch updated lead')
|
||||
}
|
||||
|
||||
this._logger.forBot().info('Lead updated successfully', { leadId: lead.id })
|
||||
return lead
|
||||
} catch (error) {
|
||||
throw new Error(getErrorMessage(error))
|
||||
}
|
||||
}
|
||||
|
||||
// search leads by query
|
||||
public async searchLeads(query: string): Promise<KommoLead[]> {
|
||||
try {
|
||||
this._logger.forBot().debug('Searching for leads', { query })
|
||||
const response = await this._axios.get<KommoSearchLeadResponse>('/leads', {
|
||||
params: { query },
|
||||
})
|
||||
|
||||
const leads = response.data._embedded?.leads || []
|
||||
|
||||
this._logger.forBot().info('Leads found:', { count: leads.length })
|
||||
return leads
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error) && error.response?.status === 404) {
|
||||
this._logger.forBot().info('No leads found', { query })
|
||||
return []
|
||||
}
|
||||
throw new Error(getErrorMessage(error))
|
||||
}
|
||||
}
|
||||
// -----Contacts-----
|
||||
public async createContact(data: CreateContactRequest): Promise<KommoContact> {
|
||||
try {
|
||||
this._logger.forBot().debug('Creating contact in Kommo', { name: data.name })
|
||||
// contacts sent to kommo as an array
|
||||
const response = await this._axios.post<KommoCreateContactResponse>('/contacts', [data])
|
||||
|
||||
// get the ID from the response
|
||||
const createdContactId = response.data._embedded.contacts[0]?.id
|
||||
if (!createdContactId) {
|
||||
throw new Error('No contact ID returned from Kommo')
|
||||
}
|
||||
// fetch full contact details to return to user
|
||||
const contact = await this.getContact(createdContactId)
|
||||
if (!contact) {
|
||||
throw new Error('Failed to fetch created contact')
|
||||
}
|
||||
this._logger.forBot().info('Contact created successfully', { contactId: contact.id })
|
||||
|
||||
return contact
|
||||
} catch (error) {
|
||||
throw new Error(getErrorMessage(error))
|
||||
}
|
||||
}
|
||||
|
||||
// internal method for create contact
|
||||
public async getContact(contactId: number): Promise<KommoContact | undefined> {
|
||||
try {
|
||||
this._logger.forBot().debug('Fetching contact from Kommo', { contactId })
|
||||
|
||||
const response = await this._axios.get<KommoContact>(`/contacts/${contactId}`)
|
||||
const contact = response.data
|
||||
return contact
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error) && error.response?.status === 404) {
|
||||
this._logger.forBot().info('Contact not found', { contactId })
|
||||
return undefined
|
||||
}
|
||||
|
||||
throw new Error(getErrorMessage(error))
|
||||
}
|
||||
}
|
||||
|
||||
// method to search contacts by phone number, name or email
|
||||
public async searchContacts(query: string): Promise<KommoContact[]> {
|
||||
try {
|
||||
this._logger.forBot().debug('Searching contacts in Kommo', { query })
|
||||
|
||||
const response = await this._axios.get<KommoSearchContactsResponse>('/contacts', {
|
||||
params: { query },
|
||||
})
|
||||
|
||||
const contacts = response.data._embedded?.contacts || []
|
||||
|
||||
this._logger.forBot().info('Contacts found:', { count: contacts.length })
|
||||
return contacts
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error) && error.response?.status === 404) {
|
||||
this._logger.forBot().info('No contacts found', { query })
|
||||
return []
|
||||
}
|
||||
|
||||
throw new Error(getErrorMessage(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
import { z } from '@botpress/sdk'
|
||||
|
||||
// -----LEADS------
|
||||
|
||||
export const kommoLeadSchema = z.object({
|
||||
id: z.number(),
|
||||
name: z.string(),
|
||||
price: z.number(),
|
||||
responsible_user_id: z.number(),
|
||||
group_id: z.number(),
|
||||
status_id: z.number(),
|
||||
pipeline_id: z.number(),
|
||||
loss_reason_id: z.number().nullable(),
|
||||
created_by: z.number(),
|
||||
updated_by: z.number(),
|
||||
created_at: z.number(),
|
||||
updated_at: z.number(),
|
||||
closed_at: z.number().nullable(),
|
||||
closest_task_at: z.number().nullable(),
|
||||
is_deleted: z.boolean(),
|
||||
score: z.number().nullable(),
|
||||
account_id: z.number(),
|
||||
labor_cost: z.number().nullable(),
|
||||
is_price_computed: z.boolean(),
|
||||
custom_fields_values: z
|
||||
.array(
|
||||
z.object({
|
||||
field_id: z.number(),
|
||||
field_name: z.string(),
|
||||
field_code: z.string().nullable().optional(),
|
||||
field_type: z.string(),
|
||||
values: z.array(
|
||||
z.object({
|
||||
value: z.union([z.string(), z.number()]),
|
||||
enum_id: z.number().optional(),
|
||||
})
|
||||
),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
_embedded: z
|
||||
.object({
|
||||
tags: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z.number(),
|
||||
name: z.string(),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
companies: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z.number(),
|
||||
_links: z.object({
|
||||
self: z.object({
|
||||
href: z.string(),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
contacts: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z.number(),
|
||||
is_main: z.boolean(),
|
||||
_links: z.object({
|
||||
self: z.object({
|
||||
href: z.string(),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
_links: z
|
||||
.object({
|
||||
self: z.object({
|
||||
href: z.string(),
|
||||
}),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
|
||||
export type KommoLead = z.infer<typeof kommoLeadSchema>
|
||||
|
||||
export const createLeadRequestSchema = z.object({
|
||||
name: z.string(),
|
||||
price: z.number().optional(),
|
||||
responsible_user_id: z.number().optional(),
|
||||
pipeline_id: z.number().optional(),
|
||||
status_id: z.number().optional(),
|
||||
created_by: z.number().optional(),
|
||||
updated_by: z.number().optional(),
|
||||
created_at: z.number().optional(),
|
||||
updated_at: z.number().optional(),
|
||||
closed_at: z.number().optional(),
|
||||
custom_fields_values: z
|
||||
.array(
|
||||
z.object({
|
||||
field_id: z.number(),
|
||||
values: z.array(
|
||||
z.object({
|
||||
value: z.union([z.string(), z.number()]),
|
||||
})
|
||||
),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
_embedded: z
|
||||
.object({
|
||||
tags: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z.number().optional(),
|
||||
name: z.string().optional(),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
contacts: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z.number(),
|
||||
is_main: z.boolean().optional(),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
companies: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z.number(),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
|
||||
export type CreateLeadRequest = z.infer<typeof createLeadRequestSchema>
|
||||
|
||||
export const updateLeadRequestSchema = z.object({
|
||||
id: z.number().optional(),
|
||||
name: z.string().optional(),
|
||||
price: z.number().optional(),
|
||||
responsible_user_id: z.number().optional(),
|
||||
status_id: z.number().optional(),
|
||||
pipeline_id: z.number().optional(),
|
||||
custom_fields_values: z
|
||||
.array(
|
||||
z.object({
|
||||
field_id: z.number(),
|
||||
values: z.array(
|
||||
z.object({
|
||||
value: z.union([z.string(), z.number()]),
|
||||
})
|
||||
),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
})
|
||||
|
||||
export type UpdateLeadRequest = z.infer<typeof updateLeadRequestSchema>
|
||||
|
||||
export const kommoCreateResponseSchema = z.object({
|
||||
_links: z.object({
|
||||
self: z.object({
|
||||
href: z.string(),
|
||||
}),
|
||||
}),
|
||||
_embedded: z.object({
|
||||
leads: z.array(
|
||||
z.object({
|
||||
id: z.number(),
|
||||
request_id: z.string(),
|
||||
_links: z.object({
|
||||
self: z.object({
|
||||
href: z.string(),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
),
|
||||
}),
|
||||
})
|
||||
|
||||
export type KommoCreateResponse = z.infer<typeof kommoCreateResponseSchema>
|
||||
|
||||
export const kommoUpdateResponseSchema = z.object({
|
||||
_links: z.object({
|
||||
self: z.object({
|
||||
href: z.string(),
|
||||
}),
|
||||
}),
|
||||
_embedded: z.object({
|
||||
leads: z.array(kommoLeadSchema),
|
||||
}),
|
||||
})
|
||||
|
||||
export type KommoUpdateResponse = z.infer<typeof kommoUpdateResponseSchema>
|
||||
|
||||
export const kommoSearchLeadResponseSchema = z.object({
|
||||
_page: z.number(),
|
||||
_links: z.object({
|
||||
self: z.object({
|
||||
href: z.string(),
|
||||
}),
|
||||
}),
|
||||
_embedded: z.object({
|
||||
leads: z.array(kommoLeadSchema),
|
||||
}),
|
||||
})
|
||||
|
||||
export type KommoSearchLeadResponse = z.infer<typeof kommoSearchLeadResponseSchema>
|
||||
|
||||
// -------------CONTACTS-------------
|
||||
|
||||
export const kommoContactSchema = z.object({
|
||||
id: z.number(),
|
||||
name: z.string(),
|
||||
first_name: z.string(),
|
||||
last_name: z.string(),
|
||||
responsible_user_id: z.number(),
|
||||
group_id: z.number(),
|
||||
updated_by: z.number(),
|
||||
created_at: z.number(),
|
||||
updated_at: z.number(),
|
||||
closest_task_at: z.number().nullable(),
|
||||
is_deleted: z.boolean(),
|
||||
account_id: z.number(),
|
||||
})
|
||||
|
||||
export type KommoContact = z.infer<typeof kommoContactSchema>
|
||||
|
||||
export const createContactRequestSchema = z.object({
|
||||
name: z.string().optional(),
|
||||
first_name: z.string().optional(),
|
||||
last_name: z.string().optional(),
|
||||
responsible_user_id: z.number(),
|
||||
created_by: z.number(),
|
||||
updated_by: z.number().optional(),
|
||||
})
|
||||
|
||||
export type CreateContactRequest = z.infer<typeof createContactRequestSchema>
|
||||
|
||||
export const kommoCreateContactResponseSchema = z.object({
|
||||
_links: z.object({
|
||||
self: z.object({
|
||||
href: z.string(),
|
||||
}),
|
||||
}),
|
||||
_embedded: z.object({
|
||||
contacts: z.array(
|
||||
z.object({
|
||||
id: z.number(),
|
||||
request_id: z.string(),
|
||||
_links: z.object({
|
||||
self: z.object({
|
||||
href: z.string(),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
),
|
||||
}),
|
||||
})
|
||||
|
||||
export type KommoCreateContactResponse = z.infer<typeof kommoCreateContactResponseSchema>
|
||||
|
||||
export const kommoSearchContactsResponseSchema = z.object({
|
||||
_page: z.number(),
|
||||
_links: z.object({
|
||||
self: z.object({
|
||||
href: z.string(),
|
||||
}),
|
||||
next: z
|
||||
.object({
|
||||
href: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
}),
|
||||
_embedded: z.object({
|
||||
contacts: z.array(kommoContactSchema),
|
||||
}),
|
||||
})
|
||||
|
||||
export type KommoSearchContactsResponse = z.infer<typeof kommoSearchContactsResponseSchema>
|
||||
|
||||
//----General-----
|
||||
|
||||
export const kommoErrorResponseSchema = z.object({
|
||||
title: z.string(),
|
||||
type: z.string(),
|
||||
status: z.number(),
|
||||
detail: z.string(),
|
||||
validation_errors: z
|
||||
.array(
|
||||
z.object({
|
||||
request_id: z.string(),
|
||||
errors: z.array(
|
||||
z.object({
|
||||
code: z.string(),
|
||||
path: z.string(),
|
||||
detail: z.string(),
|
||||
})
|
||||
),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
})
|
||||
|
||||
export type KommoErrorResponse = z.infer<typeof kommoErrorResponseSchema>
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"paths": { "*": ["./*"] },
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": [".botpress/**/*", "definitions/**/*", "src/**/*", "*.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user