botpress--botpress
126 行
4.3 KiB
TypeScript
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
|
|
}
|