chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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,44 @@
|
||||
The Line integration enables your chatbot to seamlessly interact with Line, one of the leading messaging platforms. Connect your AI-powered chatbot to Line and unlock powerful communication capabilities to engage with your audience. With this integration, you can automate customer interactions, provide instant support, deliver personalized messages, and handle inquiries seamlessly within the Line messaging environment. Leverage Line's rich features such as text, images, stickers, and location sharing to create dynamic and engaging conversations. Elevate your chatbot's reach and impact by integrating it with Line using the Line Integration for Botpress.
|
||||
|
||||
## Migrating from version `1.x.x` to `2.x.x`
|
||||
|
||||
### Changes in proactive conversations (and proactive users)
|
||||
|
||||
- The process of proactively creating conversations and users has changed. You must now use the actions `getOrCreateConversation` and `getOrCreateUser` to create conversations and users.
|
||||
|
||||
### Removal of `markdown` message type
|
||||
|
||||
- The `markdown` message type has been removed. Markdown messages must now be sent as regular `text` messages.
|
||||
|
||||
## Configuration
|
||||
|
||||
1. In your [Line Developers console](https://developers.line.biz/), click on "Providers", select the provider you would like to use or create a new one, and then select or create the Channel you would like to use. If you are creating a new one, select "Messaging API" from the creation menu.
|
||||
2. For the selected channel, select the Messaging API tab in your channel settings. Scroll to the bottom to "Channel Access Token", and generate a new one or copy an existing one.
|
||||
3. Paste this into the "Channel Access Token" field in the Botpress Line configuration
|
||||
4. Go to the channel "Basic settings", scroll to the bottom and copy the "Channel secret".
|
||||
5. Paste this into the "Channel Secret" in the Botpress Line configuration.
|
||||
6. Copy the Webhook endpoint url from the Botpress Line configuration.
|
||||
7. In Line, go to the "Messaging API" tab of the Channel settings, and paste the Botpress Integration Webhook endpoint url into the "Webhook URL", under Webhook settings.
|
||||
8. Enable the "Use webhook" toggle in Line.
|
||||
|
||||
## Content Types Mapping
|
||||
|
||||
### From Botpress to LINE
|
||||
|
||||
`Text` is mapped to `Text Object`
|
||||
`Text with Markdown` is mapped to `Text Object`
|
||||
`Image` is mapped to `Image Object`
|
||||
`Audio` is mapped to `Audio Object`
|
||||
`Video` is mapped to `Video Object`
|
||||
`File` isn’t supported in LINE Message Types
|
||||
`Location` is mapped to `Location Object`
|
||||
`Carousel` is mapped to `Flex Object`
|
||||
`Card` is mapped to `Flex Object`
|
||||
`Dropdown` is mapped to `Flex Object`
|
||||
`Choice` is mapped to `Flex Object`
|
||||
|
||||
|
||||
### From LINE to Botpress
|
||||
|
||||
`Text` is mapped to `Text Object`
|
||||
The rest of the types aren’t currently processed by Botpress
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="2500" height="2500" viewBox="0 0 377.764 377.764"><path fill-rule="evenodd" clip-rule="evenodd" fill="#3ACE01" d="M77.315 0h223.133c42.523 0 77.315 34.792 77.315 77.315v223.133c0 42.523-34.792 77.315-77.315 77.315H77.315C34.792 377.764 0 342.972 0 300.448V77.315C0 34.792 34.792 0 77.315 0z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#FFF" d="M188.515 62.576c76.543 0 138.593 49.687 138.593 110.979 0 21.409-7.576 41.398-20.691 58.351-.649.965-1.497 2.031-2.566 3.209l-.081.088c-4.48 5.36-9.525 10.392-15.072 15.037-38.326 35.425-101.41 77.601-109.736 71.094-7.238-5.656 11.921-33.321-10.183-37.925-1.542-.177-3.08-.367-4.605-.583l-.029-.002v-.002c-64.921-9.223-114.222-54.634-114.222-109.267-.002-61.292 62.049-110.979 138.592-110.979z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#3ACE01" d="M108.103 208.954h27.952c3.976 0 7.228-3.253 7.228-7.229v-.603c0-3.976-3.252-7.228-7.228-7.228h-20.121v-45.779c0-3.976-3.252-7.228-7.228-7.228h-.603c-3.976 0-7.228 3.252-7.228 7.228v53.609c0 3.977 3.252 7.23 7.228 7.23zm173.205-33.603v-.603c0-3.976-3.253-7.228-7.229-7.228h-20.12v-11.445h20.12c3.976 0 7.229-3.252 7.229-7.228v-.603c0-3.976-3.253-7.228-7.229-7.228h-27.952c-3.976 0-7.228 3.252-7.228 7.228v53.609c0 3.976 3.252 7.229 7.228 7.229h27.952c3.976 0 7.229-3.253 7.229-7.229v-.603c0-3.976-3.253-7.228-7.229-7.228h-20.12v-11.445h20.12c3.976.002 7.229-3.251 7.229-7.226zm-53.755 31.448l.002-.003a7.207 7.207 0 0 0 2.09-5.07v-53.609c0-3.976-3.252-7.228-7.229-7.228h-.603c-3.976 0-7.228 3.252-7.228 7.228v31.469l-26.126-35.042c-1.248-2.179-3.598-3.655-6.276-3.655h-.603c-3.976 0-7.229 3.252-7.229 7.228v53.609c0 3.976 3.252 7.229 7.229 7.229h.603c3.976 0 7.228-3.253 7.228-7.229v-32.058l26.314 35.941c.162.252.339.494.53.724l.001.002c.723.986 1.712 1.662 2.814 2.075.847.35 1.773.544 2.742.544h.603a7.162 7.162 0 0 0 3.377-.844c.723-.344 1.332-.788 1.761-1.311zm-71.208 2.155h.603c3.976 0 7.228-3.253 7.228-7.229v-53.609c0-3.976-3.252-7.228-7.228-7.228h-.603c-3.976 0-7.229 3.252-7.229 7.228v53.609c0 3.976 3.253 7.229 7.229 7.229z"/></svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1,125 @@
|
||||
import { z, IntegrationDefinition, messages } from '@botpress/sdk'
|
||||
import proactiveConversation from 'bp_modules/proactive-conversation'
|
||||
import proactiveUser from 'bp_modules/proactive-user'
|
||||
import typingIndicator from 'bp_modules/typing-indicator'
|
||||
|
||||
export default new IntegrationDefinition({
|
||||
name: 'line',
|
||||
version: '2.0.7',
|
||||
title: 'Line',
|
||||
description: 'Interact with customers using a rich set of features.',
|
||||
icon: 'icon.svg',
|
||||
readme: 'hub.md',
|
||||
configuration: {
|
||||
schema: z.object({
|
||||
channelAccessToken: z
|
||||
.string()
|
||||
.min(1)
|
||||
.title('Channel Access Token')
|
||||
.describe('Token used to authenticate the bot with the Line API'),
|
||||
channelSecret: z
|
||||
.string()
|
||||
.min(1)
|
||||
.title('Channel Secret')
|
||||
.describe('Secret used to verify the signature of webhook events'),
|
||||
}),
|
||||
},
|
||||
channels: {
|
||||
channel: {
|
||||
title: 'Line conversation',
|
||||
description: 'Channel for a Line conversation',
|
||||
messages: { ...messages.defaults, bloc: messages.markdownBloc },
|
||||
message: {
|
||||
tags: {
|
||||
msgId: {
|
||||
title: 'Message ID',
|
||||
description: 'Line message ID',
|
||||
},
|
||||
},
|
||||
},
|
||||
conversation: {
|
||||
tags: {
|
||||
usrId: {
|
||||
title: 'User ID',
|
||||
description: 'Line user ID taking part in the conversation',
|
||||
},
|
||||
destId: {
|
||||
title: 'Destination ID',
|
||||
description: 'Line user ID of the bot',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
actions: {},
|
||||
events: {
|
||||
followed: {
|
||||
title: 'Followed',
|
||||
description: 'The bot was followed by someone.',
|
||||
schema: z.object({
|
||||
destinationId: z.string().title('Destination ID').describe('The Line user ID of the bot'),
|
||||
userId: z.string().title('User ID').describe('The Line ID of the user'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
states: {
|
||||
conversation: {
|
||||
type: 'conversation',
|
||||
schema: z.object({
|
||||
replyToken: z
|
||||
.string()
|
||||
.optional()
|
||||
.title('Reply Token')
|
||||
.describe('Token used to reply to a message received as a request on the webhook'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
|
||||
user: {
|
||||
tags: {
|
||||
usrId: {
|
||||
title: 'User ID',
|
||||
description: 'Line user ID',
|
||||
},
|
||||
},
|
||||
},
|
||||
entities: {
|
||||
user: {
|
||||
schema: z
|
||||
.object({
|
||||
id: z.string().title('User ID').describe('The Line ID of the user'),
|
||||
})
|
||||
.title('User')
|
||||
.describe('The user object fields'),
|
||||
title: 'User',
|
||||
description: 'A Line user',
|
||||
},
|
||||
conversation: {
|
||||
schema: z
|
||||
.object({
|
||||
id: z.string().title('User ID').describe('The Line ID of the user in the conversation'),
|
||||
destinationId: z.string().title('Destination ID').describe('Line user ID of the bot'),
|
||||
})
|
||||
.title('Conversation')
|
||||
.describe('The conversation object fields'),
|
||||
title: 'Conversation',
|
||||
description: 'A conversation with a Line user',
|
||||
},
|
||||
},
|
||||
attributes: {
|
||||
category: 'Communication & Channels',
|
||||
guideSlug: 'line',
|
||||
repo: 'botpress',
|
||||
},
|
||||
})
|
||||
.extend(typingIndicator, ({}) => ({ entities: {} }))
|
||||
.extend(proactiveUser, ({ entities }) => ({
|
||||
entities: {
|
||||
user: entities.user,
|
||||
},
|
||||
}))
|
||||
.extend(proactiveConversation, ({ entities }) => ({
|
||||
entities: {
|
||||
conversation: entities.conversation,
|
||||
},
|
||||
}))
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "@botpresshub/line",
|
||||
"description": "Line 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/cli": "workspace:*",
|
||||
"@botpress/client": "workspace:*",
|
||||
"@botpress/sdk": "workspace:*",
|
||||
"@botpress/sdk-addons": "workspace:*",
|
||||
"@line/bot-sdk": "^9.5.0",
|
||||
"remark": "^15.0.1",
|
||||
"remark-gfm": "^4.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@botpress/cli": "workspace:*",
|
||||
"@botpress/common": "workspace:*",
|
||||
"@botpress/sdk": "workspace:*",
|
||||
"@botpresshub/proactive-conversation": "workspace:*",
|
||||
"@botpresshub/proactive-user": "workspace:*",
|
||||
"@botpresshub/typing-indicator": "workspace:*",
|
||||
"@types/mdast": "^4.0.4"
|
||||
},
|
||||
"bpDependencies": {
|
||||
"typing-indicator": "../../interfaces/typing-indicator",
|
||||
"proactive-user": "../../interfaces/proactive-user",
|
||||
"proactive-conversation": "../../interfaces/proactive-conversation"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,527 @@
|
||||
import { RuntimeError } from '@botpress/client'
|
||||
import { transformMarkdown } from '@botpress/common'
|
||||
import { reporting } from '@botpress/sdk-addons'
|
||||
import { messagingApi as lineMessagingApi } from '@line/bot-sdk'
|
||||
import crypto from 'crypto'
|
||||
import getOrCreateConversation from './proactive-conversation'
|
||||
import getOrCreateUser from './proactive-user'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
type MessageHandlerProps = bp.AnyMessageProps
|
||||
type SendOrReplyLineProps = Pick<MessageHandlerProps, 'ctx' | 'conversation' | 'client' | 'ack'>
|
||||
|
||||
const DEFAULT_TIMEOUT_MS = 5000
|
||||
const LOADING_TIMEOUT_MULTIPLE = 5
|
||||
const MAX_TIMEOUT_SECONDS = 60
|
||||
|
||||
const replyOrSendLineMessage = async (props: SendOrReplyLineProps, message: lineMessagingApi.Message) => {
|
||||
const { ctx, conversation, client, ack } = props
|
||||
const config = {
|
||||
channelAccessToken: ctx.configuration.channelAccessToken,
|
||||
}
|
||||
|
||||
const lineClient = new lineMessagingApi.MessagingApiClient(config)
|
||||
|
||||
const { state } = await client.getState({
|
||||
id: conversation.id,
|
||||
name: 'conversation',
|
||||
type: 'conversation',
|
||||
})
|
||||
const replyToken = state.payload.replyToken
|
||||
try {
|
||||
let lineResponse: lineMessagingApi.ReplyMessageResponse | lineMessagingApi.PushMessageResponse
|
||||
if (replyToken) {
|
||||
await client.setState({
|
||||
id: conversation.id,
|
||||
name: 'conversation',
|
||||
type: 'conversation',
|
||||
payload: { replyToken: undefined },
|
||||
})
|
||||
lineResponse = await lineClient.replyMessage({
|
||||
replyToken,
|
||||
messages: [message],
|
||||
})
|
||||
} else {
|
||||
const usrId = conversation.tags.usrId
|
||||
if (!usrId) {
|
||||
throw new RuntimeError('No user id found in conversation tags')
|
||||
}
|
||||
lineResponse = await lineClient.pushMessage({
|
||||
to: usrId,
|
||||
messages: [message],
|
||||
})
|
||||
}
|
||||
|
||||
const sentMessage = lineResponse.sentMessages[0]
|
||||
if (sentMessage) {
|
||||
await ack({ tags: { msgId: sentMessage.id } })
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error(`Error: ${e}`)
|
||||
}
|
||||
}
|
||||
|
||||
const tryTransformMarkdown = (text: string) => {
|
||||
try {
|
||||
return transformMarkdown(text)
|
||||
} catch {
|
||||
console.error('Failed to transform the markdown. The message will be sent as text without transfoming markdown.')
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
const integration = new bp.Integration({
|
||||
register: async () => {},
|
||||
unregister: async () => {},
|
||||
actions: {
|
||||
startTypingIndicator: async ({ client, ctx, input }) => {
|
||||
const config = {
|
||||
channelAccessToken: ctx.configuration.channelAccessToken,
|
||||
}
|
||||
const lineClient = new lineMessagingApi.MessagingApiClient(config)
|
||||
const { conversationId, timeout } = input
|
||||
const { conversation } = await client.getConversation({ id: conversationId })
|
||||
const lineUserId = conversation.tags.usrId
|
||||
if (!lineUserId) {
|
||||
throw new RuntimeError('No user id found in conversation tags')
|
||||
}
|
||||
const timeoutSeconds = Math.ceil((timeout ?? DEFAULT_TIMEOUT_MS) / 1000)
|
||||
const loadingSeconds = Math.min(
|
||||
Math.ceil(timeoutSeconds / LOADING_TIMEOUT_MULTIPLE) * LOADING_TIMEOUT_MULTIPLE,
|
||||
MAX_TIMEOUT_SECONDS
|
||||
)
|
||||
await lineClient.showLoadingAnimation({
|
||||
chatId: lineUserId,
|
||||
loadingSeconds,
|
||||
})
|
||||
return {}
|
||||
},
|
||||
stopTypingIndicator: async () => ({}),
|
||||
getOrCreateConversation,
|
||||
getOrCreateUser,
|
||||
},
|
||||
channels: {
|
||||
channel: {
|
||||
messages: {
|
||||
text: async ({ payload, ctx, conversation, ack, client }) => {
|
||||
await replyOrSendLineMessage(
|
||||
{ ctx, conversation, client, ack },
|
||||
{
|
||||
type: 'text',
|
||||
text: tryTransformMarkdown(payload.text),
|
||||
}
|
||||
)
|
||||
},
|
||||
image: async ({ payload, ctx, conversation, ack, client }) => {
|
||||
await replyOrSendLineMessage(
|
||||
{ ctx, conversation, client, ack },
|
||||
{
|
||||
type: 'image',
|
||||
originalContentUrl: payload.imageUrl,
|
||||
previewImageUrl: payload.imageUrl, // Can use this - later when the upload is ready: https://www.npmjs.com/package/image-thumbnail
|
||||
}
|
||||
)
|
||||
},
|
||||
// TODO: fix audio, its not working
|
||||
audio: async ({ payload, ctx, conversation, ack, client }) => {
|
||||
await replyOrSendLineMessage(
|
||||
{ ctx, conversation, client, ack },
|
||||
{
|
||||
type: 'audio',
|
||||
originalContentUrl: payload.audioUrl,
|
||||
duration: -1,
|
||||
}
|
||||
)
|
||||
},
|
||||
video: async ({ payload, ctx, conversation, ack, client }) => {
|
||||
//TODO: Upload the thumbnail so it is ready to be passed as URL to Line
|
||||
await replyOrSendLineMessage(
|
||||
{ ctx, conversation, client, ack },
|
||||
{
|
||||
type: 'video',
|
||||
originalContentUrl: payload.videoUrl,
|
||||
previewImageUrl: 'https://example.com/preview.jpg',
|
||||
}
|
||||
)
|
||||
},
|
||||
file: async () => {
|
||||
console.error(
|
||||
'Documents & files are not supported by Line - https://developers.line.biz/en/reference/messaging-api'
|
||||
)
|
||||
},
|
||||
location: async ({ payload, ctx, conversation, ack, client }) => {
|
||||
await replyOrSendLineMessage(
|
||||
{ ctx, conversation, client, ack },
|
||||
{
|
||||
type: 'location',
|
||||
title: payload.title ?? '',
|
||||
latitude: payload.latitude,
|
||||
longitude: payload.longitude,
|
||||
address: payload.address ?? '',
|
||||
}
|
||||
)
|
||||
},
|
||||
carousel: async ({ payload, ctx, conversation, ack, client }) => {
|
||||
const sections: lineMessagingApi.FlexBubble[] = []
|
||||
for (let indexS = 0; indexS < payload.items.length; indexS++) {
|
||||
const item = payload.items[indexS]
|
||||
if (!item) {
|
||||
continue //just to pass the build validation
|
||||
}
|
||||
|
||||
const buttons: lineMessagingApi.FlexButton[] = []
|
||||
for (let indexR = 0; indexR < item.actions.length; indexR++) {
|
||||
const action = item.actions[indexR]
|
||||
|
||||
if (!action) {
|
||||
continue //just to pass the build validation
|
||||
}
|
||||
|
||||
if (action.action === 'postback') {
|
||||
buttons.push({
|
||||
type: 'button',
|
||||
action: {
|
||||
type: 'message',
|
||||
label: action.label.substring(0, 40),
|
||||
text: action.value.substring(0, 300),
|
||||
},
|
||||
})
|
||||
} else {
|
||||
buttons.push({
|
||||
type: 'button',
|
||||
action: {
|
||||
type: 'uri',
|
||||
label: action.label.substring(0, 40),
|
||||
uri: action.value.substring(0, 1000),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
sections.push({
|
||||
type: 'bubble',
|
||||
body: {
|
||||
type: 'box',
|
||||
layout: 'vertical',
|
||||
contents: [
|
||||
{
|
||||
type: 'image',
|
||||
url: item.imageUrl ?? '',
|
||||
},
|
||||
{
|
||||
type: 'separator',
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
text: item.title,
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
text: item.subtitle ?? '',
|
||||
},
|
||||
{
|
||||
type: 'separator',
|
||||
},
|
||||
...buttons,
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
if (sections.length === 12) {
|
||||
console.warn(
|
||||
'Only 12 items are allowed - https://developers.line.biz/en/reference/messaging-api/#f-carousel'
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
await replyOrSendLineMessage(
|
||||
{ ctx, conversation, client, ack },
|
||||
{
|
||||
type: 'flex',
|
||||
altText: 'this is a flex message',
|
||||
contents: {
|
||||
type: 'carousel',
|
||||
contents: sections,
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
card: async ({ payload, ctx, conversation, ack, client }) => {
|
||||
const buttons: lineMessagingApi.FlexButton[] = []
|
||||
for (let index = 0; index < payload.actions.length; index++) {
|
||||
const action = payload.actions[index]
|
||||
|
||||
if (!action) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (action.action === 'postback') {
|
||||
buttons.push({
|
||||
type: 'button', // Add generic styles to the buttons: https://developers.line.biz/en/reference/messaging-api/#button
|
||||
action: {
|
||||
type: 'message',
|
||||
label: action.label.substring(0, 40),
|
||||
text: action.value.substring(0, 300),
|
||||
},
|
||||
})
|
||||
} else {
|
||||
buttons.push({
|
||||
type: 'button',
|
||||
action: {
|
||||
type: 'uri',
|
||||
label: action.label.substring(0, 40),
|
||||
uri: action.value.substring(0, 1000),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
await replyOrSendLineMessage(
|
||||
{ ctx, conversation, client, ack },
|
||||
{
|
||||
type: 'flex',
|
||||
altText: payload.title,
|
||||
contents: {
|
||||
type: 'bubble',
|
||||
body: {
|
||||
type: 'box',
|
||||
layout: 'vertical',
|
||||
contents: [
|
||||
{
|
||||
type: 'image',
|
||||
url: payload.imageUrl ?? '',
|
||||
},
|
||||
{
|
||||
type: 'separator',
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
text: payload.title,
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
text: payload.subtitle ?? '',
|
||||
},
|
||||
{
|
||||
type: 'separator',
|
||||
},
|
||||
...buttons,
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
dropdown: async ({ payload, ctx, conversation, ack, client }) => {
|
||||
const buttons: lineMessagingApi.FlexButton[] = []
|
||||
for (let index = 0; index < payload.options.length; index++) {
|
||||
const choice = payload.options[index]
|
||||
|
||||
if (!choice) {
|
||||
continue
|
||||
}
|
||||
|
||||
buttons.push({
|
||||
type: 'button', // Add generic styles to the buttons: https://developers.line.biz/en/reference/messaging-api/#button
|
||||
action: {
|
||||
type: 'message',
|
||||
label: choice.label.substring(0, 40),
|
||||
text: choice.value.substring(0, 300),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
await replyOrSendLineMessage(
|
||||
{ ctx, conversation, client, ack },
|
||||
{
|
||||
type: 'flex',
|
||||
altText: payload.text,
|
||||
contents: {
|
||||
type: 'bubble',
|
||||
body: {
|
||||
type: 'box',
|
||||
layout: 'vertical',
|
||||
contents: [
|
||||
{
|
||||
type: 'text',
|
||||
text: payload.text,
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
text: payload.text,
|
||||
},
|
||||
{
|
||||
type: 'separator',
|
||||
},
|
||||
...buttons,
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
choice: async ({ payload, ctx, conversation, ack, client }) => {
|
||||
const buttons: lineMessagingApi.FlexButton[] = []
|
||||
for (let index = 0; index < payload.options.length; index++) {
|
||||
const choice = payload.options[index]
|
||||
if (!choice) {
|
||||
continue
|
||||
}
|
||||
|
||||
buttons.push({
|
||||
type: 'button', // Add generic styles to the buttons: https://developers.line.biz/en/reference/messaging-api/#button
|
||||
action: {
|
||||
type: 'message',
|
||||
label: choice.label.substring(0, 40),
|
||||
text: choice.value.substring(0, 300),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const contents: lineMessagingApi.FlexBubble = {
|
||||
type: 'bubble',
|
||||
body: {
|
||||
type: 'box',
|
||||
layout: 'vertical',
|
||||
contents: [
|
||||
{
|
||||
type: 'text',
|
||||
text: payload.text,
|
||||
},
|
||||
{
|
||||
type: 'separator',
|
||||
},
|
||||
...buttons,
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
await replyOrSendLineMessage({ ctx, conversation, client, ack }, {
|
||||
type: 'flex',
|
||||
altText: payload.text,
|
||||
contents,
|
||||
} satisfies lineMessagingApi.FlexMessage)
|
||||
},
|
||||
bloc: () => {
|
||||
throw new RuntimeError('Not implemented')
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
handler: async ({ req, client, ctx }) => {
|
||||
console.info('Handler received request')
|
||||
|
||||
if (!req.body) {
|
||||
throw new Error('Handler received an empty body')
|
||||
}
|
||||
|
||||
if (req.body && JSON.parse(req.body).events.length === 0) {
|
||||
return {
|
||||
status: 200,
|
||||
}
|
||||
}
|
||||
|
||||
if (!req.body) {
|
||||
console.warn('Handler received an empty body')
|
||||
return
|
||||
}
|
||||
|
||||
const channelSecret = ctx.configuration.channelSecret // Channel secret string
|
||||
const body = req.body // Request body string
|
||||
const signature = crypto.createHmac('SHA256', channelSecret).update(body).digest('base64')
|
||||
// Compare x-line-signature request header and the signature
|
||||
|
||||
if (req.headers['x-line-signature'] !== signature) {
|
||||
console.warn('Wrong Signature')
|
||||
return {
|
||||
status: 401,
|
||||
}
|
||||
}
|
||||
|
||||
const data = JSON.parse(req.body) as LinePayload
|
||||
|
||||
for (const event of data.events) {
|
||||
if (event.type === 'message') {
|
||||
await handleMessage(event, data.destination, client)
|
||||
} else if (event.type === 'follow') {
|
||||
await client.createEvent({
|
||||
type: 'followed',
|
||||
payload: {
|
||||
destinationId: data.destination,
|
||||
userId: event.source.userId,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
},
|
||||
})
|
||||
|
||||
export default reporting.wrapIntegration(integration)
|
||||
|
||||
async function handleMessage(events: LineEvents, destination: string, client: bp.Client) {
|
||||
const message = events.message
|
||||
if (message.type) {
|
||||
const { conversation } = await client.getOrCreateConversation({
|
||||
channel: 'channel',
|
||||
tags: {
|
||||
usrId: events.source.userId,
|
||||
destId: destination,
|
||||
},
|
||||
})
|
||||
|
||||
await client.setState({
|
||||
id: conversation.id,
|
||||
name: 'conversation',
|
||||
type: 'conversation',
|
||||
payload: { replyToken: events.replyToken },
|
||||
})
|
||||
|
||||
const { user } = await client.getOrCreateUser({
|
||||
tags: {
|
||||
usrId: events.source.userId,
|
||||
},
|
||||
})
|
||||
|
||||
if (message.type === 'text') {
|
||||
await client.createMessage({
|
||||
tags: { msgId: message.id },
|
||||
type: 'text',
|
||||
userId: user.id,
|
||||
conversationId: conversation.id,
|
||||
payload: { text: message.text },
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type LinePayload = {
|
||||
destination: string
|
||||
events: LineEvents[]
|
||||
}
|
||||
|
||||
type LineEvents = {
|
||||
type: string
|
||||
webhookEventId: string
|
||||
timestamp: string
|
||||
replyToken: string
|
||||
mode: string
|
||||
deliveryContext: {
|
||||
isRedelivery: boolean
|
||||
}
|
||||
source: LineSource
|
||||
message: LineMessage
|
||||
}
|
||||
|
||||
type LineSource = {
|
||||
type: string
|
||||
userId: string
|
||||
}
|
||||
|
||||
type LineMessage = {
|
||||
type: string
|
||||
id: string
|
||||
text: string
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { RuntimeError } from '@botpress/sdk'
|
||||
import { messagingApi as lineMessagingApi } from '@line/bot-sdk'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
const getOrCreateConversation: bp.IntegrationProps['actions']['getOrCreateConversation'] = async ({
|
||||
client,
|
||||
ctx,
|
||||
input,
|
||||
}) => {
|
||||
const usrId = input.conversation.id
|
||||
const destId = input.conversation.destinationId
|
||||
if (!usrId) {
|
||||
throw new RuntimeError('User ID is required')
|
||||
}
|
||||
if (!destId) {
|
||||
throw new RuntimeError('Destination ID is required')
|
||||
}
|
||||
|
||||
const lineClient = new lineMessagingApi.MessagingApiClient({
|
||||
channelAccessToken: ctx.configuration.channelAccessToken,
|
||||
})
|
||||
const profile = await lineClient.getProfile(usrId)
|
||||
|
||||
const { conversation } = await client.getOrCreateConversation({
|
||||
channel: 'channel',
|
||||
tags: { usrId: `${profile.userId}`, destId },
|
||||
})
|
||||
|
||||
return {
|
||||
conversationId: conversation.id,
|
||||
}
|
||||
}
|
||||
|
||||
export default getOrCreateConversation
|
||||
@@ -0,0 +1,24 @@
|
||||
import { RuntimeError } from '@botpress/sdk'
|
||||
import { messagingApi as lineMessagingApi } from '@line/bot-sdk'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
const getOrCreateUser: bp.IntegrationProps['actions']['getOrCreateUser'] = async ({ client, ctx, input }) => {
|
||||
const userId = input.user.id
|
||||
if (!userId) {
|
||||
throw new RuntimeError('User ID is required')
|
||||
}
|
||||
|
||||
const lineClient = new lineMessagingApi.MessagingApiClient({
|
||||
channelAccessToken: ctx.configuration.channelAccessToken,
|
||||
})
|
||||
|
||||
const profile = await lineClient.getProfile(userId)
|
||||
|
||||
const { user } = await client.getOrCreateUser({ tags: { usrId: `${profile.userId}` } })
|
||||
|
||||
return {
|
||||
userId: user.id,
|
||||
}
|
||||
}
|
||||
|
||||
export default getOrCreateUser
|
||||
@@ -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