diff --git a/.gitignore b/.gitignore index 345b501c..0ae9ed95 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,19 @@ node_modules -.env -.botpress -.idea +bp_modules +dist +gen +local/data .DS_Store -dist/ -.turbo \ No newline at end of file +*.tsbuildinfo +__snapshots__ +.ignore.me.* +.env* +.botpress +.botpresshome +.botpresshome.* +.turbo +.genenv/ +.genenv.* +tilt_config.json +/.idea +hubspot.config.yml \ No newline at end of file diff --git a/integrations/chatwoot/bp_modules/hitl/definition/actions/createUser/index.ts b/integrations/chatwoot/bp_modules/hitl/definition/actions/createUser/index.ts new file mode 100644 index 00000000..f5f4eaa1 --- /dev/null +++ b/integrations/chatwoot/bp_modules/hitl/definition/actions/createUser/index.ts @@ -0,0 +1,17 @@ +/* eslint-disable */ +/* tslint:disable */ +// This file is generated. Do not edit it manually. +import * as input from "./input"; +export * as input from "./input"; +import * as output from "./output"; +export * as output from "./output"; + +export const createUser = { + "input": input.input, + "output": output.output, + "title": "Create external user", + "description": "Create an end user in the external service and in Botpress", + "billable": false, + "cacheable": false, + "attributes": { "bpActionHiddenInStudio": "true" }, +} diff --git a/integrations/chatwoot/bp_modules/hitl/definition/actions/createUser/input.ts b/integrations/chatwoot/bp_modules/hitl/definition/actions/createUser/input.ts new file mode 100644 index 00000000..8220a789 --- /dev/null +++ b/integrations/chatwoot/bp_modules/hitl/definition/actions/createUser/input.ts @@ -0,0 +1,31 @@ +/* eslint-disable */ +/* tslint:disable */ +// This file is generated. Do not edit it manually. + +import { z } from "@botpress/sdk"; +export const input = { + schema: z + .object({ + name: z + .string() + .title("Display name") + .describe("Display name of the end user"), + pictureUrl: z + .optional( + z + .string() + .title("Picture URL") + .describe("URL of the end user\'s avatar"), + ) + .describe("URL of the end user\'s avatar"), + email: z + .optional( + z + .string() + .title("Email address") + .describe("Email address of the end user"), + ) + .describe("Email address of the end user"), + }) + .catchall(z.never()), +}; diff --git a/integrations/chatwoot/bp_modules/hitl/definition/actions/createUser/output.ts b/integrations/chatwoot/bp_modules/hitl/definition/actions/createUser/output.ts new file mode 100644 index 00000000..6a62321f --- /dev/null +++ b/integrations/chatwoot/bp_modules/hitl/definition/actions/createUser/output.ts @@ -0,0 +1,15 @@ +/* eslint-disable */ +/* tslint:disable */ +// This file is generated. Do not edit it manually. + +import { z } from "@botpress/sdk"; +export const output = { + schema: z + .object({ + userId: z + .string() + .title("Botpress user ID") + .describe("ID of the Botpress user representing the end user"), + }) + .catchall(z.never()), +}; diff --git a/integrations/chatwoot/bp_modules/hitl/definition/actions/index.ts b/integrations/chatwoot/bp_modules/hitl/definition/actions/index.ts new file mode 100644 index 00000000..ac93a788 --- /dev/null +++ b/integrations/chatwoot/bp_modules/hitl/definition/actions/index.ts @@ -0,0 +1,15 @@ +/* eslint-disable */ +/* tslint:disable */ +// This file is generated. Do not edit it manually. +import * as createUser from "./createUser/index"; +export * as createUser from "./createUser/index"; +import * as startHitl from "./startHitl/index"; +export * as startHitl from "./startHitl/index"; +import * as stopHitl from "./stopHitl/index"; +export * as stopHitl from "./stopHitl/index"; + +export const actions = { + "createUser": createUser.createUser, + "startHitl": startHitl.startHitl, + "stopHitl": stopHitl.stopHitl, +} diff --git a/integrations/chatwoot/bp_modules/hitl/definition/actions/startHitl/index.ts b/integrations/chatwoot/bp_modules/hitl/definition/actions/startHitl/index.ts new file mode 100644 index 00000000..44ad701e --- /dev/null +++ b/integrations/chatwoot/bp_modules/hitl/definition/actions/startHitl/index.ts @@ -0,0 +1,17 @@ +/* eslint-disable */ +/* tslint:disable */ +// This file is generated. Do not edit it manually. +import * as input from "./input"; +export * as input from "./input"; +import * as output from "./output"; +export * as output from "./output"; + +export const startHitl = { + "input": input.input, + "output": output.output, + "title": "Start new HITL session", + "description": "Create a new HITL session in the external service and in Botpress", + "billable": false, + "cacheable": false, + "attributes": { "bpActionHiddenInStudio": "true" }, +} diff --git a/integrations/chatwoot/bp_modules/hitl/definition/actions/startHitl/input.ts b/integrations/chatwoot/bp_modules/hitl/definition/actions/startHitl/input.ts new file mode 100644 index 00000000..6e0fc111 --- /dev/null +++ b/integrations/chatwoot/bp_modules/hitl/definition/actions/startHitl/input.ts @@ -0,0 +1,450 @@ +/* eslint-disable */ +/* tslint:disable */ +// This file is generated. Do not edit it manually. + +import { z } from "@botpress/sdk"; +export const input = { + schema: z + .object({ + userId: z + .string() + .title("User ID") + .describe("ID of the Botpress user representing the end user"), + title: z + .optional( + z + .string() + .title("Title") + .describe( + "Title of the HITL session. This corresponds to a ticket title in systems that use tickets.", + ), + ) + .describe( + "Title of the HITL session. This corresponds to a ticket title in systems that use tickets.", + ), + description: z + .optional( + z + .string() + .title("Description") + .describe( + "Description of the HITL session. This corresponds to a ticket description in systems that use tickets.", + ), + ) + .describe( + "Description of the HITL session. This corresponds to a ticket description in systems that use tickets.", + ), + hitlSession: z.optional(z.object({}).catchall(z.never())), + messageHistory: z + .array( + z.union([ + z + .object({ + source: z.union([ + z + .object({ + type: z.literal("user"), + userId: z.string(), + }) + .catchall(z.never()), + z + .object({ + type: z.literal("bot"), + }) + .catchall(z.never()), + ]), + type: z.literal("text"), + payload: z + .object({ + text: z.string().min(1, undefined), + }) + .catchall(z.never()), + }) + .catchall(z.never()), + z + .object({ + source: z.union([ + z + .object({ + type: z.literal("user"), + userId: z.string(), + }) + .catchall(z.never()), + z + .object({ + type: z.literal("bot"), + }) + .catchall(z.never()), + ]), + type: z.literal("image"), + payload: z + .object({ + imageUrl: z.string().min(1, undefined), + }) + .catchall(z.never()), + }) + .catchall(z.never()), + z + .object({ + source: z.union([ + z + .object({ + type: z.literal("user"), + userId: z.string(), + }) + .catchall(z.never()), + z + .object({ + type: z.literal("bot"), + }) + .catchall(z.never()), + ]), + type: z.literal("audio"), + payload: z + .object({ + audioUrl: z.string().min(1, undefined), + }) + .catchall(z.never()), + }) + .catchall(z.never()), + z + .object({ + source: z.union([ + z + .object({ + type: z.literal("user"), + userId: z.string(), + }) + .catchall(z.never()), + z + .object({ + type: z.literal("bot"), + }) + .catchall(z.never()), + ]), + type: z.literal("video"), + payload: z + .object({ + videoUrl: z.string().min(1, undefined), + }) + .catchall(z.never()), + }) + .catchall(z.never()), + z + .object({ + source: z.union([ + z + .object({ + type: z.literal("user"), + userId: z.string(), + }) + .catchall(z.never()), + z + .object({ + type: z.literal("bot"), + }) + .catchall(z.never()), + ]), + type: z.literal("file"), + payload: z + .object({ + fileUrl: z.string().min(1, undefined), + title: z.optional(z.string().min(1, undefined)), + }) + .catchall(z.never()), + }) + .catchall(z.never()), + z + .object({ + source: z.union([ + z + .object({ + type: z.literal("user"), + userId: z.string(), + }) + .catchall(z.never()), + z + .object({ + type: z.literal("bot"), + }) + .catchall(z.never()), + ]), + type: z.literal("location"), + payload: z + .object({ + latitude: z.number(), + longitude: z.number(), + address: z.optional(z.string()), + title: z.optional(z.string()), + }) + .catchall(z.never()), + }) + .catchall(z.never()), + z + .object({ + source: z.union([ + z + .object({ + type: z.literal("user"), + userId: z.string(), + }) + .catchall(z.never()), + z + .object({ + type: z.literal("bot"), + }) + .catchall(z.never()), + ]), + type: z.literal("carousel"), + payload: z + .object({ + items: z.array( + z + .object({ + title: z.string().min(1, undefined), + subtitle: z.optional(z.string().min(1, undefined)), + imageUrl: z.optional(z.string().min(1, undefined)), + actions: z.array( + z + .object({ + action: z.enum(["postback", "url", "say"]), + label: z.string().min(1, undefined), + value: z.string().min(1, undefined), + }) + .catchall(z.never()), + ), + }) + .catchall(z.never()), + ), + }) + .catchall(z.never()), + }) + .catchall(z.never()), + z + .object({ + source: z.union([ + z + .object({ + type: z.literal("user"), + userId: z.string(), + }) + .catchall(z.never()), + z + .object({ + type: z.literal("bot"), + }) + .catchall(z.never()), + ]), + type: z.literal("card"), + payload: z + .object({ + title: z.string().min(1, undefined), + subtitle: z.optional(z.string().min(1, undefined)), + imageUrl: z.optional(z.string().min(1, undefined)), + actions: z.array( + z + .object({ + action: z.enum(["postback", "url", "say"]), + label: z.string().min(1, undefined), + value: z.string().min(1, undefined), + }) + .catchall(z.never()), + ), + }) + .catchall(z.never()), + }) + .catchall(z.never()), + z + .object({ + source: z.union([ + z + .object({ + type: z.literal("user"), + userId: z.string(), + }) + .catchall(z.never()), + z + .object({ + type: z.literal("bot"), + }) + .catchall(z.never()), + ]), + type: z.literal("dropdown"), + payload: z + .object({ + text: z.string().min(1, undefined), + options: z.array( + z + .object({ + label: z.string().min(1, undefined), + value: z.string().min(1, undefined), + }) + .catchall(z.never()), + ), + }) + .catchall(z.never()), + }) + .catchall(z.never()), + z + .object({ + source: z.union([ + z + .object({ + type: z.literal("user"), + userId: z.string(), + }) + .catchall(z.never()), + z + .object({ + type: z.literal("bot"), + }) + .catchall(z.never()), + ]), + type: z.literal("choice"), + payload: z + .object({ + text: z.string().min(1, undefined), + options: z.array( + z + .object({ + label: z.string().min(1, undefined), + value: z.string().min(1, undefined), + }) + .catchall(z.never()), + ), + }) + .catchall(z.never()), + }) + .catchall(z.never()), + z + .object({ + source: z.union([ + z + .object({ + type: z.literal("user"), + userId: z.string(), + }) + .catchall(z.never()), + z + .object({ + type: z.literal("bot"), + }) + .catchall(z.never()), + ]), + type: z.literal("bloc"), + payload: z + .object({ + items: z.array( + z.union([ + z + .object({ + type: z.literal("text"), + payload: z + .object({ + text: z.string().min(1, undefined), + }) + .catchall(z.never()), + }) + .catchall(z.never()), + z + .object({ + type: z.literal("markdown"), + payload: z + .object({ + markdown: z.string().min(1, undefined), + }) + .catchall(z.never()), + }) + .catchall(z.never()), + z + .object({ + type: z.literal("image"), + payload: z + .object({ + imageUrl: z.string().min(1, undefined), + }) + .catchall(z.never()), + }) + .catchall(z.never()), + z + .object({ + type: z.literal("audio"), + payload: z + .object({ + audioUrl: z.string().min(1, undefined), + }) + .catchall(z.never()), + }) + .catchall(z.never()), + z + .object({ + type: z.literal("video"), + payload: z + .object({ + videoUrl: z.string().min(1, undefined), + }) + .catchall(z.never()), + }) + .catchall(z.never()), + z + .object({ + type: z.literal("file"), + payload: z + .object({ + fileUrl: z.string().min(1, undefined), + title: z.optional(z.string().min(1, undefined)), + }) + .catchall(z.never()), + }) + .catchall(z.never()), + z + .object({ + type: z.literal("location"), + payload: z + .object({ + latitude: z.number(), + longitude: z.number(), + address: z.optional(z.string()), + title: z.optional(z.string()), + }) + .catchall(z.never()), + }) + .catchall(z.never()), + ]), + ), + }) + .catchall(z.never()), + }) + .catchall(z.never()), + z + .object({ + source: z.union([ + z + .object({ + type: z.literal("user"), + userId: z.string(), + }) + .catchall(z.never()), + z + .object({ + type: z.literal("bot"), + }) + .catchall(z.never()), + ]), + type: z.literal("markdown"), + payload: z + .object({ + markdown: z.string().min(1, undefined), + }) + .catchall(z.never()), + }) + .catchall(z.never()), + ]), + ) + .title("Conversation history") + .describe( + "History of all messages in the conversation up to this point. Should be displayed to the human agent in the external service.", + ), + }) + .catchall(z.never()), +}; diff --git a/integrations/chatwoot/bp_modules/hitl/definition/actions/startHitl/output.ts b/integrations/chatwoot/bp_modules/hitl/definition/actions/startHitl/output.ts new file mode 100644 index 00000000..4a5cecca --- /dev/null +++ b/integrations/chatwoot/bp_modules/hitl/definition/actions/startHitl/output.ts @@ -0,0 +1,17 @@ +/* eslint-disable */ +/* tslint:disable */ +// This file is generated. Do not edit it manually. + +import { z } from "@botpress/sdk"; +export const output = { + schema: z + .object({ + conversationId: z + .string() + .title("HITL session ID") + .describe( + "ID of the Botpress conversation representing the HITL session", + ), + }) + .catchall(z.never()), +}; diff --git a/integrations/chatwoot/bp_modules/hitl/definition/actions/stopHitl/index.ts b/integrations/chatwoot/bp_modules/hitl/definition/actions/stopHitl/index.ts new file mode 100644 index 00000000..1a66be9b --- /dev/null +++ b/integrations/chatwoot/bp_modules/hitl/definition/actions/stopHitl/index.ts @@ -0,0 +1,17 @@ +/* eslint-disable */ +/* tslint:disable */ +// This file is generated. Do not edit it manually. +import * as input from "./input"; +export * as input from "./input"; +import * as output from "./output"; +export * as output from "./output"; + +export const stopHitl = { + "input": input.input, + "output": output.output, + "title": "Stop HITL session", + "description": "Stop an existing HITL session in the external service", + "billable": false, + "cacheable": false, + "attributes": { "bpActionHiddenInStudio": "true" }, +} diff --git a/integrations/chatwoot/bp_modules/hitl/definition/actions/stopHitl/input.ts b/integrations/chatwoot/bp_modules/hitl/definition/actions/stopHitl/input.ts new file mode 100644 index 00000000..a29d6d2d --- /dev/null +++ b/integrations/chatwoot/bp_modules/hitl/definition/actions/stopHitl/input.ts @@ -0,0 +1,17 @@ +/* eslint-disable */ +/* tslint:disable */ +// This file is generated. Do not edit it manually. + +import { z } from "@botpress/sdk"; +export const input = { + schema: z + .object({ + conversationId: z + .string() + .title("HITL session ID") + .describe( + "ID of the Botpress conversation representing the HITL session", + ), + }) + .catchall(z.never()), +}; diff --git a/integrations/chatwoot/bp_modules/hitl/definition/actions/stopHitl/output.ts b/integrations/chatwoot/bp_modules/hitl/definition/actions/stopHitl/output.ts new file mode 100644 index 00000000..40360d7e --- /dev/null +++ b/integrations/chatwoot/bp_modules/hitl/definition/actions/stopHitl/output.ts @@ -0,0 +1,8 @@ +/* eslint-disable */ +/* tslint:disable */ +// This file is generated. Do not edit it manually. + +import { z } from "@botpress/sdk"; +export const output = { + schema: z.object({}).catchall(z.never()), +}; diff --git a/integrations/chatwoot/bp_modules/hitl/definition/channels/hitl/index.ts b/integrations/chatwoot/bp_modules/hitl/definition/channels/hitl/index.ts new file mode 100644 index 00000000..b6d1b516 --- /dev/null +++ b/integrations/chatwoot/bp_modules/hitl/definition/channels/hitl/index.ts @@ -0,0 +1,14 @@ +/* eslint-disable */ +/* tslint:disable */ +// This file is generated. Do not edit it manually. + +import { messages } from './messages/index' +export * from './messages/index' + +export const hitl = { + title: undefined, + description: undefined, + messages: messages, + message: { "tags": {} }, + conversation: { "tags": { "assignedAt": {}, "assigneeUserId": {}, "assigneeWorkspaceMemberId": {}, "description": {}, "requesterUserId": {}, "status": {}, "title": {} }, "creation": { "enabled": false, "requiredTags": [] } }, +} \ No newline at end of file diff --git a/integrations/chatwoot/bp_modules/hitl/definition/channels/hitl/messages/audio.ts b/integrations/chatwoot/bp_modules/hitl/definition/channels/hitl/messages/audio.ts new file mode 100644 index 00000000..5f626709 --- /dev/null +++ b/integrations/chatwoot/bp_modules/hitl/definition/channels/hitl/messages/audio.ts @@ -0,0 +1,22 @@ +/* eslint-disable */ +/* tslint:disable */ +// This file is generated. Do not edit it manually. + +import { z } from "@botpress/sdk"; +export const audio = { + schema: z + .object({ + audioUrl: z.string().min(1, undefined), + userId: z + .optional( + z + .string() + .describe( + "Allows sending a message pretending to be a certain user", + ), + ) + .describe("Allows sending a message pretending to be a certain user"), + metadata: z.optional(z.record(z.string(), z.any())), + }) + .catchall(z.never()), +}; diff --git a/integrations/chatwoot/bp_modules/hitl/definition/channels/hitl/messages/bloc.ts b/integrations/chatwoot/bp_modules/hitl/definition/channels/hitl/messages/bloc.ts new file mode 100644 index 00000000..ab2c81e9 --- /dev/null +++ b/integrations/chatwoot/bp_modules/hitl/definition/channels/hitl/messages/bloc.ts @@ -0,0 +1,99 @@ +/* eslint-disable */ +/* tslint:disable */ +// This file is generated. Do not edit it manually. + +import { z } from "@botpress/sdk"; +export const bloc = { + schema: z + .object({ + items: z.array( + z.union([ + z + .object({ + type: z.literal("text"), + payload: z + .object({ + text: z.string().min(1, undefined), + }) + .catchall(z.never()), + }) + .catchall(z.never()), + z + .object({ + type: z.literal("markdown"), + payload: z + .object({ + markdown: z.string().min(1, undefined), + }) + .catchall(z.never()), + }) + .catchall(z.never()), + z + .object({ + type: z.literal("image"), + payload: z + .object({ + imageUrl: z.string().min(1, undefined), + }) + .catchall(z.never()), + }) + .catchall(z.never()), + z + .object({ + type: z.literal("audio"), + payload: z + .object({ + audioUrl: z.string().min(1, undefined), + }) + .catchall(z.never()), + }) + .catchall(z.never()), + z + .object({ + type: z.literal("video"), + payload: z + .object({ + videoUrl: z.string().min(1, undefined), + }) + .catchall(z.never()), + }) + .catchall(z.never()), + z + .object({ + type: z.literal("file"), + payload: z + .object({ + fileUrl: z.string().min(1, undefined), + title: z.optional(z.string().min(1, undefined)), + }) + .catchall(z.never()), + }) + .catchall(z.never()), + z + .object({ + type: z.literal("location"), + payload: z + .object({ + latitude: z.number(), + longitude: z.number(), + address: z.optional(z.string()), + title: z.optional(z.string()), + }) + .catchall(z.never()), + }) + .catchall(z.never()), + ]), + ), + userId: z + .optional( + z + .string() + .describe( + "Allows sending a message pretending to be a certain user", + ), + ) + .describe("Allows sending a message pretending to be a certain user"), + metadata: z.optional(z.record(z.string(), z.any())), + }) + .catchall(z.never()), +}; diff --git a/integrations/chatwoot/bp_modules/hitl/definition/channels/hitl/messages/card.ts b/integrations/chatwoot/bp_modules/hitl/definition/channels/hitl/messages/card.ts new file mode 100644 index 00000000..be9ff3c6 --- /dev/null +++ b/integrations/chatwoot/bp_modules/hitl/definition/channels/hitl/messages/card.ts @@ -0,0 +1,25 @@ +/* eslint-disable */ +/* tslint:disable */ +// This file is generated. Do not edit it manually. + +import { z } from "@botpress/sdk"; +export const card = { + schema: z + .object({ + title: z.string().min(1, undefined), + subtitle: z.optional(z.string().min(1, undefined)), + imageUrl: z.optional(z.string().min(1, undefined)), + actions: z.array( + z + .object({ + action: z.enum(["postback", "url", "say"]), + label: z.string().min(1, undefined), + value: z.string().min(1, undefined), + }) + .catchall(z.never()), + ), + userId: z.optional(z.string()), + metadata: z.optional(z.record(z.string(), z.any())), + }) + .catchall(z.never()), +}; diff --git a/integrations/chatwoot/bp_modules/hitl/definition/channels/hitl/messages/carousel.ts b/integrations/chatwoot/bp_modules/hitl/definition/channels/hitl/messages/carousel.ts new file mode 100644 index 00000000..e3ad3e6c --- /dev/null +++ b/integrations/chatwoot/bp_modules/hitl/definition/channels/hitl/messages/carousel.ts @@ -0,0 +1,31 @@ +/* eslint-disable */ +/* tslint:disable */ +// This file is generated. Do not edit it manually. + +import { z } from "@botpress/sdk"; +export const carousel = { + schema: z + .object({ + items: z.array( + z + .object({ + title: z.string().min(1, undefined), + subtitle: z.optional(z.string().min(1, undefined)), + imageUrl: z.optional(z.string().min(1, undefined)), + actions: z.array( + z + .object({ + action: z.enum(["postback", "url", "say"]), + label: z.string().min(1, undefined), + value: z.string().min(1, undefined), + }) + .catchall(z.never()), + ), + }) + .catchall(z.never()), + ), + userId: z.optional(z.string()), + metadata: z.optional(z.record(z.string(), z.any())), + }) + .catchall(z.never()), +}; diff --git a/integrations/chatwoot/bp_modules/hitl/definition/channels/hitl/messages/choice.ts b/integrations/chatwoot/bp_modules/hitl/definition/channels/hitl/messages/choice.ts new file mode 100644 index 00000000..b2d91b49 --- /dev/null +++ b/integrations/chatwoot/bp_modules/hitl/definition/channels/hitl/messages/choice.ts @@ -0,0 +1,22 @@ +/* eslint-disable */ +/* tslint:disable */ +// This file is generated. Do not edit it manually. + +import { z } from "@botpress/sdk"; +export const choice = { + schema: z + .object({ + text: z.string().min(1, undefined), + options: z.array( + z + .object({ + label: z.string().min(1, undefined), + value: z.string().min(1, undefined), + }) + .catchall(z.never()), + ), + userId: z.optional(z.string()), + metadata: z.optional(z.record(z.string(), z.any())), + }) + .catchall(z.never()), +}; diff --git a/integrations/chatwoot/bp_modules/hitl/definition/channels/hitl/messages/dropdown.ts b/integrations/chatwoot/bp_modules/hitl/definition/channels/hitl/messages/dropdown.ts new file mode 100644 index 00000000..2b182626 --- /dev/null +++ b/integrations/chatwoot/bp_modules/hitl/definition/channels/hitl/messages/dropdown.ts @@ -0,0 +1,22 @@ +/* eslint-disable */ +/* tslint:disable */ +// This file is generated. Do not edit it manually. + +import { z } from "@botpress/sdk"; +export const dropdown = { + schema: z + .object({ + text: z.string().min(1, undefined), + options: z.array( + z + .object({ + label: z.string().min(1, undefined), + value: z.string().min(1, undefined), + }) + .catchall(z.never()), + ), + userId: z.optional(z.string()), + metadata: z.optional(z.record(z.string(), z.any())), + }) + .catchall(z.never()), +}; diff --git a/integrations/chatwoot/bp_modules/hitl/definition/channels/hitl/messages/file.ts b/integrations/chatwoot/bp_modules/hitl/definition/channels/hitl/messages/file.ts new file mode 100644 index 00000000..cd8f17e7 --- /dev/null +++ b/integrations/chatwoot/bp_modules/hitl/definition/channels/hitl/messages/file.ts @@ -0,0 +1,23 @@ +/* eslint-disable */ +/* tslint:disable */ +// This file is generated. Do not edit it manually. + +import { z } from "@botpress/sdk"; +export const file = { + schema: z + .object({ + fileUrl: z.string().min(1, undefined), + title: z.optional(z.string().min(1, undefined)), + userId: z + .optional( + z + .string() + .describe( + "Allows sending a message pretending to be a certain user", + ), + ) + .describe("Allows sending a message pretending to be a certain user"), + metadata: z.optional(z.record(z.string(), z.any())), + }) + .catchall(z.never()), +}; diff --git a/integrations/chatwoot/bp_modules/hitl/definition/channels/hitl/messages/image.ts b/integrations/chatwoot/bp_modules/hitl/definition/channels/hitl/messages/image.ts new file mode 100644 index 00000000..a85ed57b --- /dev/null +++ b/integrations/chatwoot/bp_modules/hitl/definition/channels/hitl/messages/image.ts @@ -0,0 +1,22 @@ +/* eslint-disable */ +/* tslint:disable */ +// This file is generated. Do not edit it manually. + +import { z } from "@botpress/sdk"; +export const image = { + schema: z + .object({ + imageUrl: z.string().min(1, undefined), + userId: z + .optional( + z + .string() + .describe( + "Allows sending a message pretending to be a certain user", + ), + ) + .describe("Allows sending a message pretending to be a certain user"), + metadata: z.optional(z.record(z.string(), z.any())), + }) + .catchall(z.never()), +}; diff --git a/integrations/chatwoot/bp_modules/hitl/definition/channels/hitl/messages/index.ts b/integrations/chatwoot/bp_modules/hitl/definition/channels/hitl/messages/index.ts new file mode 100644 index 00000000..5ddaf6bb --- /dev/null +++ b/integrations/chatwoot/bp_modules/hitl/definition/channels/hitl/messages/index.ts @@ -0,0 +1,42 @@ +/* eslint-disable */ +/* tslint:disable */ +// This file is generated. Do not edit it manually. +import * as bloc from "./bloc"; +export * as bloc from "./bloc"; +import * as choice from "./choice"; +export * as choice from "./choice"; +import * as dropdown from "./dropdown"; +export * as dropdown from "./dropdown"; +import * as card from "./card"; +export * as card from "./card"; +import * as carousel from "./carousel"; +export * as carousel from "./carousel"; +import * as location from "./location"; +export * as location from "./location"; +import * as file from "./file"; +export * as file from "./file"; +import * as video from "./video"; +export * as video from "./video"; +import * as audio from "./audio"; +export * as audio from "./audio"; +import * as image from "./image"; +export * as image from "./image"; +import * as markdown from "./markdown"; +export * as markdown from "./markdown"; +import * as text from "./text"; +export * as text from "./text"; + +export const messages = { + "bloc": bloc.bloc, + "choice": choice.choice, + "dropdown": dropdown.dropdown, + "card": card.card, + "carousel": carousel.carousel, + "location": location.location, + "file": file.file, + "video": video.video, + "audio": audio.audio, + "image": image.image, + "markdown": markdown.markdown, + "text": text.text, +} diff --git a/integrations/chatwoot/bp_modules/hitl/definition/channels/hitl/messages/location.ts b/integrations/chatwoot/bp_modules/hitl/definition/channels/hitl/messages/location.ts new file mode 100644 index 00000000..38f79e14 --- /dev/null +++ b/integrations/chatwoot/bp_modules/hitl/definition/channels/hitl/messages/location.ts @@ -0,0 +1,17 @@ +/* eslint-disable */ +/* tslint:disable */ +// This file is generated. Do not edit it manually. + +import { z } from "@botpress/sdk"; +export const location = { + schema: z + .object({ + latitude: z.number(), + longitude: z.number(), + address: z.optional(z.string()), + title: z.optional(z.string()), + userId: z.optional(z.string()), + metadata: z.optional(z.record(z.string(), z.any())), + }) + .catchall(z.never()), +}; diff --git a/integrations/chatwoot/bp_modules/hitl/definition/channels/hitl/messages/markdown.ts b/integrations/chatwoot/bp_modules/hitl/definition/channels/hitl/messages/markdown.ts new file mode 100644 index 00000000..d037f839 --- /dev/null +++ b/integrations/chatwoot/bp_modules/hitl/definition/channels/hitl/messages/markdown.ts @@ -0,0 +1,14 @@ +/* eslint-disable */ +/* tslint:disable */ +// This file is generated. Do not edit it manually. + +import { z } from "@botpress/sdk"; +export const markdown = { + schema: z + .object({ + markdown: z.string().min(1, undefined), + userId: z.optional(z.string()), + metadata: z.optional(z.record(z.string(), z.any())), + }) + .catchall(z.never()), +}; diff --git a/integrations/chatwoot/bp_modules/hitl/definition/channels/hitl/messages/text.ts b/integrations/chatwoot/bp_modules/hitl/definition/channels/hitl/messages/text.ts new file mode 100644 index 00000000..4d427395 --- /dev/null +++ b/integrations/chatwoot/bp_modules/hitl/definition/channels/hitl/messages/text.ts @@ -0,0 +1,22 @@ +/* eslint-disable */ +/* tslint:disable */ +// This file is generated. Do not edit it manually. + +import { z } from "@botpress/sdk"; +export const text = { + schema: z + .object({ + text: z.string().min(1, undefined), + userId: z + .optional( + z + .string() + .describe( + "Allows sending a message pretending to be a certain user", + ), + ) + .describe("Allows sending a message pretending to be a certain user"), + metadata: z.optional(z.record(z.string(), z.any())), + }) + .catchall(z.never()), +}; diff --git a/integrations/chatwoot/bp_modules/hitl/definition/channels/hitl/messages/video.ts b/integrations/chatwoot/bp_modules/hitl/definition/channels/hitl/messages/video.ts new file mode 100644 index 00000000..8c2f3526 --- /dev/null +++ b/integrations/chatwoot/bp_modules/hitl/definition/channels/hitl/messages/video.ts @@ -0,0 +1,22 @@ +/* eslint-disable */ +/* tslint:disable */ +// This file is generated. Do not edit it manually. + +import { z } from "@botpress/sdk"; +export const video = { + schema: z + .object({ + videoUrl: z.string().min(1, undefined), + userId: z + .optional( + z + .string() + .describe( + "Allows sending a message pretending to be a certain user", + ), + ) + .describe("Allows sending a message pretending to be a certain user"), + metadata: z.optional(z.record(z.string(), z.any())), + }) + .catchall(z.never()), +}; diff --git a/integrations/chatwoot/bp_modules/hitl/definition/channels/index.ts b/integrations/chatwoot/bp_modules/hitl/definition/channels/index.ts new file mode 100644 index 00000000..1f1b9785 --- /dev/null +++ b/integrations/chatwoot/bp_modules/hitl/definition/channels/index.ts @@ -0,0 +1,9 @@ +/* eslint-disable */ +/* tslint:disable */ +// This file is generated. Do not edit it manually. +import * as hitl from "./hitl/index"; +export * as hitl from "./hitl/index"; + +export const channels = { + "hitl": hitl.hitl, +} diff --git a/integrations/chatwoot/bp_modules/hitl/definition/configuration/index.ts b/integrations/chatwoot/bp_modules/hitl/definition/configuration/index.ts new file mode 100644 index 00000000..8da68be9 --- /dev/null +++ b/integrations/chatwoot/bp_modules/hitl/definition/configuration/index.ts @@ -0,0 +1,8 @@ +/* eslint-disable */ +/* tslint:disable */ +// This file is generated. Do not edit it manually. + +import { z } from "@botpress/sdk"; +export const configuration = { + schema: z.object({}).catchall(z.never()), +}; diff --git a/integrations/chatwoot/bp_modules/hitl/definition/configurations/index.ts b/integrations/chatwoot/bp_modules/hitl/definition/configurations/index.ts new file mode 100644 index 00000000..2cd6dfa3 --- /dev/null +++ b/integrations/chatwoot/bp_modules/hitl/definition/configurations/index.ts @@ -0,0 +1,6 @@ +/* eslint-disable */ +/* tslint:disable */ +// This file is generated. Do not edit it manually. + +export const configurations = { +} diff --git a/integrations/chatwoot/bp_modules/hitl/definition/entities/hitlSession.ts b/integrations/chatwoot/bp_modules/hitl/definition/entities/hitlSession.ts new file mode 100644 index 00000000..dc774036 --- /dev/null +++ b/integrations/chatwoot/bp_modules/hitl/definition/entities/hitlSession.ts @@ -0,0 +1,8 @@ +/* eslint-disable */ +/* tslint:disable */ +// This file is generated. Do not edit it manually. + +import { z } from "@botpress/sdk"; +export const hitlSession = { + schema: z.object({}).catchall(z.never()), +}; diff --git a/integrations/chatwoot/bp_modules/hitl/definition/entities/index.ts b/integrations/chatwoot/bp_modules/hitl/definition/entities/index.ts new file mode 100644 index 00000000..ae3ca013 --- /dev/null +++ b/integrations/chatwoot/bp_modules/hitl/definition/entities/index.ts @@ -0,0 +1,9 @@ +/* eslint-disable */ +/* tslint:disable */ +// This file is generated. Do not edit it manually. +import * as hitlSession from "./hitlSession"; +export * as hitlSession from "./hitlSession"; + +export const entities = { + "hitlSession": hitlSession.hitlSession, +} diff --git a/integrations/chatwoot/bp_modules/hitl/definition/events/hitlAssigned.ts b/integrations/chatwoot/bp_modules/hitl/definition/events/hitlAssigned.ts new file mode 100644 index 00000000..5b537e34 --- /dev/null +++ b/integrations/chatwoot/bp_modules/hitl/definition/events/hitlAssigned.ts @@ -0,0 +1,24 @@ +/* eslint-disable */ +/* tslint:disable */ +// This file is generated. Do not edit it manually. + +import { z } from "@botpress/sdk"; +export const hitlAssigned = { + attributes: { bpActionHiddenInStudio: "true" }, + schema: z + .object({ + conversationId: z + .string() + .title("HITL session ID") + .describe( + "ID of the Botpress conversation representing the HITL session", + ), + userId: z + .string() + .title("Human agent user ID") + .describe( + "ID of the Botpress user representing the human agent assigned to the HITL session", + ), + }) + .catchall(z.never()), +}; diff --git a/integrations/chatwoot/bp_modules/hitl/definition/events/hitlStopped.ts b/integrations/chatwoot/bp_modules/hitl/definition/events/hitlStopped.ts new file mode 100644 index 00000000..89dbd737 --- /dev/null +++ b/integrations/chatwoot/bp_modules/hitl/definition/events/hitlStopped.ts @@ -0,0 +1,18 @@ +/* eslint-disable */ +/* tslint:disable */ +// This file is generated. Do not edit it manually. + +import { z } from "@botpress/sdk"; +export const hitlStopped = { + attributes: { bpActionHiddenInStudio: "true" }, + schema: z + .object({ + conversationId: z + .string() + .title("HITL session ID") + .describe( + "ID of the Botpress conversation representing the HITL session", + ), + }) + .catchall(z.never()), +}; diff --git a/integrations/chatwoot/bp_modules/hitl/definition/events/index.ts b/integrations/chatwoot/bp_modules/hitl/definition/events/index.ts new file mode 100644 index 00000000..c46ec10c --- /dev/null +++ b/integrations/chatwoot/bp_modules/hitl/definition/events/index.ts @@ -0,0 +1,12 @@ +/* eslint-disable */ +/* tslint:disable */ +// This file is generated. Do not edit it manually. +import * as hitlAssigned from "./hitlAssigned"; +export * as hitlAssigned from "./hitlAssigned"; +import * as hitlStopped from "./hitlStopped"; +export * as hitlStopped from "./hitlStopped"; + +export const events = { + "hitlAssigned": hitlAssigned.hitlAssigned, + "hitlStopped": hitlStopped.hitlStopped, +} diff --git a/integrations/chatwoot/bp_modules/hitl/definition/index.ts b/integrations/chatwoot/bp_modules/hitl/definition/index.ts new file mode 100644 index 00000000..e0fbd780 --- /dev/null +++ b/integrations/chatwoot/bp_modules/hitl/definition/index.ts @@ -0,0 +1,37 @@ +/* eslint-disable */ +/* tslint:disable */ +// This file is generated. Do not edit it manually. + +import * as sdk from "@botpress/sdk" + +import * as configuration from "./configuration/index" +import * as configurations from "./configurations/index" +import * as actions from "./actions/index" +import * as channels from "./channels/index" +import * as events from "./events/index" +import * as states from "./states/index" +import * as entities from "./entities/index" +import * as interfaces from "./interfaces/index" +export * as configuration from "./configuration/index" +export * as configurations from "./configurations/index" +export * as actions from "./actions/index" +export * as channels from "./channels/index" +export * as events from "./events/index" +export * as states from "./states/index" +export * as entities from "./entities/index" +export * as interfaces from "./interfaces/index" + +export default { + name: "hitl", + version: "2.0.2", + attributes: {}, + user: { "tags": { "email": {}, "workspaceMemberId": {} }, "creation": { "enabled": false, "requiredTags": [] } }, + configuration: configuration.configuration, + configurations: configurations.configurations, + actions: actions.actions, + channels: channels.channels, + events: events.events, + states: states.states, + entities: entities.entities, + interfaces: interfaces.interfaces, +} satisfies sdk.IntegrationPackage["definition"] \ No newline at end of file diff --git a/integrations/chatwoot/bp_modules/hitl/definition/interfaces/hitl-hitlSession-.ts b/integrations/chatwoot/bp_modules/hitl/definition/interfaces/hitl-hitlSession-.ts new file mode 100644 index 00000000..9aeade73 --- /dev/null +++ b/integrations/chatwoot/bp_modules/hitl/definition/interfaces/hitl-hitlSession-.ts @@ -0,0 +1,32 @@ +export const hitlHitlSession = { + "id": "ifver_01K0STHY79Y0GFGFGDXXGQKDVE", + "name": "hitl", + "version": "2.0.0", + "createdAt": "2025-08-06T21:57:07.449Z", + "updatedAt": "2025-08-06T21:57:07.449Z", + "entities": { + "hitlSession": { + "name": "hitlSession" + } + }, + "actions": { + "createUser": { + "name": "createUser" + }, + "startHitl": { + "name": "startHitl" + }, + "stopHitl": { + "name": "stopHitl" + } + }, + "events": { + "hitlAssigned": { + "name": "hitlAssigned" + }, + "hitlStopped": { + "name": "hitlStopped" + } + }, + "channels": {} +} \ No newline at end of file diff --git a/integrations/chatwoot/bp_modules/hitl/definition/interfaces/index.ts b/integrations/chatwoot/bp_modules/hitl/definition/interfaces/index.ts new file mode 100644 index 00000000..1461af9e --- /dev/null +++ b/integrations/chatwoot/bp_modules/hitl/definition/interfaces/index.ts @@ -0,0 +1,9 @@ +/* eslint-disable */ +/* tslint:disable */ +// This file is generated. Do not edit it manually. +import * as hitlHitlSession from "./hitl-hitlSession-"; +export * as hitlHitlSession from "./hitl-hitlSession-"; + +export const interfaces = { + "hitl": hitlHitlSession.hitlHitlSession, +} diff --git a/integrations/chatwoot/bp_modules/hitl/definition/states/hitlMessageHistory.ts b/integrations/chatwoot/bp_modules/hitl/definition/states/hitlMessageHistory.ts new file mode 100644 index 00000000..f1dc1db2 --- /dev/null +++ b/integrations/chatwoot/bp_modules/hitl/definition/states/hitlMessageHistory.ts @@ -0,0 +1,422 @@ +/* eslint-disable */ +/* tslint:disable */ +// This file is generated. Do not edit it manually. + +import { z } from "@botpress/sdk"; +export const hitlMessageHistory = { + type: "conversation" as const, + schema: z + .object({ + value: z + .array( + z.union([ + z + .object({ + source: z.union([ + z + .object({ + type: z.literal("user"), + userId: z.string(), + }) + .catchall(z.never()), + z + .object({ + type: z.literal("bot"), + }) + .catchall(z.never()), + ]), + type: z.literal("text"), + payload: z + .object({ + text: z.string().min(1, undefined), + }) + .catchall(z.never()), + }) + .catchall(z.never()), + z + .object({ + source: z.union([ + z + .object({ + type: z.literal("user"), + userId: z.string(), + }) + .catchall(z.never()), + z + .object({ + type: z.literal("bot"), + }) + .catchall(z.never()), + ]), + type: z.literal("image"), + payload: z + .object({ + imageUrl: z.string().min(1, undefined), + }) + .catchall(z.never()), + }) + .catchall(z.never()), + z + .object({ + source: z.union([ + z + .object({ + type: z.literal("user"), + userId: z.string(), + }) + .catchall(z.never()), + z + .object({ + type: z.literal("bot"), + }) + .catchall(z.never()), + ]), + type: z.literal("audio"), + payload: z + .object({ + audioUrl: z.string().min(1, undefined), + }) + .catchall(z.never()), + }) + .catchall(z.never()), + z + .object({ + source: z.union([ + z + .object({ + type: z.literal("user"), + userId: z.string(), + }) + .catchall(z.never()), + z + .object({ + type: z.literal("bot"), + }) + .catchall(z.never()), + ]), + type: z.literal("video"), + payload: z + .object({ + videoUrl: z.string().min(1, undefined), + }) + .catchall(z.never()), + }) + .catchall(z.never()), + z + .object({ + source: z.union([ + z + .object({ + type: z.literal("user"), + userId: z.string(), + }) + .catchall(z.never()), + z + .object({ + type: z.literal("bot"), + }) + .catchall(z.never()), + ]), + type: z.literal("file"), + payload: z + .object({ + fileUrl: z.string().min(1, undefined), + title: z.optional(z.string().min(1, undefined)), + }) + .catchall(z.never()), + }) + .catchall(z.never()), + z + .object({ + source: z.union([ + z + .object({ + type: z.literal("user"), + userId: z.string(), + }) + .catchall(z.never()), + z + .object({ + type: z.literal("bot"), + }) + .catchall(z.never()), + ]), + type: z.literal("location"), + payload: z + .object({ + latitude: z.number(), + longitude: z.number(), + address: z.optional(z.string()), + title: z.optional(z.string()), + }) + .catchall(z.never()), + }) + .catchall(z.never()), + z + .object({ + source: z.union([ + z + .object({ + type: z.literal("user"), + userId: z.string(), + }) + .catchall(z.never()), + z + .object({ + type: z.literal("bot"), + }) + .catchall(z.never()), + ]), + type: z.literal("carousel"), + payload: z + .object({ + items: z.array( + z + .object({ + title: z.string().min(1, undefined), + subtitle: z.optional(z.string().min(1, undefined)), + imageUrl: z.optional(z.string().min(1, undefined)), + actions: z.array( + z + .object({ + action: z.enum(["postback", "url", "say"]), + label: z.string().min(1, undefined), + value: z.string().min(1, undefined), + }) + .catchall(z.never()), + ), + }) + .catchall(z.never()), + ), + }) + .catchall(z.never()), + }) + .catchall(z.never()), + z + .object({ + source: z.union([ + z + .object({ + type: z.literal("user"), + userId: z.string(), + }) + .catchall(z.never()), + z + .object({ + type: z.literal("bot"), + }) + .catchall(z.never()), + ]), + type: z.literal("card"), + payload: z + .object({ + title: z.string().min(1, undefined), + subtitle: z.optional(z.string().min(1, undefined)), + imageUrl: z.optional(z.string().min(1, undefined)), + actions: z.array( + z + .object({ + action: z.enum(["postback", "url", "say"]), + label: z.string().min(1, undefined), + value: z.string().min(1, undefined), + }) + .catchall(z.never()), + ), + }) + .catchall(z.never()), + }) + .catchall(z.never()), + z + .object({ + source: z.union([ + z + .object({ + type: z.literal("user"), + userId: z.string(), + }) + .catchall(z.never()), + z + .object({ + type: z.literal("bot"), + }) + .catchall(z.never()), + ]), + type: z.literal("dropdown"), + payload: z + .object({ + text: z.string().min(1, undefined), + options: z.array( + z + .object({ + label: z.string().min(1, undefined), + value: z.string().min(1, undefined), + }) + .catchall(z.never()), + ), + }) + .catchall(z.never()), + }) + .catchall(z.never()), + z + .object({ + source: z.union([ + z + .object({ + type: z.literal("user"), + userId: z.string(), + }) + .catchall(z.never()), + z + .object({ + type: z.literal("bot"), + }) + .catchall(z.never()), + ]), + type: z.literal("choice"), + payload: z + .object({ + text: z.string().min(1, undefined), + options: z.array( + z + .object({ + label: z.string().min(1, undefined), + value: z.string().min(1, undefined), + }) + .catchall(z.never()), + ), + }) + .catchall(z.never()), + }) + .catchall(z.never()), + z + .object({ + source: z.union([ + z + .object({ + type: z.literal("user"), + userId: z.string(), + }) + .catchall(z.never()), + z + .object({ + type: z.literal("bot"), + }) + .catchall(z.never()), + ]), + type: z.literal("bloc"), + payload: z + .object({ + items: z.array( + z.union([ + z + .object({ + type: z.literal("text"), + payload: z + .object({ + text: z.string().min(1, undefined), + }) + .catchall(z.never()), + }) + .catchall(z.never()), + z + .object({ + type: z.literal("markdown"), + payload: z + .object({ + markdown: z.string().min(1, undefined), + }) + .catchall(z.never()), + }) + .catchall(z.never()), + z + .object({ + type: z.literal("image"), + payload: z + .object({ + imageUrl: z.string().min(1, undefined), + }) + .catchall(z.never()), + }) + .catchall(z.never()), + z + .object({ + type: z.literal("audio"), + payload: z + .object({ + audioUrl: z.string().min(1, undefined), + }) + .catchall(z.never()), + }) + .catchall(z.never()), + z + .object({ + type: z.literal("video"), + payload: z + .object({ + videoUrl: z.string().min(1, undefined), + }) + .catchall(z.never()), + }) + .catchall(z.never()), + z + .object({ + type: z.literal("file"), + payload: z + .object({ + fileUrl: z.string().min(1, undefined), + title: z.optional(z.string().min(1, undefined)), + }) + .catchall(z.never()), + }) + .catchall(z.never()), + z + .object({ + type: z.literal("location"), + payload: z + .object({ + latitude: z.number(), + longitude: z.number(), + address: z.optional(z.string()), + title: z.optional(z.string()), + }) + .catchall(z.never()), + }) + .catchall(z.never()), + ]), + ), + }) + .catchall(z.never()), + }) + .catchall(z.never()), + z + .object({ + source: z.union([ + z + .object({ + type: z.literal("user"), + userId: z.string(), + }) + .catchall(z.never()), + z + .object({ + type: z.literal("bot"), + }) + .catchall(z.never()), + ]), + type: z.literal("markdown"), + payload: z + .object({ + markdown: z.string().min(1, undefined), + }) + .catchall(z.never()), + }) + .catchall(z.never()), + ]), + ) + .title("Conversation history") + .describe( + "History of all messages in the conversation up to this point. Should be displayed to the human agent in the external service.", + ), + }) + .catchall(z.never()), +}; diff --git a/integrations/chatwoot/bp_modules/hitl/definition/states/index.ts b/integrations/chatwoot/bp_modules/hitl/definition/states/index.ts new file mode 100644 index 00000000..58a5aef5 --- /dev/null +++ b/integrations/chatwoot/bp_modules/hitl/definition/states/index.ts @@ -0,0 +1,9 @@ +/* eslint-disable */ +/* tslint:disable */ +// This file is generated. Do not edit it manually. +import * as hitlMessageHistory from "./hitlMessageHistory"; +export * as hitlMessageHistory from "./hitlMessageHistory"; + +export const states = { + "hitlMessageHistory": hitlMessageHistory.hitlMessageHistory, +} diff --git a/integrations/chatwoot/bp_modules/hitl/index.ts b/integrations/chatwoot/bp_modules/hitl/index.ts new file mode 100644 index 00000000..92e4f78e --- /dev/null +++ b/integrations/chatwoot/bp_modules/hitl/index.ts @@ -0,0 +1,16 @@ +/* eslint-disable */ +/* tslint:disable */ +// This file is generated. Do not edit it manually. + +import * as sdk from "@botpress/sdk" + +import definition from "./definition" + +export default { + type: "interface", + id: "ifver_01K0STHY79Y0GFGFGDXXGQKDVE", + uri: undefined, + name: "hitl", + version: "2.0.0", + definition, +} satisfies sdk.InterfacePackage \ No newline at end of file diff --git a/integrations/chatwoot/hub.md b/integrations/chatwoot/hub.md new file mode 100644 index 00000000..b044e8ec --- /dev/null +++ b/integrations/chatwoot/hub.md @@ -0,0 +1,9 @@ +# Chatwoot + +Connect your Botpress bot to Chatwoot for messaging and human handoff. + +## Setup + +1. Get your **API Access Token** from Profile Settings +2. Create an **API Inbox** and copy its ID from the URL +3. Add a **Webhook** pointing to your Botpress webhook URL with `message_created` and `conversation_status_changed` events diff --git a/integrations/chatwoot/icon.svg b/integrations/chatwoot/icon.svg new file mode 100644 index 00000000..5126e6d6 --- /dev/null +++ b/integrations/chatwoot/icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/integrations/chatwoot/integration.definition.ts b/integrations/chatwoot/integration.definition.ts new file mode 100644 index 00000000..d410bad6 --- /dev/null +++ b/integrations/chatwoot/integration.definition.ts @@ -0,0 +1,143 @@ +import { z, IntegrationDefinition } from '@botpress/sdk' +import hitl from './bp_modules/hitl' + +export default new IntegrationDefinition({ + name: 'plus/chatwoot', + title: 'Chatwoot', + description: 'Connect your Botpress bot to Chatwoot with HITL support', + version: '1.0.4', + readme: 'hub.md', + icon: 'icon.svg', + + configuration: { + schema: z.object({ + apiAccessToken: z.string().min(1).describe('Your Chatwoot API access token'), + inboxId: z.string().min(1).describe('Chatwoot Inbox ID for HITL and messaging channel conversations'), + accountId: z.string().min(1).describe('Chatwoot Account ID'), + }), + }, + + events: { + hitlStarted: { + title: 'HITL Started', + description: 'Triggered when a HITL session started', + schema: z.object({ + userId: z.string(), + title: z.string(), + description: z.string().optional(), + conversationId: z.string(), + }), + }, + }, + + states: { + userInfo: { + type: 'user', + schema: z.object({ + email: z.string(), + chatwootContactId: z.string(), + }), + }, + }, + + channels: { + hitl: { + conversation: { + tags: { + id: { title: 'Chatwoot Conversation ID', description: 'The ID of the conversation in Chatwoot' }, + bpUserId: { title: 'Botpress User ID', description: 'The Botpress user ID' }, + }, + }, + messages: { + text: { + schema: z.object({ + text: z.string(), + }), + }, + image: { + schema: z.object({ + imageUrl: z.string(), + }), + }, + file: { + schema: z.object({ + fileUrl: z.string(), + title: z.string().optional(), + }), + }, + video: { + schema: z.object({ + videoUrl: z.string(), + title: z.string().optional(), + }), + }, + choice: { + schema: z.object({ + text: z.string(), + options: z.array(z.object({ label: z.string(), value: z.string() })), + }), + }, + }, + message: { + tags: { + id: {}, + conversationId: {}, + }, + }, + }, + + channel: { + title: 'Chatwoot Messaging Channel', + description: 'Direct messaging channel for bot conversations', + conversation: { + tags: { + id: { title: 'Chatwoot Conversation ID' }, + }, + }, + messages: { + text: { schema: z.object({ text: z.string() }) }, + image: { schema: z.object({ imageUrl: z.string() }) }, + file: { schema: z.object({ fileUrl: z.string(), title: z.string().optional() }) }, + video: { schema: z.object({ videoUrl: z.string(), title: z.string().optional() }) }, + choice: { + schema: z.object({ + text: z.string(), + options: z.array(z.object({ label: z.string(), value: z.string() })), + }), + }, + }, + message: { + tags: { + id: {}, + conversationId: {}, + }, + }, + }, + }, + + user: { + tags: { + email: { title: 'User Email' }, + chatwootAgentId: { title: 'Chatwoot Agent ID' }, + chatwootContactId: { title: 'Chatwoot Contact ID' }, + }, + }, + + entities: { + ticket: { + title: 'Ticket', + description: 'A HITL ticket/session', + schema: z.object({}), + }, + }, +}).extend(hitl, (self) => ({ + entities: { + hitlSession: self.entities.ticket, + }, + channels: { + hitl: { + title: 'Chatwoot HITL', + description: 'Chatwoot HITL channel for human handoff', + }, + }, +})) diff --git a/integrations/chatwoot/package.json b/integrations/chatwoot/package.json new file mode 100644 index 00000000..f9a7ab4a --- /dev/null +++ b/integrations/chatwoot/package.json @@ -0,0 +1,28 @@ +{ + "name": "chatwoot", + "scripts": { + "check:type": "tsc --noEmit", + "version": "bp --version", + "login": "bp login", + "gen": "bp gen", + "deploy": "bp deploy", + "build": "bp build" + }, + "private": true, + "dependencies": { + "@botpress/client": "1.27.1", + "@botpress/sdk": "4.21.0", + "axios": "^1.8.1", + "form-data": "^4.0.5", + "zod": "^3.24.2" + }, + "devDependencies": { + "@botpress/cli": "4.27.4", + "@types/node": "^18.19.76", + "ts-node": "^10.9.2", + "typescript": "^5.8.2" + }, + "bpDependencies": { + "hitl": "integration:hitl@2.0.2" + } +} diff --git a/integrations/chatwoot/src/actions/hitl.ts b/integrations/chatwoot/src/actions/hitl.ts new file mode 100644 index 00000000..5f260be7 --- /dev/null +++ b/integrations/chatwoot/src/actions/hitl.ts @@ -0,0 +1,142 @@ +import * as bp from '.botpress' +import { RuntimeError } from '@botpress/sdk' +import { + searchContactByEmail, + createContact, + createConversation, + resolveConversation, + getPreviousAgentId, + assignConversation, + sendMessage, + getActiveConversation, + getApiAccessToken, +} from '../client' + +export const createUser: bp.IntegrationProps['actions']['createUser'] = async ({ ctx, client, input, logger }) => { + const { email } = input + + if (!email) { + throw new RuntimeError('Email is required for HITL') + } + + try { + const accountId = ctx.configuration.accountId + + const { user: botpressUser } = await client.getOrCreateUser({ + tags: { email }, + }) + + let chatwootContactId: string + const searchResult = await searchContactByEmail(getApiAccessToken(ctx), accountId, email) + + const existingContact = searchResult.payload?.[0] + if (existingContact) { + chatwootContactId = existingContact.id.toString() + logger.forBot().info(`Found Chatwoot contact: ${chatwootContactId}`) + } else { + const newContact = await createContact(getApiAccessToken(ctx), accountId, email, ctx.configuration.inboxId) + chatwootContactId = newContact.payload.contact.id.toString() + logger.forBot().info(`Created Chatwoot contact: ${chatwootContactId}`) + } + + await client.setState({ + id: botpressUser.id, + type: 'user', + name: 'userInfo', + payload: { email, chatwootContactId }, + }) + + return { userId: botpressUser.id } + } catch (error) { + logger.forBot().error(`createUser failed: ${error}`) + throw new RuntimeError(`Failed to create user: ${error instanceof Error ? error.message : String(error)}`) + } +} + +export const startHitl: bp.IntegrationProps['actions']['startHitl'] = async ({ ctx, client, input, logger }) => { + const { userId, title, description = 'HITL started' } = input + + try { + const apiAccessToken = getApiAccessToken(ctx) + + const userState = await client.getState({ id: userId, name: 'userInfo', type: 'user' }) + + if (!userState?.state?.payload?.chatwootContactId) { + throw new RuntimeError('Call createUser first') + } + + const { chatwootContactId } = userState.state.payload + const accountId = ctx.configuration.accountId + + const activeConversation = await getActiveConversation(apiAccessToken, accountId, chatwootContactId) + + let chatwootConvId: string + + if (activeConversation) { + chatwootConvId = activeConversation.id.toString() + logger.forBot().info(`Reusing existing conversation: ${chatwootConvId}`) + } else { + const chatwootConv = await createConversation( + apiAccessToken, + accountId, + chatwootContactId, + ctx.configuration.inboxId + ) + chatwootConvId = chatwootConv.id.toString() + logger.forBot().info(`Created new conversation: ${chatwootConvId}`) + } + + await sendMessage(apiAccessToken, accountId, chatwootConvId, description, 'incoming') + + try { + const previousAgentId = await getPreviousAgentId(apiAccessToken, accountId, chatwootContactId) + if (previousAgentId) { + await assignConversation(apiAccessToken, accountId, chatwootConvId, previousAgentId.toString()) + logger.forBot().info(`Assigned to previous agent: ${previousAgentId}`) + } + } catch (error) { + logger.forBot().warn(`Failed to assign to previous agent: ${error}`) + } + + const { conversation } = await client.getOrCreateConversation({ + channel: 'hitl', + tags: { id: chatwootConvId, bpUserId: userId }, + }) + + await client.createEvent({ + type: 'hitlStarted', + conversationId: conversation.id, + payload: { conversationId: conversation.id, userId, title: title ?? 'HITL', description }, + }) + + logger.forBot().info(`HITL started: ${conversation.id} → Chatwoot ${chatwootConvId}`) + return { conversationId: conversation.id } + } catch (error) { + throw new RuntimeError(`Failed to start HITL: ${error instanceof Error ? error.message : String(error)}`) + } +} + +export const stopHitl: bp.IntegrationProps['actions']['stopHitl'] = async ({ ctx, client, input }) => { + const { conversationId } = input + + try { + const { conversation } = await client.getConversation({ id: conversationId }) + const chatwootConvId = conversation.tags.id + + if (!chatwootConvId) { + return { success: false, message: 'No Chatwoot conversation ID' } + } + + const accountId = ctx.configuration.accountId + await resolveConversation(getApiAccessToken(ctx), accountId, chatwootConvId) + + await client.createEvent({ + type: 'hitlStopped', + payload: { conversationId }, + }) + + return { success: true, message: 'HITL stopped' } + } catch (error) { + return { success: false, message: String(error) } + } +} diff --git a/integrations/chatwoot/src/actions/index.ts b/integrations/chatwoot/src/actions/index.ts new file mode 100644 index 00000000..5d59c938 --- /dev/null +++ b/integrations/chatwoot/src/actions/index.ts @@ -0,0 +1,8 @@ +import { startHitl, stopHitl, createUser } from './hitl' +import * as bp from '.botpress' + +export default { + startHitl, + stopHitl, + createUser, +} satisfies bp.IntegrationProps['actions'] diff --git a/integrations/chatwoot/src/channels.ts b/integrations/chatwoot/src/channels.ts new file mode 100644 index 00000000..93f9e3bd --- /dev/null +++ b/integrations/chatwoot/src/channels.ts @@ -0,0 +1,111 @@ +import * as bp from '.botpress' +import { RuntimeError } from '@botpress/sdk' +import { sendMessage, sendAttachment, getApiAccessToken } from './client' + +type ConversationWithTags = { tags: { id?: string } } +type MessageDirection = 'toChatwoot' | 'fromChatwoot' + +const directionMap: Record = { + toChatwoot: 'incoming', + fromChatwoot: 'outgoing', +} + +const getConversationContext = async (ctx: bp.Context, conversation: ConversationWithTags) => { + const chatwootConvId = conversation.tags.id + if (!chatwootConvId) throw new RuntimeError('No Chatwoot conversation ID') + const accountId = ctx.configuration.accountId + return { chatwootConvId, accountId } +} + +const unsupportedHandler = + (type: string) => + async ({ logger }: { logger: bp.Logger }) => { + logger.forBot().warn(`Unsupported message type: ${type}`) + } + +const unsupportedMessages = { + audio: unsupportedHandler('Audio'), + bloc: unsupportedHandler('Bloc'), + card: unsupportedHandler('Card'), + carousel: unsupportedHandler('Carousel'), + dropdown: unsupportedHandler('Dropdown'), + location: unsupportedHandler('Location'), + markdown: unsupportedHandler('Markdown'), +} + +const createMessageHandlers = (direction: MessageDirection) => ({ + text: async ({ + ctx, + conversation, + payload, + }: { + ctx: bp.Context + conversation: ConversationWithTags + payload: { text: string } + }) => { + const { chatwootConvId, accountId } = await getConversationContext(ctx, conversation) + await sendMessage(getApiAccessToken(ctx), accountId, chatwootConvId, payload.text, directionMap[direction]) + }, + image: async ({ + ctx, + conversation, + payload, + }: { + ctx: bp.Context + conversation: ConversationWithTags + payload: { imageUrl: string } + }) => { + const { chatwootConvId, accountId } = await getConversationContext(ctx, conversation) + const res = await fetch(payload.imageUrl) + const buffer = Buffer.from(await res.arrayBuffer()) + await sendAttachment(getApiAccessToken(ctx), accountId, chatwootConvId, buffer, 'image.png') + }, + file: async ({ + ctx, + conversation, + payload, + }: { + ctx: bp.Context + conversation: ConversationWithTags + payload: { fileUrl: string; title?: string } + }) => { + const { chatwootConvId, accountId } = await getConversationContext(ctx, conversation) + const res = await fetch(payload.fileUrl) + const buffer = Buffer.from(await res.arrayBuffer()) + await sendAttachment(getApiAccessToken(ctx), accountId, chatwootConvId, buffer, payload.title || 'file') + }, + video: async ({ + ctx, + conversation, + payload, + }: { + ctx: bp.Context + conversation: ConversationWithTags + payload: { videoUrl: string; title?: string } + }) => { + const { chatwootConvId, accountId } = await getConversationContext(ctx, conversation) + const res = await fetch(payload.videoUrl) + const buffer = Buffer.from(await res.arrayBuffer()) + await sendAttachment(getApiAccessToken(ctx), accountId, chatwootConvId, buffer, payload.title || 'video.mp4') + }, + choice: async ({ + ctx, + conversation, + payload, + }: { + ctx: bp.Context + conversation: ConversationWithTags + payload: { text: string; options: Array<{ label: string }> } + }) => { + const { chatwootConvId, accountId } = await getConversationContext(ctx, conversation) + const options = payload.options.map((opt, i) => `${i + 1}. ${opt.label}`).join('\n') + const text = `${payload.text}\n\n${options}` + await sendMessage(getApiAccessToken(ctx), accountId, chatwootConvId, text, directionMap[direction]) + }, + ...unsupportedMessages, +}) + +export const channels = { + hitl: { messages: createMessageHandlers('toChatwoot') }, + channel: { messages: createMessageHandlers('fromChatwoot') }, +} satisfies bp.IntegrationProps['channels'] diff --git a/integrations/chatwoot/src/client.ts b/integrations/chatwoot/src/client.ts new file mode 100644 index 00000000..607e7e4a --- /dev/null +++ b/integrations/chatwoot/src/client.ts @@ -0,0 +1,210 @@ +import * as bp from '.botpress' +import axios, { AxiosInstance } from 'axios' +import { RuntimeError } from '@botpress/sdk' +import { + ChatwootMessageResponse, + ChatwootProfile, + ChatwootContactSearchResponse, + ChatwootContactCreateResponse, + ChatwootConversationResponse, + ChatwootStatusToggleResponse, + ChatwootConversation, + ChatwootAgent, + chatwootProfileSchema, + chatwootMessageResponseSchema, + chatwootContactSearchResponseSchema, + chatwootContactCreateResponseSchema, + chatwootConversationResponseSchema, + chatwootStatusToggleResponseSchema, + chatwootAgentSchema, + chatwootConversationSchema, +} from './misc/types' +import FormData from 'form-data' + +const chatwootClient = (apiAccessToken: string): AxiosInstance => { + return axios.create({ + baseURL: 'https://app.chatwoot.com/api/v1', + headers: { api_access_token: apiAccessToken }, + }) +} + +export const getApiAccessToken = (ctx: bp.Context) => { + const apiAccessToken = ctx.configuration.apiAccessToken + if (!apiAccessToken) { + throw new RuntimeError('API access token is required') + } + return apiAccessToken +} + +export const getProfile = async (apiAccessToken: string): Promise => { + try { + const client = chatwootClient(apiAccessToken) + const response = await client.get(`/profile`) + return chatwootProfileSchema.parse(response.data) + } catch (error) { + throw new RuntimeError(`Failed to get profile: ${error instanceof Error ? error.message : String(error)}`) + } +} + +export const sendMessage = async ( + apiAccessToken: string, + accountId: string, + conversationId: string, + content: string, + messageType: 'incoming' | 'outgoing' +): Promise => { + try { + const client = chatwootClient(apiAccessToken) + const response = await client.post(`/accounts/${accountId}/conversations/${conversationId}/messages`, { + content, + message_type: messageType, + private: false, + }) + return chatwootMessageResponseSchema.parse(response.data).id + } catch (error) { + throw new RuntimeError(`Failed to send message: ${error instanceof Error ? error.message : String(error)}`) + } +} + +export const sendAttachment = async ( + apiAccessToken: string, + accountId: string, + conversationId: string, + fileBuffer: Buffer, + fileName: string +): Promise => { + try { + const client = chatwootClient(apiAccessToken) + const formData = new FormData() + formData.append('attachments[]', fileBuffer, fileName) + const response = await client.post(`/accounts/${accountId}/conversations/${conversationId}/messages`, formData, { + headers: { ...formData.getHeaders() }, + }) + return chatwootMessageResponseSchema.parse(response.data).id + } catch (error) { + throw new RuntimeError(`Failed to send attachment: ${error instanceof Error ? error.message : String(error)}`) + } +} + +export const searchContactByEmail = async ( + apiAccessToken: string, + accountId: string, + email: string +): Promise => { + try { + const client = chatwootClient(apiAccessToken) + const response = await client.get(`/accounts/${accountId}/contacts/search`, { params: { q: email } }) + return chatwootContactSearchResponseSchema.parse(response.data) + } catch (error) { + throw new RuntimeError( + `Failed to search contact by email: ${error instanceof Error ? error.message : String(error)}` + ) + } +} + +export const createContact = async ( + apiAccessToken: string, + accountId: string, + email: string, + inboxId: string +): Promise => { + try { + const client = chatwootClient(apiAccessToken) + const response = await client.post(`/accounts/${accountId}/contacts`, { + email: email, + name: email, + inbox_id: inboxId, + }) + return chatwootContactCreateResponseSchema.parse(response.data) + } catch (error) { + throw new RuntimeError(`Failed to create contact: ${error instanceof Error ? error.message : String(error)}`) + } +} + +export const createConversation = async ( + apiAccessToken: string, + accountId: string, + contactId: string, + inboxId: string +): Promise => { + try { + const client = chatwootClient(apiAccessToken) + const response = await client.post(`/accounts/${accountId}/conversations`, { + contact_id: contactId, + inbox_id: inboxId, + }) + return chatwootConversationResponseSchema.parse(response.data) + } catch (error) { + throw new RuntimeError(`Failed to create conversation: ${error instanceof Error ? error.message : String(error)}`) + } +} + +export const resolveConversation = async ( + apiAccessToken: string, + accountId: string, + conversationId: string +): Promise => { + try { + const client = chatwootClient(apiAccessToken) + const response = await client.post(`/accounts/${accountId}/conversations/${conversationId}/toggle_status`, { + status: 'resolved', + }) + return chatwootStatusToggleResponseSchema.parse(response.data) + } catch (error) { + throw new RuntimeError(`Failed to resolve conversation: ${error instanceof Error ? error.message : String(error)}`) + } +} + +export const getContactConversations = async ( + apiAccessToken: string, + accountId: string, + contactId: string +): Promise => { + try { + const client = chatwootClient(apiAccessToken) + const response = await client.get(`/accounts/${accountId}/contacts/${contactId}/conversations`) + return chatwootConversationSchema.array().parse(response.data.payload ?? []) + } catch (error) { + throw new RuntimeError( + `Failed to get contact conversations: ${error instanceof Error ? error.message : String(error)}` + ) + } +} + +export const assignConversation = async ( + apiAccessToken: string, + accountId: string, + conversationId: string, + assigneeId: string +): Promise => { + try { + const client = chatwootClient(apiAccessToken) + const response = await client.post(`/accounts/${accountId}/conversations/${conversationId}/assignments`, { + assignee_id: assigneeId, + }) + return chatwootAgentSchema.parse(response.data) + } catch (error) { + throw new RuntimeError(`Failed to assign conversation: ${error instanceof Error ? error.message : String(error)}`) + } +} + +export const getPreviousAgentId = async ( + apiAccessToken: string, + accountId: string, + contactId: string +): Promise => { + const conversations = await getContactConversations(apiAccessToken, accountId, contactId) + + const withAssignee = conversations.filter((c) => c.meta?.assignee?.id).sort((a, b) => b.id - a.id) + + return withAssignee[0]?.meta?.assignee?.id || null +} + +export const getActiveConversation = async ( + apiAccessToken: string, + accountId: string, + contactId: string +): Promise => { + const conversations = await getContactConversations(apiAccessToken, accountId, contactId) + return conversations.find((c) => c.status === 'open') || null +} diff --git a/integrations/chatwoot/src/index.ts b/integrations/chatwoot/src/index.ts new file mode 100644 index 00000000..a57b706d --- /dev/null +++ b/integrations/chatwoot/src/index.ts @@ -0,0 +1,12 @@ +import * as bp from '.botpress' +import actions from './actions' +import { register, unregister, handler } from './setup' +import { channels } from './channels' + +export default new bp.Integration({ + register, + unregister, + actions, + handler, + channels, +}) diff --git a/integrations/chatwoot/src/misc/types.ts b/integrations/chatwoot/src/misc/types.ts new file mode 100644 index 00000000..8a5e6ce3 --- /dev/null +++ b/integrations/chatwoot/src/misc/types.ts @@ -0,0 +1,121 @@ +import { z } from '@botpress/sdk' + +export const chatwootEventTypeSchema = z.enum([ + 'message_created', + 'message_updated', + 'conversation_created', + 'conversation_updated', + 'conversation_status_changed', + 'webwidget_triggered', + 'contact_created', + 'contact_updated', +]) + +export const chatwootSenderSchema = z.object({ + id: z.number().optional(), + name: z.string().nullable(), +}) + +export const chatwootAgentSchema = z.object({ + id: z.number(), +}) + +export const chatwootConversationMetaSchema = z.object({ + assignee: z + .object({ + id: z.number(), + }) + .nullable(), +}) + +export const chatwootConversationSchema = z.object({ + id: z.number(), + status: z.string().optional(), + meta: chatwootConversationMetaSchema.optional(), +}) + +export const chatwootAttachmentSchema = z.object({ + file_type: z.string(), + data_url: z.string().optional(), + file_url: z.string().optional(), + filename: z.string().optional(), +}) + +export const chatwootWebhookPayloadSchema = z.object({ + event: chatwootEventTypeSchema, + id: z.number().optional(), + status: z.string().optional(), + content: z.string().nullable(), + message_type: z.union([z.string(), z.number()]).optional(), + sender: chatwootSenderSchema.optional(), + conversation: chatwootConversationSchema.optional(), + attachments: z.array(chatwootAttachmentSchema).optional(), +}) + +export const chatwootProfileSchema = z.object({ + id: z.number(), + name: z.string(), + email: z.string(), + accounts: z.array( + z.object({ + id: z.number(), + name: z.string(), + role: z.string().optional(), + }) + ), +}) + +export const chatwootMessageResponseSchema = z.object({ + id: z.number(), +}) + +export const chatwootContactSchema = z.object({ + id: z.number(), +}) + +export const chatwootContactSearchResponseSchema = z.object({ + payload: z.array(chatwootContactSchema), +}) + +export const chatwootContactCreateResponseSchema = z.object({ + payload: z.object({ + contact: chatwootContactSchema, + }), +}) + +export const chatwootConversationResponseSchema = z.object({ + id: z.number(), +}) + +export const chatwootStatusToggleResponseSchema = z.object({ + payload: z + .object({ + success: z.boolean(), + current_status: z.string(), + conversation_id: z.number(), + }) + .optional(), + success: z.boolean().optional(), + current_status: z.string().optional(), + conversation_id: z.number().optional(), +}) + +export const chatwootContactConversationsResponseSchema = z.object({ + payload: z.array(chatwootConversationSchema), +}) + +export type ChatwootEventType = z.infer +export type ChatwootSender = z.infer +export type ChatwootAgent = z.infer +export type ChatwootConversation = z.infer +export type ChatwootAttachment = z.infer +export type ChatwootWebhookPayload = z.infer +export type ChatwootProfile = z.infer +export type ChatwootMessageResponse = z.infer +export type ChatwootContact = z.infer +export type ChatwootContactSearchResponse = z.infer +export type ChatwootContactCreateResponse = z.infer +export type ChatwootConversationResponse = z.infer +export type ChatwootStatusToggleResponse = z.infer +export type ChatwootContactConversationsResponse = z.infer +export type ChatwootConversationMeta = z.infer diff --git a/integrations/chatwoot/src/setup/handler.ts b/integrations/chatwoot/src/setup/handler.ts new file mode 100644 index 00000000..6988176d --- /dev/null +++ b/integrations/chatwoot/src/setup/handler.ts @@ -0,0 +1,141 @@ +import * as bp from '.botpress' +import { ChatwootWebhookPayload, chatwootWebhookPayloadSchema } from '../misc/types' + +export const handler: bp.IntegrationProps['handler'] = async ({ req, client, logger }) => { + const payload = chatwootWebhookPayloadSchema.parse(JSON.parse(req.body || '{}')) + + if (payload.event === 'conversation_status_changed' && payload.status === 'resolved' && payload.id) { + await handleConversationResolvedById(payload.id.toString(), client, logger) + return + } + + if (payload.event !== 'message_created') return + + // message_type: 0/"incoming" or 1/"outgoing" - string for API calls, number for webhooks + const isOutgoing = payload.message_type === 1 || payload.message_type === 'outgoing' + const isIncoming = payload.message_type === 0 || payload.message_type === 'incoming' + + if (isOutgoing) { + const chatwootConvId = payload.conversation?.id?.toString() + if (!chatwootConvId) return + + const hitlConv = await findHitlConversation(client, chatwootConvId, logger) + if (!hitlConv) return + + await handleHitlMessage(payload, client, hitlConv) + } else if (isIncoming) { + await handleMessagingChannelMessage(payload, client) + } +} + +async function handleConversationResolvedById(chatwootConvId: string, client: bp.Client, logger: bp.Logger) { + const hitlConv = await findHitlConversation(client, chatwootConvId, logger) + if (!hitlConv) return + + await client.createEvent({ + type: 'hitlStopped', + payload: { conversationId: hitlConv.id }, + }) +} + +async function findHitlConversation(client: bp.Client, chatwootConvId: string, logger: bp.Logger) { + const { conversations } = await client.listConversations({ + tags: { id: chatwootConvId }, + }) + const hitlConv = conversations.find((c) => c.tags?.bpUserId) + if (!hitlConv) { + logger.forBot().error(`No hitl conversation found for chatwoot conversation ${chatwootConvId}`) + } + return hitlConv +} + +async function handleHitlMessage( + payload: ChatwootWebhookPayload, + client: bp.Client, + hitlConv: { id: string; tags?: Record } +) { + const chatwootConvId = payload.conversation?.id?.toString() + if (!chatwootConvId) return + + const { user: agentUser } = await client.getOrCreateUser({ + tags: { chatwootAgentId: payload.sender?.id?.toString() || 'unknown' }, + name: payload.sender?.name || 'Agent', + }) + + await createBotpressMessages(client, payload, hitlConv.id, agentUser.id, chatwootConvId) +} + +async function handleMessagingChannelMessage(payload: ChatwootWebhookPayload, client: bp.Client) { + const chatwootConvId = payload.conversation?.id?.toString() + if (!chatwootConvId) { + return + } + + const { conversation } = await client.getOrCreateConversation({ + channel: 'channel', + tags: { id: chatwootConvId }, + }) + + const { user: contactUser } = await client.getOrCreateUser({ + tags: { chatwootContactId: payload.sender?.id?.toString() || 'unknown' }, + name: payload.sender?.name || 'Contact', + }) + + await createBotpressMessages(client, payload, conversation.id, contactUser.id, chatwootConvId) +} + +async function createBotpressMessages( + client: bp.Client, + payload: ChatwootWebhookPayload, + conversationId: string, + userId: string, + chatwootConvId: string +) { + if (payload.content?.trim()) { + await client.createMessage({ + conversationId, + userId, + type: 'text', + payload: { text: payload.content }, + tags: { id: payload.id?.toString() || '', conversationId: chatwootConvId }, + }) + } + + if (payload.attachments?.length) { + for (const attachment of payload.attachments) { + const baseTags = { id: payload.id?.toString() || '', conversationId: chatwootConvId } + const attachmentUrl = attachment.data_url || attachment.file_url + if (!attachmentUrl) continue + + switch (attachment.file_type) { + case 'image': + await client.createMessage({ + conversationId, + userId, + type: 'image', + payload: { imageUrl: attachmentUrl }, + tags: baseTags, + }) + break + case 'video': + await client.createMessage({ + conversationId, + userId, + type: 'video', + payload: { videoUrl: attachmentUrl }, + tags: baseTags, + }) + break + case 'file': + await client.createMessage({ + conversationId, + userId, + type: 'file', + payload: { fileUrl: attachmentUrl, title: attachment.filename || 'File' }, + tags: baseTags, + }) + break + } + } + } +} diff --git a/integrations/chatwoot/src/setup/index.ts b/integrations/chatwoot/src/setup/index.ts new file mode 100644 index 00000000..cb0db42d --- /dev/null +++ b/integrations/chatwoot/src/setup/index.ts @@ -0,0 +1,3 @@ +export { register } from './register' +export { unregister } from './unregister' +export { handler } from './handler' diff --git a/integrations/chatwoot/src/setup/register.ts b/integrations/chatwoot/src/setup/register.ts new file mode 100644 index 00000000..33d3fdf6 --- /dev/null +++ b/integrations/chatwoot/src/setup/register.ts @@ -0,0 +1,17 @@ +import * as bp from '.botpress' +import { RuntimeError } from '@botpress/sdk' +import { getApiAccessToken, getProfile } from '../client' + +export const register: bp.IntegrationProps['register'] = async ({ ctx, logger }) => { + logger.forBot().info('Registering Chatwoot integration...') + + const apiAccessToken = getApiAccessToken(ctx) + const accountId = ctx.configuration.accountId + + const profile = await getProfile(apiAccessToken) + const account = profile.accounts.find((account) => account.id.toString() === accountId) + if (!account) { + throw new RuntimeError('Account not found') + } + logger.forBot().info('Chatwoot integration registered successfully') +} diff --git a/integrations/chatwoot/src/setup/unregister.ts b/integrations/chatwoot/src/setup/unregister.ts new file mode 100644 index 00000000..35a2688e --- /dev/null +++ b/integrations/chatwoot/src/setup/unregister.ts @@ -0,0 +1,5 @@ +import * as bp from '.botpress' + +export const unregister: bp.IntegrationProps['unregister'] = async ({ logger }) => { + logger.forBot().info('Chatwoot integration unregistered') +} diff --git a/integrations/chatwoot/tsconfig.json b/integrations/chatwoot/tsconfig.json new file mode 100644 index 00000000..462fc49c --- /dev/null +++ b/integrations/chatwoot/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "lib": ["es2022"], + "module": "commonjs", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node", + "allowUnusedLabels": false, + "allowUnreachableCode": false, + "noFallthroughCasesInSwitch": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noUncheckedIndexedAccess": true, + "noUnusedParameters": true, + "target": "es2017", + "baseUrl": ".", + "outDir": "dist", + "checkJs": false, + "exactOptionalPropertyTypes": false, + "resolveJsonModule": true, + "noPropertyAccessFromIndexSignature": false, + "noUnusedLocals": false + }, + "include": [".botpress/**/*", "src/**/*", "*.ts", "*.json"] +}