项目文件夹

文件
2026-07-13 13:34:48 +08:00

126 行
4.3 KiB
TypeScript

import sdk from '@botpress/sdk'
import * as crypto from 'crypto'
import { TodoistClient } from '../todoist-api'
import { handleCommentAddedEvent, isCommentAddedEvent } from './handlers/comment-added'
import { oauthCallbackHandler } from './handlers/oauth-callback'
import { handlePriorityChangedEvent, isPriorityChangedEvent } from './handlers/priority-changed'
import { handleTaskCompletedEvent, isTaskCompletedEvent } from './handlers/task-completed'
import { handleTaskCreatedEvent, isTaskCreatedEvent } from './handlers/task-created'
import { eventSchema, Event } from './schemas'
import * as bp from '.botpress'
export type WebhookEventHandler<T extends Event> = (
props: bp.HandlerProps & { event: T; initiatorUserId: string }
) => Promise<void> | void
type WebhookEventHandlerEntry<T extends Event> = Readonly<[(event: Event) => event is T, WebhookEventHandler<T>]>
const EVENT_HANDLERS: Readonly<WebhookEventHandlerEntry<any>[]> = [
[isCommentAddedEvent, handleCommentAddedEvent],
[isTaskCreatedEvent, handleTaskCreatedEvent],
[isPriorityChangedEvent, handlePriorityChangedEvent],
[isTaskCompletedEvent, handleTaskCompletedEvent],
] as const
export const handler: bp.IntegrationProps['handler'] = async (props: bp.HandlerProps) => {
const { logger, req } = props
if (props.req.path.startsWith('/oauth')) {
logger.forBot().info('Handling Todoist OAuth callback')
return await oauthCallbackHandler(props)
}
if (!req.body) {
return {}
}
console.debug(req)
if (props.ctx.configurationType !== 'apiToken') {
await _ensureWebhookIsAuthenticated(props)
}
await _dispatchEvent(props)
return {}
}
const _ensureWebhookIsAuthenticated = async ({ req }: bp.HandlerProps) => {
// Calculating the checksum is computationally expensive, so we first check
// whether a specific string (the "shared secret") is present in the query
// parameters to short-circuit the validation process.
if (!_isSharedSecretValid(req) || !_isChecksumValid(req)) {
throw new sdk.RuntimeError('Webhook request is not properly authenticated')
}
}
const _isSharedSecretValid = (req: sdk.Request) => {
const searchParams = new URLSearchParams(req.query)
const sharedSecret = searchParams.get('shared_secret')
return sharedSecret === bp.secrets.WEBHOOK_SHARED_SECRET
}
const _isChecksumValid = (req: sdk.Request) => {
const http_headers = new Headers(req.headers as Record<string, string>)
const todoist_checksum = http_headers.get('X-Todoist-Hmac-SHA256')
const actual_checksum = _computeRequestChecksum(req)
return todoist_checksum === actual_checksum
}
const _computeRequestChecksum = (req: sdk.Request) =>
crypto
.createHmac('sha256', bp.secrets.CLIENT_SECRET)
.update(req.body ?? '')
.digest('base64')
const _dispatchEvent = async (props: bp.HandlerProps) => {
const event = _getEventData(props)
if (await _isEventFromBot(props, event)) {
return
}
await _handleEventWithMatchingHandler(props, event)
}
const _isEventFromBot = async (props: bp.HandlerProps, event: Event): Promise<boolean> => {
const { user: botUser } = await props.client.getUser({ id: props.ctx.botUserId })
return botUser.id === event.initiator.id
}
const _handleEventWithMatchingHandler = async (props: bp.HandlerProps, event: Event): Promise<void> => {
const matchingHandler = _findMatchingHandler(props, event)
if (!matchingHandler) {
console.warn('Unsupported todoist event', event)
return
}
const initiator = await _getEventInitiator({ client: props.client, event })
await matchingHandler({ ...props, event, initiatorUserId: initiator.id })
}
const _findMatchingHandler = (props: bp.HandlerProps, event: Event) => {
for (const [eventGuard, eventHandler] of EVENT_HANDLERS) {
if (eventGuard(event)) {
props.logger.forBot().debug(`Event matched with ${eventGuard.name}: firing handler ${eventHandler.name}`)
return eventHandler
}
}
return null
}
const _getEventData = ({ req }: bp.HandlerProps) => eventSchema.parse(JSON.parse(req.body ?? ''))
const _getEventInitiator = async ({ client, event }: { client: bp.Client; event: Event }) => {
const { user } = await client.getOrCreateUser({
name: event.initiator.full_name,
pictureUrl: event.initiator.image_id
? TodoistClient.getUserAvatarUrl({ imageId: event.initiator.image_id })
: undefined,
tags: { id: event.initiator.id },
})
return user
}