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
+144
View File
@@ -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)
+133
View File
@@ -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 }
}
+41
View File
@@ -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 } })
}