chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
# Zoom Transcript Integration
|
||||
|
||||
Easily receive and automate Zoom meeting transcripts in Botpress for meetings hosted by specific Zoom users.
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Zoom meeting ends** → Zoom sends a webhook when transcript is ready.
|
||||
2. **Integration filters `host_id`** → Only allows events from specified Zoom user(s).
|
||||
3. **Transcript is fetched and cleaned** → Downloaded from Zoom and converted to plain text.
|
||||
4. **Event emitted** → A `transcriptReceived` event with `meetingUUID` and `transcript` is available to your flows.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
You must be:
|
||||
|
||||
- A Zoom **account owner**, **admin**, or have the **“Zoom for Developers” role** to be able to create the Zoom OAuth App
|
||||
- On a **Zoom premium plan** (free tier doesn't support cloud recordings)
|
||||
|
||||
---
|
||||
|
||||
## Step-by-Step Setup
|
||||
|
||||
### 1. Create a Zoom OAuth App
|
||||
|
||||
- Visit: [https://marketplace.zoom.us/](https://marketplace.zoom.us/)
|
||||
- Go to **Develop > Build App** → Choose **Server-to-Server OAuth** → Name your app
|
||||
- On the **App Credentials** page, copy:
|
||||
- **Account ID**
|
||||
- **Client ID**
|
||||
- **Client Secret**
|
||||
|
||||
You’ll use these in your Botpress integration configuration later.
|
||||
|
||||
- In **Information**, fill out the necessary information about yourself and the app.
|
||||
- In **Features**, copy the **Secret Token**
|
||||
- In **Scopes**, add:
|
||||
cloud_recording:read:list_user_recordings:admin
|
||||
cloud_recording:read:list_recording_files:admin
|
||||
cloud_recording:read:recording:admin
|
||||
|
||||
- Activate the app under the **Activation** tab
|
||||
|
||||
### 2. Get Zoom `host_id`
|
||||
|
||||
You can find your Zoom `host_id` directly from the Zoom web portal.
|
||||
|
||||
#### Steps:
|
||||
|
||||
1. Log in to your Zoom account and navigate to:
|
||||
**Zoom Admin Panel > User Management > Users**
|
||||
|
||||
2. Click on the name of the user you want to get the `host_id` for.
|
||||
|
||||
3. Look at the URL in your browser's address bar. It will be in this format:
|
||||
https://yourdomain.zoom.us/user/xxxxxxxxxxxxx/profile you want to copy the xxxxxxxxxxxxx as that is your host_id
|
||||
|
||||
---
|
||||
|
||||
### 3. Configure the Botpress Integration
|
||||
|
||||
- Install this integration into your Bot
|
||||
- Paste:
|
||||
- `Zoom Client ID`
|
||||
- `Zoom Client Secret`
|
||||
- `Zoom Account ID`
|
||||
- `Secret Token`
|
||||
- `Allowed Zoom User IDs` → Paste your `host_id` (you can include multiple)
|
||||
|
||||
Click **Save Configuration**.
|
||||
|
||||
---
|
||||
|
||||
### 4. Set Webhook in Zoom
|
||||
|
||||
Back in your Zoom OAuth App:
|
||||
|
||||
- Go to **Features** → Enable **Event Subscriptions**
|
||||
- Name: `Transcript Received` (Can choose a different name as well)
|
||||
- Method: `Webhook`
|
||||
- Endpoint URL: use the **Botpress integration URL**
|
||||
- Add Event:
|
||||
- Under **Recording**, select **only**:
|
||||
- `Recording transcript files have completed` (`recording.transcript_completed`)
|
||||
- Click **Done**
|
||||
- Click **Validate** next to the endpoint URL (you should see Validated)
|
||||
- Click **Save**
|
||||
|
||||
---
|
||||
|
||||
## Done!
|
||||
|
||||
Your Botpress bot will now receive transcripts for allowed Zoom users when cloud recordings complete. Make sure:
|
||||
|
||||
- You **record to the cloud**
|
||||
- You’re on a **paid Zoom plan**
|
||||
- You’ve correctly added all intended `host_id`s
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="256px" height="256px" viewBox="0 0 256 256" version="1.1">
|
||||
<defs>
|
||||
<linearGradient id="linear0" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="1" y2="0" gradientTransform="matrix(134.566488,-233.075995,233.075995,134.566488,60.82202,244.452418)">
|
||||
<stop offset="0" style="stop-color:rgb(3.137255%,27.058824%,74.901961%);stop-opacity:1;"/>
|
||||
<stop offset="0.6" style="stop-color:rgb(4.313725%,36.078431%,100%);stop-opacity:1;"/>
|
||||
<stop offset="1" style="stop-color:rgb(30.980392%,56.470588%,93.333333%);stop-opacity:1;"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g id="surface1">
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:url(#linear0);" d="M 255.855469 127.914062 C 255.855469 141.578125 254.753906 154.972656 252.65625 168.035156 C 245.714844 211.273438 211.460938 245.523438 168.222656 252.46875 C 154.957031 254.597656 141.542969 255.667969 128.105469 255.664062 C 114.445312 255.664062 101.050781 254.566406 87.984375 252.46875 C 44.746094 245.523438 10.496094 211.273438 3.550781 168.035156 C 1.421875 154.765625 0.351562 141.351562 0.355469 127.914062 C 0.355469 114.253906 1.453125 100.859375 3.550781 87.796875 C 10.496094 44.558594 44.746094 10.304688 87.984375 3.363281 C 101.253906 1.230469 114.667969 0.164062 128.105469 0.164062 C 141.765625 0.164062 155.160156 1.265625 168.222656 3.363281 C 211.460938 10.304688 245.714844 44.558594 252.65625 87.796875 C 254.789062 101.0625 255.855469 114.480469 255.855469 127.914062 Z M 255.855469 127.914062 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 76.652344 147.480469 L 44.960938 147.480469 C 42.746094 147.484375 40.746094 146.144531 39.902344 144.097656 C 39.050781 142.050781 39.519531 139.691406 41.089844 138.128906 L 63.039062 116.179688 L 47.308594 116.179688 C 42.984375 116.179688 39.480469 112.675781 39.480469 108.351562 L 68.707031 108.351562 C 70.929688 108.351562 72.917969 109.679688 73.765625 111.730469 C 74.621094 113.777344 74.152344 116.136719 72.582031 117.703125 L 50.628906 139.652344 L 68.824219 139.652344 C 73.148438 139.652344 76.652344 143.15625 76.652344 147.480469 Z M 216.726562 123.023438 C 216.726562 114.613281 209.882812 107.765625 201.46875 107.765625 C 196.96875 107.765625 192.917969 109.722656 190.121094 112.832031 C 187.324219 109.722656 183.273438 107.765625 178.777344 107.765625 C 170.359375 107.765625 163.515625 114.609375 163.515625 123.023438 L 163.515625 147.480469 C 167.835938 147.480469 171.339844 143.976562 171.339844 139.652344 L 171.339844 123.023438 C 171.339844 118.925781 174.675781 115.589844 178.773438 115.589844 C 182.875 115.589844 186.207031 118.925781 186.207031 123.023438 L 186.207031 139.652344 C 186.207031 143.976562 189.710938 147.480469 194.035156 147.480469 L 194.035156 123.023438 C 194.035156 118.925781 197.367188 115.589844 201.46875 115.589844 C 205.566406 115.589844 208.902344 118.925781 208.902344 123.023438 L 208.902344 139.652344 C 208.902344 143.976562 212.40625 147.480469 216.726562 147.480469 Z M 159.601562 127.914062 C 159.601562 139.046875 150.582031 148.066406 139.453125 148.066406 C 128.320312 148.066406 119.300781 139.046875 119.300781 127.914062 C 119.300781 116.785156 128.320312 107.765625 139.453125 107.765625 C 150.582031 107.765625 159.601562 116.785156 159.601562 127.914062 Z M 151.777344 127.914062 C 151.777344 121.109375 146.257812 115.589844 139.453125 115.589844 C 132.644531 115.589844 127.125 121.109375 127.125 127.914062 C 127.125 134.722656 132.644531 140.242188 139.449219 140.242188 C 146.257812 140.242188 151.777344 134.722656 151.777344 127.914062 Z M 116.171875 127.914062 C 116.171875 139.046875 107.148438 148.066406 96.023438 148.066406 C 84.890625 148.066406 75.871094 139.046875 75.871094 127.914062 C 75.871094 116.785156 84.890625 107.765625 96.023438 107.765625 C 107.148438 107.765625 116.171875 116.785156 116.171875 127.914062 Z M 108.34375 127.914062 C 108.34375 121.109375 102.828125 115.589844 96.023438 115.589844 C 89.210938 115.589844 83.695312 121.109375 83.695312 127.914062 C 83.695312 134.722656 89.210938 140.242188 96.023438 140.242188 C 102.828125 140.242188 108.34375 134.722656 108.34375 127.914062 Z M 108.34375 127.914062 "/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.2 KiB |
@@ -0,0 +1,49 @@
|
||||
import { z, IntegrationDefinition } from '@botpress/sdk'
|
||||
|
||||
export default new IntegrationDefinition({
|
||||
name: 'zoom',
|
||||
version: '0.1.1',
|
||||
title: 'Zoom',
|
||||
description: 'Receives Zoom webhook and processes transcript for meetings.',
|
||||
icon: 'icon.svg',
|
||||
readme: 'hub.md',
|
||||
configuration: {
|
||||
schema: z.object({
|
||||
zoomAccountId: z
|
||||
.string()
|
||||
.title('Zoom Account ID')
|
||||
.describe('Zoom Account ID (Found on Zoom OAuth App under App Credentials)'),
|
||||
zoomClientId: z
|
||||
.string()
|
||||
.title('Zoom Client ID')
|
||||
.describe('Zoom Client ID (Found on Zoom OAuth App under App Credentials)'),
|
||||
zoomClientSecret: z
|
||||
.string()
|
||||
.title('Zoom Client Secret')
|
||||
.describe('Zoom Client Secret (Found on Zoom OAuth App under App Credentials)'),
|
||||
secretToken: z.string().title('Secret Token').describe('Secret Token (Found on Zoom OAuth App under Features)'),
|
||||
allowedZoomUserIds: z
|
||||
.array(z.string())
|
||||
.title('Allowed Zoom User IDs')
|
||||
.describe('Process events from these Zoom User IDs'),
|
||||
}),
|
||||
},
|
||||
channels: {},
|
||||
events: {
|
||||
transcriptReceived: {
|
||||
title: 'Transcript Received',
|
||||
description: 'Fires when a transcript is received from Zoom',
|
||||
schema: z.object({
|
||||
meetingUUID: z.string().title('Meeting UUID').describe('The UUID of the Zoom meeting'),
|
||||
hostId: z.string().title('Host ID').describe('The ID of the host'),
|
||||
transcript: z.string().title('Transcript').describe('The transcript of the meeting'),
|
||||
audioUrl: z.string().title('Audio URL').describe('The URL of the meeting audio'),
|
||||
rawVtt: z.string().title('Raw VTT').describe('The raw VTT file content of the meeting transcript').optional(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
attributes: {
|
||||
category: 'Communication & Channels',
|
||||
repo: 'botpress',
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "@botpresshub/zoom",
|
||||
"scripts": {
|
||||
"build": "bp add -y && bp build",
|
||||
"check:bplint": "bp lint",
|
||||
"check:type": "tsc --noEmit"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@botpress/client": "workspace:*",
|
||||
"@botpress/sdk": "workspace:*",
|
||||
"axios": "^1.13.2",
|
||||
"exponential-backoff": "^3.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@botpress/cli": "workspace:*"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import { RuntimeError, z } from '@botpress/sdk'
|
||||
import axios from 'axios'
|
||||
import { backOff } from 'exponential-backoff'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
const accessTokenResponseSchema = z.object({
|
||||
access_token: z.string().optional(),
|
||||
error: z.string().optional(),
|
||||
error_description: z.string().optional(),
|
||||
})
|
||||
|
||||
const recordingFileSchema = z.object({
|
||||
file_type: z.string(),
|
||||
file_extension: z.string(),
|
||||
download_url: z.string().optional(),
|
||||
})
|
||||
|
||||
const recordingsResponseSchema = z.object({
|
||||
recording_files: z.array(recordingFileSchema).optional(),
|
||||
})
|
||||
|
||||
export class ZoomClient {
|
||||
public constructor(
|
||||
private _config: Pick<bp.configuration.Configuration, 'zoomClientId' | 'zoomClientSecret' | 'zoomAccountId'>,
|
||||
private _logger: bp.Logger
|
||||
) {}
|
||||
|
||||
public async getAccessToken(): Promise<string> {
|
||||
const { zoomClientId, zoomClientSecret, zoomAccountId } = this._config
|
||||
const tokenUrl = `https://zoom.us/oauth/token?grant_type=account_credentials&account_id=${zoomAccountId}`
|
||||
const authHeader = Buffer.from(`${zoomClientId}:${zoomClientSecret}`).toString('base64')
|
||||
|
||||
try {
|
||||
const { data } = await axios.post(tokenUrl, null, {
|
||||
headers: {
|
||||
Authorization: `Basic ${authHeader}`,
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
timeout: 10000,
|
||||
})
|
||||
|
||||
const validation = accessTokenResponseSchema.safeParse(data)
|
||||
if (!validation.success || !validation.data.access_token) {
|
||||
throw new RuntimeError('Invalid access token response')
|
||||
}
|
||||
|
||||
return validation.data.access_token
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error) && error.response) {
|
||||
const validation = accessTokenResponseSchema.safeParse(error.response.data)
|
||||
const errorData = validation.success ? validation.data : {}
|
||||
const errorMsg = `Zoom access token fetch failed (HTTP ${error.response.status}): ${errorData.error_description || errorData.error || 'Unknown error'}`
|
||||
this._logger.forBot().error(errorMsg, errorData)
|
||||
throw new RuntimeError(errorMsg)
|
||||
}
|
||||
this._logger.forBot().error('Failed to fetch Zoom access token', error)
|
||||
throw new RuntimeError('Failed to fetch Zoom access token')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch transcript file URL with retries
|
||||
* Zoom may take time to generate the transcript, so we retry up to 3 times
|
||||
*
|
||||
* API: GET /meetings/{meetingId}/recordings
|
||||
* Docs: https://developers.zoom.us/docs/api/meetings/#tag/cloud-recording
|
||||
*/
|
||||
public async fetchTranscriptUrl(
|
||||
meetingUUID: string,
|
||||
accessToken: string
|
||||
): Promise<{ transcriptUrl: string; audioUrl: string } | null> {
|
||||
const encodedUUID = encodeURIComponent(meetingUUID)
|
||||
|
||||
try {
|
||||
return await backOff(
|
||||
async () => {
|
||||
const { data } = await axios.get(`https://api.zoom.us/v2/meetings/${encodedUUID}/recordings`, {
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
timeout: 15000,
|
||||
})
|
||||
|
||||
const validation = recordingsResponseSchema.safeParse(data)
|
||||
if (!validation.success) {
|
||||
this._logger.forBot().error('Invalid recordings response format')
|
||||
throw new RuntimeError('Invalid recordings response format')
|
||||
}
|
||||
|
||||
const transcriptFile = validation.data.recording_files?.find(
|
||||
(f) => f.file_type === 'TRANSCRIPT' && f.file_extension === 'VTT'
|
||||
)
|
||||
|
||||
if (!transcriptFile?.download_url) {
|
||||
throw new Error('Transcript not ready')
|
||||
}
|
||||
|
||||
const audioFile = validation.data.recording_files?.find((f) => f.file_type === 'M4A')
|
||||
|
||||
if (!audioFile?.download_url) {
|
||||
throw new Error('Audio not ready')
|
||||
}
|
||||
|
||||
return { transcriptUrl: transcriptFile.download_url, audioUrl: audioFile.download_url }
|
||||
},
|
||||
{ numOfAttempts: 3, startingDelay: 20000 }
|
||||
)
|
||||
} catch (error) {
|
||||
// Return null if transcript or audio wasn't found after retries
|
||||
if (error instanceof Error && (error.message === 'Transcript not ready' || error.message === 'Audio not ready')) {
|
||||
this._logger
|
||||
.forBot()
|
||||
.warn(`Recording files not found for meeting ${meetingUUID} after retries: ${error.message}`)
|
||||
return null
|
||||
}
|
||||
|
||||
// Re-throw other errors (network failures, auth errors, etc.)
|
||||
if (axios.isAxiosError(error)) {
|
||||
const status = error.response?.status
|
||||
this._logger.forBot().error(`Failed to fetch recordings for meeting ${meetingUUID}: HTTP ${status}`, error)
|
||||
throw new RuntimeError(`Failed to fetch recordings: HTTP ${status || 'network error'}`)
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { register, unregister, handler } from './setup'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
export default new bp.Integration({
|
||||
register,
|
||||
unregister,
|
||||
handler,
|
||||
actions: {},
|
||||
channels: {},
|
||||
})
|
||||
@@ -0,0 +1,117 @@
|
||||
import { Request, RuntimeError } from '@botpress/sdk'
|
||||
import axios from 'axios'
|
||||
import crypto from 'crypto'
|
||||
import { ZoomClient } from '../client'
|
||||
import { zoomWebhookSchema, TranscriptCompletedPayload } from '../types'
|
||||
import { cleanVtt } from '../utils'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
const handleTranscriptCompleted = async ({
|
||||
payload,
|
||||
config,
|
||||
client,
|
||||
logger,
|
||||
}: {
|
||||
payload: TranscriptCompletedPayload
|
||||
config: bp.configuration.Configuration
|
||||
client: bp.Client
|
||||
logger: bp.Logger
|
||||
}): Promise<{ status: number; body: string }> => {
|
||||
try {
|
||||
const allowedUserIds = config.allowedZoomUserIds
|
||||
const hostId = payload.payload.object.host_id
|
||||
|
||||
if (!allowedUserIds.includes(hostId)) {
|
||||
return { status: 200, body: 'Event ignored: userId not allowed' }
|
||||
}
|
||||
|
||||
const zoomClient = new ZoomClient(config, logger)
|
||||
const accessToken = await zoomClient.getAccessToken()
|
||||
const meetingUUID = payload.payload.object.uuid
|
||||
|
||||
const response = await zoomClient.fetchTranscriptUrl(meetingUUID, accessToken)
|
||||
|
||||
if (!response) {
|
||||
throw new RuntimeError('Transcript file not found after retries')
|
||||
}
|
||||
|
||||
const { transcriptUrl, audioUrl } = response
|
||||
|
||||
const { data: vttText } = await axios.get<string>(transcriptUrl, {
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
responseType: 'text',
|
||||
timeout: 30000,
|
||||
})
|
||||
const plainText = cleanVtt(vttText)
|
||||
|
||||
await client.createEvent({
|
||||
type: 'transcriptReceived',
|
||||
payload: {
|
||||
meetingUUID,
|
||||
hostId,
|
||||
transcript: plainText,
|
||||
audioUrl,
|
||||
rawVtt: vttText,
|
||||
},
|
||||
})
|
||||
|
||||
logger.forBot().info(`Transcript processed successfully for meeting ${meetingUUID}`)
|
||||
return { status: 200, body: 'Transcript processed.' }
|
||||
} catch (err: unknown) {
|
||||
const errorMsg = err instanceof Error ? err.message : 'Unknown error'
|
||||
logger.forBot().error(`Transcript handler error: ${errorMsg}`)
|
||||
return { status: 500, body: `Zoom handler error: ${errorMsg}` }
|
||||
}
|
||||
}
|
||||
|
||||
export const handler: bp.IntegrationProps['handler'] = async ({ req, ctx, client, logger }) => {
|
||||
const config = ctx.configuration
|
||||
const isSignatureValid = _isSignatureValid(req, config.secretToken)
|
||||
if (!isSignatureValid) {
|
||||
logger.forBot().warn('Request signature was invalid')
|
||||
return { status: 401, body: 'Invalid signature' }
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = typeof req.body === 'string' ? JSON.parse(req.body) : req.body
|
||||
const validation = zoomWebhookSchema.safeParse(parsed)
|
||||
if (!validation.success) {
|
||||
logger.forBot().error(`Invalid webhook body format: ${validation.error.message}`)
|
||||
return { status: 400, body: 'Invalid request body format' }
|
||||
}
|
||||
|
||||
const webhookBody = validation.data
|
||||
|
||||
if (webhookBody.event === 'endpoint.url_validation') {
|
||||
const plainToken = webhookBody.payload.plainToken
|
||||
const encryptedToken = crypto.createHmac('sha256', config.secretToken).update(plainToken).digest('hex')
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: JSON.stringify({ plainToken, encryptedToken }),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
}
|
||||
|
||||
if (webhookBody.event === 'recording.transcript_completed') {
|
||||
return await handleTranscriptCompleted({ payload: webhookBody, config, client, logger })
|
||||
}
|
||||
|
||||
return { status: 200, body: 'Event ignored.' }
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : 'Unknown error'
|
||||
logger.forBot().error(`Webhook handler error: ${errorMsg}`)
|
||||
return { status: 400, body: 'Invalid request' }
|
||||
}
|
||||
}
|
||||
|
||||
const _isSignatureValid = (req: Request, secret: string) => {
|
||||
const timestamp = req.headers?.['x-zm-request-timestamp']
|
||||
const signature = req.headers?.['x-zm-signature']
|
||||
if (!timestamp || !signature) {
|
||||
return false
|
||||
}
|
||||
const message = `v0:${timestamp}:${typeof req.body === 'string' ? req.body : JSON.stringify(req.body)}`
|
||||
const expectedSig = 'v0=' + crypto.createHmac('sha256', secret).update(message).digest('hex')
|
||||
return signature === expectedSig
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { handler } from './handler'
|
||||
export { register } from './register'
|
||||
export { unregister } from './unregister'
|
||||
@@ -0,0 +1,18 @@
|
||||
import { RuntimeError } from '@botpress/sdk'
|
||||
import { ZoomClient } from '../client'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
export const register: bp.IntegrationProps['register'] = async ({ ctx, logger }) => {
|
||||
try {
|
||||
const zoomClient = new ZoomClient(ctx.configuration, logger)
|
||||
await zoomClient.getAccessToken()
|
||||
|
||||
logger.forBot().info('Zoom integration registered successfully')
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : 'Unknown error'
|
||||
logger.forBot().error(`Failed to connect to Zoom API: ${errorMsg}`)
|
||||
throw new RuntimeError(
|
||||
`Configuration Error! Please check your Zoom Account ID, Client ID, and Client Secret. Error: ${errorMsg}`
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import * as bp from '.botpress'
|
||||
|
||||
export const unregister: bp.IntegrationProps['unregister'] = async ({ logger }) => {
|
||||
logger.forBot().info('Unregister process for Zoom integration invoked. No resources to clean up.')
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { z } from '@botpress/sdk'
|
||||
|
||||
const urlValidationSchema = z.object({
|
||||
event: z.literal('endpoint.url_validation'),
|
||||
payload: z.object({
|
||||
plainToken: z.string(),
|
||||
}),
|
||||
})
|
||||
|
||||
const transcriptCompletedSchema = z.object({
|
||||
event: z.literal('recording.transcript_completed'),
|
||||
payload: z.object({
|
||||
object: z.object({
|
||||
uuid: z.string(),
|
||||
host_id: z.string(),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
export const zoomWebhookSchema = z.discriminatedUnion('event', [urlValidationSchema, transcriptCompletedSchema])
|
||||
|
||||
export type TranscriptCompletedPayload = z.infer<typeof transcriptCompletedSchema>
|
||||
@@ -0,0 +1,7 @@
|
||||
export function cleanVtt(vtt: string): string {
|
||||
return vtt
|
||||
.replace(/WEBVTT\n/g, '')
|
||||
.replace(/\d{2}:\d{2}:\d{2}\.\d{3} --> .*\n/g, '')
|
||||
.replace(/\n+/g, ' ')
|
||||
.trim()
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"paths": { "*": ["./*"] },
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": [".botpress/**/*", "definitions/**/*", "src/**/*", "*.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user