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
+1
View File
@@ -0,0 +1 @@
.botpress
+13
View File
@@ -0,0 +1,13 @@
import rootConfig from '../../eslint.config.mjs'
export default [
...rootConfig,
{
languageOptions: {
parserOptions: {
project: ['./tsconfig.json'],
tsconfigRootDir: import.meta.dirname,
},
},
},
]
+11
View File
@@ -0,0 +1,11 @@
The Viber integration allows your AI-powered chatbot to seamlessly connect with Viber, a popular messaging platform with a global user base. Integrate your chatbot with Viber to engage with your audience in real-time conversations, provide instant support, and deliver personalized experiences. With this integration, you can automate interactions, send messages, handle inquiries, and perform actions directly within Viber. Leverage Viber's features such as text messages, rich media, stickers, and chatbot extensions to create engaging and interactive chatbot experiences. Expand your reach and connect with Viber users effectively with the Viber Integration for Botpress.
## Migrating from version `0.x.x` to `1.x.x`
### Removal of proactive conversations (and proactive users)
- Viber does not currently support proactive conversations, so any bots using this feature will need to be updated to use the normal conversation flow.
### Removal of `markdown` message type
- The `markdown` message type has been removed. Markdown messages must now be sent as regular `text` messages.
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="Layer_1" viewBox="0 0 631.99 666.39"><defs id="defs178"><style id="style176">.cls-1{fill:#7360f2}.cls-2{fill:none;stroke:#7360f2;stroke-linecap:round;stroke-linejoin:round;stroke-width:16.86px}</style></defs><path id="path182" d="M560.651 64.998c-16.56-15.28-83.48-63.86-232.54-64.52 0 0-175.78-10.6-261.47 68-47.7 47.71-64.48 117.52-66.25 204.07-1.77 86.55-4.06 248.75 152.29 292.73h.15l-.1 67.11s-1 27.17 16.89 32.71c21.64 6.72 34.34-13.93 55-36.19 11.34-12.22 27-30.17 38.8-43.89 106.93 9 189.17-11.57 198.51-14.61 21.59-7 143.76-22.66 163.63-184.84 20.51-167.17-9.92-272.91-64.91-320.57zm18.12 308.58c-16.77 135.42-115.86 143.93-134.13 149.79-7.77 2.5-80 20.47-170.83 14.54 0 0-67.68 81.65-88.82 102.88-3.3 3.32-7.18 4.66-9.77 4-3.64-.89-4.64-5.2-4.6-11.5.06-9 .58-111.52.58-111.52s-.08 0 0 0c-132.26-36.72-124.55-174.77-123.05-247.06 1.5-72.29 15.08-131.51 55.42-171.34 72.48-65.65 221.79-55.84 221.79-55.84 126.09.55 186.51 38.52 200.52 51.24 46.52 39.83 70.22 135.14 52.89 274.77z" class="cls-1"/><path id="path184" d="M389.471 268.768q-2.46-49.59-50.38-52.09" class="cls-2"/><path id="path186" d="M432.721 283.268q1-46.2-27.37-77.2c-19-20.74-45.3-32.16-79.05-34.63" class="cls-2"/><path id="path188" d="M477.001 300.588q-.61-80.17-47.91-126.28t-117.65-46.6" class="cls-2"/><path id="path190" d="M340.761 381.678s11.85 1 18.23-6.86l12.44-15.65c6-7.76 20.48-12.71 34.66-4.81a366.67 366.67 0 0130.91 19.74c9.41 6.92 28.68 23 28.74 23 9.18 7.75 11.3 19.13 5.05 31.13 0 .07-.05.19-.05.25a129.81 129.81 0 01-25.89 31.88c-.12.06-.12.12-.23.18q-13.38 11.18-26.29 12.71a17.39 17.39 0 01-3.84.24 35 35 0 01-11.18-1.72l-.28-.41c-13.26-3.74-35.4-13.1-72.27-33.44a430.39 430.39 0 01-60.72-40.11 318.31 318.31 0 01-27.31-24.22l-.92-.92-.92-.92-.92-.92c-.31-.3-.61-.61-.92-.92a318.31 318.31 0 01-24.22-27.31 430.83 430.83 0 01-40.11-60.71c-20.34-36.88-29.7-59-33.44-72.28l-.41-.28a35 35 0 01-1.71-11.18 16.87 16.87 0 01.23-3.84q1.61-12.89 12.73-26.31c.06-.11.12-.11.18-.23a129.53 129.53 0 0131.88-25.88c.06 0 .18-.06.25-.06 12-6.25 23.38-4.13 31.12 5 .06.06 16.11 19.33 23 28.74a366.67 366.67 0 0119.74 30.94c7.9 14.17 2.95 28.68-4.81 34.66l-15.65 12.44c-7.9 6.38-6.86 18.23-6.86 18.23s23.18 87.73 109.79 109.84z" class="cls-1"/></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

@@ -0,0 +1,47 @@
import { z, IntegrationDefinition, messages } from '@botpress/sdk'
export default new IntegrationDefinition({
name: 'viber',
version: '1.0.6',
title: 'Viber',
description: 'Send and receive SMS messages.',
icon: 'icon.svg',
readme: 'hub.md',
configuration: {
schema: z.object({
authToken: z.string().min(1).title('Auth Token').describe('The authorization token'),
botName: z.string().min(1).title('Bot Name').describe("The bot's name"),
botAvatar: z.string().min(1).title('Bot Avatar').describe("The bot's avatar"),
}),
},
channels: {
channel: {
title: 'Viber conversation',
description: 'Channel for a Viber conversation',
messages: { ...messages.defaults, bloc: messages.markdownBloc },
message: {
tags: {
id: { title: 'Message ID', description: 'Viber message ID' },
},
},
conversation: {
tags: {
id: { title: 'User ID', description: 'Viber user ID taking part in the conversation' },
},
},
},
},
actions: {},
events: {},
user: {
tags: {
id: { title: 'User ID', description: 'Viber user ID' },
},
},
attributes: {
category: 'Communication & Channels',
guideSlug: 'viber',
repo: 'botpress',
},
})
+22
View File
@@ -0,0 +1,22 @@
{
"name": "@botpresshub/viber",
"description": "Viber 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": "^1.2.1"
},
"devDependencies": {
"@botpress/cli": "workspace:*",
"@botpress/common": "workspace:*",
"@botpress/sdk": "workspace:*"
}
}
+419
View File
@@ -0,0 +1,419 @@
import { RuntimeError } from '@botpress/client'
import { transformMarkdown } from '@botpress/common'
import { reporting } from '@botpress/sdk-addons'
import axios from 'axios'
import * as bp from '.botpress'
const integration = new bp.Integration({
register: async ({ webhookUrl, ctx }) => {
await setViberWebhook(webhookUrl, ctx.configuration.authToken)
},
unregister: async ({ ctx }) => {
await setViberWebhook(undefined, ctx.configuration.authToken)
},
actions: {},
channels: {
channel: {
messages: {
text: async (props) => {
await sendViberMessage({
...props,
payload: {
type: 'text',
text: tryTransformMarkdown(props.payload.text, props.logger.forBot()),
},
})
},
image: async (props) => {
await sendViberMessage({
...props,
payload: {
type: 'picture',
text: '',
media: props.payload.imageUrl,
},
})
},
audio: async (props) => {
await sendViberMessage({
...props,
payload: {
type: 'url',
media: props.payload.audioUrl,
},
})
},
video: async (props) => {
await sendViberMessage({
...props,
payload: {
type: 'video',
media: props.payload.videoUrl,
size: 26214390, // TODO: get video size
},
})
},
file: async (props) => {
const name = props.payload.fileUrl.split('/')
await sendViberMessage({
...props,
payload: {
type: 'file',
media: props.payload.fileUrl,
size: 1000, // TODO: get file size
file_name: name[name.length - 1],
},
})
},
location: async (props) => {
await sendViberMessage({
...props,
payload: {
type: 'location',
location: {
lat: props.payload.latitude,
lon: props.payload.longitude,
},
},
})
},
carousel: async (props) => {
const carrousel = props.payload.items.flatMap(renderCard)
await sendViberMessage({
...props,
payload: {
type: 'rich_media',
rich_media: {
Type: 'rich_media',
ButtonsGroupColumns: 6,
ButtonsGroupRows: 7,
BgColor: '#FFFFFF',
Buttons: carrousel,
},
},
})
},
card: async (props) => {
await sendViberMessage({
...props,
payload: {
type: 'rich_media',
rich_media: {
Type: 'rich_media',
ButtonsGroupColumns: 6,
ButtonsGroupRows: 7,
BgColor: '#FFFFFF',
Buttons: renderCard(props.payload),
},
},
})
},
dropdown: async (props) => {
await sendViberMessage({
...props,
payload: {
type: 'rich_media',
rich_media: {
Type: 'rich_media',
ButtonsGroupColumns: 6,
ButtonsGroupRows: 5,
BgColor: '#FFFFFF',
Buttons: renderChoice(props.payload),
},
},
})
},
choice: async (props) => {
await sendViberMessage({
...props,
payload: {
type: 'rich_media',
rich_media: {
Type: 'rich_media',
ButtonsGroupColumns: 6,
ButtonsGroupRows: 5,
BgColor: '#FFFFFF',
Buttons: renderChoice(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)
if (data.event.bot_id) {
return
}
if (data.event === 'message') {
if (!data.sender) {
throw Error('Sender information unavailable')
}
const { conversation } = await client.getOrCreateConversation({
channel: 'channel',
tags: {
id: data.sender.id,
},
})
const { user } = await client.getOrCreateUser({
tags: {
id: data.sender.id,
},
})
switch (data.message.type) {
case 'text':
await client.createMessage({
tags: { id: data.message_token.toString() },
type: 'text',
userId: user.id,
conversationId: conversation.id,
payload: { text: data.message.text },
})
break
case 'picture':
await client.createMessage({
tags: { id: data.message_token.toString() },
type: 'image',
userId: user.id,
conversationId: conversation.id,
payload: {
imageUrl: data.message.media,
// TODO: declare in definition
// caption: '',
},
})
break
case 'video':
await client.createMessage({
tags: { id: data.message_token.toString() },
type: 'video',
userId: user.id,
conversationId: conversation.id,
payload: {
videoUrl: data.message.media,
// TODO: declare in definition
// size: data.message.size
},
})
break
case 'file':
await client.createMessage({
tags: { id: data.message_token.toString() },
type: 'file',
userId: user.id,
conversationId: conversation.id,
payload: {
fileUrl: data.message.media,
// TODO: declare in definition
// fileName: data.message.file_name,
// fileSize: data.message.size,
},
})
break
case 'location':
await client.createMessage({
tags: { id: data.message_token.toString() },
type: 'location',
userId: user.id,
conversationId: conversation.id,
payload: { latitude: data.message.location.lat, longitude: data.message.location.lon },
})
break
default:
console.info('unsupported message type: ', data.message)
return
}
}
return
},
})
export default reporting.wrapIntegration(integration)
type SendMessageProps = Pick<bp.AnyMessageProps, 'ctx' | 'conversation' | 'ack'> & {
payload: any // TODO: type this
}
export async function setViberWebhook(webhookUrl: string | undefined, token: string): Promise<void> {
await axios.post(
'https://chatapi.viber.com/pa/set_webhook',
{
url: webhookUrl,
send_name: true,
},
{
headers: {
'X-Viber-Auth-Token': token,
'Content-Type': 'application/json',
},
}
)
}
export async function sendViberMessage({ conversation, ctx, ack, payload }: SendMessageProps) {
const target = conversation.tags.id
const { data } = await axios.post(
'https://chatapi.viber.com/pa/send_message',
{
...payload,
receiver: target,
min_api_version: 2,
sender: {
name: ctx.configuration.botName,
avatar: ctx.configuration.botAvatar,
},
},
{
headers: {
'X-Viber-Auth-Token': ctx.configuration.authToken,
'Content-Type': 'application/json',
},
}
)
if (!data) {
throw Error('Error sending Viber message')
}
if (!data.message_token) {
throw Error(data.status_message)
}
await ack({
tags: {
id: data.message_token.toString(),
},
})
return data
}
type Card = bp.channels.channel.card.Card
type CardAction = bp.channels.channel.card.Card['actions'][number]
const renderCard = (payload: Card) => {
const card = [
{
Columns: 6,
Rows: 3,
ActionType: 'open-url',
ActionBody: '',
Image: payload.imageUrl,
},
{
Columns: 6,
Rows: 1,
ActionType: 'open-url',
ActionBody: '',
Text: `<font color=#323232><b>${payload.title}</b></font><font color=#777777><br>${payload.subtitle}</font>`,
TextSize: 'medium',
TextVAlign: 'middle',
TextHAlign: 'left',
},
]
payload.actions.forEach((item) => {
switch (item.action) {
case 'say':
card.push(renderButtonSay(item))
return
case 'postback':
return card.push(renderButtonPostback(item))
case 'url':
return card.push(renderButtonUrl(item))
default:
throw Error(`Unknown action type ${item.action}`)
}
})
return card
}
function renderButtonUrl(action: CardAction) {
return {
Columns: 6,
Rows: 1,
ActionType: 'open-url',
ActionBody: action.value,
Text: `<font color=#8367db>${action.label}</font>`,
TextSize: 'small',
TextVAlign: 'middle',
TextHAlign: 'middle',
}
}
function renderButtonPostback(action: CardAction) {
return {
Columns: 6,
Rows: 1,
ActionType: 'reply',
ActionBody: action.value,
Text: `<font color=#8367db>${action.label}</font>`,
TextSize: 'small',
TextVAlign: 'middle',
TextHAlign: 'middle',
}
}
function renderButtonSay(action: CardAction) {
return {
Columns: 6,
Rows: 1,
ActionType: 'reply',
ActionBody: action.value,
Text: `<font color=#8367db>${action.label}</font>`,
TextSize: 'small',
TextVAlign: 'middle',
TextHAlign: 'middle',
}
}
type Choice = bp.channels.channel.choice.Choice
function renderChoice(payload: Choice) {
const choice = [
{
Columns: 6,
Rows: 2,
ActionType: 'open-url',
ActionBody: '',
Text: `<font color=#323232><b>${payload.text}</b></font>`,
TextSize: 'medium',
TextVAlign: 'middle',
TextHAlign: 'left',
},
]
payload.options.forEach((c) => {
choice.push({
Columns: 6,
Rows: 1,
ActionType: 'reply',
ActionBody: c.value,
Text: `<font color=#8367db>${c.label}</font>`,
TextSize: 'small',
TextVAlign: 'middle',
TextHAlign: 'middle',
})
})
return choice
}
const tryTransformMarkdown = (text: string, logger: bp.Logger) => {
try {
return transformMarkdown(text)
} catch {
logger.error('Failed to transform the markdown. The message will be sent as text without transforming markdown.')
return text
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"paths": { "*": ["./*"] },
"outDir": "dist"
},
"include": [".botpress/**/*", "definitions/**/*", "src/**/*", "*.ts"]
}
+2
View File
@@ -0,0 +1,2 @@
import config from '../../vitest.config'
export default config