Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions integrations/wechat/definitions/channels.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { IntegrationDefinitionProps, messages } from '@botpress/sdk'

const _wechatMessageChannels = {
text: {
...messages.defaults.text,
schema: messages.defaults.text.schema.extend({
text: messages.defaults.text.schema.shape.text
.max(4096)
.describe('The text content of the WeChat message (Limit 4096 characters)'),
}),
},
image: messages.defaults.image,
video: messages.defaults.video,
}

export const channels = {
channel: {
title: 'Channel',
description: 'WeChat Channel',
messages: _wechatMessageChannels,
message: {
tags: {
id: { title: 'ID', description: 'The message ID' },
chatId: { title: 'Chat ID', description: 'The message chat ID' },
},
},
conversation: {
tags: {
id: { title: 'ID', description: "The WeChat conversation ID (This is also the Recipient's UserId)" },
},
},
},
} as const satisfies IntegrationDefinitionProps['channels']
3 changes: 3 additions & 0 deletions integrations/wechat/definitions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './channels'
export * from './user'
export * from './states'
22 changes: 22 additions & 0 deletions integrations/wechat/definitions/states.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { type IntegrationDefinitionProps, z } from '@botpress/sdk'

export const states = {
configuration: {
type: 'integration',
schema: z.object({
auth: z
.object({
accessToken: z.string().title('Access Token').describe('The access token for the integration'),
expiresAt: z
.number()
.min(0)
.title('Expires At')
.describe('The expiry time of the access token represented as a Unix timestamp (seconds)'),
})
.nullable()
.default(null)
.title('Auth Parameters')
.describe('The parameters used for accessing the WeChat API'),
}),
},
} as const satisfies IntegrationDefinitionProps['states']
7 changes: 7 additions & 0 deletions integrations/wechat/definitions/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { IntegrationDefinitionProps } from '@botpress/sdk'

export const user = {
tags: {
id: { title: 'ID', description: 'The ID of the user' },
},
} as const satisfies IntegrationDefinitionProps['user']
93 changes: 93 additions & 0 deletions integrations/wechat/hub.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# WeChat Official Account Integration

Connect your Botpress chatbot to WeChat Official Accounts and engage with your Chinese audience in real-time.

## Prerequisites

Before you begin, you need:

1. **WeChat Official Account** (Service Account or Subscription Account)
- Create one at: https://mp.weixin.qq.com/
2. **App ID and App Secret** from your WeChat Official Account settings
3. **Server Configuration** enabled in WeChat Admin Panel

## Configuration

### 1. Get Your WeChat Credentials

In your WeChat Official Account admin panel:

1. Go to **Settings & Development** > **Basic Configuration**
2. Copy your **AppID** and **AppSecret**
3. Generate a **Token** (any random string, you'll use this in both WeChat and Botpress)

### 2. Install the Integration in Botpress

1. Install this integration in your Botpress workspace
2. Configure with your credentials:
- **WeChat Token**: The token you generated (used for signature verification)
- **App ID**: Your WeChat Official Account AppID
- **App Secret**: Your WeChat Official Account AppSecret

### 3. Configure WeChat Webhook

In your WeChat Official Account admin panel:

1. Go to **Settings & Development** > **Basic Configuration**
2. Click **Enable** Server Configuration
3. Set the **URL** to https://wechat.botpress.tech/{your Botpress webhook ID}, where the webhook ID is the string provided after installing the integration.
4. Set the **Token** to the same token you used in Botpress configuration
5. Click **Submit** - WeChat will verify your server

## Supported Features

### Receiving Messages

Your bot can receive the following message types from WeChat users:

- **Text messages** - Plain text messages
- **Image messages** - Photos sent by users (PicUrl provided)
- **Video messages** - Video content
- **Link messages** - Shared links with title, description, and URL

### Sending Messages

Your bot can send the following message types to WeChat users:

- **Text messages** - Up to 4,096 characters
- **Image messages** - Images (automatically uploaded to WeChat)

## Limitations

### WeChat Platform Limitations

- **Official Account Required**: Personal WeChat accounts cannot be used
- **Customer Service API Window**: Can only send messages to users who have messaged you within the last 48 hours
- **Message Length**: Text messages limited to 4,096 characters
- **No Proactive Messaging**: Cannot initiate conversations; users must message first

## Troubleshooting

### Webhook Verification Fails

- Ensure your **Token** matches exactly in both Botpress and WeChat
- Check that your webhook URL is publicly accessible
- Verify the URL ends with your integration webhook path

### Messages Not Appearing in Botpress

- Check your WeChat Admin Panel logs for delivery errors
- Verify your **App ID** and **App Secret** are correct
- Ensure Server Configuration is enabled in WeChat

### Bot Not Responding

- Check Botpress logs for errors
- Verify the user messaged you within the last 48 hours
- Ensure your bot flow is properly configured

## Additional Resources

- [WeChat Official Account Platform Docs](https://developers.weixin.qq.com/doc/offiaccount/en/Getting_Started/Overview.html)
- [WeChat API Reference](https://developers.weixin.qq.com/doc/offiaccount/en/Message_Management/Receiving_standard_messages.html)
- [Botpress Documentation](https://botpress.com/docs)
9 changes: 9 additions & 0 deletions integrations/wechat/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 24 additions & 0 deletions integrations/wechat/integration.definition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { z, IntegrationDefinition } from '@botpress/sdk'
import { channels, states, user } from 'definitions'

export default new IntegrationDefinition({
name: 'wechat',
title: 'WeChat',
description: 'Engage with your WeChat audience in real-time.',
version: '0.1.0',
readme: 'hub.md',
icon: 'icon.svg',
configuration: {
schema: z.object({
appId: z.string().title('App ID').describe('WeChat Official Account App ID'),
appSecret: z.string().title('App Secret').describe('WeChat Official Account App Secret'),
webhookSigningSecret: z
.string()
.title('WeChat Token')
.describe('WeChat Token used for webhook signature verification'),
}),
},
channels,
states,
user,
})
21 changes: 21 additions & 0 deletions integrations/wechat/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "@botpresshub/wechat",
"private": true,
"scripts": {
"build": "bp add -y && bp build",
"check:type": "tsc --noEmit",
"check:bplint": "bp lint"
},
"dependencies": {
"@botpress/client": "workspace:*",
"@botpress/sdk": "workspace:*",
"axios": "^1.13.6",
"fast-xml-parser": "^5.4.2",
"lodash": "^4.17.21",
"nanoid": "^5.1.5"
},
"devDependencies": {
"@botpress/cli": "workspace:*",
"@types/lodash": "^4.14.191"
}
}
100 changes: 100 additions & 0 deletions integrations/wechat/src/api/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { RuntimeError } from '@botpress/sdk'
import axios from 'axios'
import { Result } from '../types'
import { usePromiseToResult } from '../utils'
import { WECHAT_API_BASE } from './constants'
import { weChatAuthTokenRespSchema } from './schemas'
import * as bp from '.botpress'

const MS_PER_SECOND = 1000
const SECONDS_PER_MINUTE = 60 as const
const TOKEN_EXPIRY_BUFFER = 5 * SECONDS_PER_MINUTE

type TokenResp = { accessToken: string; expiresAt: number }

async function _getFreshAccessToken(appId: string, appSecret: string): Promise<Result<TokenResp>> {
const tokenIssuedAtMs = Date.now()
const respResult = await axios
.get(`${WECHAT_API_BASE}/token?grant_type=client_credential&appid=${appId}&secret=${appSecret}`)
.then(...usePromiseToResult('Failed to acquire a WeChat access token'))
if (!respResult.success) return respResult
const resp = respResult.data

const result = weChatAuthTokenRespSchema.safeParse(resp.data)
if (!result.success) {
return {
success: false,
error: new RuntimeError(`Unexpected access token response received -> ${result.error.message}`),
}
}

const { data } = result
if ('errcode' in data) {
return {
success: false,
error: new RuntimeError(
`Failed to acquire a WeChat access token (Error Code: ${data.errcode}) -> ${data.errmsg}`
),
}
}

return {
success: true,
data: {
accessToken: data.access_token,
expiresAt: tokenIssuedAtMs / MS_PER_SECOND + data.expires_in,
},
}
}

async function _getCachedAccessToken(client: bp.Client, ctx: bp.Context): Promise<Result<TokenResp>> {
const state = await client.getState({
type: 'integration',
name: 'configuration',
id: ctx.integrationId,
})

const { auth = null } = state.state.payload
if (auth === null) {
return {
success: false,
error: new RuntimeError('No access token has been cached'),
}
}

return {
success: true,
data: auth,
}
}

const _applyTokenToCache = async (client: bp.Client, ctx: bp.Context, resp: TokenResp) => {
await client.setState({
type: 'integration',
name: 'configuration',
id: ctx.integrationId,
payload: {
auth: resp,
},
})
}

export const getOrRefreshAccessToken = async ({ client, ctx }: bp.CommonHandlerProps) => {
let tokenResult = await _getCachedAccessToken(client, ctx)

let cacheToken = false
if (!tokenResult.success || Date.now() / MS_PER_SECOND >= tokenResult.data.expiresAt - TOKEN_EXPIRY_BUFFER) {
const { appId, appSecret } = ctx.configuration
tokenResult = await _getFreshAccessToken(appId, appSecret)
cacheToken = true
}

if (!tokenResult.success) throw tokenResult.error
const tokenResp = tokenResult.data

if (cacheToken) {
await _applyTokenToCache(client, ctx, tokenResp)
}

return tokenResp.accessToken
}
Loading
Loading