chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
import { z, isApiError } from '@botpress/sdk'
|
||||
import {
|
||||
TriggerSubscriber,
|
||||
ZapierTriggersStateName,
|
||||
ZapierTriggersStateSchema,
|
||||
ZapierTriggersState,
|
||||
Client,
|
||||
} from './types'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
export async function unsubscribeZapierHook(url: string, ctx: bp.Context, client: Client) {
|
||||
let subscribers = await getTriggerSubscribers(ctx, client)
|
||||
subscribers = subscribers.filter((x) => x.url !== url)
|
||||
await saveTriggerSubscribers(subscribers, ctx, client)
|
||||
console.info(`Zapier hook ${url} was unsubscribed`)
|
||||
}
|
||||
|
||||
export async function getTriggerSubscribers(ctx: bp.Context, client: Client) {
|
||||
const state = await getTriggersState(ctx, client)
|
||||
return state.subscribers
|
||||
}
|
||||
|
||||
export async function saveTriggerSubscribers(subscribers: TriggerSubscriber[], ctx: bp.Context, client: Client) {
|
||||
await client.setState({
|
||||
type: 'integration',
|
||||
name: ZapierTriggersStateName,
|
||||
id: ctx.integrationId,
|
||||
payload: buildTriggersState({ subscribers }),
|
||||
})
|
||||
}
|
||||
|
||||
export async function getTriggersState(ctx: bp.Context, client: Client) {
|
||||
const defaultState = buildTriggersState()
|
||||
|
||||
return await client
|
||||
.getState({
|
||||
type: 'integration',
|
||||
name: ZapierTriggersStateName,
|
||||
id: ctx.integrationId,
|
||||
})
|
||||
.then((res) => ZapierTriggersStateSchema.parse(res.state.payload))
|
||||
.catch((e) => {
|
||||
// TODO: Remove hard-coded "No State found" message check once the bridge client correctly receives the ResourceNotFoundError
|
||||
if ((isApiError(e) && e.type === 'ResourceNotFound') || e.message === 'No State found') {
|
||||
console.info("Zapier triggers state doesn't exist yet and will be initialized")
|
||||
return defaultState
|
||||
} else if (z.is.zuiError(e)) {
|
||||
console.warn(`Zapier triggers state will be reset as it's corrupted: ${e.message}`)
|
||||
return defaultState
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function buildTriggersState(partial?: Partial<ZapierTriggersState>) {
|
||||
return <ZapierTriggersState>{
|
||||
subscribers: [],
|
||||
...partial,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { reporting } from '@botpress/sdk-addons'
|
||||
import axios, { isAxiosError } from 'axios'
|
||||
import { constants } from 'http2'
|
||||
import { getTriggerSubscribers, saveTriggerSubscribers, unsubscribeZapierHook } from './helpers'
|
||||
import { Client, TriggerRequestBody, IntegrationEvent, IntegrationEventSchema, EventSchema, Event } from './types'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
const integration = new bp.Integration({
|
||||
register: async () => {},
|
||||
unregister: async () => {},
|
||||
channels: {},
|
||||
actions: {
|
||||
trigger: async ({ ctx, input, client, logger }) => {
|
||||
console.info(`Zapier trigger called with payload: ${JSON.stringify(input)}`)
|
||||
|
||||
const subscribers = await getTriggerSubscribers(ctx, client)
|
||||
|
||||
console.info(`Notifying ${subscribers.length} Zapier trigger REST hooks`)
|
||||
|
||||
for (const { url: zapierHookUrl } of subscribers) {
|
||||
const request: TriggerRequestBody = {
|
||||
botId: ctx.botId,
|
||||
data: input.data,
|
||||
correlationId: input.correlationId,
|
||||
}
|
||||
|
||||
await axios
|
||||
.post(zapierHookUrl, request)
|
||||
.then(() => {
|
||||
console.info(`Successfully notified Zapier trigger REST hook: ${zapierHookUrl}`)
|
||||
})
|
||||
.catch(async (e) => {
|
||||
logger.forBot().warn(`Failed to notify Zapier trigger REST hook: ${zapierHookUrl} (Error: ${e.message})`)
|
||||
console.warn(`Failed to notify Zapier trigger REST hook: ${zapierHookUrl} (Error: ${e.message})`)
|
||||
|
||||
if (isAxiosError(e) && e.response?.status === constants.HTTP_STATUS_GONE) {
|
||||
// Zapier REST hooks will send back a HTTP 410 Gone error if the hook is no longer valid.
|
||||
await unsubscribeZapierHook(zapierHookUrl, ctx, client)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {}
|
||||
},
|
||||
},
|
||||
handler: async ({ req, ctx, client }) => {
|
||||
if (!req.body) {
|
||||
console.warn('Event handler received an empty body')
|
||||
return
|
||||
}
|
||||
|
||||
const body = JSON.parse(req.body)
|
||||
|
||||
const integrationEventParse = IntegrationEventSchema.safeParse(body)
|
||||
if (integrationEventParse.success) {
|
||||
await handleIntegrationEvent(integrationEventParse.data, ctx, client)
|
||||
return
|
||||
}
|
||||
|
||||
const eventParse = EventSchema.safeParse(body)
|
||||
if (!eventParse.success) {
|
||||
console.warn(`Received invalid event: ${eventParse.error}`)
|
||||
return
|
||||
}
|
||||
|
||||
await handleEvent(eventParse.data, client)
|
||||
},
|
||||
})
|
||||
|
||||
export default reporting.wrapIntegration(integration)
|
||||
|
||||
async function handleIntegrationEvent(event: IntegrationEvent, ctx: bp.Context, client: Client) {
|
||||
console.info('Received integration event: ', event)
|
||||
|
||||
let subscribers = await getTriggerSubscribers(ctx, client)
|
||||
|
||||
if (event.action === 'subscribe:triggers') {
|
||||
subscribers.push({ url: event.url })
|
||||
|
||||
// Send a demo trigger call to the Zapier REST hook so the user can easily test it when setting up the bot trigger in their Zap.
|
||||
await axios
|
||||
.post(event.url, <TriggerRequestBody>{
|
||||
botId: ctx.botId,
|
||||
data: '{"message": "Hello from Botpress! This is an automated test message to confirm that your bot is now able to trigger this Zap."}',
|
||||
correlationId: '12345',
|
||||
})
|
||||
.then(() => {
|
||||
console.info(`Successfully sent demo trigger to Zapier REST hook: ${event.url}`)
|
||||
})
|
||||
.catch((e) => {
|
||||
console.warn(`Failed to send demo trigger to Zapier REST hook: ${event.url} (Error: ${e.message})`)
|
||||
})
|
||||
} else if (event.action === 'unsubscribe:triggers') {
|
||||
subscribers = subscribers.filter((x) => x.url !== event.url)
|
||||
}
|
||||
|
||||
await saveTriggerSubscribers(subscribers, ctx, client)
|
||||
|
||||
console.info(`Successfully updated trigger subscribers: ${JSON.stringify(subscribers)}`)
|
||||
}
|
||||
|
||||
export async function handleEvent(event: Event, client: Client) {
|
||||
await client.createEvent({
|
||||
type: 'event',
|
||||
payload: event,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { z } from '@botpress/sdk'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
export type Client = bp.Client
|
||||
|
||||
export const TriggerSubscriberSchema = z.object({
|
||||
url: z.string().title('URL').describe('The webhook URL of the subscriber'),
|
||||
})
|
||||
|
||||
export type TriggerSubscriber = z.infer<typeof TriggerSubscriberSchema>
|
||||
|
||||
export const ZapierTriggersStateName = 'triggers' as const
|
||||
|
||||
export const ZapierTriggersStateSchema = z.object({
|
||||
subscribers: z.array(TriggerSubscriberSchema).describe('The subscribers').title('Subscribers'),
|
||||
})
|
||||
|
||||
export type ZapierTriggersState = z.infer<typeof ZapierTriggersStateSchema>
|
||||
|
||||
export const IntegrationEventSchema = z.object({
|
||||
action: z.enum(['subscribe:triggers', 'unsubscribe:triggers']),
|
||||
url: z.string(),
|
||||
})
|
||||
|
||||
export type IntegrationEvent = z.infer<typeof IntegrationEventSchema>
|
||||
|
||||
export const TriggerSchema = z.object({
|
||||
data: z
|
||||
.string()
|
||||
.placeholder('{ "message": "Hello Zapier!" }')
|
||||
.describe('The data you want to send to Zapier trigger. Can be any string including JSON.')
|
||||
.title('Trigger Data to send to Zapier'),
|
||||
correlationId: z
|
||||
.string()
|
||||
.title('Correlation ID')
|
||||
.describe('Can be used to receive a response back from Zapier by listening for an `event.correlationId`')
|
||||
.optional(),
|
||||
})
|
||||
|
||||
export type Trigger = z.infer<typeof TriggerSchema>
|
||||
|
||||
export type TriggerRequestBody = Trigger & {
|
||||
botId: string
|
||||
}
|
||||
|
||||
export const EventSchema = z.object({
|
||||
data: z
|
||||
.string()
|
||||
.placeholder('{ "message": "Hello Botpress!" }')
|
||||
.describe('The data the Zapier action sent. Can be any string including JSON.')
|
||||
.title('Event Data received from Zapier'),
|
||||
correlationId: z
|
||||
.string()
|
||||
.title('Correlation ID')
|
||||
.describe('Can be used to correlate the response from Zapier when used with an `action.correlationId`')
|
||||
.optional(),
|
||||
})
|
||||
|
||||
export type Event = z.infer<typeof EventSchema>
|
||||
Reference in New Issue
Block a user