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
@@ -0,0 +1,43 @@
import * as sdk from '@botpress/sdk'
import * as crypto from 'crypto'
import { getOAuthClientSecret } from '../dropbox-api/oauth-client'
import { handleFileChangeEvent, isFileChangeNotification } from './handlers/file-change'
import { isWebhookVerificationRequest, handleWebhookVerificationRequest } from './handlers/webhook-verification'
import { oauthCallbackHandler } from './oauth'
import * as bp from '.botpress'
export const handler: bp.IntegrationProps['handler'] = async (props) => {
if (props.req.path.startsWith('/oauth')) {
return await oauthCallbackHandler(props)
}
if (isWebhookVerificationRequest(props)) {
return await handleWebhookVerificationRequest(props)
}
_validatePayloadSignature(props)
if (isFileChangeNotification(props)) {
return await handleFileChangeEvent(props)
}
throw new sdk.RuntimeError('Unsupported webhook event')
}
const _validatePayloadSignature = (props: bp.HandlerProps) => {
const bodySignatureFromDropbox = props.req.headers['X-Dropbox-Signature'] ?? props.req.headers['x-dropbox-signature']
if (!bodySignatureFromDropbox) {
throw new sdk.RuntimeError('Missing Dropbox signature in request headers')
}
const clientSecret = getOAuthClientSecret({ ctx: props.ctx })
const bodySignatureFromBotpress = crypto
.createHmac('sha256', clientSecret)
.update(props.req.body ?? '')
.digest('hex')
if (bodySignatureFromDropbox !== bodySignatureFromBotpress) {
throw new sdk.RuntimeError('Dropbox signature does not match the expected signature')
}
}
@@ -0,0 +1,173 @@
import * as sdk from '@botpress/sdk'
import { File as FileEntity, Folder as FolderEntity, Deleted as DeletedEntity } from '../../../definitions'
import { DropboxClient } from '../../dropbox-api'
import { FileTree, type FileTreeDiff } from '../../dropbox-api/file-tree'
import * as filesReadonlyMapping from '../../files-readonly/mapping'
import * as bp from '.botpress'
const NOTIFICATION_PAYLOAD = sdk.z.object({
list_folder: sdk.z.object({
accounts: sdk.z.array(sdk.z.string()),
}),
})
export const isFileChangeNotification = (props: bp.HandlerProps) =>
props.req.method.toUpperCase() === 'POST' &&
props.req.body &&
NOTIFICATION_PAYLOAD.safeParse(JSON.parse(props.req.body)).success
export const handleFileChangeEvent: bp.IntegrationProps['handler'] = async (props) => {
const payload: sdk.z.infer<typeof NOTIFICATION_PAYLOAD> = JSON.parse(props.req.body!)
const dropboxClient = await DropboxClient.create(props)
const accountId = dropboxClient.getAccountId()
if (!payload.list_folder.accounts.includes(accountId)) {
// If the account ID is not in the list of accounts, we can ignore this notification
return
}
const { syncCursor: prevSyncCursor, fileTree } = await _getSyncState(props)
const { newSyncCursor } = await _collectChangesAndBroadcast(props, prevSyncCursor, fileTree)
await _updateSyncState(props, newSyncCursor, fileTree)
}
const _getSyncState = async (props: bp.HandlerProps): Promise<{ syncCursor?: string; fileTree: FileTree }> => {
const { state } = await props.client.getOrSetState({
id: props.ctx.integrationId,
type: 'integration',
name: 'realTimeSync',
payload: {
syncCursor: '',
fileTreeJson: '[]',
},
})
return { syncCursor: state.payload.syncCursor || undefined, fileTree: FileTree.fromJSON(state.payload.fileTreeJson) }
}
const _updateSyncState = async (props: bp.HandlerProps, syncCursor: string, fileTree: FileTree) => {
await props.client.setState({
id: props.ctx.integrationId,
type: 'integration',
name: 'realTimeSync',
payload: {
syncCursor,
fileTreeJson: fileTree.toJSON(),
},
})
}
const _collectChangesAndBroadcast = async (
props: bp.HandlerProps,
prevSyncCursor: string | undefined,
fileTree: FileTree
) => {
const { items: modifiedItems, newSyncCursor } = await _getModifiedItems(props, prevSyncCursor)
const diff = fileTree.pushItems(modifiedItems)
const integrationSetupDate = await _getIntegrationSetupDate(props)
const isFileModifiedAfterSetup = (item: FileEntity.InferredType | FolderEntity.InferredType) =>
item.itemType === 'file' &&
// If it is the first sync (prevSyncCursor is undefined), omit files that
// were created/modified before the integration was set up:
(prevSyncCursor ? true : !item.modifiedAt || new Date(item.modifiedAt) > integrationSetupDate)
await _broadcastChanges(props, {
deleted: diff.deleted,
updated: diff.updated.filter(isFileModifiedAfterSetup),
added: diff.added.filter(isFileModifiedAfterSetup),
})
return { newSyncCursor }
}
const _getModifiedItems = async (props: bp.HandlerProps, prevSyncCursor: string | undefined) => {
const dropboxClient = await DropboxClient.create(props)
let currentSyncCursor: string | undefined = prevSyncCursor
let hasMore: boolean = false
const items: (FileEntity.InferredType | FolderEntity.InferredType | DeletedEntity.InferredType)[] = []
do {
const itemBatch = await dropboxClient.listItemsInFolder({ path: '', recursive: true, nextToken: currentSyncCursor })
items.push(...itemBatch.items)
currentSyncCursor = itemBatch.nextToken
hasMore = itemBatch.hasMore
} while (hasMore)
return {
newSyncCursor: currentSyncCursor,
items,
}
}
const _getIntegrationSetupDate = async (props: bp.HandlerProps): Promise<Date> => {
try {
const { state } = await props.client.getState({
type: 'integration',
id: props.ctx.integrationId,
name: 'setupMeta',
})
return new Date(state.payload.integrationRegisteredAt)
} catch {
return new Date(0)
}
}
const _broadcastChanges = async (props: bp.HandlerProps, fileTreeDiff: FileTreeDiff) => {
for (const diffBatch of _getDiffBatches(fileTreeDiff)) {
await props.client.createEvent({
type: 'aggregateFileChanges',
payload: {
modifiedItems: {
created: (diffBatch.added as FileEntity.InferredType[]).map(filesReadonlyMapping.mapFile),
updated: (diffBatch.updated as FileEntity.InferredType[]).map(filesReadonlyMapping.mapFile),
deleted: diffBatch.deleted.map((item) =>
item.itemType === 'file' ? filesReadonlyMapping.mapFile(item) : filesReadonlyMapping.mapFolder(item)
),
},
},
})
}
}
const _getDiffBatches = function* ({ added, updated, deleted }: FileTreeDiff) {
const MAX_BATCH_SIZE = 50
let addedCursor = 0
let updatedCursor = 0
let deletedCursor = 0
// Continue until all arrays are exhausted:
while (addedCursor < added.length || updatedCursor < updated.length || deletedCursor < deleted.length) {
const currentBatch: FileTreeDiff = { added: [], deleted: [], updated: [] }
for (let currentBatchSize = 0; currentBatchSize < MAX_BATCH_SIZE; ) {
const startSize = currentBatchSize
if (addedCursor < added.length && currentBatchSize < MAX_BATCH_SIZE) {
currentBatch.added.push(added[addedCursor++]!)
currentBatchSize++
}
if (updatedCursor < updated.length && currentBatchSize < MAX_BATCH_SIZE) {
currentBatch.updated.push(updated[updatedCursor++]!)
currentBatchSize++
}
if (deletedCursor < deleted.length && currentBatchSize < MAX_BATCH_SIZE) {
currentBatch.deleted.push(deleted[deletedCursor++]!)
currentBatchSize++
}
// If no item was added, the array is exhausted:
if (currentBatchSize === startSize) {
break
}
}
yield currentBatch
}
}
@@ -0,0 +1,22 @@
import * as bp from '.botpress'
export const isWebhookVerificationRequest = (props: bp.HandlerProps) => {
const searchParams = new URLSearchParams(props.req.query)
return props.req.method === 'GET' && searchParams.has('challenge')
}
export const handleWebhookVerificationRequest: bp.IntegrationProps['handler'] = async ({ req }) => {
const searchParams = new URLSearchParams(req.query)
const challenge = searchParams.get('challenge')
if (!challenge) {
return
}
return {
headers: { 'Content-Type': 'text/plain', 'X-Content-Type-Options': 'nosniff' },
status: 200,
body: challenge,
}
}
@@ -0,0 +1 @@
export * from './handler-dispatcher'
@@ -0,0 +1,22 @@
import { isOAuthWizardUrl, getInterstitialUrl, generateRedirection } from '@botpress/common/src/oauth-wizard'
import * as wizard from './wizard'
import * as bp from '.botpress'
export const oauthCallbackHandler: bp.IntegrationProps['handler'] = async (props) => {
const { req, logger } = props
if (!isOAuthWizardUrl(req.path)) {
return {
status: 404,
body: 'Invalid OAuth endpoint',
}
}
try {
return await wizard.handler(props)
} catch (thrown: unknown) {
const error = thrown instanceof Error ? thrown : Error(String(thrown))
const errorMessage = 'OAuth registration Error: ' + error.message
logger.forBot().error(errorMessage)
return generateRedirection(getInterstitialUrl(false, errorMessage))
}
}
@@ -0,0 +1,145 @@
import * as oauthWizard from '@botpress/common/src/oauth-wizard'
import { DropboxOAuthClient, getOAuthClientId } from '../../dropbox-api/oauth-client'
import * as bp from '.botpress'
type WizardHandler = oauthWizard.WizardStepHandler<bp.HandlerProps>
export const handler = async (props: bp.HandlerProps) => {
const wizard = new oauthWizard.OAuthWizardBuilder(props)
.addStep({ id: 'start-confirm', handler: _startHandler })
.addStep({ id: 'redirect-to-dropbox', handler: _redirectToDropboxHandler })
.addStep({ id: 'oauth-callback', handler: _oauthCallbackHandler })
.addStep({ id: 'end', handler: _endHandler })
.build()
const response = await wizard.handleRequest()
return response
}
const _startHandler: WizardHandler = async (props) => {
const { responses } = props
// When nothing is connected yet there's nothing to reset, so skip the
// confirmation and go straight to authorizing with Dropbox.
if (!(await _isAlreadyConnected(props))) {
return _redirectToDropboxHandler(props)
}
return responses.displayButtons({
pageTitle: 'Reset Configuration',
htmlOrMarkdownPageContents:
'This wizard will reset your configuration, so the bot will stop working on Dropbox until a new configuration is put in place, continue?',
buttons: [
{
action: 'navigate',
label: 'Yes',
navigateToStep: 'redirect-to-dropbox',
buttonType: 'primary',
},
{
action: 'close',
label: 'No',
buttonType: 'secondary',
},
],
})
}
const _isAlreadyConnected = async ({ client, ctx }: bp.HandlerProps): Promise<boolean> => {
try {
const result = await client.getState({
id: ctx.integrationId,
type: 'integration',
name: 'authorization',
})
return Boolean(result?.state?.payload?.refreshToken)
} catch {
return false
}
}
const _redirectToDropboxHandler: WizardHandler = async (props) => {
const { responses, ctx } = props
const clientId = getOAuthClientId({ ctx })
if (!clientId) {
return responses.endWizard({
success: false,
errorMessage: 'Dropbox App Key (APP_KEY) is not configured. Please configure it in the integration settings.',
})
}
const redirectUri = _getOAuthRedirectUri()
const dropboxAuthUrl =
'https://www.dropbox.com/oauth2/authorize?' +
'response_type=code' +
'&token_access_type=offline' +
`&client_id=${encodeURIComponent(clientId)}` +
`&redirect_uri=${encodeURIComponent(redirectUri)}` +
`&state=${encodeURIComponent(ctx.webhookId)}`
return responses.redirectToExternalUrl(dropboxAuthUrl)
}
const _getOAuthRedirectUri = (ctx?: bp.Context) => oauthWizard.getWizardStepUrl('oauth-callback', ctx).toString()
const _oauthCallbackHandler: WizardHandler = async (props) => {
const { responses, query, client, ctx, logger, setIntegrationIdentifier } = props
const oauthError = query.get('error')
const oauthErrorDescription = query.get('error_description')
if (oauthError) {
const errorMessage = oauthErrorDescription
? `OAuth error: ${oauthError} - ${oauthErrorDescription}`
: `OAuth error: ${oauthError}`
logger.forBot().warn(errorMessage)
return responses.endWizard({
success: false,
errorMessage,
})
}
const authorizationCode = query.get('code')
if (!authorizationCode) {
const errorMessage =
'No authorization code received from Dropbox. ' +
'This may happen if you denied the authorization request, ' +
'if the authorization code expired, or if there was an error during the OAuth flow. ' +
'Please try authorizing again.'
logger.forBot().warn(errorMessage)
return responses.endWizard({
success: false,
errorMessage,
})
}
try {
const redirectUri = _getOAuthRedirectUri()
const oauthClient = new DropboxOAuthClient({ client, ctx })
await oauthClient.processAuthorizationCode(authorizationCode, redirectUri)
logger.forBot().info('Successfully exchanged authorization code for refresh token')
setIntegrationIdentifier(ctx.webhookId)
return responses.endWizard({
success: true,
})
} catch (error) {
const errorMessage =
error instanceof Error
? `Failed to process authorization code: ${error.message}. Please make sure the code is correct and hasn't expired.`
: 'Failed to process authorization code. Please try again.'
logger.forBot().error({ err: error }, errorMessage)
return responses.endWizard({
success: false,
errorMessage,
})
}
}
const _endHandler: WizardHandler = ({ responses }) => {
return responses.endWizard({
success: true,
})
}