chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
import { getOrCreateConversation } from './proactive-conversation'
|
||||
import { getOrCreateUser } from './proactive-user'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
export const actions = {
|
||||
getOrCreateConversation,
|
||||
getOrCreateUser,
|
||||
} satisfies bp.IntegrationProps['actions']
|
||||
@@ -0,0 +1,21 @@
|
||||
import { RuntimeError } from '@botpress/sdk'
|
||||
import { getAuthenticatedIntercomClient } from '../auth'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
export const getOrCreateConversation: bp.IntegrationProps['actions']['getOrCreateConversation'] = async (props) => {
|
||||
const { client, ctx, input } = props
|
||||
const { id: conversationId } = input.conversation
|
||||
if (!conversationId) {
|
||||
throw new RuntimeError('Conversation ID is required')
|
||||
}
|
||||
|
||||
const intercomClient = await getAuthenticatedIntercomClient(client, ctx)
|
||||
const chat = await intercomClient.conversations.find({ id: conversationId })
|
||||
|
||||
const { conversation } = await client.getOrCreateConversation({
|
||||
channel: 'channel',
|
||||
tags: { id: chat.id },
|
||||
})
|
||||
|
||||
return { conversationId: conversation.id }
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { RuntimeError } from '@botpress/sdk'
|
||||
import { getAuthenticatedIntercomClient } from '../auth'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
export const getOrCreateUser: bp.IntegrationProps['actions']['getOrCreateUser'] = async (props) => {
|
||||
const { client, ctx, input } = props
|
||||
const { id: userId } = input.user
|
||||
if (!userId) {
|
||||
throw new RuntimeError('User ID is required')
|
||||
}
|
||||
|
||||
const intercomClient = await getAuthenticatedIntercomClient(client, ctx)
|
||||
const { id, email } = await intercomClient.contacts.find({ id: userId })
|
||||
if (!id || !email) {
|
||||
throw new RuntimeError('User not found')
|
||||
}
|
||||
|
||||
const { user } = await client.getOrCreateUser({ tags: { id, email }, discriminateByTags: ['id'] })
|
||||
return { userId: user.id }
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import { generateRedirection, getInterstitialUrl } from '@botpress/common/src/oauth-wizard/interstitial'
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import { RuntimeError, z } from '@botpress/sdk'
|
||||
import axios from 'axios'
|
||||
import { Client as IntercomClient } from 'intercom-client'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
export const getAuthenticatedIntercomClient = async (client: bp.Client, ctx: bp.Context): Promise<IntercomClient> => {
|
||||
// TODO: Change null for 'manual' once the Intercom app is approved
|
||||
if (ctx.configurationType === null) {
|
||||
return new IntercomClient({ tokenAuth: { token: ctx.configuration.accessToken } })
|
||||
}
|
||||
|
||||
const { state } = await client.getState({
|
||||
id: ctx.integrationId,
|
||||
name: 'credentials',
|
||||
type: 'integration',
|
||||
})
|
||||
const { accessToken } = state.payload
|
||||
return new IntercomClient({ tokenAuth: { token: accessToken } })
|
||||
}
|
||||
|
||||
const saveAuthCredentials = async (
|
||||
client: bp.Client,
|
||||
ctx: bp.Context,
|
||||
{ accessToken, adminId }: { accessToken: string; adminId: string }
|
||||
): Promise<void> => {
|
||||
await client.setState({
|
||||
id: ctx.integrationId,
|
||||
name: 'credentials',
|
||||
type: 'integration',
|
||||
payload: { accessToken, adminId },
|
||||
})
|
||||
}
|
||||
|
||||
const exchangeCodeForAccessToken = async (code: string): Promise<string> => {
|
||||
const response = await axios.post('https://api.intercom.io/auth/eagle/token', {
|
||||
code,
|
||||
client_id: bp.secrets.CLIENT_ID,
|
||||
client_secret: bp.secrets.CLIENT_SECRET,
|
||||
})
|
||||
const responseSchema = z.object({
|
||||
access_token: z.string(),
|
||||
})
|
||||
const accessToken = responseSchema.safeParse(response.data).data?.access_token
|
||||
if (!accessToken) {
|
||||
throw new RuntimeError('Failed to exchange code for access token')
|
||||
}
|
||||
return accessToken
|
||||
}
|
||||
|
||||
export const getSignatureSecret = (ctx: bp.Context): string | undefined => {
|
||||
// TODO: Change null for 'manual' once the Intercom app is approved
|
||||
if (ctx.configurationType === null) {
|
||||
return ctx.configuration.clientSecret
|
||||
}
|
||||
return bp.secrets.CLIENT_SECRET
|
||||
}
|
||||
|
||||
export const handleOAuth = async ({ client, ctx, req, logger }: bp.HandlerProps): Promise<sdk.Response> => {
|
||||
logger.forBot().info('Handling OAuth callback')
|
||||
|
||||
try {
|
||||
const searchParams = new URLSearchParams(req.query)
|
||||
const error = searchParams.get('error')
|
||||
if (error) {
|
||||
throw new Error(`${error} - ${searchParams.get('error_description') ?? ''}`)
|
||||
}
|
||||
|
||||
const code = searchParams.get('code')
|
||||
if (!code) {
|
||||
throw new Error('Authorization code not present in OAuth callback')
|
||||
}
|
||||
|
||||
const accessToken = await exchangeCodeForAccessToken(code)
|
||||
const adminId = await getAdminId(ctx)
|
||||
await saveAuthCredentials(client, ctx, { accessToken, adminId })
|
||||
await client.configureIntegration({ identifier: adminId })
|
||||
return generateRedirection(getInterstitialUrl(true))
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err)
|
||||
const errorMessage = 'OAuth error: ' + msg
|
||||
logger.forBot().error(errorMessage)
|
||||
return generateRedirection(getInterstitialUrl(false, errorMessage))
|
||||
}
|
||||
}
|
||||
|
||||
const responseSchema = z.object({
|
||||
type: z.string(),
|
||||
id: z.string(),
|
||||
})
|
||||
|
||||
export const getAdminId = async (ctx: bp.Context): Promise<string> => {
|
||||
const response = await axios.get('https://api.intercom.io/me', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${ctx.configuration.accessToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
const parsedResponse = responseSchema.safeParse(response.data)
|
||||
if (!parsedResponse.success) {
|
||||
throw new RuntimeError('Failed to parse admin ID from response')
|
||||
}
|
||||
return parsedResponse.data.id
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
import { RuntimeError } from '@botpress/client'
|
||||
import { ReplyToConversationMessageType } from 'intercom-client'
|
||||
import { getAuthenticatedIntercomClient } from './auth'
|
||||
import * as html from './html.utils'
|
||||
import * as types from './types'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
type Card = bp.channels.channel.card.Card
|
||||
type Location = bp.channels.channel.location.Location
|
||||
|
||||
export const channels: bp.IntegrationProps['channels'] = {
|
||||
channel: {
|
||||
messages: {
|
||||
text: async ({ payload, conversation, ack, client, ctx }) => {
|
||||
await sendMessage({
|
||||
body: payload.text,
|
||||
conversation,
|
||||
client,
|
||||
ctx,
|
||||
ack,
|
||||
})
|
||||
},
|
||||
image: async ({ payload, client, ctx, conversation, ack }) => {
|
||||
await sendMessage({
|
||||
body: '',
|
||||
conversation,
|
||||
client,
|
||||
ctx,
|
||||
ack,
|
||||
attachmentUrls: [payload.imageUrl],
|
||||
})
|
||||
},
|
||||
audio: async ({ client, ctx, conversation, ack, payload }) => {
|
||||
await sendMessage({
|
||||
body: '',
|
||||
conversation,
|
||||
client,
|
||||
ctx,
|
||||
ack,
|
||||
attachmentUrls: [payload.audioUrl],
|
||||
})
|
||||
},
|
||||
video: async ({ client, ctx, conversation, ack, payload }) => {
|
||||
await sendMessage({
|
||||
body: '',
|
||||
conversation,
|
||||
client,
|
||||
ctx,
|
||||
ack,
|
||||
attachmentUrls: [payload.videoUrl],
|
||||
})
|
||||
},
|
||||
file: async ({ client, ctx, conversation, ack, payload }) => {
|
||||
await sendMessage({
|
||||
body: '',
|
||||
conversation,
|
||||
client,
|
||||
ctx,
|
||||
ack,
|
||||
attachmentUrls: [payload.fileUrl],
|
||||
})
|
||||
},
|
||||
location: async ({ client, ctx, conversation, ack, payload }) => {
|
||||
await sendMessage({
|
||||
body: formatGoogleMapLink(payload),
|
||||
conversation,
|
||||
client,
|
||||
ctx,
|
||||
ack,
|
||||
})
|
||||
},
|
||||
carousel: async ({ client, ctx, conversation, ack, payload }) => {
|
||||
const carousel = payload.items.map((card) => createCard(card)).join('')
|
||||
|
||||
await sendMessage({
|
||||
body: carousel,
|
||||
conversation,
|
||||
client,
|
||||
ctx,
|
||||
ack,
|
||||
})
|
||||
},
|
||||
card: async ({ client, ctx, conversation, ack, payload }) => {
|
||||
await sendMessage({
|
||||
body: createCard(payload),
|
||||
conversation,
|
||||
client,
|
||||
ctx,
|
||||
ack,
|
||||
})
|
||||
},
|
||||
dropdown: async ({ client, ctx, conversation, ack, payload }) => {
|
||||
const choices = payload.options.map((choice) => html.li(choice.value))
|
||||
|
||||
const message = composeMessage(
|
||||
html.p(payload.text),
|
||||
html.p('Type one of the following options:'),
|
||||
choices.length > 0 ? html.ol(choices.join('')) : ''
|
||||
)
|
||||
|
||||
await sendMessage({
|
||||
body: message,
|
||||
conversation,
|
||||
client,
|
||||
ctx,
|
||||
ack,
|
||||
})
|
||||
},
|
||||
choice: async ({ client, ctx, conversation, ack, payload }) => {
|
||||
const choices = payload.options.map((choice) => html.li(choice.value))
|
||||
|
||||
const message = composeMessage(
|
||||
html.p(payload.text),
|
||||
html.p('Type one of the following options:'),
|
||||
choices.length > 0 ? html.ol(choices.join('')) : ''
|
||||
)
|
||||
|
||||
await sendMessage({
|
||||
body: message,
|
||||
conversation,
|
||||
client,
|
||||
ctx,
|
||||
ack,
|
||||
})
|
||||
},
|
||||
bloc: () => {
|
||||
throw new RuntimeError('Not implemented')
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
async function sendMessage(
|
||||
props: Pick<types.MessageHandlerProps, 'conversation' | 'client' | 'ctx' | 'ack'> & {
|
||||
body: string
|
||||
attachmentUrls?: string[]
|
||||
}
|
||||
) {
|
||||
const { body, attachmentUrls, client, ctx, conversation, ack } = props
|
||||
const { state } = await client.getState({ type: 'integration', name: 'credentials', id: ctx.integrationId })
|
||||
const { adminId } = state.payload
|
||||
|
||||
const intercomClient = await getAuthenticatedIntercomClient(client, ctx)
|
||||
|
||||
const {
|
||||
conversation_parts: { conversation_parts: conversationParts },
|
||||
} = await intercomClient.conversations.replyByIdAsAdmin({
|
||||
id: conversation.tags.id ?? '',
|
||||
adminId,
|
||||
messageType: ReplyToConversationMessageType.COMMENT,
|
||||
body,
|
||||
attachmentUrls,
|
||||
})
|
||||
|
||||
const lastMessageId = conversationParts.at(-1)?.id
|
||||
await ack({ tags: { id: lastMessageId } })
|
||||
}
|
||||
|
||||
function composeMessage(...parts: string[]) {
|
||||
return parts.join('')
|
||||
}
|
||||
|
||||
function createCard({ title, subtitle, imageUrl, actions }: Card) {
|
||||
const image = imageUrl ? html.img(imageUrl) : ''
|
||||
const text = html.b(title) + html.p(subtitle ? subtitle : '')
|
||||
|
||||
const links = actions.filter((item) => item.action === 'url').map((item) => html.li(html.a(item.value, item.label)))
|
||||
|
||||
const choices = actions
|
||||
.filter((item) => item.action !== 'url')
|
||||
.map((item) => html.li(item.value))
|
||||
.join('')
|
||||
|
||||
return composeMessage(
|
||||
image,
|
||||
text,
|
||||
links.length > 0 ? html.ul(links.join('')) : '',
|
||||
html.p('Type one of the following options:'),
|
||||
html.ol(choices)
|
||||
)
|
||||
}
|
||||
|
||||
function formatGoogleMapLink(payload: Location) {
|
||||
return `https://www.google.com/maps/search/?api=1&query=${payload.latitude},${payload.longitude}`
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
import { RuntimeError } from '@botpress/client'
|
||||
import { Request, z } from '@botpress/sdk'
|
||||
import * as crypto from 'crypto'
|
||||
import { getSignatureSecret, handleOAuth } from './auth'
|
||||
import * as html from './html.utils'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
type IntercomMessage = z.infer<typeof conversationSourceSchema>
|
||||
|
||||
type VerifyResult =
|
||||
| { result: 'success'; isError: false; parsedNotification: z.infer<typeof webhookNotificationSchema> }
|
||||
| { result: 'error'; isError: true; message: string }
|
||||
| { result: 'ignore'; isError: false; message?: string }
|
||||
|
||||
const conversationSourceSchema = z.object({
|
||||
id: z.string(),
|
||||
author: z.object({
|
||||
id: z.string(),
|
||||
email: z.string().nullable(),
|
||||
type: z.string(),
|
||||
}),
|
||||
body: z.string().nullable(),
|
||||
})
|
||||
|
||||
const conversationPartSchema = conversationSourceSchema.extend({
|
||||
type: z.literal('conversation_part'),
|
||||
})
|
||||
|
||||
const conversationSchema = z.object({
|
||||
type: z.literal('conversation'),
|
||||
admin_assignee_id: z
|
||||
.number()
|
||||
.nullable()
|
||||
.transform((val) => (val ? val.toString() : null)),
|
||||
id: z.string(),
|
||||
source: conversationSourceSchema,
|
||||
conversation_parts: z.object({
|
||||
conversation_parts: z.array(conversationPartSchema),
|
||||
}),
|
||||
})
|
||||
|
||||
const pingSchema = z.object({
|
||||
type: z.literal('ping'),
|
||||
})
|
||||
|
||||
const webhookNotificationSchema = z.object({
|
||||
type: z.literal('notification_event'),
|
||||
topic: z.string(),
|
||||
data: z.object({
|
||||
item: z.union([conversationSchema, pingSchema]),
|
||||
}),
|
||||
})
|
||||
|
||||
export const handler: bp.IntegrationProps['handler'] = async (props) => {
|
||||
console.info('Handler received request')
|
||||
|
||||
const { req, client, ctx } = props
|
||||
if (req.path.startsWith('/oauth')) {
|
||||
return await handleOAuth(props)
|
||||
}
|
||||
|
||||
const { state } = await client.getState({ type: 'integration', name: 'credentials', id: ctx.integrationId })
|
||||
const { adminId } = state.payload
|
||||
|
||||
const verifyResult = verifyRequest(req, ctx, adminId)
|
||||
if (verifyResult.isError) {
|
||||
throw new RuntimeError(`Invalid request received: ${verifyResult.message}`)
|
||||
} else if (verifyResult.result === 'ignore') {
|
||||
console.info(`Handler ignored request: ${verifyResult.message ?? 'Unknown reason'}`)
|
||||
return
|
||||
}
|
||||
|
||||
const notification = verifyResult.parsedNotification
|
||||
if (notification.data.item.type === 'ping') {
|
||||
console.info('Handler received a ping event')
|
||||
return
|
||||
}
|
||||
|
||||
const {
|
||||
topic,
|
||||
data: {
|
||||
item: {
|
||||
id: conversationId,
|
||||
conversation_parts: { conversation_parts },
|
||||
source: firstConversationPart,
|
||||
},
|
||||
},
|
||||
} = notification
|
||||
|
||||
const { conversation } = await client.getOrCreateConversation({
|
||||
channel: 'channel',
|
||||
tags: {
|
||||
id: conversationId,
|
||||
},
|
||||
})
|
||||
|
||||
// this uses the message payload from intercom to create the message in the bot
|
||||
const createMessage = async (intercomMessage: IntercomMessage) => {
|
||||
const {
|
||||
author: { id: authorId, email, type: authorType },
|
||||
body,
|
||||
id: messageId,
|
||||
} = intercomMessage
|
||||
|
||||
if (!messageId) {
|
||||
throw new Error('Handler received an empty message id')
|
||||
}
|
||||
|
||||
if (!body) {
|
||||
return // ignore no body messages
|
||||
}
|
||||
|
||||
if (authorType === 'bot') {
|
||||
console.info(`Handler received a bot message with id ${messageId}`)
|
||||
return // ignore bot messages
|
||||
}
|
||||
|
||||
const user = await getOrCreateUserAndUpdate(client, {
|
||||
id: authorId,
|
||||
email,
|
||||
})
|
||||
|
||||
await client.getOrCreateMessage({
|
||||
tags: { id: messageId },
|
||||
type: 'text',
|
||||
userId: user.id,
|
||||
conversationId: conversation.id,
|
||||
payload: { text: html.stripTags(body) },
|
||||
})
|
||||
}
|
||||
|
||||
if (topic === 'conversation.user.created') {
|
||||
await createMessage(firstConversationPart) // important, intercom keeps the first message in a separate object
|
||||
}
|
||||
|
||||
for (const part of conversation_parts) {
|
||||
await createMessage(part)
|
||||
}
|
||||
|
||||
console.info('Handler finished processing request')
|
||||
return
|
||||
}
|
||||
|
||||
function extractSignature(req: Request) {
|
||||
const signatureKv = req.headers['x-hub-signature']
|
||||
if (!signatureKv) {
|
||||
return undefined
|
||||
}
|
||||
const signature = signatureKv.split('=')[1]
|
||||
return signature
|
||||
}
|
||||
|
||||
function isSignatureValid(signature: string, body: string, secret: string) {
|
||||
const hash = crypto.createHmac('sha1', secret).update(body).digest('hex')
|
||||
return hash === signature
|
||||
}
|
||||
|
||||
function verifyRequest(req: Request, ctx: bp.Context, adminId: string): VerifyResult {
|
||||
if (!req.body) {
|
||||
return { result: 'error', isError: true, message: 'Handler received an empty body' }
|
||||
}
|
||||
const signature = extractSignature(req)
|
||||
const secret = getSignatureSecret(ctx)
|
||||
if (secret && (!signature || !isSignatureValid(signature, req.body, secret))) {
|
||||
return { result: 'error', isError: true, message: 'Handler received request with invalid signature' }
|
||||
}
|
||||
|
||||
let parsedJSON
|
||||
try {
|
||||
parsedJSON = JSON.parse(req.body)
|
||||
} catch {
|
||||
return { result: 'error', isError: true, message: 'Handler received an invalid JSON body' }
|
||||
}
|
||||
const parsedBody = webhookNotificationSchema.safeParse(parsedJSON)
|
||||
if (!parsedBody.success) {
|
||||
return { result: 'error', isError: true, message: `Handler received an invalid body: ${parsedBody.error}` }
|
||||
}
|
||||
|
||||
const parsedNotification = parsedBody.data
|
||||
if (parsedNotification.data.item.type === 'ping') {
|
||||
// No further validation for ping events
|
||||
return { result: 'success', isError: false, parsedNotification }
|
||||
}
|
||||
|
||||
const SUBSCRIBED_TOPICS = ['conversation.user.created', 'conversation.user.replied']
|
||||
if (!SUBSCRIBED_TOPICS.includes(parsedNotification.topic)) {
|
||||
return { result: 'ignore', isError: false, message: `Ignoring topic: ${parsedNotification.topic}` }
|
||||
}
|
||||
|
||||
if (adminId !== parsedNotification.data.item.admin_assignee_id) {
|
||||
// Ignore conversations not assigned to the bot
|
||||
return { result: 'ignore', isError: false, message: 'Ignoring conversations not assigned to the bot' }
|
||||
}
|
||||
|
||||
return { result: 'success', isError: false, parsedNotification }
|
||||
}
|
||||
|
||||
const getOrCreateUserAndUpdate = async (client: bp.Client, { id, email }: { id: string; email?: string | null }) => {
|
||||
let { user } = await client.getOrCreateUser({
|
||||
tags: { id },
|
||||
})
|
||||
if (email && email !== user.tags.email) {
|
||||
const updateResponse = await client.updateUser({
|
||||
id: user.id,
|
||||
tags: { email },
|
||||
})
|
||||
user = updateResponse.user
|
||||
}
|
||||
return user
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
export type HtmlTag = 'p' | 'b' | 'h1' | 'h2' | 'ul' | 'ol' | 'li' | 'pre' | 'code' | 'i' | 'em' | 'strong'
|
||||
|
||||
export function stripTags(str: string) {
|
||||
return str.replace(/<[^>]*>?/gm, '')
|
||||
}
|
||||
|
||||
export function ol(str: string) {
|
||||
return addHtmlTag(str, 'ol')
|
||||
}
|
||||
|
||||
export function li(str: string) {
|
||||
return addHtmlTag(str, 'li')
|
||||
}
|
||||
|
||||
export function ul(str: string) {
|
||||
return addHtmlTag(str, 'ul')
|
||||
}
|
||||
|
||||
export function p(str: string) {
|
||||
return addHtmlTag(str, 'p')
|
||||
}
|
||||
|
||||
export function b(str: string) {
|
||||
return addHtmlTag(str, 'b')
|
||||
}
|
||||
|
||||
export function h1(str: string) {
|
||||
return addHtmlTag(str, 'h1')
|
||||
}
|
||||
|
||||
export function h2(str: string) {
|
||||
return addHtmlTag(str, 'h2')
|
||||
}
|
||||
|
||||
export function img(src: string) {
|
||||
return `<img src="${src}" />`
|
||||
}
|
||||
|
||||
export function a(href: string, text: string) {
|
||||
return `<a href="${href}">${text}</a>`
|
||||
}
|
||||
|
||||
function addHtmlTag(str: string, tag: HtmlTag) {
|
||||
return `<${tag}>${str}</${tag}>`
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { reporting } from '@botpress/sdk-addons'
|
||||
import { actions } from './actions'
|
||||
import { getAdminId } from './auth'
|
||||
import { channels } from './channels'
|
||||
import { handler } from './handler'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
const integration = new bp.Integration({
|
||||
register: async ({ client, ctx }) => {
|
||||
const adminId = ctx.configuration.adminId || (await getAdminId(ctx))
|
||||
|
||||
await client.setState({
|
||||
id: ctx.integrationId,
|
||||
name: 'credentials',
|
||||
type: 'integration',
|
||||
payload: { adminId, accessToken: ctx.configuration.accessToken },
|
||||
})
|
||||
await client.updateUser({
|
||||
id: ctx.botUserId,
|
||||
tags: { id: adminId },
|
||||
})
|
||||
},
|
||||
unregister: async () => {},
|
||||
actions,
|
||||
channels,
|
||||
handler,
|
||||
})
|
||||
|
||||
export default reporting.wrapIntegration(integration)
|
||||
@@ -0,0 +1,22 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
export type Handler = bp.IntegrationProps['handler']
|
||||
export type HandlerProps = bp.HandlerProps
|
||||
export type IntegrationCtx = bp.Context
|
||||
export type Logger = bp.Logger
|
||||
|
||||
export type Client = bp.Client
|
||||
export type Conversation = bp.ClientResponses['getConversation']['conversation']
|
||||
export type Message = bp.ClientResponses['getMessage']['message']
|
||||
export type User = bp.ClientResponses['getUser']['user']
|
||||
export type Event = bp.ClientResponses['getEvent']['event']
|
||||
|
||||
export type EventDefinition = sdk.EventDefinition
|
||||
export type ActionDefinition = sdk.ActionDefinition
|
||||
export type ChannelDefinition = sdk.ChannelDefinition
|
||||
export type MessageDefinition = sdk.MessageDefinition
|
||||
|
||||
export type ActionProps = bp.AnyActionProps
|
||||
export type MessageHandlerProps = bp.AnyMessageProps
|
||||
export type AckFunction = bp.AnyAckFunction
|
||||
Reference in New Issue
Block a user