chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1 @@
|
||||
.botpress
|
||||
@@ -0,0 +1,13 @@
|
||||
import rootConfig from '../../eslint.config.mjs'
|
||||
|
||||
export default [
|
||||
...rootConfig,
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
The Vonage integration enables seamless communication between your AI-powered chatbot and Vonage, a leading cloud communications platform. Connect your chatbot to Vonage and leverage a variety of communication channels, including SMS, voice calls, and messaging apps. With this integration, you can automate interactions, send notifications, handle inquiries, and provide customer support directly through Vonage. Utilize Vonage's robust features such as SMS delivery, voice call routing, messaging app integration, and more to create reliable and effective conversational experiences. Enhance your communication capabilities and engage with users effortlessly with the Vonage Integration for Botpress.
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 229.59 200">
|
||||
<path fill="#000" d="M45.3408,0 L-0.0002,0 L64.6808,146.958 C65.1748,148.081 66.7718,148.07 67.2508,146.942 L88.7628,96.337 L45.3408,0 Z"/>
|
||||
<path fill="#000" d="M183.4502,0 C183.4502,0 113.9562,159.156 104.6482,173.833 C93.8292,190.896 86.6592,197.409 73.3912,199.496 C73.2682,199.515 73.1772,199.621 73.1772,199.746 C73.1772,199.886 73.2912,200 73.4312,200 L114.9552,200 C132.9432,200 145.9152,184.979 153.1042,171.714 C161.2742,156.637 229.5902,0 229.5902,0 L183.4502,0 Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 567 B |
@@ -0,0 +1,89 @@
|
||||
import { z, IntegrationDefinition, messages } from '@botpress/sdk'
|
||||
import proactiveConversation from 'bp_modules/proactive-conversation'
|
||||
import proactiveUser from 'bp_modules/proactive-user'
|
||||
|
||||
export default new IntegrationDefinition({
|
||||
name: 'vonage',
|
||||
version: '1.0.5',
|
||||
title: 'Vonage',
|
||||
description: 'Send and receive SMS messages.',
|
||||
icon: 'icon.svg',
|
||||
readme: 'hub.md',
|
||||
configuration: {
|
||||
schema: z.object({
|
||||
apiKey: z.string().min(1).describe('The API Key').title('API Key'),
|
||||
apiSecret: z.string().min(1).describe('The API Secret').title('API Secret'),
|
||||
signatureSecret: z.string().min(1).describe('The signature secret').title('Signature Secret'),
|
||||
useTestingApi: z.boolean().describe('Chooses if we should use the test api').title('Use Testing API'),
|
||||
}),
|
||||
},
|
||||
channels: {
|
||||
channel: {
|
||||
title: 'Channel',
|
||||
description: 'The vonage Channel',
|
||||
messages: { ...messages.defaults, bloc: messages.markdownBloc },
|
||||
message: {
|
||||
tags: {
|
||||
id: { title: 'ID', description: 'The id of the message' },
|
||||
},
|
||||
},
|
||||
conversation: {
|
||||
tags: {
|
||||
userId: { title: 'User ID', description: 'The User id' },
|
||||
channel: { title: 'Channel', description: 'The conversation channel' },
|
||||
channelId: { title: 'Channel ID', description: 'The channel id' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
events: {},
|
||||
user: {
|
||||
tags: {
|
||||
userId: { title: 'User ID', description: 'The user id' },
|
||||
channel: { title: 'Channel', description: 'The channel of the user' },
|
||||
},
|
||||
},
|
||||
|
||||
entities: {
|
||||
conversation: {
|
||||
schema: z.object({
|
||||
userId: z.string(),
|
||||
channelId: z.string(),
|
||||
channel: z.string(),
|
||||
}),
|
||||
},
|
||||
user: {
|
||||
schema: z.object({
|
||||
channel: z.string().describe('The channel of the user').title('Channel'),
|
||||
userId: z.string().describe('The user id').title('User ID'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
attributes: {
|
||||
category: 'Communication & Channels',
|
||||
guideSlug: 'vonage',
|
||||
repo: 'botpress',
|
||||
},
|
||||
})
|
||||
.extend(proactiveConversation, ({ entities }) => ({
|
||||
entities: {
|
||||
conversation: entities.conversation,
|
||||
},
|
||||
actions: {
|
||||
getOrCreateConversation: {
|
||||
name: 'startConversation',
|
||||
title: 'Start proactive conversation',
|
||||
description: 'Start a proactive conversation given a user',
|
||||
},
|
||||
},
|
||||
}))
|
||||
.extend(proactiveUser, ({ entities }) => ({
|
||||
entities: { user: entities.user },
|
||||
actions: {
|
||||
getOrCreateUser: {
|
||||
name: 'getOrCreateUser',
|
||||
title: 'Get or create user',
|
||||
description: 'Get or create a user in the Vonage channel',
|
||||
},
|
||||
},
|
||||
}))
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "@botpresshub/vonage",
|
||||
"description": "Vonage integration for Botpress",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "bp add -y && bp build",
|
||||
"check:type": "tsc --noEmit",
|
||||
"check:bplint": "bp lint",
|
||||
"test": "vitest --run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@botpress/client": "workspace:*",
|
||||
"@botpress/sdk": "workspace:*",
|
||||
"@botpress/sdk-addons": "workspace:*",
|
||||
"axios": "^0.27.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@botpress/cli": "workspace:*",
|
||||
"@botpress/common": "workspace:*",
|
||||
"@botpress/sdk": "workspace:*"
|
||||
},
|
||||
"bpDependencies": {
|
||||
"proactive-user": "../../interfaces/proactive-user",
|
||||
"proactive-conversation": "../../interfaces/proactive-conversation"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import { RuntimeError } from '@botpress/client'
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import * as common from '@botpress/sdk-addons'
|
||||
import * as formatter from './payloadFormatter'
|
||||
import * as vonage from './vonage'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
const integration = new bp.Integration({
|
||||
register: async () => {},
|
||||
unregister: async () => {},
|
||||
actions: {
|
||||
async startConversation(props) {
|
||||
const vonageChannel = props.input.conversation.channel
|
||||
const channelId = props.input.conversation.channelId
|
||||
const userId = props.input.conversation.userId
|
||||
|
||||
if (!(vonageChannel && channelId && userId)) {
|
||||
throw new sdk.RuntimeError('Could not create conversation: missing channel, channelId or userId')
|
||||
}
|
||||
|
||||
const { conversation } = await props.client.getOrCreateConversation({
|
||||
tags: {
|
||||
channel: vonageChannel,
|
||||
channelId,
|
||||
userId,
|
||||
},
|
||||
channel: 'channel',
|
||||
})
|
||||
|
||||
return {
|
||||
conversationId: conversation.id,
|
||||
}
|
||||
},
|
||||
async getOrCreateUser(props) {
|
||||
const vonageChannel = props.input.user.channel
|
||||
const userId = props.input.user.userId
|
||||
if (!(vonageChannel && userId)) {
|
||||
throw new sdk.RuntimeError('Could not create a user: missing channel or userId')
|
||||
}
|
||||
|
||||
const { user } = await props.client.getOrCreateUser({
|
||||
tags: {
|
||||
channel: vonageChannel,
|
||||
userId,
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
userId: user.id,
|
||||
}
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
channel: {
|
||||
messages: {
|
||||
text: async (props) => {
|
||||
const payload = { message_type: 'text', text: props.payload.text }
|
||||
await vonage.sendMessage(props, payload)
|
||||
},
|
||||
image: async (props) => {
|
||||
const payload = { message_type: 'image', image: { url: props.payload.imageUrl } }
|
||||
await vonage.sendMessage(props, payload)
|
||||
},
|
||||
audio: async (props) => {
|
||||
const payload = { message_type: 'audio', audio: { url: props.payload.audioUrl } }
|
||||
await vonage.sendMessage(props, payload)
|
||||
},
|
||||
video: async (props) => {
|
||||
const payload = { message_type: 'video', video: { url: props.payload.videoUrl } }
|
||||
await vonage.sendMessage(props, payload)
|
||||
},
|
||||
file: async (props) => {
|
||||
const payload = { message_type: 'file', file: { url: props.payload.fileUrl } }
|
||||
await vonage.sendMessage(props, payload)
|
||||
},
|
||||
location: async (props) => {
|
||||
const payload = formatter.formatLocationPayload(props.payload)
|
||||
await vonage.sendMessage(props, payload)
|
||||
},
|
||||
carousel: async (props) => {
|
||||
const payloads = formatter.formatCarouselPayload(props.payload)
|
||||
for (const payload of payloads) {
|
||||
await vonage.sendMessage(props, payload)
|
||||
}
|
||||
},
|
||||
card: async (props) => {
|
||||
const payload = formatter.formatCardPayload(props.payload)
|
||||
await vonage.sendMessage(props, payload)
|
||||
},
|
||||
dropdown: async (props) => {
|
||||
const payload = formatter.formatDropdownPayload(props.payload)
|
||||
await vonage.sendMessage(props, payload)
|
||||
},
|
||||
choice: async (props) => {
|
||||
const payload = formatter.formatChoicePayload(props.payload)
|
||||
await vonage.sendMessage(props, payload)
|
||||
},
|
||||
bloc: () => {
|
||||
throw new RuntimeError('Not implemented')
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
handler: async ({ req, client }) => {
|
||||
if (!req.body) {
|
||||
console.warn('Handler received an empty body')
|
||||
return
|
||||
}
|
||||
|
||||
const data = JSON.parse(req.body)
|
||||
|
||||
console.info(`Handler received request of type ${data.message_type}`)
|
||||
|
||||
if (data.message_type !== 'text') {
|
||||
throw new Error('Handler received an invalid message type')
|
||||
}
|
||||
|
||||
const { conversation } = await client.getOrCreateConversation({
|
||||
channel: 'channel',
|
||||
tags: {
|
||||
channel: data.channel,
|
||||
channelId: data.to,
|
||||
userId: data.from,
|
||||
},
|
||||
})
|
||||
|
||||
const { user } = await client.getOrCreateUser({
|
||||
tags: {
|
||||
channel: data.channel,
|
||||
userId: data.from,
|
||||
},
|
||||
})
|
||||
|
||||
await client.createMessage({
|
||||
tags: { id: data.message_uuid },
|
||||
type: 'text',
|
||||
userId: user.id,
|
||||
conversationId: conversation.id,
|
||||
payload: { text: data.text },
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export default common.reporting.wrapIntegration(integration)
|
||||
@@ -0,0 +1,133 @@
|
||||
import * as bp from '.botpress'
|
||||
|
||||
export function formatLocationPayload(payload: bp.channels.channel.location.Location) {
|
||||
return {
|
||||
message_type: 'custom',
|
||||
custom: {
|
||||
type: 'location',
|
||||
location: {
|
||||
latitude: payload.latitude,
|
||||
longitude: payload.longitude,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function formatDropdownPayload(payload: bp.channels.channel.dropdown.Dropdown) {
|
||||
return {
|
||||
message_type: 'custom',
|
||||
custom: {
|
||||
type: 'interactive',
|
||||
interactive: {
|
||||
type: 'list',
|
||||
body: {
|
||||
text: payload.text,
|
||||
},
|
||||
action: {
|
||||
button: 'Select an option',
|
||||
sections: [
|
||||
{
|
||||
rows: payload.options.map((x, i) => ({ id: `slot-${i}::${x.value}`, title: x.label })),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function formatChoicePayload(payload: bp.channels.channel.choice.Choice) {
|
||||
if (payload.options.length < 3) {
|
||||
return {
|
||||
message_type: 'custom',
|
||||
custom: {
|
||||
type: 'interactive',
|
||||
interactive: {
|
||||
type: 'button',
|
||||
body: {
|
||||
text: payload.text,
|
||||
},
|
||||
action: {
|
||||
buttons: payload.options.map((x, i) => ({
|
||||
type: 'reply',
|
||||
reply: { id: `slot-${i}::${x.value}`, title: x.label },
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (payload.options.length <= 10) {
|
||||
return {
|
||||
message_type: 'custom',
|
||||
custom: {
|
||||
type: 'interactive',
|
||||
interactive: {
|
||||
type: 'list',
|
||||
body: {
|
||||
text: payload.text,
|
||||
},
|
||||
action: {
|
||||
button: 'Select an option',
|
||||
sections: [
|
||||
{
|
||||
rows: payload.options.map((x, i) => ({ id: `slot-${i}::${x.value}`, title: x.label })),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
message_type: 'text',
|
||||
text: `${payload.text}\n\n${payload.options.map(({ label }, idx) => `*(${idx + 1})* ${label}`).join('\n')}`,
|
||||
}
|
||||
}
|
||||
|
||||
export function formatCarouselPayload(payload: bp.channels.channel.carousel.Carousel) {
|
||||
let count = 0
|
||||
return payload.items.map((card) => {
|
||||
const cardPayload = formatCardPayload(card, count)
|
||||
count += card.actions.length
|
||||
return cardPayload
|
||||
})
|
||||
}
|
||||
|
||||
type CardOption = CardSay | CardPostback | CardUrl
|
||||
|
||||
type CardSay = { title: string; type: 'say'; value: string }
|
||||
type CardPostback = { title: string; type: 'postback'; value: string }
|
||||
type CardUrl = { title: string; type: 'url' }
|
||||
|
||||
export function formatCardPayload(payload: bp.channels.channel.card.Card, count: number = 0) {
|
||||
const options: CardOption[] = []
|
||||
|
||||
payload.actions.forEach((action) => {
|
||||
if (action.action === 'say') {
|
||||
options.push({ title: action.label, type: 'say', value: action.value })
|
||||
} else if (action.action === 'url') {
|
||||
options.push({ title: `${action.label} : ${action.value}`, type: 'url' })
|
||||
} else if (action.action === 'postback') {
|
||||
options.push({ title: action.label, type: 'postback', value: action.value })
|
||||
}
|
||||
})
|
||||
|
||||
const body = `*${payload.title}*\n\n${payload.subtitle ? `${payload.subtitle}\n\n` : ''}${options
|
||||
.map(({ title }, idx) => `*(${idx + count + 1})* ${title}`)
|
||||
.join('\n')}`
|
||||
|
||||
if (payload.imageUrl) {
|
||||
return {
|
||||
message_type: 'image',
|
||||
image: {
|
||||
url: payload.imageUrl,
|
||||
caption: body,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return { message_type: 'text', text: body }
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import axios from 'axios'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
function getRequestMetadata(conversation: SendMessageProps['conversation']) {
|
||||
const channel = conversation.tags?.channel
|
||||
const channelId = conversation.tags?.channelId
|
||||
const userId = conversation.tags?.userId
|
||||
|
||||
if (!channelId) {
|
||||
throw new Error('Invalid channel id')
|
||||
}
|
||||
|
||||
if (!userId) {
|
||||
throw new Error('Invalid user id')
|
||||
}
|
||||
|
||||
if (!channel) {
|
||||
throw new Error('Invalid channel')
|
||||
}
|
||||
|
||||
return { to: userId, from: channelId, channel }
|
||||
}
|
||||
|
||||
type SendMessageProps = Pick<bp.AnyMessageProps, 'ctx' | 'conversation' | 'ack'>
|
||||
export async function sendMessage({ conversation, ctx, ack }: SendMessageProps, payload: any) {
|
||||
const { to, from, channel } = getRequestMetadata(conversation)
|
||||
const response = await axios.post(
|
||||
'https://api.nexmo.com/v1/messages',
|
||||
{
|
||||
...payload,
|
||||
from,
|
||||
to,
|
||||
channel,
|
||||
},
|
||||
{
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
auth: { username: ctx.configuration.apiKey, password: ctx.configuration.apiSecret },
|
||||
}
|
||||
)
|
||||
await ack({ tags: { id: response.data.message_uuid } })
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"paths": { "*": ["./*"] },
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": [".botpress/**/*", "definitions/**/*", "src/**/*", "*.ts"]
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
import config from '../../vitest.config'
|
||||
export default config
|
||||
Reference in New Issue
Block a user