chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:34:48 +08:00
commit 77bb5bf71f
3762 changed files with 353249 additions and 0 deletions
+152
View File
@@ -0,0 +1,152 @@
import { RuntimeError } from '@botpress/client'
import { buildConversationTranscript } from '@botpress/common'
import { AxiosError } from 'axios'
import { getFreshchatClient } from 'src/client'
import * as bp from '.botpress'
export const startHitl: bp.IntegrationProps['actions']['startHitl'] = async ({ ctx, client, input, logger }) => {
try {
const freshchatClient = getFreshchatClient({ ...ctx.configuration }, logger)
const { user } = await client.getUser({
// Retrieve the user ID created in the createUser action
id: input.userId,
})
if (!user.tags.id?.length) {
throw new RuntimeError("Input user doesn't have a Freshchat User Id")
}
const { title, description, messageHistory } = input
const {
state: {
payload: { channelId },
},
} = await client.getState({
type: 'integration',
id: ctx.integrationId,
name: 'freshchat',
})
const messages = [
{
message_parts: [
{
text: {
content: `New Conversation Started
Title: ${title}
Description: ${description}
`,
},
},
],
channel_id: channelId,
message_type: 'normal',
actor_type: 'user',
actor_id: user.tags.id,
},
]
messages.push({
message_parts: [
{
text: {
content:
'Transcript:\n\n' +
(await buildConversationTranscript({
ctx,
client,
messages: messageHistory,
customTranscriptFormatter: (msgs) =>
msgs.map((msg) => (msg.isBot ? 'Bot: ' : 'User: ') + msg.text.join('\n')).join('\n\n'),
})),
},
},
],
channel_id: channelId,
message_type: 'normal',
actor_type: 'user',
actor_id: user.tags.id,
})
const freshchatConversation = await freshchatClient.createConversation({
userId: user.tags.id as string,
messages,
channelId,
priority: input.hitlSession?.priority,
})
const { conversation } = await client.getOrCreateConversation({
channel: 'hitl',
tags: {
id: `${freshchatConversation.conversation_id}`,
},
})
return {
conversationId: conversation.id,
}
} catch (error: any) {
logger.forBot().error('Error Starting Freshchat Hitl: ' + error.message, error?.response?.data)
throw new RuntimeError(error.message)
}
}
export const stopHitl: bp.IntegrationProps['actions']['stopHitl'] = async ({ ctx, input, client, logger }) => {
const { conversation } = await client.getConversation({
id: input.conversationId,
})
const freshchatConversationId: string | undefined = conversation.tags.id
if (!freshchatConversationId) {
return {}
}
const freshchatClient = getFreshchatClient({ ...ctx.configuration }, logger)
try {
await freshchatClient.setConversationAsResolved(freshchatConversationId)
} catch (thrown: unknown) {
const error: AxiosError = thrown instanceof AxiosError ? thrown : new AxiosError(String(thrown))
logger.forBot().error('Error resolving HITL conversation on Freshchat: ' + error.message, error?.response?.data)
}
return {}
}
// create a user in both platforms
export const createUser: bp.IntegrationProps['actions']['createUser'] = async ({ client, input, ctx, logger }) => {
try {
const freshchatClient = getFreshchatClient({ ...ctx.configuration }, logger)
const { user: botpressUser } = await client.getOrCreateUser({
...input,
tags: {
email: input.email,
},
})
const freshchatUser = await freshchatClient.getOrCreateUser({ ...input, botpressUserId: botpressUser.id })
if (!freshchatUser.id) {
throw new RuntimeError('Failed to create Freshchat User')
}
await client.updateUser({
...input,
id: botpressUser.id,
tags: {
id: freshchatUser.id,
},
})
return {
userId: botpressUser.id, // always return the newly created botpress user id
}
} catch (error: any) {
throw new RuntimeError(error.message)
}
}
@@ -0,0 +1,8 @@
import { startHitl, stopHitl, createUser } from './hitl'
import * as bp from '.botpress'
export const actions = {
startHitl,
stopHitl,
createUser,
} satisfies bp.IntegrationProps['actions']
+173
View File
@@ -0,0 +1,173 @@
import * as bpCommon from '@botpress/common'
import * as sdk from '@botpress/sdk'
import { getFreshchatClient } from './client'
import { getMediaMetadata } from './util'
import * as bp from '.botpress'
const wrapChannel = bpCommon.createChannelWrapper<bp.IntegrationProps>()({
toolFactories: {
freshchatClient({ ctx, logger }) {
return getFreshchatClient({ ...ctx.configuration }, logger)
},
async freshchatUserId({ client, payload, user: attachedUser }) {
const user = payload.userId ? (await client.getUser({ id: payload.userId })).user : attachedUser
return user.tags.id
},
async freshchatConversationId({ conversation }) {
const freshchatConversationId = conversation.tags.id
if (!freshchatConversationId) {
throw new sdk.RuntimeError('Freshchat conversation id not found')
}
return freshchatConversationId
},
},
})
export const channels = {
hitl: {
messages: {
text: wrapChannel(
{ channelName: 'hitl', messageType: 'text' },
async ({ ack, payload, freshchatClient, freshchatUserId, freshchatConversationId }) => {
const freshchatMessageId = await freshchatClient.sendMessage(freshchatUserId, freshchatConversationId, [
{ text: { content: payload.text } },
])
await ack({
tags: { id: freshchatMessageId },
})
}
),
image: wrapChannel(
{ channelName: 'hitl', messageType: 'image' },
async ({ ack, payload, freshchatClient, freshchatUserId, freshchatConversationId }) => {
const freshchatMessageId = await freshchatClient.sendMessage(freshchatUserId, freshchatConversationId, [
{ image: { url: payload.imageUrl } },
])
await ack({
tags: { id: freshchatMessageId },
})
}
),
audio: wrapChannel(
{ channelName: 'hitl', messageType: 'audio' },
async ({ ack, payload, freshchatClient, freshchatUserId, freshchatConversationId }) => {
const metadata = await getMediaMetadata(payload.audioUrl)
const freshchatMessageId = await freshchatClient.sendMessage(freshchatUserId, freshchatConversationId, [
{ video: { url: payload.audioUrl, content_type: metadata.mimeType } },
])
await ack({
tags: { id: freshchatMessageId },
})
}
),
video: wrapChannel(
{ channelName: 'hitl', messageType: 'video' },
async ({ ack, payload, freshchatClient, freshchatUserId, freshchatConversationId }) => {
const metadata = await getMediaMetadata(payload.videoUrl)
const freshchatMessageId = await freshchatClient.sendMessage(freshchatUserId, freshchatConversationId, [
{ video: { url: payload.videoUrl, content_type: metadata.mimeType } },
])
await ack({
tags: { id: freshchatMessageId },
})
}
),
file: wrapChannel(
{ channelName: 'hitl', messageType: 'file' },
async ({ ack, payload, freshchatClient, freshchatUserId, freshchatConversationId }) => {
const metadata = await getMediaMetadata(payload.fileUrl)
const freshchatMessageId = await freshchatClient.sendMessage(freshchatUserId, freshchatConversationId, [
{
file: {
url: payload.fileUrl,
name: payload.title || metadata.fileName || 'file',
contentType: metadata.mimeType,
},
},
])
await ack({
tags: { id: freshchatMessageId },
})
}
),
bloc: wrapChannel(
{ channelName: 'hitl', messageType: 'bloc' },
async ({ ack, payload, freshchatClient, freshchatUserId, freshchatConversationId }) => {
for (const item of payload.items) {
switch (item.type) {
case 'text':
await freshchatClient.sendMessage(freshchatUserId, freshchatConversationId, [
{ text: { content: item.payload.text } },
])
break
case 'markdown':
await freshchatClient.sendMessage(freshchatUserId, freshchatConversationId, [
{ text: { content: item.payload.markdown } },
])
break
case 'image':
await freshchatClient.sendMessage(freshchatUserId, freshchatConversationId, [
{ image: { url: item.payload.imageUrl } },
])
break
case 'video':
const videoMetadata = await getMediaMetadata(item.payload.videoUrl)
await freshchatClient.sendMessage(freshchatUserId, freshchatConversationId, [
{ video: { url: item.payload.videoUrl, content_type: videoMetadata.mimeType } },
])
break
case 'audio':
const audioMetadata = await getMediaMetadata(item.payload.audioUrl)
await freshchatClient.sendMessage(freshchatUserId, freshchatConversationId, [
{ video: { url: item.payload.audioUrl, content_type: audioMetadata.mimeType } },
])
break
case 'file':
const fileMetadata = await getMediaMetadata(item.payload.fileUrl)
await freshchatClient.sendMessage(freshchatUserId, freshchatConversationId, [
{
file: {
url: item.payload.fileUrl,
name: item.payload.title || fileMetadata.fileName || 'file',
contentType: fileMetadata.mimeType,
},
},
])
break
case 'location':
const { title, address, latitude, longitude } = item.payload
const messageParts = []
if (title) {
messageParts.push(title, '')
}
if (address) {
messageParts.push(address, '')
}
messageParts.push(`Latitude: ${latitude}`, `Longitude: ${longitude}`)
await freshchatClient.sendMessage(freshchatUserId, freshchatConversationId, [
{ text: { content: messageParts.join('\n') } },
])
break
default:
item satisfies never
}
}
await ack({ tags: {} })
}
),
},
},
} satisfies bp.IntegrationProps['channels']
+193
View File
@@ -0,0 +1,193 @@
import * as sdk from '@botpress/sdk'
import axios, { type Axios } from 'axios'
import type {
FreshchatMessage,
FreshchatAgent,
FreshchatChannel,
FreshchatConfiguration,
FreshchatUser,
} from './definitions/schemas'
import * as bp from '.botpress'
// API docs: https://developers.freshchat.com/api/
class FreshchatClient {
private _client: Axios
public constructor(
private _config: FreshchatConfiguration,
private _logger: bp.Logger
) {
this._client = axios.create({
baseURL: `https://${this._config.domain}.freshchat.com/v2`,
})
this._client.interceptors.request.use((axionsConfig) => {
// @ts-ignore
axionsConfig.headers = {
...axionsConfig.headers,
Authorization: 'Bearer ' + this._config.token,
}
return axionsConfig
})
}
public async createConversation(args: {
userId: string
messages: FreshchatMessage[]
channelId: string
priority?: 'Low' | 'Medium' | 'High' | 'Urgent'
}): Promise<{ conversation_id: string; channel_id: string }> {
const { data } = await this._client.post('/conversations', {
channel_id: args.channelId,
messages: args.messages,
users: [
{
id: args.userId,
},
],
properties: {
...(args.priority ? { priority: args.priority } : {}),
},
})
return data
}
public async setConversationAsResolved(freshchatConversationId: string) {
await this._client.put(`/conversations/${freshchatConversationId}`, {
status: 'resolved',
})
}
public async sendMessage(
fromUserId: string | undefined,
freshchatConversationId: string,
messageParts: Array<
| { text: { content: string } }
| { image: { url: string } }
| { video: { url: string; content_type?: string } }
| {
file: {
url: string
name: string
file_size_in_bytes?: number
contentType?: string
file_extension?: string
fileSource?: 'FRESHCHAT' | 'FRESHBOTS'
}
}
>
): Promise<string> {
const response = await this._client.post<FreshchatMessage & { id: string }>(
`/conversations/${freshchatConversationId}/messages`,
{
message_parts: messageParts,
message_type: 'normal',
...(fromUserId?.length && { user_id: fromUserId }),
actor_type: fromUserId?.length ? 'user' : 'bot',
...(fromUserId?.length && { actor_id: fromUserId }),
}
)
const messageId = response.data.id
if (!messageId) {
throw new sdk.RuntimeError('Failed to send message')
}
return messageId
}
public async getOrCreateUser(props: {
name?: string
email?: string
pictureUrl?: string
botpressUserId: string
}): Promise<FreshchatUser> {
let user = await this._getUserByReferenceId(props.botpressUserId)
if (!user) {
user = await this._getUserByEmail(props.email)
}
if (!user) {
user = await this._createUser(props)
}
return user
}
private async _getUserByEmail(email?: string): Promise<FreshchatUser | undefined> {
if (!email) {
return undefined
}
try {
const result = await this._client.get('/users', {
params: {
email,
},
})
return result?.data?.users[0]
} catch (e: any) {
this._logger.forBot().error('Failed to get user by email: ' + email, e.message, e?.response?.data)
throw new sdk.RuntimeError('Failed to get user by email ' + e.message)
}
}
private async _getUserByReferenceId(referenceId: string): Promise<FreshchatUser | undefined> {
try {
const result = await this._client.get('/users?reference_id=' + referenceId)
return result?.data?.users[0]
} catch (e: any) {
this._logger.forBot().error('Failed to get user by reference_id: ' + referenceId, e.message, e?.response?.data)
throw new sdk.RuntimeError('Failed to get user by reference_id ' + e.message)
}
}
private async _createUser(props: {
name?: string
email?: string
pictureUrl?: string
botpressUserId: string
}): Promise<FreshchatUser> {
const result = await this._client.post('/users', {
email: props.email,
first_name: props.name,
reference_id: props.botpressUserId,
avatar: { url: props.pictureUrl },
})
return result.data
}
public async getChannels(): Promise<FreshchatChannel[]> {
try {
const result = await this._client.get<{ channels: FreshchatChannel[] }>('/channels?items_per_page=9999')
return result?.data?.channels
} catch (e: any) {
this._logger.forBot().error('Failed to get channels : ' + e.message, e?.response?.data)
throw new sdk.RuntimeError('Failed to get channels: ' + e.message)
}
}
public async verifyToken(): Promise<boolean> {
try {
const result = await this._client.get<{ organisation_id: number }>('/accounts/configuration')
return !!result?.data?.organisation_id
} catch {
return false
}
}
public async getAgentById(id: string): Promise<FreshchatAgent | undefined> {
try {
const result = await this._client.get('/agents/' + id)
return result?.data
} catch (e: any) {
this._logger.forBot().error('Failed to get user by id: ' + id, e.message, e?.response?.data)
throw e
}
}
}
export const getFreshchatClient = (config: FreshchatConfiguration, logger: bp.Logger) =>
new FreshchatClient(config, logger)
+1
View File
@@ -0,0 +1 @@
export const INTEGRATION_NAME = 'freshchat'
@@ -0,0 +1,3 @@
import { IntegrationDefinitionProps } from '@botpress/sdk'
export const channels = undefined satisfies IntegrationDefinitionProps['channels']
@@ -0,0 +1,137 @@
// Enum for actor types
type ActorType = 'user' | 'agent' | 'system'
// Enum for message types
type MessageType = 'normal' | 'private'
// Actor definition
type Actor = {
actor_type: ActorType
actor_id: string
}
// Generic Event type that all specific event types will extend
export type FreshchatEvent<T> = {
actor: Actor
action: 'message_create' | 'conversation_assignment' | 'conversation_resolution' | 'conversation_reopen'
action_time: string
data: T
}
// Message part and message for the message_create event
export type TextMessagePart = {
text: {
content: string
}
}
export type FileMessagePart = {
file: {
name: string
url: string
file_size_in_bytes: number
content_type: string
}
}
export type ImageMessagePart = {
image: {
url: string
}
}
type MessagePart = TextMessagePart | FileMessagePart | ImageMessagePart
type Message = {
message_parts: MessagePart[]
app_id: string
actor_id: string
id: string
channel_id: string
conversation_id: string
interaction_id: string
actor_type: ActorType
created_time: string
user_id: string
restrictResponse?: boolean
botsPrivateNote?: boolean
message_type?: MessageType
message_source?: string
}
type MessageCreateData = {
message: Message
}
// Types for conversation events
type Conversation = {
conversation_id: string
app_id: string
status: string
channel_id: string
skill_id: number
sla_policy_id: number
sla_breached: boolean
assigned_agent_id?: string
assigned_org_agent_id?: string
assigned_group_id?: string
}
type ReopenData = {
reopener: string
reopener_id: string
conversation: Conversation
interaction_id: string
}
type ResolveData = {
resolve: Resolve
}
type Resolve = {
resolver: string
resolver_id: string
conversation: Conversation
interaction_id: string
user?: User
}
type AssignmentData = {
assignment: Assignment
}
type Assignment = {
assignor: string
assignor_id: string
to_agent_id: string
to_group_id: string
from_agent_id: string
from_group_id: string
conversation: Conversation
interaction_id: string
}
type User = {
properties: Property[]
created_time: string
updated_time: string
id: string
first_name: string
avatar: Avatar
login_status: boolean
}
type Property = {
name: string
value: string
}
type Avatar = {
url: string
}
// Event type instances
export type MessageCreateFreshchatEvent = FreshchatEvent<MessageCreateData>
export type ConversationReopenFreshchatEvent = FreshchatEvent<ReopenData>
export type ConversationResolutionFreshchatEvent = FreshchatEvent<ResolveData>
export type ConversationAssignmentFreshchatEvent = FreshchatEvent<AssignmentData>
@@ -0,0 +1,26 @@
import { z, IntegrationDefinitionProps } from '@botpress/sdk'
import { FreshchatConfigurationSchema } from './schemas'
export { channels } from './channels'
export const events = {} satisfies IntegrationDefinitionProps['events']
export const configuration = {
schema: FreshchatConfigurationSchema,
} satisfies IntegrationDefinitionProps['configuration']
export const states = {
freshchat: {
type: 'integration',
schema: z.object({
channelId: z.string().title('Channel Id').describe('Id from the channel topic'),
}),
},
} satisfies IntegrationDefinitionProps['states']
export const user = {
tags: {
id: { description: 'Freshchat User Id', title: 'Freshchat User Id' },
email: { description: 'Freshchat User Email', title: 'Freshchat User Email' },
},
} satisfies IntegrationDefinitionProps['user']
@@ -0,0 +1,152 @@
import { z } from '@botpress/sdk'
export const CreateConversationResponseSchema = z.object({
conversation_id: z.string(),
channel_id: z.string(),
})
export type CreateConversationResponse = z.output<typeof CreateConversationResponseSchema>
export type FreshchatConfiguration = z.infer<typeof FreshchatConfigurationSchema>
export const FreshchatConfigurationSchema = z.object({
topic_name: z.string().title('Topic name').describe('Name from the default topic that is going to be used for HITL'),
token: z.string().title('Api Key').describe('API key from Freshchat'),
domain: z
.string()
.title('Domain Name')
.describe('Your Freshchat domain from the Freshchat chat URL (example: yourcompany-5b321a95b1dfee217185497)'),
agentAvatarUrl: z
.string()
.title('Default Agent Avatar URL')
.optional()
.describe(
'Default URL for the agent avatar, if not provided, the first letter of the agent name will be used (Webchat Only)'
),
})
//Freshchat API Schemas
export const FreshchatAvatarSchema = z.object({
url: z.string().optional(),
})
export const FreshchatPropertySchema = z.object({
name: z.string(),
value: z.string().optional(),
})
export const FreshchatUserSchema = z.object({
id: z.string().optional(), // Optional as it's auto-generated
created_time: z.string().optional(),
updated_time: z.string().optional(),
avatar: FreshchatAvatarSchema.optional(), // Optional as it's not stated if mandatory
email: z.string(),
first_name: z.string().optional(),
last_name: z.string().optional(),
login_status: z.boolean().optional(),
org_contact_id: z.string().optional(),
phone: z.string().optional(), // Optional as it's not stated if mandatory
properties: z.array(FreshchatPropertySchema).optional(), // Optional as it's not stated if mandatory
reference_id: z.string().optional(),
restore_id: z.string().optional(),
})
export const FreshchatChannelSchema = z.object({
id: z.string().uuid(),
icon: z.object({}).optional(), // Adjust schema for `icon` if it contains specific properties
updated_time: z.string().datetime(),
enabled: z.boolean(),
public: z.boolean(),
name: z.string(),
tags: z.array(z.string()), // Assuming `tags` is an array of strings
welcome_message: z.object({
message_parts: z.array(
z.object({
text: z.object({
content: z.string(),
}),
})
),
message_type: z.enum(['normal']), // Adjust if there are other valid message types
restrictResponse: z.boolean(),
botsPrivateNote: z.boolean(),
isBotsInput: z.boolean(),
}),
source: z.literal('FRESHCHAT'), // Assuming "FRESHCHAT" is the only valid value
})
export const FreshchatAgentSchema = z.object({
id: z.string().uuid().optional(),
created_time: z.string().datetime().optional(),
agent_capacity: z.number().optional(),
agent_status: z
.object({
name: z.string(),
})
.optional(),
availability_status: z.enum(['AVAILABLE', 'UNAVAILABLE', 'AWAY', 'BUSY']).optional(),
avatar: z
.object({
url: z.string().url(),
})
.optional(),
biography: z.string().optional(),
email: z.string().email().optional(),
first_name: z.string().optional(),
freshid_group_ids: z.array(z.string().uuid()).optional(),
freshid_uuid: z.string().optional(),
groups: z.array(z.string().uuid()).optional(),
is_deactivated: z.boolean().optional(),
is_deleted: z.boolean().optional(),
last_name: z.string().optional(),
license_type: z.enum(['fulltime', 'parttime', 'contract']).optional(),
locale: z.string().optional(),
login_status: z.boolean().optional(),
org_contact_id: z.string().optional(),
role_id: z.string().optional(),
role_name: z.string().optional(),
routing_type: z.enum(['INTELLIASSIGN', 'MANUAL']).optional(),
skill_id: z.string().uuid().optional(),
social_profiles: z
.array(
z.object({
type: z.string(),
id: z.string(),
})
)
.optional(),
timezone: z.string().optional(),
})
export const FreshchatConversationSchema = z.object({
conversation_id: z.string().optional(),
channel_id: z.string().optional(),
user_id: z.string().optional(),
})
export type FreshchatConversation = z.infer<typeof FreshchatConversationSchema>
export type FreshchatUser = z.infer<typeof FreshchatUserSchema>
export type FreshchatAgent = z.infer<typeof FreshchatAgentSchema>
export type FreshchatChannel = z.infer<typeof FreshchatChannelSchema>
export type FreshchatMessage = {
message_parts: {
text: {
content: string
}
}[]
channel_id: string
message_type: string
actor_type: string
actor_id: string
}
// Extended Botpress User/Conversation Schemas
export const UserSchema = z.object({ id: z.string() })
export const ConversationSchema = z.object({ id: z.string() })
// Event specific schema
export const MessageEventSchema = z.object({ text: z.string() })
@@ -0,0 +1,45 @@
import { ConversationAssignmentFreshchatEvent } from '../definitions/freshchat-events'
import { updateAgentUser } from '../util'
import * as bp from '.botpress'
export const executeConversationAssignment = async ({
freshchatEvent,
client,
ctx,
logger,
}: {
freshchatEvent: ConversationAssignmentFreshchatEvent
client: bp.Client
ctx: bp.Context
logger: bp.Logger
}) => {
if (
!freshchatEvent.data.assignment.to_agent_id.length ||
freshchatEvent.data.assignment.from_agent_id === freshchatEvent.data.assignment.to_agent_id
) {
return
}
const { conversation } = await client.getOrCreateConversation({
channel: 'hitl',
tags: {
id: freshchatEvent.data.assignment.conversation.conversation_id,
},
})
const { user } = await client.getOrCreateUser({
tags: {
id: freshchatEvent.data.assignment.to_agent_id,
},
})
await updateAgentUser(user, client, ctx, logger, true)
await client.createEvent({
type: 'hitlAssigned',
payload: {
conversationId: conversation.id,
userId: user.id as string,
},
})
}
@@ -0,0 +1,24 @@
import { ConversationResolutionFreshchatEvent } from '../definitions/freshchat-events'
import * as bp from '.botpress'
export const executeConversationResolution = async ({
freshchatEvent,
client,
}: {
freshchatEvent: ConversationResolutionFreshchatEvent
client: bp.Client
}) => {
const { conversation } = await client.getOrCreateConversation({
channel: 'hitl',
tags: {
id: freshchatEvent.data.resolve.conversation.conversation_id,
},
})
await client.createEvent({
type: 'hitlStopped',
payload: {
conversationId: conversation.id,
},
})
}
@@ -0,0 +1,117 @@
import { Message } from '@botpress/common'
import { MessageCreateFreshchatEvent } from '../definitions/freshchat-events'
import { updateAgentUser } from '../util'
import * as bp from '.botpress'
export const executeMessageCreate = async ({
freshchatEvent,
client,
ctx,
logger,
}: {
freshchatEvent: MessageCreateFreshchatEvent
client: bp.Client
ctx: bp.Context
logger: bp.Logger
}) => {
// Ignore non agent messages
if (freshchatEvent.actor.actor_type === 'user') {
return
}
// Ignore private messages
if (freshchatEvent.data.message.message_type === 'private') {
return
}
const { conversation } = await client.getOrCreateConversation({
channel: 'hitl',
tags: {
id: freshchatEvent.data.message.conversation_id,
},
})
const { user } = await client.getOrCreateUser({
tags: {
id: freshchatEvent.actor.actor_id,
},
})
const items: Extract<Message, { type: 'bloc' }>['payload']['items'] = []
for (const messagePart of freshchatEvent.data.message.message_parts) {
if ('text' in messagePart) {
if (!messagePart.text.content.trim().length) {
continue
}
items.push({
type: 'text',
payload: {
text: messagePart.text.content,
},
})
} else if ('file' in messagePart) {
const { name, url, content_type } = messagePart.file as {
name: string
url: string
file_size_in_bytes: number
content_type: string
}
if (content_type.startsWith('image/')) {
items.push({
type: 'image',
payload: {
imageUrl: url,
},
})
} else if (content_type.startsWith('audio/')) {
items.push({
type: 'audio',
payload: {
audioUrl: url,
},
})
} else if (content_type.startsWith('video/')) {
items.push({
type: 'video',
payload: {
videoUrl: url,
},
})
} else {
items.push({
type: 'file',
payload: {
title: name,
fileUrl: url,
},
})
}
} else if ('image' in messagePart) {
items.push({
type: 'image',
payload: {
imageUrl: messagePart.image.url,
},
})
} else {
logger.forBot().warn(`Unsupported message part type ${Object.keys(messagePart)[0]}`)
}
}
// Only create message if there are items to send
if (items.length > 0) {
// An unassigned agent might send a message
await updateAgentUser(user, client, ctx, logger)
await client.createMessage({
type: 'bloc',
payload: {
items,
},
tags: {},
userId: user?.id as string,
conversationId: conversation.id,
})
}
}
+31
View File
@@ -0,0 +1,31 @@
import { FreshchatEvent } from './definitions/freshchat-events'
import { executeConversationAssignment } from './events/conversationAssignment'
import { executeConversationResolution } from './events/conversationResolution'
import { executeMessageCreate } from './events/messageCreate'
import * as bp from '.botpress'
export const handler: bp.IntegrationProps['handler'] = async ({ ctx, req, logger, client }) => {
if (!req.body) {
logger.forBot().warn('Handler received an empty body')
return
}
logger.forBot().debug('Handler received request from Freshchat with payload:', req.body)
//https://crmsupport.freshworks.com/en/support/solutions/articles/50000004461-freshchat-webhooks-payload-structure-and-authentication
const freshchatEvent = JSON.parse(req.body) as FreshchatEvent<any>
switch (freshchatEvent.action) {
case 'message_create':
await executeMessageCreate({ freshchatEvent, client, ctx, logger })
break
case 'conversation_assignment':
await executeConversationAssignment({ freshchatEvent, client, ctx, logger })
break
case 'conversation_resolution':
await executeConversationResolution({ freshchatEvent, client })
break
default:
logger.forBot().warn('Invalid Freshchat event of type: ' + freshchatEvent.action)
}
}
+41
View File
@@ -0,0 +1,41 @@
import { RuntimeError } from '@botpress/client'
import { actions } from './actions'
import { channels } from './channels'
import { getFreshchatClient } from './client'
import { handler } from './handler'
import * as bp from '.botpress'
export default new bp.Integration({
register: async ({ client, ctx, logger }) => {
const freshchatClient = getFreshchatClient({ ...ctx.configuration }, logger)
if (!(await freshchatClient.verifyToken())) {
throw new RuntimeError('Invalid API token or domain name')
}
const channels = await freshchatClient.getChannels()
if (!ctx.configuration?.topic_name?.length) {
throw new RuntimeError('Topic name required')
}
const channel = channels.find((channel) => channel.name === ctx.configuration.topic_name)
if (!channel) {
throw new RuntimeError(`Topic with name '${ctx.configuration.topic_name}' doesn't exist on your account`)
}
await client.setState({
type: 'integration',
id: ctx.integrationId,
name: 'freshchat',
payload: {
channelId: channel.id,
},
})
},
unregister: async () => {},
actions,
channels,
handler,
})
+79
View File
@@ -0,0 +1,79 @@
import * as bp from '../.botpress'
import { getFreshchatClient } from './client'
type IntegrationUser = Awaited<ReturnType<bp.Client['getUser']>>['user']
export const updateAgentUser = async (
user: IntegrationUser,
client: bp.Client,
ctx: bp.Context,
logger: bp.Logger,
forceUpdate?: boolean
): Promise<{ updatedAgentUser: IntegrationUser }> => {
if (!forceUpdate && user?.name?.length) {
return { updatedAgentUser: user }
}
let updatedFields: Record<string, any> = {}
try {
const freshchatClient = getFreshchatClient({ ...ctx.configuration }, logger)
const agentData = await freshchatClient.getAgentById(user.tags.id as string)
updatedFields = {
name: agentData?.first_name + ' ' + agentData?.last_name,
...(agentData?.avatar?.url?.length && { pictureUrl: agentData?.avatar?.url }),
}
} catch (thrown) {
const err = thrown instanceof Error ? thrown : new Error(String(thrown))
logger.forBot().error(`Couldn't get the agent profile from Freshchat: ${err.message}`)
}
if (!updatedFields?.pictureUrl?.length && ctx.configuration?.agentAvatarUrl?.length) {
updatedFields.pictureUrl = ctx.configuration.agentAvatarUrl
}
if (updatedFields.name !== user.name || updatedFields.pictureUrl !== user.pictureUrl) {
const { user: updatedUser } = await client.updateUser({
...user,
...updatedFields,
})
return { updatedAgentUser: updatedUser }
}
return { updatedAgentUser: user }
}
export type FileMetadata = { mimeType: string; fileSize?: number; fileName?: string }
export async function getMediaMetadata(url: string): Promise<FileMetadata> {
const response = await fetch(url, { method: 'HEAD' })
if (!response.ok) {
throw new Error(`Failed to fetch metadata for URL: ${url}`)
}
const mimeType = response.headers.get('content-type') ?? 'application/octet-stream'
const contentLength = response.headers.get('content-length')
const contentDisposition = response.headers.get('content-disposition')
const fileSize = contentLength ? Number(contentLength) : undefined
if (fileSize !== undefined && isNaN(fileSize)) {
throw new Error(`Failed to parse file size from response: ${contentLength}`)
}
// Try to extract filename from content-disposition
let fileName: string | undefined
if (contentDisposition) {
const match = contentDisposition.match(/filename\*?=(?:UTF-8'')?"?([^"]+)"?/i)
const rawFileName = match?.[1]
if (rawFileName) {
fileName = decodeURIComponent(rawFileName)
}
}
return {
mimeType,
fileSize,
fileName,
}
}