diff --git a/integrations/sunco/integration.definition.ts b/integrations/sunco/integration.definition.ts index f56ef9a8a23..08ce74e5da2 100644 --- a/integrations/sunco/integration.definition.ts +++ b/integrations/sunco/integration.definition.ts @@ -7,7 +7,7 @@ import { events } from './definitions' export default new IntegrationDefinition({ name: 'sunco', - version: '1.3.0', + version: '1.4.0', title: 'Sunshine Conversations', description: 'Give your bot access to a powerful omnichannel messaging platform.', icon: 'icon.svg', diff --git a/integrations/sunco/src/actions/get-or-create-conversation.ts b/integrations/sunco/src/actions/get-or-create-conversation.ts new file mode 100644 index 00000000000..78d589e7d75 --- /dev/null +++ b/integrations/sunco/src/actions/get-or-create-conversation.ts @@ -0,0 +1,23 @@ +import { createClient } from '../sunshine-api' +import * as bp from '.botpress' + +export const getOrCreateConversation: bp.IntegrationProps['actions']['getOrCreateConversation'] = async ({ + client, + input, + ctx, +}) => { + const suncoClient = createClient(ctx.configuration.keyId, ctx.configuration.keySecret) + const suncoConversation = await suncoClient.conversations.getConversation( + ctx.configuration.appId, + input.conversation.id + ) + + const { conversation } = await client.getOrCreateConversation({ + channel: 'channel', + tags: { id: `${suncoConversation.conversation?.id}` }, + }) + + return { + conversationId: conversation.id, + } +} diff --git a/integrations/sunco/src/actions/get-or-create-user.ts b/integrations/sunco/src/actions/get-or-create-user.ts new file mode 100644 index 00000000000..27e6750a413 --- /dev/null +++ b/integrations/sunco/src/actions/get-or-create-user.ts @@ -0,0 +1,19 @@ +import { createClient } from '../sunshine-api' +import * as bp from '.botpress' + +export const getOrCreateUser: bp.IntegrationProps['actions']['getOrCreateUser'] = async ({ client, input, ctx }) => { + const suncoClient = createClient(ctx.configuration.keyId, ctx.configuration.keySecret) + const suncoUser = await suncoClient.users.getUser(ctx.configuration.appId, input.user.id) + const suncoProfile = suncoUser.user?.profile + + const name = input.name ?? [suncoProfile?.givenName, suncoProfile?.surname].join(' ').trim() + const { user } = await client.getOrCreateUser({ + tags: { id: `${suncoUser.user?.id}` }, + name, + pictureUrl: input.pictureUrl ?? suncoProfile?.avatarUrl, + }) + + return { + userId: user.id, + } +} diff --git a/integrations/sunco/src/actions/index.ts b/integrations/sunco/src/actions/index.ts new file mode 100644 index 00000000000..97f0bb46d81 --- /dev/null +++ b/integrations/sunco/src/actions/index.ts @@ -0,0 +1,11 @@ +import { getOrCreateConversation } from './get-or-create-conversation' +import { getOrCreateUser } from './get-or-create-user' +import { startTypingIndicator, stopTypingIndicator } from './typing-indicator' +import * as bp from '.botpress' + +export const actions = { + startTypingIndicator, + stopTypingIndicator, + getOrCreateUser, + getOrCreateConversation, +} satisfies bp.IntegrationProps['actions'] diff --git a/integrations/sunco/src/actions/typing-indicator.ts b/integrations/sunco/src/actions/typing-indicator.ts new file mode 100644 index 00000000000..07e890fcf02 --- /dev/null +++ b/integrations/sunco/src/actions/typing-indicator.ts @@ -0,0 +1,59 @@ +import { createClient } from '../sunshine-api' +import { getConversationId } from '../util' +import * as bp from '.botpress' + +type SendActivityProps = Pick & { + conversationId: string + typingStatus?: 'start' | 'stop' + markAsRead?: boolean +} + +async function sendActivity({ client, ctx, conversationId, typingStatus, markAsRead }: SendActivityProps) { + const { conversation } = await client.getConversation({ id: conversationId }) + const suncoConversationId = getConversationId(conversation) + const { appId, keyId, keySecret } = ctx.configuration + const suncoClient = createClient(keyId, keySecret) + if (markAsRead) { + await suncoClient.activities.postActivity(appId, suncoConversationId, { + type: 'conversation:read', + author: { type: 'business' }, + }) + } + if (typingStatus) { + await suncoClient.activities.postActivity(appId, suncoConversationId, { + type: `typing:${typingStatus}`, + author: { type: 'business' }, + }) + } +} + +export const startTypingIndicator: bp.IntegrationProps['actions']['startTypingIndicator'] = async ({ + client, + ctx, + input, +}) => { + const { conversationId } = input + await sendActivity({ + client, + ctx, + conversationId, + typingStatus: 'start', + markAsRead: true, + }) + return {} +} + +export const stopTypingIndicator: bp.IntegrationProps['actions']['stopTypingIndicator'] = async ({ + client, + ctx, + input, +}) => { + const { conversationId } = input + await sendActivity({ + client, + ctx, + conversationId, + typingStatus: 'stop', + }) + return {} +} diff --git a/integrations/sunco/src/channels.ts b/integrations/sunco/src/channels.ts new file mode 100644 index 00000000000..e1bb9b1714a --- /dev/null +++ b/integrations/sunco/src/channels.ts @@ -0,0 +1,144 @@ +import { RuntimeError } from '@botpress/client' +import { Action, CarouselItem, MessageContent, PostMessageRequest, createClient } from './sunshine-api' +import { Carousel, Choice } from './types' +import { getConversationId } from './util' +import * as bp from '.botpress' + +export const channels = { + channel: { + messages: { + text: async (props) => { + await sendMessage(props, { type: 'text', text: props.payload.text }) + }, + image: async (props) => { + await sendMessage(props, { type: 'image', mediaUrl: props.payload.imageUrl }) + }, + markdown: async (props) => { + await sendMessage(props, { type: 'text', text: props.payload.markdown }) + }, + audio: async (props) => { + await sendMessage(props, { type: 'file', mediaUrl: props.payload.audioUrl }) + }, + video: async (props) => { + await sendMessage(props, { type: 'file', mediaUrl: props.payload.videoUrl }) + }, + file: async (props) => { + try { + await sendMessage(props, { type: 'file', mediaUrl: props.payload.fileUrl }) + } catch (e) { + const err = e as any + // 400 errors can be sent if file has unsupported type + // See: https://docs.smooch.io/guide/validating-files/#rejections + if (err.status === 400 && err.response?.text) { + console.info(err.response.text) + } + throw e + } + }, + location: async (props) => { + await sendMessage(props, { + type: 'location', + coordinates: { + lat: props.payload.latitude, + long: props.payload.longitude, + }, + }) + }, + carousel: async (props) => { + await sendCarousel(props, props.payload) + }, + card: async (props) => { + await sendCarousel(props, { items: [props.payload] }) + }, + dropdown: async (props) => { + await sendMessage(props, renderChoiceMessage(props.payload)) + }, + choice: async (props) => { + await sendMessage(props, renderChoiceMessage(props.payload)) + }, + bloc: () => { + throw new RuntimeError('Not implemented') + }, + }, + }, +} satisfies bp.IntegrationProps['channels'] + +const POSTBACK_PREFIX = 'postback::' +const SAY_PREFIX = 'say::' + +function renderChoiceMessage(payload: Choice): MessageContent { + return { + type: 'text', + text: payload.text, + actions: payload.options.map((r) => ({ type: 'reply' as const, text: r.label, payload: r.value })), + } +} + +type SendMessageProps = Pick + +async function sendMessage({ conversation, ctx, ack }: SendMessageProps, payload: MessageContent) { + const client = createClient(ctx.configuration.keyId, ctx.configuration.keySecret) + + const data: PostMessageRequest = { + author: { type: 'business' }, + content: payload, + } + + const { messages } = await client.messages.postMessage(ctx.configuration.appId, getConversationId(conversation), data) + + const message = messages?.[0] + + if (!message) { + throw new Error('Message not sent') + } + + await ack({ tags: { id: message.id } }) + + if (messages.length > 1) { + console.warn('More than one message was sent') + } +} + +const sendCarousel = async (props: SendMessageProps, payload: Carousel) => { + const items: CarouselItem[] = [] + + for (const card of payload.items) { + const actions: Action[] = [] + for (const button of card.actions) { + if (button.action === 'url') { + actions.push({ + type: 'link', + text: button.label, + uri: button.value, + }) + } else if (button.action === 'postback') { + actions.push({ + type: 'postback', + text: button.label, + payload: `${POSTBACK_PREFIX}${button.value}`, + }) + } else if (button.action === 'say') { + actions.push({ + type: 'postback', + text: button.label, + payload: `${SAY_PREFIX}${button.label}`, + }) + } + } + + if (actions.length === 0) { + actions.push({ + type: 'postback', + text: card.title, + payload: card.title, + }) + } + + items.push({ title: card.title, description: card.subtitle, mediaUrl: card.imageUrl, actions }) + } + + await sendMessage(props, { + type: 'carousel', + items, + }) +} diff --git a/integrations/sunco/src/index.ts b/integrations/sunco/src/index.ts index ec175b5aea8..1ac02299b67 100644 --- a/integrations/sunco/src/index.ts +++ b/integrations/sunco/src/index.ts @@ -1,160 +1,16 @@ -import { RuntimeError } from '@botpress/client' import { sentry as sentryHelpers } from '@botpress/sdk-addons' +import { actions } from './actions' +import { channels } from './channels' import { executeConversationCreated, handleConversationMessage } from './events' import { isSuncoWebhookPayload } from './messaging-events' import { register, unregister } from './setup' -import { createClient } from './sunshine-api' import * as bp from '.botpress' -const SunshineConversationsClient = require('sunshine-conversations-client') - -type SmoochBaseAction = { - type: string - text: string -} - -type SmoochLinkAction = { - type: 'link' - uri: string -} & SmoochBaseAction - -type SmoochPostbackAction = { - type: 'postback' - payload: string -} & SmoochBaseAction - -type SmoochReplyAction = { - type: 'reply' - payload: string -} & SmoochBaseAction - -type SmoochAction = SmoochLinkAction | SmoochPostbackAction | SmoochReplyAction - -type SmoochCard = { - title: string - description?: string - mediaUrl?: string - actions: SmoochAction[] -} - -const POSTBACK_PREFIX = 'postback::' -const SAY_PREFIX = 'say::' const integration = new bp.Integration({ register, unregister, - actions: { - startTypingIndicator: async ({ client, ctx, input }) => { - const { conversationId } = input - await sendActivity({ - client, - ctx, - conversationId, - typingStatus: 'start', - markAsRead: true, - }) - return {} - }, - stopTypingIndicator: async ({ client, ctx, input }) => { - const { conversationId } = input - await sendActivity({ - client, - ctx, - conversationId, - typingStatus: 'stop', - }) - return {} - }, - getOrCreateUser: async ({ client, input, ctx }) => { - const suncoClient = createClient(ctx.configuration.keyId, ctx.configuration.keySecret) - const suncoUser = await suncoClient.users.getUser(ctx.configuration.appId, input.user.id) - const suncoProfile = suncoUser.user?.profile - - const name = input.name ?? [suncoProfile?.givenName, suncoProfile?.surname].join(' ').trim() - const { user } = await client.getOrCreateUser({ - tags: { id: `${suncoUser.user?.id}` }, - name, - pictureUrl: input.pictureUrl ?? suncoProfile?.avatarUrl, - }) - - return { - userId: user.id, - } - }, - getOrCreateConversation: async ({ client, input, ctx }) => { - const suncoClient = createClient(ctx.configuration.keyId, ctx.configuration.keySecret) - const suncoConversation = await suncoClient.conversations.getConversation( - ctx.configuration.appId, - input.conversation.id - ) - - const { conversation } = await client.getOrCreateConversation({ - channel: 'channel', - tags: { id: `${suncoConversation.conversation?.id}` }, - }) - - return { - conversationId: conversation.id, - } - }, - }, - channels: { - channel: { - messages: { - text: async (props) => { - await sendMessage(props, { type: 'text', text: props.payload.text }) - }, - image: async (props) => { - await sendMessage(props, { type: 'image', mediaUrl: props.payload.imageUrl }) - }, - markdown: async (props) => { - await sendMessage(props, { type: 'text', text: props.payload.markdown }) - }, - audio: async (props) => { - await sendMessage(props, { type: 'file', mediaUrl: props.payload.audioUrl }) - }, - video: async (props) => { - await sendMessage(props, { type: 'file', mediaUrl: props.payload.videoUrl }) - }, - file: async (props) => { - try { - await sendMessage(props, { type: 'file', mediaUrl: props.payload.fileUrl }) - } catch (e) { - const err = e as any - // 400 errors can be sent if file has unsupported type - // See: https://docs.smooch.io/guide/validating-files/#rejections - if (err.status === 400 && err.response?.text) { - console.info(err.response.text) - } - throw e - } - }, - location: async (props) => { - await sendMessage(props, { - type: 'location', - coordinates: { - lat: props.payload.latitude, - long: props.payload.longitude, - }, - }) - }, - carousel: async (props) => { - await sendCarousel(props, props.payload) - }, - card: async (props) => { - await sendCarousel(props, { items: [props.payload] }) - }, - dropdown: async (props) => { - await sendMessage(props, renderChoiceMessage(props.payload)) - }, - choice: async (props) => { - await sendMessage(props, renderChoiceMessage(props.payload)) - }, - bloc: () => { - throw new RuntimeError('Not implemented') - }, - }, - }, - }, + actions, + channels, handler: async ({ req, client, logger }) => { if (!req.body) { console.warn('Handler received an empty body') @@ -185,119 +41,3 @@ export default sentryHelpers.wrapIntegration(integration, { environment: bp.secrets.SENTRY_ENVIRONMENT, release: bp.secrets.SENTRY_RELEASE, }) - -type Choice = bp.channels.channel.choice.Choice - -function renderChoiceMessage(payload: Choice) { - return { - type: 'text', - text: payload.text, - actions: payload.options.map((r) => ({ type: 'reply', text: r.label, payload: r.value })), - } -} - -type Carousel = bp.channels.channel.carousel.Carousel - -const sendCarousel = async (props: SendMessageProps, payload: Carousel) => { - const items: SmoochCard[] = [] - - for (const card of payload.items) { - const actions: SmoochAction[] = [] - for (const button of card.actions) { - if (button.action === 'url') { - actions.push({ - type: 'link', - text: button.label, - uri: button.value, - }) - } else if (button.action === 'postback') { - actions.push({ - type: 'postback', - text: button.label, - payload: `${POSTBACK_PREFIX}${button.value}`, - }) - } else if (button.action === 'say') { - actions.push({ - type: 'postback', - text: button.label, - payload: `${SAY_PREFIX}${button.label}`, - }) - } - } - - if (actions.length === 0) { - actions.push({ - type: 'postback', - text: card.title, - payload: card.title, - }) - } - - items.push({ title: card.title, description: card.subtitle, mediaUrl: card.imageUrl, actions }) - } - - await sendMessage(props, { - type: 'carousel', - items, - }) -} - -function getConversationId(conversation: SendMessageProps['conversation']) { - const conversationId = conversation.tags.id - - if (!conversationId) { - throw new Error('Conversation does not have a sunco identifier') - } - - return conversationId -} - -type SendMessageProps = Pick - -async function sendMessage({ conversation, ctx, ack }: SendMessageProps, payload: any) { - const client = createClient(ctx.configuration.keyId, ctx.configuration.keySecret) - - const data = new SunshineConversationsClient.MessagePost() - data.content = payload - data.author = { - type: 'business', - } - - const { messages } = await client.messages.postMessage(ctx.configuration.appId, getConversationId(conversation), data) - - const message = messages?.[0] - - if (!message) { - throw new Error('Message not sent') - } - - await ack({ tags: { id: message.id } }) - - if (messages.length > 1) { - console.warn('More than one message was sent') - } -} - -type SendActivityProps = Pick & { - conversationId: string - typingStatus?: 'start' | 'stop' - markAsRead?: boolean -} -async function sendActivity({ client, ctx, conversationId, typingStatus, markAsRead }: SendActivityProps) { - const { conversation } = await client.getConversation({ id: conversationId }) - const suncoConversationId = getConversationId(conversation) - const { appId, keyId, keySecret } = ctx.configuration - const suncoClient = createClient(keyId, keySecret) - if (markAsRead) { - await suncoClient.activities.postActivity(appId, suncoConversationId, { - type: 'conversation:read', - author: { type: 'business' }, - }) - } - if (typingStatus) { - await suncoClient.activities.postActivity(appId, suncoConversationId, { - type: `typing:${typingStatus}`, - author: { type: 'business' }, - }) - } -} diff --git a/integrations/sunco/src/sunshine-api.ts b/integrations/sunco/src/sunshine-api.ts index f05003ca01f..6e212ee1885 100644 --- a/integrations/sunco/src/sunshine-api.ts +++ b/integrations/sunco/src/sunshine-api.ts @@ -202,6 +202,7 @@ export type MessageAuthor = { export type TextMessageContent = { type: 'text' text: string + actions?: Action[] } export type ImageMessageContent = { @@ -226,9 +227,42 @@ export type LocationMessageContent = { } } +// ============================================================================ +// Action Types (for interactive messages like carousels) +// ============================================================================ + +export type ActionBase = { + type: string + text: string +} + +export type LinkAction = { + type: 'link' + uri: string +} & ActionBase + +export type PostbackAction = { + type: 'postback' + payload: string +} & ActionBase + +export type ReplyAction = { + type: 'reply' + payload: string +} & ActionBase + +export type Action = LinkAction | PostbackAction | ReplyAction + +export type CarouselItem = { + title: string + description?: string + mediaUrl?: string + actions: Action[] +} + export type CarouselMessageContent = { type: 'carousel' - items: unknown[] + items: CarouselItem[] } export type ListMessageContent = { diff --git a/integrations/sunco/src/types.ts b/integrations/sunco/src/types.ts index 86310bdc6b5..22125746308 100644 --- a/integrations/sunco/src/types.ts +++ b/integrations/sunco/src/types.ts @@ -23,3 +23,7 @@ export type AckFunction = bp.AnyAckFunction export type CreateMessageInput = Parameters[0] export type CreateMessageInputType = CreateMessageInput['type'] export type CreateMessageInputPayload = CreateMessageInput['payload'] + +// Channel message payload types +export type Choice = bp.channels.channel.choice.Choice +export type Carousel = bp.channels.channel.carousel.Carousel diff --git a/integrations/sunco/src/util.ts b/integrations/sunco/src/util.ts index 2d3ffb75cb8..98b01d78ba2 100644 --- a/integrations/sunco/src/util.ts +++ b/integrations/sunco/src/util.ts @@ -1,3 +1,13 @@ +export function getConversationId(conversation: { tags: { id?: string } }): string { + const conversationId = conversation.tags.id + + if (!conversationId) { + throw new Error('Conversation does not have a sunco identifier') + } + + return conversationId +} + export function isNetworkError(error: unknown): error is { status?: number body?: any diff --git a/integrations/trello/definitions/actions.ts b/integrations/trello/definitions/actions.ts deleted file mode 100644 index a38cf6f56b6..00000000000 --- a/integrations/trello/definitions/actions.ts +++ /dev/null @@ -1,234 +0,0 @@ -import { IntegrationDefinitionProps } from '@botpress/sdk' -import { - addCardCommentInputSchema, - addCardCommentOutputSchema, - createCardInputSchema, - createCardOutputSchema, - getAllBoardMembersInputSchema, - getAllBoardMembersOutputSchema, - getAllBoardsInputSchema, - getAllBoardsOutputSchema, - getBoardByIdInputSchema, - getBoardByIdOutputSchema, - getBoardMembersByDisplayNameInputSchema, - getBoardMembersByDisplayNameOutputSchema, - getBoardsByDisplayNameInputSchema, - getBoardsByDisplayNameOutputSchema, - getCardByIdInputSchema, - getCardByIdOutputSchema, - getCardsByDisplayNameInputSchema, - getCardsByDisplayNameOutputSchema, - getCardsInListInputSchema, - getCardsInListOutputSchema, - getListByIdInputSchema, - getListByIdOutputSchema, - getListsByDisplayNameInputSchema, - getListsByDisplayNameOutputSchema, - getListsInBoardInputSchema, - getListsInBoardOutputSchema, - getMemberByIdOrUsernameInputSchema, - getMemberByIdOrUsernameOutputSchema, - moveCardDownInputSchema, - moveCardDownOutputSchema, - moveCardToListInputSchema, - moveCardToListOutputSchema, - moveCardUpInputSchema, - moveCardUpOutputSchema, - updateCardInputSchema, - updateCardOutputSchema, - getAllCardMembersInputSchema, - getAllCardMembersOutputSchema, -} from './schemas' - -export const actions = { - getBoardsByDisplayName: { - title: 'Get boards by name', - description: 'Find all boards whose display name match this name', - input: { - schema: getBoardsByDisplayNameInputSchema, - }, - output: { - schema: getBoardsByDisplayNameOutputSchema, - }, - }, - getListsByDisplayName: { - title: 'Get lists by name', - description: 'Find all lists whose display name match this name', - input: { - schema: getListsByDisplayNameInputSchema, - }, - output: { - schema: getListsByDisplayNameOutputSchema, - }, - }, - getListById: { - title: 'Get list by ID', - description: 'Get a list by its unique identifier', - input: { - schema: getListByIdInputSchema, - }, - output: { - schema: getListByIdOutputSchema, - }, - }, - getCardsByDisplayName: { - title: 'Find cards by name name', - description: 'Find all lists whose display name match this name', - input: { - schema: getCardsByDisplayNameInputSchema, - }, - output: { - schema: getCardsByDisplayNameOutputSchema, - }, - }, - createCard: { - title: 'Create new card', - description: 'Create a card and add it to a list', - input: { - schema: createCardInputSchema, - }, - output: { - schema: createCardOutputSchema, - }, - }, - moveCardUp: { - title: 'Move card up', - description: 'Move a card n spaces up', - input: { - schema: moveCardUpInputSchema, - }, - output: { - schema: moveCardUpOutputSchema, - }, - }, - moveCardDown: { - title: 'Move card down', - description: 'Move a card n spaces down', - input: { - schema: moveCardDownInputSchema, - }, - output: { - schema: moveCardDownOutputSchema, - }, - }, - moveCardToList: { - title: 'Move card to another list', - description: 'Move a card to another list within the same board', - input: { - schema: moveCardToListInputSchema, - }, - output: { - schema: moveCardToListOutputSchema, - }, - }, - addCardComment: { - title: 'Add card comment', - description: 'Add a new comment to a card', - input: { - schema: addCardCommentInputSchema, - }, - output: { - schema: addCardCommentOutputSchema, - }, - }, - updateCard: { - title: 'Update card', - description: 'Update the details of a card', - input: { - schema: updateCardInputSchema, - }, - output: { - schema: updateCardOutputSchema, - }, - }, - getBoardMembersByDisplayName: { - title: 'Get members by name', - description: 'Find all members whose display name match this name', - input: { - schema: getBoardMembersByDisplayNameInputSchema, - }, - output: { - schema: getBoardMembersByDisplayNameOutputSchema, - }, - }, - getAllBoardMembers: { - title: 'Get all board members', - description: 'Get all members of a board', - input: { - schema: getAllBoardMembersInputSchema, - }, - output: { - schema: getAllBoardMembersOutputSchema, - }, - }, - getAllBoards: { - title: 'Get all boards', - description: 'Get all boards managed by the authenticated user', - input: { - schema: getAllBoardsInputSchema, - }, - output: { - schema: getAllBoardsOutputSchema, - }, - }, - getBoardById: { - title: 'Get board by ID', - description: 'Get a board by its unique identifier', - input: { - schema: getBoardByIdInputSchema, - }, - output: { - schema: getBoardByIdOutputSchema, - }, - }, - getListsInBoard: { - title: 'Get lists in board', - description: 'Get all lists in a board', - input: { - schema: getListsInBoardInputSchema, - }, - output: { - schema: getListsInBoardOutputSchema, - }, - }, - getCardsInList: { - title: 'Get cards in list', - description: 'Get all cards in a list', - input: { - schema: getCardsInListInputSchema, - }, - output: { - schema: getCardsInListOutputSchema, - }, - }, - getCardById: { - title: 'Get card by ID', - description: 'Get a card by its unique identifier', - input: { - schema: getCardByIdInputSchema, - }, - output: { - schema: getCardByIdOutputSchema, - }, - }, - getMemberByIdOrUsername: { - title: 'Get member by ID or username', - description: 'Get a member by their unique identifier or username', - input: { - schema: getMemberByIdOrUsernameInputSchema, - }, - output: { - schema: getMemberByIdOrUsernameOutputSchema, - }, - }, - getAllCardMembers: { - title: 'Get all card members', - description: 'Get all members of a card', - input: { - schema: getAllCardMembersInputSchema, - }, - output: { - schema: getAllCardMembersOutputSchema, - }, - }, -} as const satisfies NonNullable diff --git a/integrations/trello/definitions/actions/board-actions.ts b/integrations/trello/definitions/actions/board-actions.ts new file mode 100644 index 00000000000..379b73dace6 --- /dev/null +++ b/integrations/trello/definitions/actions/board-actions.ts @@ -0,0 +1,40 @@ +import { ActionDefinition, z } from '@botpress/sdk' +import { boardSchema } from 'definitions/schemas' +import { hasBoardId, noInput, outputsBoard, outputsBoards } from './common' + +export const getBoardById = { + title: 'Get board by ID', + description: 'Get a board by its unique identifier', + input: { + schema: hasBoardId.describe('Input schema for getting a board from its ID'), + }, + output: { + schema: outputsBoard.describe('Output schema for getting a board from its ID'), + }, +} as const satisfies ActionDefinition + +export const getBoardsByDisplayName = { + title: 'Get boards by name', + description: 'Find all boards whose display name match this name', + input: { + schema: z + .object({ + boardName: boardSchema.shape.name.title('Board Name').describe('Display name of the board'), + }) + .describe('Input schema for getting a board ID from its name'), + }, + output: { + schema: outputsBoards.describe('Output schema for getting a board from its name'), + }, +} as const satisfies ActionDefinition + +export const getAllBoards = { + title: 'Get all boards', + description: 'Get all boards managed by the authenticated user', + input: { + schema: noInput.describe('Input schema for getting all boards'), + }, + output: { + schema: outputsBoards.describe('Output schema for getting all boards'), + }, +} as const satisfies ActionDefinition diff --git a/integrations/trello/definitions/actions/card-actions.ts b/integrations/trello/definitions/actions/card-actions.ts new file mode 100644 index 00000000000..af9f511c795 --- /dev/null +++ b/integrations/trello/definitions/actions/card-actions.ts @@ -0,0 +1,216 @@ +import { ActionDefinition, z } from '@botpress/sdk' +import { cardSchema, listSchema, trelloIdSchema } from 'definitions/schemas' +import { hasCardId, hasListId, hasMessage, outputsCard, outputsCards } from './common' + +export const getCardById = { + title: 'Get card by ID', + description: 'Get a card by its unique identifier', + input: { + schema: hasCardId.describe('Input schema for getting a card from its ID'), + }, + output: { + schema: outputsCard.describe('Output schema for getting a card from its ID'), + }, +} as const satisfies ActionDefinition + +export const getCardsByDisplayName = { + title: 'Find cards by name name', + description: 'Find all lists whose display name match this name', + input: { + schema: hasListId + .extend({ + cardName: cardSchema.shape.name.title('Card Name').describe('Display name of the card'), + }) + .describe('Input schema for getting a card ID from its name'), + }, + output: { + schema: outputsCards.describe('Output schema for getting a card ID from its name'), + }, +} as const satisfies ActionDefinition + +export const getCardsInList = { + title: 'Get cards in list', + description: 'Get all cards in a list', + input: { + schema: hasListId.describe('Input schema for getting all cards in a list'), + }, + output: { + schema: outputsCards.describe('Output schema for getting all cards in a list'), + }, +} as const satisfies ActionDefinition + +export const createCard = { + title: 'Create new card', + description: 'Create a card and add it to a list', + input: { + schema: z + .object({ + listId: listSchema.shape.id.title('List ID').describe('ID of the list in which to insert the new card'), + cardName: cardSchema.shape.name.title('Card Name').describe('Name of the new card'), + cardBody: cardSchema.shape.description.optional().title('Card Body').describe('Body text of the new card'), + members: z + .array(trelloIdSchema) + .optional() + .title('Members') + .describe('Members to add to the card (Optional). This should be a list of member IDs.'), + labels: z + .array(trelloIdSchema) + .optional() + .title('Labels') + .describe('Labels to add to the card (Optional). This should be a list of label IDs.'), + dueDate: cardSchema.shape.dueDate + .optional() + .title('Due Date') + .describe('The due date of the card in ISO 8601 format (Optional).'), + }) + .describe('Input schema for creating a new card'), + }, + output: { + schema: hasMessage + .extend({ + newCardId: cardSchema.shape.id.describe('Unique identifier of the new card'), + }) + .describe('Output schema for creating a card'), + }, +} as const satisfies ActionDefinition + +export const updateCard = { + title: 'Update card', + description: 'Update the details of a card', + input: { + schema: hasCardId + .extend({ + name: cardSchema.shape.name + .optional() + .title('Name') + .describe('The name of the card (Optional) (e.g. "My Test Card"). Leave empty to keep the current name.'), + bodyText: cardSchema.shape.description + .optional() + .title('Body Text') + .describe('Body text of the new card (Optional). Leave empty to keep the current body.'), + closedState: z + .enum(['open', 'archived']) + .optional() + .title('Closed State') + .describe( + 'Whether the card should be archived (Optional). Enter "open", "archived" (without quotes), or leave empty to keep the previous status.' + ) + .optional(), + completeState: z + .enum(['complete', 'incomplete']) + .optional() + .title('State Completion') + .describe( + 'Whether the card should be marked as complete (Optional). Enter "complete", "incomplete" (without quotes), or leave empty to keep the previous status.' + ) + .optional(), + membersToAdd: z + .array(trelloIdSchema) + .optional() + .title('Members to Add') + .describe( + 'Members to add to the card (Optional). This should be a list of member IDs. Leave empty to keep the current members.' + ), + membersToRemove: z + .array(trelloIdSchema) + .optional() + .title('Members to Remove') + .describe( + 'Members to remove from the card (Optional). This should be a list of member IDs. Leave empty to keep the current members.' + ), + labelsToAdd: z + .array(trelloIdSchema) + .optional() + .title('Labels to Add') + .describe( + 'Labels to add to the card (Optional). This should be a list of label IDs. Leave empty to keep the current labels.' + ), + labelsToRemove: z + .array(trelloIdSchema) + .optional() + .title('Labels to Remove') + .describe( + 'Labels to remove from the card (Optional). This should be a list of label IDs. Leave empty to keep the current labels.' + ), + dueDate: cardSchema.shape.dueDate + .optional() + .title('Due Date') + .describe( + 'The due date of the card in ISO 8601 format (Optional). Leave empty to keep the current due date.' + ), + }) + .describe('Input schema for creating a new card'), + }, + output: { + schema: hasMessage.describe('Output schema for updating a card'), + }, +} as const satisfies ActionDefinition + +export const addCardComment = { + title: 'Add card comment', + description: 'Add a new comment to a card', + input: { + schema: z + .object({ + cardId: cardSchema.shape.id + .title('Card ID') + .describe('Unique identifier of the card to which a comment will be added'), + commentBody: z.string().title('Comment Body').describe('The body text of the comment'), + }) + .describe('Input schema for adding a comment to a card'), + }, + output: { + schema: hasMessage + .extend({ + newCommentId: trelloIdSchema.describe('Unique identifier of the newly created comment'), + }) + .describe('Output schema for adding a comment to a card'), + }, +} as const satisfies ActionDefinition + +const _moveByNSpacesSchema = z.number().min(1).optional().default(1) + +export const moveCardUp = { + title: 'Move card up', + description: 'Move a card n spaces up', + input: { + schema: hasCardId.extend({ + moveUpByNSpaces: _moveByNSpacesSchema + .title('Move Up By N Spaces') + .describe('Number of spaces by which to move the card up'), + }), + }, + output: { + schema: hasMessage.describe('Output schema for moving a card up'), + }, +} as const satisfies ActionDefinition + +export const moveCardDown = { + title: 'Move card down', + description: 'Move a card n spaces down', + input: { + schema: hasCardId.extend({ + moveDownByNSpaces: _moveByNSpacesSchema + .title('Move Down By N Spaces') + .describe('Number of spaces by which to move the card down'), + }), + }, + output: { + schema: hasMessage.describe('Output schema for moving a card down'), + }, +} as const satisfies ActionDefinition + +export const moveCardToList = { + title: 'Move card to another list', + description: 'Move a card to another list within the same board', + input: { + schema: hasCardId.extend({ + newListId: listSchema.shape.id + .title('New List ID') + .describe('Unique identifier of the list in which the card will be moved'), + }), + }, + output: { + schema: hasMessage.describe('Output schema for moving a card to a list'), + }, +} as const satisfies ActionDefinition diff --git a/integrations/trello/definitions/actions/common.ts b/integrations/trello/definitions/actions/common.ts new file mode 100644 index 00000000000..0fe66a97bc8 --- /dev/null +++ b/integrations/trello/definitions/actions/common.ts @@ -0,0 +1,54 @@ +import { z } from '@botpress/sdk' +import { boardSchema, listSchema, cardSchema, memberSchema } from 'definitions/schemas' + +// ==== Common Input Schemas ==== +export const noInput = z.object({}) + +export const hasBoardId = z.object({ + boardId: boardSchema.shape.id.title('Board ID').describe('Unique identifier of the board'), +}) + +export const hasListId = z.object({ + listId: listSchema.shape.id.title('List ID').describe('Unique identifier of the list'), +}) + +export const hasCardId = z.object({ + cardId: cardSchema.shape.id.title('Card ID').describe('Unique identifier of the card'), +}) + +// ==== Common Output Schemas ==== +export const hasMessage = z.object({ + message: z.string().describe('Output message'), +}) + +export const outputsMember = z.object({ + member: memberSchema.describe('The member object'), +}) + +export const outputsMembers = z.object({ + members: z.array(memberSchema).describe('Array of member objects'), +}) + +export const outputsCard = z.object({ + card: cardSchema.describe('The card object'), +}) + +export const outputsCards = z.object({ + cards: z.array(cardSchema).describe('Array of card objects'), +}) + +export const outputsList = z.object({ + list: listSchema.describe('The list object'), +}) + +export const outputsLists = z.object({ + lists: z.array(listSchema).describe('Array of list objects'), +}) + +export const outputsBoard = z.object({ + board: boardSchema.describe('The board object'), +}) + +export const outputsBoards = z.object({ + boards: z.array(boardSchema).describe('Array of board objects'), +}) diff --git a/integrations/trello/definitions/actions/index.ts b/integrations/trello/definitions/actions/index.ts new file mode 100644 index 00000000000..7b1b02e4707 --- /dev/null +++ b/integrations/trello/definitions/actions/index.ts @@ -0,0 +1,46 @@ +import { type IntegrationDefinitionProps } from '@botpress/sdk' +import { getAllBoards, getBoardById, getBoardsByDisplayName } from './board-actions' +import { + addCardComment, + createCard, + getCardById, + getCardsByDisplayName, + getCardsInList, + moveCardDown, + moveCardToList, + moveCardUp, + updateCard, +} from './card-actions' +import { getListById, getListsByDisplayName, getListsInBoard } from './list-actions' +import { + getAllBoardMembers, + getAllCardMembers, + getBoardMembersByDisplayName, + getMemberByIdOrUsername, +} from './member-actions' + +export const actions = { + // === Board Actions === + getBoardById, + getBoardsByDisplayName, + getAllBoards, + // === List Actions === + getListById, + getListsByDisplayName, + getListsInBoard, + // === Card Actions === + getCardById, + getCardsByDisplayName, + getCardsInList, + createCard, + updateCard, + addCardComment, + moveCardUp, + moveCardDown, + moveCardToList, + // === Member Actions === + getMemberByIdOrUsername, + getBoardMembersByDisplayName, + getAllBoardMembers, + getAllCardMembers, +} as const satisfies NonNullable diff --git a/integrations/trello/definitions/actions/list-actions.ts b/integrations/trello/definitions/actions/list-actions.ts new file mode 100644 index 00000000000..2554f7029c2 --- /dev/null +++ b/integrations/trello/definitions/actions/list-actions.ts @@ -0,0 +1,40 @@ +import { ActionDefinition } from '@botpress/sdk' +import { listSchema } from 'definitions/schemas' +import { hasBoardId, hasListId, outputsList, outputsLists } from './common' + +export const getListById = { + title: 'Get list by ID', + description: 'Get a list by its unique identifier', + input: { + schema: hasListId.describe('Input schema for getting a list from its ID'), + }, + output: { + schema: outputsList.describe('Output schema for getting a list from its ID'), + }, +} as const satisfies ActionDefinition + +export const getListsByDisplayName = { + title: 'Get lists by name', + description: 'Find all lists whose display name match this name', + input: { + schema: hasBoardId + .extend({ + listName: listSchema.shape.name.title('List Name').describe('Display name of the list'), + }) + .describe('Input schema for getting a list ID from its name'), + }, + output: { + schema: outputsLists.describe('Output schema for getting a list ID from its name'), + }, +} as const satisfies ActionDefinition + +export const getListsInBoard = { + title: 'Get lists in board', + description: 'Get all lists in a board', + input: { + schema: hasBoardId.describe('Input schema for getting all lists in a board'), + }, + output: { + schema: outputsLists.describe('Output schema for getting all lists in a board'), + }, +} as const satisfies ActionDefinition diff --git a/integrations/trello/definitions/actions/member-actions.ts b/integrations/trello/definitions/actions/member-actions.ts new file mode 100644 index 00000000000..224d3f772d5 --- /dev/null +++ b/integrations/trello/definitions/actions/member-actions.ts @@ -0,0 +1,58 @@ +import { ActionDefinition, z } from '@botpress/sdk' +import { boardSchema, memberSchema } from 'definitions/schemas' +import { hasBoardId, hasCardId, outputsMember, outputsMembers } from './common' + +export const getMemberByIdOrUsername = { + title: 'Get member by ID or username', + description: 'Get a member by their unique identifier or username', + input: { + schema: z + .object({ + memberIdOrUsername: z + .union([memberSchema.shape.id, memberSchema.shape.username]) + .title('Member ID or Username') + .describe('ID or username of the member to get'), + }) + .describe('Input schema for getting a member from its ID or username'), + }, + output: { + schema: outputsMember.describe('Output schema for getting a member by its ID or username'), + }, +} as const satisfies ActionDefinition + +export const getAllCardMembers = { + title: 'Get all card members', + description: 'Get all members of a card', + input: { + schema: hasCardId.describe('Input schema for getting all members of a card'), + }, + output: { + schema: outputsMembers.describe('Output schema for getting all members of a card'), + }, +} as const satisfies ActionDefinition + +export const getAllBoardMembers = { + title: 'Get all board members', + description: 'Get all members of a board', + input: { + schema: hasBoardId.describe('Input schema for getting all members of a board'), + }, + output: { + schema: outputsMembers.describe('Output schema for getting all members of a board'), + }, +} as const satisfies ActionDefinition + +export const getBoardMembersByDisplayName = { + title: 'Get members by name', + description: 'Find all members whose display name match this name', + input: { + schema: hasBoardId + .extend({ + displayName: boardSchema.shape.name.title('Display Name').describe('Display name of the member'), + }) + .describe('Input schema for getting a member from its name'), + }, + output: { + schema: outputsMembers.describe('Output schema for getting a member from its name'), + }, +} as const satisfies ActionDefinition diff --git a/integrations/trello/definitions/entities.ts b/integrations/trello/definitions/entities.ts index 9e1b22477a7..2af0786c5e9 100644 --- a/integrations/trello/definitions/entities.ts +++ b/integrations/trello/definitions/entities.ts @@ -1,30 +1,30 @@ -import { IntegrationDefinitionProps } from '@botpress/sdk' -import { BoardSchema, CardSchema, ListSchema, MemberSchema } from './schemas/entities' +import { type IntegrationDefinitionProps } from '@botpress/sdk' +import { boardSchema, cardSchema, listSchema, memberSchema } from './schemas' export const entities = { card: { title: 'Card', description: 'A card in a Trello list', - schema: CardSchema, + schema: cardSchema, }, list: { title: 'List', description: 'A list in a Trello board', - schema: ListSchema, + schema: listSchema, }, board: { title: 'Board', description: 'A Trello board', - schema: BoardSchema, + schema: boardSchema, }, boardMember: { title: 'Board Member', description: 'A member of a Trello board', - schema: MemberSchema, + schema: memberSchema, }, cardMember: { title: 'Card Member', description: 'A member assigned to a Trello card', - schema: MemberSchema, + schema: memberSchema, }, } as const satisfies IntegrationDefinitionProps['entities'] diff --git a/integrations/trello/definitions/events.ts b/integrations/trello/definitions/events/index.ts similarity index 92% rename from integrations/trello/definitions/events.ts rename to integrations/trello/definitions/events/index.ts index ff4392631f6..dc3984211f5 100644 --- a/integrations/trello/definitions/events.ts +++ b/integrations/trello/definitions/events/index.ts @@ -1,4 +1,4 @@ -import { IntegrationDefinitionProps } from '@botpress/sdk' +import { type IntegrationDefinitionProps } from '@botpress/sdk' import { TRELLO_EVENTS, addMemberToCardEventSchema, @@ -18,7 +18,11 @@ import { voteOnCardEventSchema, addAttachmentToCardEventSchema, deleteAttachmentFromCardEventSchema, -} from './schemas/webhookEvents' + CommentCardEvent, + AllSupportedEvents, + GenericWebhookEvent, + genericWebhookEventSchema, +} from './webhookEvents' export const events = { [TRELLO_EVENTS.addMemberToCard]: { @@ -108,4 +112,11 @@ export const events = { }, } as const satisfies NonNullable -export { TRELLO_EVENTS } +export { + TRELLO_EVENTS, + type AllSupportedEvents, + type CommentCardEvent, + commentCardEventSchema, + type GenericWebhookEvent, + genericWebhookEventSchema, +} diff --git a/integrations/trello/definitions/schemas/webhookEvents.ts b/integrations/trello/definitions/events/webhookEvents.ts similarity index 84% rename from integrations/trello/definitions/schemas/webhookEvents.ts rename to integrations/trello/definitions/events/webhookEvents.ts index e8985645709..7dfe2d35aee 100644 --- a/integrations/trello/definitions/schemas/webhookEvents.ts +++ b/integrations/trello/definitions/events/webhookEvents.ts @@ -1,5 +1,5 @@ import { z } from '@botpress/sdk' -import { BoardSchema, CardSchema, ListSchema, MemberSchema, TrelloIDSchema } from './entities' +import { boardSchema, cardSchema, listSchema, memberSchema, trelloIdSchema } from '../schemas' export const TRELLO_EVENTS = { addMemberToCard: 'addMemberToCard', @@ -23,8 +23,8 @@ export const TRELLO_EVENTS = { export const genericWebhookEventSchema = z.object({ action: z.object({ - id: TrelloIDSchema.describe('Unique identifier of the action'), - idMemberCreator: MemberSchema.shape.id.describe('Unique identifier of the member who initiated the action'), + id: trelloIdSchema.describe('Unique identifier of the action'), + idMemberCreator: memberSchema.shape.id.describe('Unique identifier of the member who initiated the action'), type: z .string() .refine((e) => Reflect.ownKeys(TRELLO_EVENTS).includes(e)) @@ -33,9 +33,9 @@ export const genericWebhookEventSchema = z.object({ data: z.any(), memberCreator: z .object({ - id: MemberSchema.shape.id.describe('Unique identifier of the member'), - fullName: MemberSchema.shape.fullName.describe('Full name of the member'), - username: MemberSchema.shape.username.describe('Username of the member'), + id: memberSchema.shape.id.describe('Unique identifier of the member'), + fullName: memberSchema.shape.fullName.describe('Full name of the member'), + username: memberSchema.shape.username.describe('Username of the member'), initials: z.string().describe('Initials of the member'), avatarHash: z.string().describe('Avatar hash of the member'), avatarUrl: z.string().describe('Avatar URL of the member'), @@ -43,19 +43,19 @@ export const genericWebhookEventSchema = z.object({ .describe('Member who initiated the action'), }), model: z.object({ - id: BoardSchema.shape.id.describe('Unique identifier of the model that is being watched'), + id: boardSchema.shape.id.describe('Unique identifier of the model that is being watched'), }), webhook: z.object({ - id: TrelloIDSchema.describe('Unique identifier of the webhook'), - idModel: BoardSchema.shape.id.describe('Unique identifier of the model that is being watched'), + id: trelloIdSchema.describe('Unique identifier of the webhook'), + idModel: boardSchema.shape.id.describe('Unique identifier of the model that is being watched'), active: z.boolean().describe('Whether the webhook is active'), consecutiveFailures: z.number().min(0).describe('Number of consecutive failures'), }), }) -export type allSupportedEvents = keyof typeof TRELLO_EVENTS -export type genericWebhookEvent = Omit, 'action'> & { - action: Omit, 'type'> & { type: allSupportedEvents } +export type AllSupportedEvents = keyof typeof TRELLO_EVENTS +export type GenericWebhookEvent = Omit, 'action'> & { + action: Omit, 'type'> & { type: AllSupportedEvents } } export const addAttachmentToCardEventSchema = genericWebhookEventSchema.merge( @@ -67,30 +67,30 @@ export const addAttachmentToCardEventSchema = genericWebhookEventSchema.merge( data: z.object({ board: z .object({ - id: BoardSchema.shape.id.describe('Unique identifier of the board'), - name: BoardSchema.shape.name.describe('Name of the board'), + id: boardSchema.shape.id.describe('Unique identifier of the board'), + name: boardSchema.shape.name.describe('Name of the board'), }) .optional() .title('Board') .describe('Board where the card was updated'), card: z .object({ - id: CardSchema.shape.id.describe('Unique identifier of the card'), - name: CardSchema.shape.name.describe('Name of the card'), + id: cardSchema.shape.id.describe('Unique identifier of the card'), + name: cardSchema.shape.name.describe('Name of the card'), }) .title('Card') .describe('Card that was updated'), list: z .object({ - id: ListSchema.shape.id.describe('Unique identifier of the list'), - name: ListSchema.shape.name.describe('Name of the list'), + id: listSchema.shape.id.describe('Unique identifier of the list'), + name: listSchema.shape.name.describe('Name of the list'), }) .optional() .title('List') .describe('List where the card was updated'), attachment: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the attachment'), + id: trelloIdSchema.describe('Unique identifier of the attachment'), name: z.string().describe('Name of the attachment'), url: z.string().url().optional().describe('URL of the attachment'), previewUrl: z.string().url().optional().describe('URL of the attachment preview'), @@ -114,7 +114,7 @@ export const voteOnCardEventSchema = genericWebhookEventSchema.merge( data: z.object({ board: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the board'), + id: trelloIdSchema.describe('Unique identifier of the board'), name: z.string().describe('Name of the board'), }) .optional() @@ -122,7 +122,7 @@ export const voteOnCardEventSchema = genericWebhookEventSchema.merge( .describe('Board where the card was updated'), card: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the card'), + id: trelloIdSchema.describe('Unique identifier of the card'), name: z.string().describe('Name of the card'), }) .optional() @@ -145,7 +145,7 @@ export const updateCommentEventSchema = genericWebhookEventSchema.merge( data: z.object({ board: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the board'), + id: trelloIdSchema.describe('Unique identifier of the board'), name: z.string().describe('Name of the board'), }) .optional() @@ -153,14 +153,14 @@ export const updateCommentEventSchema = genericWebhookEventSchema.merge( .describe('Board where the card was updated'), card: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the card'), + id: trelloIdSchema.describe('Unique identifier of the card'), name: z.string().describe('Name of the card'), }) .title('Card') .describe('Card that was updated'), list: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the list'), + id: trelloIdSchema.describe('Unique identifier of the list'), name: z.string().describe('Name of the list'), }) .optional() @@ -168,7 +168,7 @@ export const updateCommentEventSchema = genericWebhookEventSchema.merge( .describe('List where the card was updated'), action: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the comment that was updated'), + id: trelloIdSchema.describe('Unique identifier of the comment that was updated'), text: z.string().describe('New text of the comment'), }) .title('Action') @@ -195,7 +195,7 @@ export const updateCheckItemStateOnCardEventSchema = genericWebhookEventSchema.m data: z.object({ board: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the board'), + id: trelloIdSchema.describe('Unique identifier of the board'), name: z.string().describe('Name of the board'), }) .optional() @@ -203,21 +203,21 @@ export const updateCheckItemStateOnCardEventSchema = genericWebhookEventSchema.m .describe('Board where the card was updated'), card: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the card'), + id: trelloIdSchema.describe('Unique identifier of the card'), name: z.string().describe('Name of the card'), }) .title('Card') .describe('Card that was updated'), checklist: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the checklist'), + id: trelloIdSchema.describe('Unique identifier of the checklist'), name: z.string().describe('Name of the checklist'), }) .title('Checklist') .describe('Checklist where the item was updated'), checkItem: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the check item'), + id: trelloIdSchema.describe('Unique identifier of the check item'), name: z.string().describe('Name of the check item'), state: z.union([z.literal('complete'), z.literal('incomplete')]).describe('State of the check item'), textData: z.object({ @@ -243,7 +243,7 @@ export const updateCheckItemEventSchema = genericWebhookEventSchema.merge( data: z.object({ board: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the board'), + id: trelloIdSchema.describe('Unique identifier of the board'), name: z.string().describe('Name of the board'), }) .optional() @@ -251,21 +251,21 @@ export const updateCheckItemEventSchema = genericWebhookEventSchema.merge( .describe('Board where the card was updated'), card: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the card'), + id: trelloIdSchema.describe('Unique identifier of the card'), name: z.string().describe('Name of the card'), }) .title('Card') .describe('Card that was updated'), checklist: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the checklist'), + id: trelloIdSchema.describe('Unique identifier of the checklist'), name: z.string().describe('Name of the checklist'), }) .title('Checklist') .describe('Checklist where the item was updated'), checkItem: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the check item'), + id: trelloIdSchema.describe('Unique identifier of the check item'), name: z.string().describe('Name of the check item'), state: z.union([z.literal('complete'), z.literal('incomplete')]).describe('State of the check item'), due: z.string().datetime().optional().describe('Due date of the check item'), @@ -298,7 +298,7 @@ export const updateCardEventSchema = genericWebhookEventSchema.merge( data: z.object({ board: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the board'), + id: trelloIdSchema.describe('Unique identifier of the board'), name: z.string().describe('Name of the board'), }) .optional() @@ -306,11 +306,11 @@ export const updateCardEventSchema = genericWebhookEventSchema.merge( .describe('Board where the card was updated'), card: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the card'), + id: trelloIdSchema.describe('Unique identifier of the card'), name: z.string().describe('Name of the card'), - idList: TrelloIDSchema.optional().describe('Unique identifier of the list where the card is located'), + idList: trelloIdSchema.optional().describe('Unique identifier of the list where the card is located'), desc: z.string().optional().describe('Description of the card'), - idLabels: z.array(TrelloIDSchema).optional().describe('Labels attached to the card'), + idLabels: z.array(trelloIdSchema).optional().describe('Labels attached to the card'), pos: z.number().optional().describe('Position of the card'), start: z.union([z.string().datetime(), z.null()]).optional().describe('Start date of the card'), due: z.union([z.string().datetime(), z.null()]).optional().describe('Due date of the card'), @@ -327,8 +327,8 @@ export const updateCardEventSchema = genericWebhookEventSchema.merge( .object({ name: z.string().describe('Previous name of the card'), desc: z.string().or(z.null()).optional().describe('Previous description of the card'), - idList: TrelloIDSchema.optional().describe('Previous list where the card was'), - idLabels: z.array(TrelloIDSchema).optional().describe('Previous labels attached to the card'), + idList: trelloIdSchema.optional().describe('Previous list where the card was'), + idLabels: z.array(trelloIdSchema).optional().describe('Previous labels attached to the card'), pos: z.number().optional().describe('Previous position of the card'), start: z .union([z.string().datetime(), z.null()]) @@ -346,7 +346,7 @@ export const updateCardEventSchema = genericWebhookEventSchema.merge( .describe('Previous state of the card'), list: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the list'), + id: trelloIdSchema.describe('Unique identifier of the list'), name: z.string().describe('Name of the list'), }) .optional() @@ -354,7 +354,7 @@ export const updateCardEventSchema = genericWebhookEventSchema.merge( .describe('List where the card was updated'), listBefore: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the previous list'), + id: trelloIdSchema.describe('Unique identifier of the previous list'), name: z.string().describe('Name of the previous list'), }) .optional() @@ -362,7 +362,7 @@ export const updateCardEventSchema = genericWebhookEventSchema.merge( .describe('Previous list where the card was located'), listAfter: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the new list'), + id: trelloIdSchema.describe('Unique identifier of the new list'), name: z.string().describe('Name of the new list'), }) .optional() @@ -384,7 +384,7 @@ export const removeMemberFromCardEventSchema = genericWebhookEventSchema.merge( data: z.object({ board: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the board'), + id: trelloIdSchema.describe('Unique identifier of the board'), name: z.string().describe('Name of the board'), }) .optional() @@ -392,14 +392,14 @@ export const removeMemberFromCardEventSchema = genericWebhookEventSchema.merge( .describe('Board where the card was updated'), card: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the card'), + id: trelloIdSchema.describe('Unique identifier of the card'), name: z.string().describe('Name of the card'), }) .title('Card') .describe('Card that was updated'), member: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the member'), + id: trelloIdSchema.describe('Unique identifier of the member'), name: z.string().describe('Full name of the member'), }) .title('Member') @@ -420,7 +420,7 @@ export const removeLabelFromCardEventSchema = genericWebhookEventSchema.merge( data: z.object({ board: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the board'), + id: trelloIdSchema.describe('Unique identifier of the board'), name: z.string().describe('Name of the board'), }) .optional() @@ -428,14 +428,14 @@ export const removeLabelFromCardEventSchema = genericWebhookEventSchema.merge( .describe('Board where the card was modified'), card: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the card'), + id: trelloIdSchema.describe('Unique identifier of the card'), name: z.string().describe('Name of the card'), }) .title('Card') .describe('Card that was modified'), label: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the label'), + id: trelloIdSchema.describe('Unique identifier of the label'), name: z.string().describe('Name of the label'), color: z.string().describe('Color of the label'), }) @@ -457,7 +457,7 @@ export const deleteCommentEventSchema = genericWebhookEventSchema.merge( data: z.object({ board: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the board'), + id: trelloIdSchema.describe('Unique identifier of the board'), name: z.string().describe('Name of the board'), }) .optional() @@ -465,14 +465,14 @@ export const deleteCommentEventSchema = genericWebhookEventSchema.merge( .describe('Board where the card was updated'), card: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the card'), + id: trelloIdSchema.describe('Unique identifier of the card'), name: z.string().describe('Name of the card'), }) .title('Card') .describe('Card that was updated'), list: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the list'), + id: trelloIdSchema.describe('Unique identifier of the list'), name: z.string().describe('Name of the list'), }) .optional() @@ -480,7 +480,7 @@ export const deleteCommentEventSchema = genericWebhookEventSchema.merge( .describe('List where the card was updated'), action: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the comment that was deleted'), + id: trelloIdSchema.describe('Unique identifier of the comment that was deleted'), }) .title('Action') .describe('The action details for the deleted comment'), @@ -500,7 +500,7 @@ export const deleteCheckItemEventSchema = genericWebhookEventSchema.merge( data: z.object({ board: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the board'), + id: trelloIdSchema.describe('Unique identifier of the board'), name: z.string().describe('Name of the board'), }) .optional() @@ -508,21 +508,21 @@ export const deleteCheckItemEventSchema = genericWebhookEventSchema.merge( .describe('Board where the card was updated'), card: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the card'), + id: trelloIdSchema.describe('Unique identifier of the card'), name: z.string().describe('Name of the card'), }) .title('Card') .describe('Card that was updated'), checklist: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the checklist'), + id: trelloIdSchema.describe('Unique identifier of the checklist'), name: z.string().describe('Name of the checklist'), }) .title('Checklist') .describe('Checklist where the item was removed'), checkItem: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the check item'), + id: trelloIdSchema.describe('Unique identifier of the check item'), name: z.string().describe('Name of the check item'), state: z.union([z.literal('complete'), z.literal('incomplete')]).describe('State of the check item'), textData: z.object({ @@ -548,7 +548,7 @@ export const deleteCardEventSchema = genericWebhookEventSchema.merge( data: z.object({ board: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the board'), + id: trelloIdSchema.describe('Unique identifier of the board'), name: z.string().describe('Name of the board'), }) .optional() @@ -556,13 +556,13 @@ export const deleteCardEventSchema = genericWebhookEventSchema.merge( .describe('Board where the card was deleted'), card: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the card'), + id: trelloIdSchema.describe('Unique identifier of the card'), }) .title('Card') .describe('Card that was deleted'), list: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the list'), + id: trelloIdSchema.describe('Unique identifier of the list'), name: z.string().describe('Name of the list'), }) .optional() @@ -584,7 +584,7 @@ export const deleteAttachmentFromCardEventSchema = genericWebhookEventSchema.mer data: z.object({ board: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the board'), + id: trelloIdSchema.describe('Unique identifier of the board'), name: z.string().describe('Name of the board'), }) .optional() @@ -592,14 +592,14 @@ export const deleteAttachmentFromCardEventSchema = genericWebhookEventSchema.mer .describe('Board where the card was updated'), card: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the card'), + id: trelloIdSchema.describe('Unique identifier of the card'), name: z.string().describe('Name of the card'), }) .title('Card') .describe('Card that was updated'), list: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the list'), + id: trelloIdSchema.describe('Unique identifier of the list'), name: z.string().describe('Name of the list'), }) .optional() @@ -607,7 +607,7 @@ export const deleteAttachmentFromCardEventSchema = genericWebhookEventSchema.mer .describe('List where the card was updated'), attachment: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the attachment'), + id: trelloIdSchema.describe('Unique identifier of the attachment'), name: z.string().describe('Name of the attachment'), }) .title('Attachment') @@ -628,7 +628,7 @@ export const createCheckItemEventSchema = genericWebhookEventSchema.merge( data: z.object({ board: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the board'), + id: trelloIdSchema.describe('Unique identifier of the board'), name: z.string().describe('Name of the board'), }) .optional() @@ -636,21 +636,21 @@ export const createCheckItemEventSchema = genericWebhookEventSchema.merge( .describe('Board where the card was updated'), card: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the card'), + id: trelloIdSchema.describe('Unique identifier of the card'), name: z.string().describe('Name of the card'), }) .title('Card') .describe('Card that was updated'), checklist: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the checklist'), + id: trelloIdSchema.describe('Unique identifier of the checklist'), name: z.string().describe('Name of the checklist'), }) .title('Checklist') .describe('Checklist where the item was added'), checkItem: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the check item'), + id: trelloIdSchema.describe('Unique identifier of the check item'), name: z.string().describe('Name of the check item'), state: z.union([z.literal('complete'), z.literal('incomplete')]).describe('State of the check item'), textData: z.object({ @@ -676,7 +676,7 @@ export const createCardEventSchema = genericWebhookEventSchema.merge( data: z.object({ board: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the board'), + id: trelloIdSchema.describe('Unique identifier of the board'), name: z.string().describe('Name of the board'), }) .optional() @@ -684,14 +684,14 @@ export const createCardEventSchema = genericWebhookEventSchema.merge( .describe('Board where the card was created'), card: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the card'), + id: trelloIdSchema.describe('Unique identifier of the card'), name: z.string().describe('Name of the card'), }) .title('Card') .describe('Card that was created'), list: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the list'), + id: trelloIdSchema.describe('Unique identifier of the list'), name: z.string().describe('Name of the list'), }) .optional() @@ -710,11 +710,11 @@ export const commentCardEventSchema = genericWebhookEventSchema.merge( z .object({ type: z.literal('commentCard').describe('Type of the action'), - id: TrelloIDSchema.describe('Unique identifier of the comment'), + id: trelloIdSchema.describe('Unique identifier of the comment'), data: z.object({ board: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the board'), + id: trelloIdSchema.describe('Unique identifier of the board'), name: z.string().describe('Name of the board'), }) .optional() @@ -722,14 +722,14 @@ export const commentCardEventSchema = genericWebhookEventSchema.merge( .describe('Board where the card was updated'), card: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the card'), + id: trelloIdSchema.describe('Unique identifier of the card'), name: z.string().describe('Name of the card'), }) .title('Card') .describe('Card that was updated'), list: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the list'), + id: trelloIdSchema.describe('Unique identifier of the list'), name: z.string().describe('Name of the list'), }) .optional() @@ -754,7 +754,7 @@ export const addMemberToCardEventSchema = genericWebhookEventSchema.merge( data: z.object({ board: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the board'), + id: trelloIdSchema.describe('Unique identifier of the board'), name: z.string().describe('Name of the board'), }) .optional() @@ -762,14 +762,14 @@ export const addMemberToCardEventSchema = genericWebhookEventSchema.merge( .describe('Board where the card was updated'), card: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the card'), + id: trelloIdSchema.describe('Unique identifier of the card'), name: z.string().describe('Name of the card'), }) .title('Card') .describe('Card that was updated'), member: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the member'), + id: trelloIdSchema.describe('Unique identifier of the member'), name: z.string().describe('Full name of the member'), }) .title('Member') @@ -790,7 +790,7 @@ export const addLabelToCardEventSchema = genericWebhookEventSchema.merge( data: z.object({ board: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the board'), + id: trelloIdSchema.describe('Unique identifier of the board'), name: z.string().describe('Name of the board'), }) .optional() @@ -798,14 +798,14 @@ export const addLabelToCardEventSchema = genericWebhookEventSchema.merge( .describe('Board where the card was modified'), card: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the card'), + id: trelloIdSchema.describe('Unique identifier of the card'), name: z.string().describe('Name of the card'), }) .title('Card') .describe('Card that was modified'), label: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the label'), + id: trelloIdSchema.describe('Unique identifier of the label'), name: z.string().describe('Name of the label'), color: z.string().describe('Color of the label'), }) @@ -827,7 +827,7 @@ export const addChecklistToCardEventSchema = genericWebhookEventSchema.merge( data: z.object({ board: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the board'), + id: trelloIdSchema.describe('Unique identifier of the board'), name: z.string().describe('Name of the board'), }) .optional() @@ -835,14 +835,14 @@ export const addChecklistToCardEventSchema = genericWebhookEventSchema.merge( .describe('Board where the card was updated'), card: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the card'), + id: trelloIdSchema.describe('Unique identifier of the card'), name: z.string().describe('Name of the card'), }) .title('Card') .describe('Card that was updated'), checklist: z .object({ - id: TrelloIDSchema.describe('Unique identifier of the checklist'), + id: trelloIdSchema.describe('Unique identifier of the checklist'), name: z.string().describe('Name of the checklist'), }) .title('Checklist') diff --git a/integrations/trello/definitions/schemas.ts b/integrations/trello/definitions/schemas.ts new file mode 100644 index 00000000000..3040f292469 --- /dev/null +++ b/integrations/trello/definitions/schemas.ts @@ -0,0 +1,46 @@ +import { z } from '@botpress/sdk' + +export const trelloIdRegex = /^[0-9a-fA-F]{24}$/ + +export const trelloIdSchema = z.string().regex(trelloIdRegex) +export type TrelloID = z.infer + +export const boardSchema = z.object({ + id: trelloIdSchema, + name: z.string(), +}) +export type Board = z.infer + +export const cardSchema = z.object({ + id: trelloIdSchema, + name: z.string(), + description: z.string(), + listId: trelloIdSchema, + verticalPosition: z.number(), + isClosed: z.boolean(), + isCompleted: z.boolean(), + dueDate: z.string().datetime().optional(), + labelIds: z.array(trelloIdSchema), + memberIds: z.array(trelloIdSchema), +}) +export type Card = z.infer + +export const listSchema = z.object({ + id: trelloIdSchema, + name: z.string(), +}) +export type List = z.infer + +export const memberSchema = z.object({ + id: trelloIdSchema, + username: z.string(), + fullName: z.string(), +}) +export type Member = z.infer + +export const webhookSchema = z.object({ + id: trelloIdSchema, + modelId: trelloIdSchema, + callbackUrl: z.string().url(), +}) +export type Webhook = z.infer diff --git a/integrations/trello/definitions/schemas/actions/inputSchemas.ts b/integrations/trello/definitions/schemas/actions/inputSchemas.ts deleted file mode 100644 index b05e6c60005..00000000000 --- a/integrations/trello/definitions/schemas/actions/inputSchemas.ts +++ /dev/null @@ -1,197 +0,0 @@ -import { z } from '@botpress/sdk' -import { BoardSchema, CardSchema, ListSchema, MemberSchema, TrelloIDSchema } from '../entities' - -const GENERIC_SHEMAS = { - noInput: z.object({}), - hasBoardId: z.object({ - boardId: BoardSchema.shape.id.title('Board ID').describe('Unique identifier of the board'), - }), - hasListId: z.object({ listId: ListSchema.shape.id.title('List ID').describe('Unique identifier of the list') }), - hasCardId: z.object({ cardId: CardSchema.shape.id.title('Card ID').describe('Unique identifier of the card') }), -} as const - -export const addCardCommentInputSchema = z - .object({ - cardId: CardSchema.shape.id - .title('Card ID') - .describe('Unique identifier of the card to which a comment will be added'), - commentBody: z.string().title('Comment Body').describe('The body text of the comment'), - }) - .describe('Input schema for adding a comment to a card') - -export const createCardInputSchema = z - .object({ - listId: ListSchema.shape.id.title('List ID').describe('ID of the list in which to insert the new card'), - cardName: CardSchema.shape.name.title('Card Name').describe('Name of the new card'), - cardBody: CardSchema.shape.description.optional().title('Card Body').describe('Body text of the new card'), - members: z - .array(TrelloIDSchema) - .optional() - .title('Members') - .describe('Members to add to the card (Optional). This should be a list of member IDs.'), - labels: z - .array(TrelloIDSchema) - .optional() - .title('Labels') - .describe('Labels to add to the card (Optional). This should be a list of label IDs.'), - dueDate: CardSchema.shape.dueDate - .optional() - .title('Due Date') - .describe('The due date of the card in ISO 8601 format (Optional).'), - }) - .describe('Input schema for creating a new card') - -export const updateCardInputSchema = GENERIC_SHEMAS.hasCardId - .merge( - z.object({ - name: CardSchema.shape.name - .optional() - .title('Name') - .describe('The name of the card (Optional) (e.g. "My Test Card"). Leave empty to keep the current name.'), - bodyText: CardSchema.shape.description - .optional() - .title('Body Text') - .describe('Body text of the new card (Optional). Leave empty to keep the current body.'), - closedState: z - .enum(['open', 'archived']) - .optional() - .title('Closed State') - .describe( - 'Whether the card should be archived (Optional). Enter "open", "archived" (without quotes), or leave empty to keep the previous status.' - ) - .optional(), - completeState: z - .enum(['complete', 'incomplete']) - .optional() - .title('State Completion') - .describe( - 'Whether the card should be marked as complete (Optional). Enter "complete", "incomplete" (without quotes), or leave empty to keep the previous status.' - ) - .optional(), - membersToAdd: z - .array(TrelloIDSchema) - .optional() - .title('Members to Add') - .describe( - 'Members to add to the card (Optional). This should be a list of member IDs. Leave empty to keep the current members.' - ), - membersToRemove: z - .array(TrelloIDSchema) - .optional() - .title('Members to Remove') - .describe( - 'Members to remove from the card (Optional). This should be a list of member IDs. Leave empty to keep the current members.' - ), - labelsToAdd: z - .array(TrelloIDSchema) - .optional() - .title('Labels to Add') - .describe( - 'Labels to add to the card (Optional). This should be a list of label IDs. Leave empty to keep the current labels.' - ), - labelsToRemove: z - .array(TrelloIDSchema) - .optional() - .title('Labels to Remove') - .describe( - 'Labels to remove from the card (Optional). This should be a list of label IDs. Leave empty to keep the current labels.' - ), - dueDate: CardSchema.shape.dueDate - .optional() - .title('Due Date') - .describe('The due date of the card in ISO 8601 format (Optional). Leave empty to keep the current due date.'), - }) - ) - .describe('Input schema for creating a new card') - -export const moveCardUpInputSchema = GENERIC_SHEMAS.hasCardId.merge( - z.object({ - moveUpByNSpaces: z - .number() - .min(1) - .optional() - .default(1) - .title('Move Up By N Spaces') - .describe('Number of spaces by which to move the card up'), - }) -) - -export const moveCardDownInputSchema = GENERIC_SHEMAS.hasCardId.merge( - z.object({ - moveDownByNSpaces: moveCardUpInputSchema.shape.moveUpByNSpaces - .title('Move Down By N Spaces') - .describe('Number of spaces by which to move the card down'), - }) -) - -export const moveCardToListInputSchema = GENERIC_SHEMAS.hasCardId.merge( - z.object({ - newListId: ListSchema.shape.id - .title('New List ID') - .describe('Unique identifier of the list in which the card will be moved'), - }) -) - -export const getMemberByIdOrUsernameInputSchema = z - .object({ - memberIdOrUsername: z - .union([MemberSchema.shape.id, MemberSchema.shape.username]) - .title('Member ID or Username') - .describe('ID or username of the member to get'), - }) - .describe('Input schema for getting a member from its ID or username') - -export const getListsInBoardInputSchema = GENERIC_SHEMAS.hasBoardId.describe( - 'Input schema for getting all lists in a board' -) - -export const getListsByDisplayNameInputSchema = GENERIC_SHEMAS.hasBoardId - .merge( - z.object({ - listName: ListSchema.shape.name.title('List Name').describe('Display name of the list'), - }) - ) - .describe('Input schema for getting a list ID from its name') - -export const getListByIdInputSchema = GENERIC_SHEMAS.hasListId.describe('Input schema for getting a list from its ID') - -export const getCardsInListInputSchema = GENERIC_SHEMAS.hasListId.describe( - 'Input schema for getting all cards in a list' -) - -export const getCardsByDisplayNameInputSchema = GENERIC_SHEMAS.hasListId - .merge( - z.object({ - cardName: CardSchema.shape.name.title('Card Name').describe('Display name of the card'), - }) - ) - .describe('Input schema for getting a card ID from its name') - -export const getCardByIdInputSchema = GENERIC_SHEMAS.hasCardId.describe('Input schema for getting a card from its ID') - -export const getBoardsByDisplayNameInputSchema = z - .object({ - boardName: BoardSchema.shape.name.title('Board Name').describe('Display name of the board'), - }) - .describe('Input schema for getting a board ID from its name') - -export const getBoardMembersByDisplayNameInputSchema = GENERIC_SHEMAS.hasBoardId - .merge( - z.object({ - displayName: BoardSchema.shape.name.title('Display Name').describe('Display name of the member'), - }) - ) - .describe('Input schema for getting a member from its name') -export const getBoardByIdInputSchema = GENERIC_SHEMAS.hasBoardId.describe( - 'Input schema for getting a board from its ID' -) - -export const getAllBoardsInputSchema = GENERIC_SHEMAS.noInput.describe('Input schema for getting all boards') - -export const getAllBoardMembersInputSchema = GENERIC_SHEMAS.hasBoardId.describe( - 'Input schema for getting all members of a board' -) - -export const getAllCardMembersInputSchema = GENERIC_SHEMAS.hasCardId.describe( - 'Input schema for getting all members of a card' -) diff --git a/integrations/trello/definitions/schemas/actions/outputSchemas.ts b/integrations/trello/definitions/schemas/actions/outputSchemas.ts deleted file mode 100644 index 5bb548394e0..00000000000 --- a/integrations/trello/definitions/schemas/actions/outputSchemas.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { z } from '@botpress/sdk' -import { TrelloIDSchema, BoardSchema, CardSchema, ListSchema, MemberSchema } from '../entities' - -const GENERIC_SHEMAS = { - hasMessage: z.object({ - message: z.string().describe('Output message'), - }), - outputsMember: z.object({ - member: MemberSchema.describe('The member object'), - }), - outputsMembers: z.object({ - members: z.array(MemberSchema).describe('Array of member objects'), - }), - outputsCard: z.object({ - card: CardSchema.describe('The card object'), - }), - outputsCards: z.object({ - cards: z.array(CardSchema).describe('Array of card objects'), - }), - outputsList: z.object({ - list: ListSchema.describe('The list object'), - }), - outputsLists: z.object({ - lists: z.array(ListSchema).describe('Array of list objects'), - }), - outputsBoard: z.object({ - board: BoardSchema.describe('The board object'), - }), - outputsBoards: z.object({ - boards: z.array(BoardSchema).describe('Array of board objects'), - }), -} as const - -export const addCardCommentOutputSchema = GENERIC_SHEMAS.hasMessage - .merge( - z.object({ - newCommentId: TrelloIDSchema.describe('Unique identifier of the newly created comment'), - }) - ) - .describe('Output schema for adding a comment to a card') - -export const createCardOutputSchema = GENERIC_SHEMAS.hasMessage - .merge( - z.object({ - newCardId: CardSchema.shape.id.describe('Unique identifier of the new card'), - }) - ) - .describe('Output schema for creating a card') - -export const getMemberByIdOrUsernameOutputSchema = GENERIC_SHEMAS.outputsMember.describe( - 'Output schema for getting a member by its ID or username' -) - -export const getListsInBoardOutputSchema = GENERIC_SHEMAS.outputsLists.describe( - 'Output schema for getting all lists in a board' -) - -export const getListsByDisplayNameOutputSchema = GENERIC_SHEMAS.outputsLists.describe( - 'Output schema for getting a list ID from its name' -) - -export const getListByIdOutputSchema = GENERIC_SHEMAS.outputsList.describe( - 'Output schema for getting a list from its ID' -) - -export const getCardsInListOutputSchema = GENERIC_SHEMAS.outputsCards.describe( - 'Output schema for getting all cards in a list' -) - -export const getCardsByDisplayNameOutputSchema = GENERIC_SHEMAS.outputsCards.describe( - 'Output schema for getting a card ID from its name' -) - -export const getCardByIdOutputSchema = GENERIC_SHEMAS.outputsCard.describe( - 'Output schema for getting a card from its ID' -) - -export const getBoardsByDisplayNameOutputSchema = GENERIC_SHEMAS.outputsBoards.describe( - 'Output schema for getting a board from its name' -) - -export const getBoardMembersByDisplayNameOutputSchema = GENERIC_SHEMAS.outputsMembers.describe( - 'Output schema for getting a member from its name' -) - -export const getBoardByIdOutputSchema = GENERIC_SHEMAS.outputsBoard.describe( - 'Output schema for getting a board from its ID' -) - -export const getAllBoardsOutputSchema = GENERIC_SHEMAS.outputsBoards.describe('Output schema for getting all boards') - -export const getAllBoardMembersOutputSchema = GENERIC_SHEMAS.outputsMembers.describe( - 'Output schema for getting all members of a board' -) - -export const moveCardDownOutputSchema = GENERIC_SHEMAS.hasMessage.describe('Output schema for moving a card down') -export const moveCardUpOutputSchema = GENERIC_SHEMAS.hasMessage.describe('Output schema for moving a card up') -export const moveCardToListOutputSchema = GENERIC_SHEMAS.hasMessage.describe( - 'Output schema for moving a card to a list' -) -export const updateCardOutputSchema = GENERIC_SHEMAS.hasMessage.describe('Output schema for updating a card') - -export const getAllCardMembersOutputSchema = GENERIC_SHEMAS.outputsMembers.describe( - 'Output schema for getting all members of a card' -) diff --git a/integrations/trello/definitions/schemas/entities.ts b/integrations/trello/definitions/schemas/entities.ts deleted file mode 100644 index 85247717281..00000000000 --- a/integrations/trello/definitions/schemas/entities.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { z } from '@botpress/sdk' - -export const trelloIdRegex = /^[0-9a-fA-F]{24}$/ - -export const TrelloIDSchema = z.string().regex(trelloIdRegex) - -export type TrelloID = z.infer - -export const BoardSchema = z.object({ - id: TrelloIDSchema, - name: z.string(), -}) - -export const CardSchema = z.object({ - id: TrelloIDSchema, - name: z.string(), - description: z.string(), - listId: TrelloIDSchema, - verticalPosition: z.number(), - isClosed: z.boolean(), - isCompleted: z.boolean(), - dueDate: z.string().datetime().optional(), - labelIds: z.array(TrelloIDSchema), - memberIds: z.array(TrelloIDSchema), -}) - -export const ListSchema = z.object({ - id: TrelloIDSchema, - name: z.string(), -}) - -export const MemberSchema = z.object({ - id: TrelloIDSchema, - username: z.string(), - fullName: z.string(), -}) - -export type Board = z.infer -export type Card = z.infer -export type List = z.infer -export type Member = z.infer diff --git a/integrations/trello/definitions/schemas/index.ts b/integrations/trello/definitions/schemas/index.ts deleted file mode 100644 index 8e3775e4cd3..00000000000 --- a/integrations/trello/definitions/schemas/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './actions/inputSchemas' -export * from './actions/outputSchemas' -export * from './entities' -export * from './states' -export * from './webhookEvents' diff --git a/integrations/trello/definitions/schemas/states.ts b/integrations/trello/definitions/schemas/states.ts deleted file mode 100644 index de4e4c83ea1..00000000000 --- a/integrations/trello/definitions/schemas/states.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { z } from '@botpress/sdk' -import { TrelloIDSchema } from './entities' - -export const webhookStateSchema = z - .object({ - trelloWebhookId: TrelloIDSchema.or(z.null()) - .title('Trello Webhook ID') - .describe('Unique id of the webhook that is created upon integration registration') - .default(null), - }) - .describe('State that stores the webhook id for the Trello integration') diff --git a/integrations/trello/definitions/states.ts b/integrations/trello/definitions/states.ts index b6a6e72cbd4..6fff3116a4f 100644 --- a/integrations/trello/definitions/states.ts +++ b/integrations/trello/definitions/states.ts @@ -1,13 +1,18 @@ -import { IntegrationDefinitionProps } from '@botpress/sdk' -import { webhookStateSchema } from './schemas' +import { type IntegrationDefinitionProps, z } from '@botpress/sdk' +import { trelloIdSchema } from './schemas' -export const States = { - webhookState: 'webhookState', -} as const +const _webhookIdStateSchema = trelloIdSchema + .nullable() + .default(null) + .title('Trello Webhook ID') + .describe('Unique id of the webhook that is created upon integration registration') +export type WebhookIdState = z.infer export const states = { - [States.webhookState]: { + webhookState: { type: 'integration', - schema: webhookStateSchema, + schema: z + .object({ trelloWebhookId: _webhookIdStateSchema }) + .describe('State that stores the webhook id for the Trello integration'), }, } as const satisfies NonNullable diff --git a/integrations/trello/hub.md b/integrations/trello/hub.md index 0536fb7693e..e112dbeecc6 100644 --- a/integrations/trello/hub.md +++ b/integrations/trello/hub.md @@ -50,6 +50,16 @@ Botpress chatbot. The integration offers actions like `createCard`, `updateCard` For more details and examples, refer to the Botpress and Trello documentation. +## Events + +In order to enable events for the integration a board id must be provided in the configuration. + +To find your board id, open the webpage for your trello board and add ".json" to the end of the URL. For example, + +- `trello.com/b/Ab12cD43/my-trello-board` **->** `trello.com/b/Ab12cD43/my-trello-board.json` + +The id of the board should be 24 characters long consisting of letters and numbers. + ## Limitations - Trello API rate limits apply. diff --git a/integrations/trello/integration.definition.ts b/integrations/trello/integration.definition.ts index f89ba78c4e5..7b429232b9b 100644 --- a/integrations/trello/integration.definition.ts +++ b/integrations/trello/integration.definition.ts @@ -12,7 +12,7 @@ import { integrationName } from './package.json' export default new sdk.IntegrationDefinition({ name: integrationName, title: 'Trello', - version: '1.1.4', + version: '1.2.0', readme: 'hub.md', description: 'Update cards, add comments, create new cards, and read board members from your chatbot.', icon: 'icon.svg', diff --git a/integrations/trello/src/actions/index.ts b/integrations/trello/src/actions/index.ts index 8a510f06575..ea07fe35d31 100644 --- a/integrations/trello/src/actions/index.ts +++ b/integrations/trello/src/actions/index.ts @@ -1,33 +1,76 @@ -export { addCardComment } from './implementations/addCardComment' -export { createCard } from './implementations/createCard' -export { getAllBoardMembers } from './implementations/getAllBoardMembers' -export { getAllBoards } from './implementations/getAllBoards' -export { getBoardById } from './implementations/getBoardById' -export { getBoardMembersByDisplayName } from './implementations/getBoardMembersByDisplayName' -export { getBoardsByDisplayName } from './implementations/getBoardsByDisplayName' -export { getCardById } from './implementations/getCardById' -export { getCardsByDisplayName } from './implementations/getCardsByDisplayName' -export { getCardsInList } from './implementations/getCardsInList' -export { getListById } from './implementations/getListById' -export { getListsByDisplayName } from './implementations/getListsByDisplayName' -export { getListsInBoard } from './implementations/getListsInBoard' -export { getMemberByIdOrUsername } from './implementations/getMemberByIdOrUsername' -export { moveCardDown } from './implementations/moveCardDown' -export { moveCardToList } from './implementations/moveCardToList' -export { moveCardUp } from './implementations/moveCardUp' -export { updateCard } from './implementations/updateCard' -export { getAllCardMembers } from './implementations/getAllCardMembers' +import { addCardComment } from './implementations/addCardComment' +import { createCard } from './implementations/createCard' +import { getAllBoardMembers } from './implementations/getAllBoardMembers' +import { getAllBoards } from './implementations/getAllBoards' +import { getAllCardMembers } from './implementations/getAllCardMembers' +import { getBoardById } from './implementations/getBoardById' +import { getBoardMembersByDisplayName } from './implementations/getBoardMembersByDisplayName' +import { getBoardsByDisplayName } from './implementations/getBoardsByDisplayName' +import { getCardById } from './implementations/getCardById' +import { getCardsByDisplayName } from './implementations/getCardsByDisplayName' +import { getCardsInList } from './implementations/getCardsInList' +import { getListById } from './implementations/getListById' +import { getListsByDisplayName } from './implementations/getListsByDisplayName' +import { getListsInBoard } from './implementations/getListsInBoard' +import { getMemberByIdOrUsername } from './implementations/getMemberByIdOrUsername' +import { boardList } from './implementations/interfaces/boardList' +import { boardMemberList } from './implementations/interfaces/boardMemberList' +import { boardMemberRead } from './implementations/interfaces/boardMemberRead' +import { boardRead } from './implementations/interfaces/boardRead' +import { cardCreate } from './implementations/interfaces/cardCreate' +import { cardDelete } from './implementations/interfaces/cardDelete' +import { cardList } from './implementations/interfaces/cardList' +import { cardMemberList } from './implementations/interfaces/cardMemberList' +import { cardMemberRead } from './implementations/interfaces/cardMemberRead' +import { cardRead } from './implementations/interfaces/cardRead' +import { cardUpdate } from './implementations/interfaces/cardUpdate' +import { listList } from './implementations/interfaces/listList' +import { listRead } from './implementations/interfaces/listRead' +import { moveCardDown } from './implementations/moveCardDown' +import { moveCardToList } from './implementations/moveCardToList' +import { moveCardUp } from './implementations/moveCardUp' +import { updateCard } from './implementations/updateCard' +import * as bp from '.botpress' -export { cardList } from './implementations/interfaces/cardList' -export { cardRead } from './implementations/interfaces/cardRead' -export { cardCreate } from './implementations/interfaces/cardCreate' -export { cardUpdate } from './implementations/interfaces/cardUpdate' -export { cardDelete } from './implementations/interfaces/cardDelete' -export { boardList } from './implementations/interfaces/boardList' -export { boardRead } from './implementations/interfaces/boardRead' -export { listList } from './implementations/interfaces/listList' -export { listRead } from './implementations/interfaces/listRead' -export { boardMemberList } from './implementations/interfaces/boardMemberList' -export { boardMemberRead } from './implementations/interfaces/boardMemberRead' -export { cardMemberList } from './implementations/interfaces/cardMemberList' -export { cardMemberRead } from './implementations/interfaces/cardMemberRead' +export const actions = { + // === Board Actions === + getBoardById, + getBoardsByDisplayName, + getAllBoards, + // === List Actions === + getListById, + getListsByDisplayName, + getListsInBoard, + // === Card Actions === + getCardById, + getCardsByDisplayName, + getCardsInList, + createCard, + updateCard, + addCardComment, + moveCardUp, + moveCardDown, + moveCardToList, + // === Member Actions === + getMemberByIdOrUsername, + getBoardMembersByDisplayName, + getAllBoardMembers, + getAllCardMembers, + // === Interface Board Actions === + boardList, + boardRead, + // === Interface List Actions === + listList, + listRead, + // === Interface Card Actions === + cardList, + cardRead, + cardCreate, + cardUpdate, + cardDelete, + // === Interface Member Actions === + boardMemberList, + boardMemberRead, + cardMemberList, + cardMemberRead, +} as const satisfies bp.IntegrationProps['actions'] diff --git a/integrations/trello/src/index.ts b/integrations/trello/src/index.ts index cd969eda190..9948cceb7b0 100644 --- a/integrations/trello/src/index.ts +++ b/integrations/trello/src/index.ts @@ -1,84 +1,14 @@ import { sentry as sentryHelpers } from '@botpress/sdk-addons' import * as bp from '../.botpress' -import { - getBoardsByDisplayName, - getCardsByDisplayName, - getListsByDisplayName, - createCard, - moveCardUp, - moveCardDown, - moveCardToList, - addCardComment, - updateCard, - getAllBoardMembers, - getAllBoards, - getBoardById, - getBoardMembersByDisplayName, - getCardById, - getCardsInList, - getListById, - getListsInBoard, - getMemberByIdOrUsername, - cardList, - cardRead, - cardCreate, - cardUpdate, - cardDelete, - listList, - listRead, - boardList, - boardRead, - cardMemberList, - cardMemberRead, - boardMemberList, - boardMemberRead, - getAllCardMembers, -} from './actions' +import { actions } from './actions' import { channels } from './channels/publisher-dispatcher' +import { register, unregister } from './setup' import { handler } from './webhook-events' -import { WebhookLifecycleManager } from './webhook-events/webhook-lifecycle-manager' const integration = new bp.Integration({ - register: WebhookLifecycleManager.registerTrelloWebhookIfNotExists, - unregister: WebhookLifecycleManager.unregisterTrelloWebhookIfExists, - - actions: { - addCardComment, - createCard, - getAllBoardMembers, - getAllBoards, - getBoardById, - getBoardMembersByDisplayName, - getBoardsByDisplayName, - getCardById, - getCardsByDisplayName, - getCardsInList, - getListById, - getListsByDisplayName, - getListsInBoard, - getMemberByIdOrUsername, - moveCardDown, - moveCardToList, - moveCardUp, - updateCard, - getAllCardMembers, - - // interfaces: - cardList, - cardRead, - cardCreate, - cardUpdate, - cardDelete, - listList, - listRead, - boardList, - boardRead, - cardMemberList, - cardMemberRead, - boardMemberList, - boardMemberRead, - }, - + register, + unregister, + actions, channels, handler, }) diff --git a/integrations/trello/src/setup.ts b/integrations/trello/src/setup.ts new file mode 100644 index 00000000000..13527ab5925 --- /dev/null +++ b/integrations/trello/src/setup.ts @@ -0,0 +1,18 @@ +import { TrelloClient } from './trello-api/trello-client' +import { + cleanupStaleWebhooks, + registerTrelloWebhookIfNotExists, + unregisterTrelloWebhooks, +} from './webhook-lifecycle-utils' +import * as bp from '.botpress' + +export const register: bp.Integration['unregister'] = async ({ webhookUrl, ...props }) => { + const trelloClient = new TrelloClient({ ctx: props.ctx }) + await cleanupStaleWebhooks(props, webhookUrl, trelloClient) + await registerTrelloWebhookIfNotExists(props, webhookUrl, trelloClient) +} + +export const unregister: bp.Integration['unregister'] = async ({ webhookUrl, ...props }) => { + const trelloClient = new TrelloClient({ ctx: props.ctx }) + await unregisterTrelloWebhooks(props, webhookUrl, trelloClient) +} diff --git a/integrations/trello/src/trello-api/error-handling/error-handler-decorator.ts b/integrations/trello/src/trello-api/error-handling/error-handler-decorator.ts deleted file mode 100644 index bf5bd30203e..00000000000 --- a/integrations/trello/src/trello-api/error-handling/error-handler-decorator.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { createErrorHandlingDecorator } from '@botpress/common' -import { wrapAsyncFnWithTryCatch } from './error-redactor' - -export const handleErrorsDecorator = createErrorHandlingDecorator(wrapAsyncFnWithTryCatch) diff --git a/integrations/trello/src/trello-api/error-handling/error-redactor.ts b/integrations/trello/src/trello-api/error-handling/error-redactor.ts deleted file mode 100644 index c1aa52ae17c..00000000000 --- a/integrations/trello/src/trello-api/error-handling/error-redactor.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { createAsyncFnWrapperWithErrorRedaction } from '@botpress/common' -import * as sdk from '@botpress/sdk' - -export const wrapAsyncFnWithTryCatch = createAsyncFnWrapperWithErrorRedaction((error: Error, customMessage: string) => { - if (error instanceof sdk.RuntimeError) { - return error - } - - console.warn(customMessage, error) - return new sdk.RuntimeError(`${customMessage}: ${error}`) -}) diff --git a/integrations/trello/src/trello-api/mapping/response-mapping.ts b/integrations/trello/src/trello-api/mapping/response-mapping.ts index 806badf4932..099c4b6d6df 100644 --- a/integrations/trello/src/trello-api/mapping/response-mapping.ts +++ b/integrations/trello/src/trello-api/mapping/response-mapping.ts @@ -1,4 +1,4 @@ -import type { List, Member, Board, TrelloID, Card } from 'definitions/schemas' +import type { List, Member, Board, TrelloID, Card, Webhook } from 'definitions/schemas' import { Models, type Models as TrelloJsModels } from 'trello.js' export namespace ResponseMapping { @@ -32,4 +32,10 @@ export namespace ResponseMapping { labelIds: card.idLabels as TrelloID[], memberIds: card.idMembers as Member['id'][], }) + + export const mapWebhook = (webhook: TrelloJsModels.Webhook): Webhook => ({ + id: mapTrelloId(webhook.id), + modelId: webhook.idModel ?? '', + callbackUrl: webhook.callbackURL ?? '', + }) } diff --git a/integrations/trello/src/trello-api/trello-client.ts b/integrations/trello/src/trello-api/trello-client.ts index 5def7fb04de..1830a722ba2 100644 --- a/integrations/trello/src/trello-api/trello-client.ts +++ b/integrations/trello/src/trello-api/trello-client.ts @@ -1,118 +1,162 @@ -import type { Board, Card, List, Member, TrelloID } from 'definitions/schemas' +import { RuntimeError } from '@botpress/sdk' +import { + webhookSchema, + type Board, + type Card, + type List, + type Member, + type TrelloID, + type Webhook, +} from 'definitions/schemas' import { TrelloClient as TrelloJs, type Models as TrelloJsModels } from 'trello.js' -import { handleErrorsDecorator as handleErrors } from './error-handling/error-handler-decorator' import { RequestMapping } from './mapping/request-mapping' import { ResponseMapping } from './mapping/response-mapping' import * as bp from '.botpress' +const _useHandleCaughtError = (message: string) => { + return (thrown: unknown) => { + const error = thrown instanceof Error ? thrown : new Error(String(thrown)) + throw new RuntimeError(`${message}: ${error.message}`) + } +} + export class TrelloClient { private readonly _trelloJs: TrelloJs + private readonly _token: string public constructor({ ctx }: { ctx: bp.Context }) { + this._token = ctx.configuration.trelloApiToken this._trelloJs = new TrelloJs({ key: ctx.configuration.trelloApiKey, token: ctx.configuration.trelloApiToken }) } - @handleErrors('Failed to retrieve board members') public async getBoardMembers({ boardId }: { boardId: Board['id'] }): Promise { - const members: TrelloJsModels.Member[] = await this._trelloJs.boards.getBoardMembers({ - id: boardId, - }) + const members = await this._trelloJs.boards + .getBoardMembers({ + id: boardId, + }) + .catch(_useHandleCaughtError('Failed to retrieve board members')) return members.map(ResponseMapping.mapMember) } - @handleErrors('Failed to retrieve board by id') public async getBoardById({ boardId }: { boardId: Board['id'] }): Promise { - const board = await this._trelloJs.boards.getBoard({ - id: boardId, - }) + const board = await this._trelloJs.boards + .getBoard({ + id: boardId, + }) + .catch(_useHandleCaughtError('Failed to retrieve board by id')) return ResponseMapping.mapBoard(board) } - @handleErrors('Failed to retrieve all boards') public async getAllBoards(): Promise { - const boards = await this._trelloJs.members.getMemberBoards({ - id: 'me', - }) + const boards = await this._trelloJs.members + .getMemberBoards({ + id: 'me', + }) + .catch(_useHandleCaughtError('Failed to retrieve all boards')) return boards.map(ResponseMapping.mapBoard) } - @handleErrors('Failed to retrieve lists in board') public async getListsInBoard({ boardId }: { boardId: Board['id'] }): Promise { - const lists = await this._trelloJs.boards.getBoardLists({ - id: boardId, - }) + const lists = await this._trelloJs.boards + .getBoardLists({ + id: boardId, + }) + .catch(_useHandleCaughtError('Failed to retrieve lists in board')) return lists.map(ResponseMapping.mapList) } - @handleErrors('Failed to add comment to card') public async addCardComment({ cardId, commentBody }: { cardId: Card['id']; commentBody: string }): Promise { - const comment = await this._trelloJs.cards.addCardComment({ - id: cardId, - text: commentBody, - }) + const comment = await this._trelloJs.cards + .addCardComment({ + id: cardId, + text: commentBody, + }) + .catch(_useHandleCaughtError('Failed to add comment to card')) return ResponseMapping.mapTrelloId(comment.id) } - @handleErrors('Failed to get card by id') public async getCardById({ cardId }: { cardId: Card['id'] }): Promise { - const card = await this._trelloJs.cards.getCard({ - id: cardId, - }) + const card = await this._trelloJs.cards + .getCard({ + id: cardId, + }) + .catch(_useHandleCaughtError('Failed to get card by id')) return ResponseMapping.mapCard(card) } - @handleErrors('Failed to create card') public async createCard({ card, }: { card: Pick & Partial }): Promise { - const newCard = await this._trelloJs.cards.createCard(RequestMapping.mapCreateCard(card)) + const newCard = await this._trelloJs.cards + .createCard(RequestMapping.mapCreateCard(card)) + .catch(_useHandleCaughtError('Failed to create card')) return ResponseMapping.mapCard(newCard) } - @handleErrors('Failed to update card') public async updateCard({ partialCard }: { partialCard: Pick & Partial }): Promise { - const updatedCard = await this._trelloJs.cards.updateCard(RequestMapping.mapUpdateCard(partialCard)) + const updatedCard = await this._trelloJs.cards + .updateCard(RequestMapping.mapUpdateCard(partialCard)) + .catch(_useHandleCaughtError('Failed to update card')) return ResponseMapping.mapCard(updatedCard) } - @handleErrors('Failed to get list by id') public async getListById({ listId }: { listId: List['id'] }): Promise { - const list: TrelloJsModels.List = await this._trelloJs.lists.getList({ - id: listId, - }) + const list = await this._trelloJs.lists + .getList({ + id: listId, + }) + .catch(_useHandleCaughtError('Failed to get list by id')) return ResponseMapping.mapList(list) } - @handleErrors('Failed to get cards in list') public async getCardsInList({ listId }: { listId: List['id'] }): Promise { - const cards = await this._trelloJs.lists.getListCards({ - id: listId, - }) + const cards = await this._trelloJs.lists + .getListCards({ + id: listId, + }) + .catch(_useHandleCaughtError('Failed to get cards in list')) return cards.map(ResponseMapping.mapCard) } - @handleErrors('Failed to get member by id or username') public async getMemberByIdOrUsername({ memberId }: { memberId: Member['id'] | Member['username'] }): Promise { - const member = await this._trelloJs.members.getMember({ - id: memberId, - }) + const member = await this._trelloJs.members + .getMember({ + id: memberId, + }) + .catch(_useHandleCaughtError('Failed to get member by id or username')) return ResponseMapping.mapMember(member) } - @handleErrors('Failed to create webhook') + public async listWebhooks(): Promise { + const rawWebhooks = await this._trelloJs.tokens + .getTokenWebhooks({ + token: this._token, + }) + .catch(_useHandleCaughtError('Failed to list webhooks')) + + const mappedWebhooks = rawWebhooks.map(ResponseMapping.mapWebhook) + const result = webhookSchema.array().safeParse(mappedWebhooks) + + if (!result.success) { + throw new RuntimeError('Unexpected webhook data format received from Trello') + } + + return result.data + } + public async createWebhook({ description, url, @@ -121,28 +165,32 @@ export class TrelloClient { description: string url: string modelId: string - }): Promise { - const webhook = await this._trelloJs.webhooks.createWebhook({ - description, - callbackURL: url, - idModel: modelId, - }) - - return ResponseMapping.mapTrelloId(webhook.id) + }): Promise { + const webhook = await this._trelloJs.webhooks + .createWebhook({ + description, + callbackURL: url, + idModel: modelId, + }) + .catch(_useHandleCaughtError('Failed to create webhook')) + + return ResponseMapping.mapWebhook(webhook) } - @handleErrors('Failed to delete webhook') public async deleteWebhook({ id }: { id: string }): Promise { - await this._trelloJs.webhooks.deleteWebhook({ - id, - }) + await this._trelloJs.webhooks + .deleteWebhook({ + id, + }) + .catch(_useHandleCaughtError('Failed to delete webhook')) } - @handleErrors('Failed to get card members') public async getCardMembers({ cardId }: { cardId: Card['id'] }): Promise { - const members: TrelloJsModels.Member[] = await this._trelloJs.cards.getCardMembers({ - id: cardId, - }) + const members = await this._trelloJs.cards + .getCardMembers({ + id: cardId, + }) + .catch(_useHandleCaughtError('Failed to get card members')) return members.map(ResponseMapping.mapMember) } diff --git a/integrations/trello/src/webhook-events/handler-dispatcher.ts b/integrations/trello/src/webhook-events/handler-dispatcher.ts index f0d59b05e3d..11c31cd702c 100644 --- a/integrations/trello/src/webhook-events/handler-dispatcher.ts +++ b/integrations/trello/src/webhook-events/handler-dispatcher.ts @@ -1,13 +1,12 @@ import { default as sdk, z } from '@botpress/sdk' -import { events } from 'definitions/events' import { - type allSupportedEvents, + events, + type AllSupportedEvents, commentCardEventSchema, - type genericWebhookEvent, + type GenericWebhookEvent, genericWebhookEventSchema, TRELLO_EVENTS, -} from 'definitions/schemas' -import { States } from 'definitions/states' +} from 'definitions/events' import { CardCommentHandler } from './handlers/card-comment' import * as bp from '.botpress' @@ -31,7 +30,7 @@ const _parseWebhookEvent = ({ req }: { req: sdk.Request }) => { throw new sdk.RuntimeError('Invalid webhook event body', error) } - return { ...data, action: { ...data.action, type: data.action.type as allSupportedEvents } } + return { ...data, action: { ...data.action, type: data.action.type as AllSupportedEvents } } } const _ensureWebhookIsAuthenticated = async ({ @@ -39,13 +38,13 @@ const _ensureWebhookIsAuthenticated = async ({ ctx, client, }: { - parsedWebhookEvent: genericWebhookEvent + parsedWebhookEvent: GenericWebhookEvent ctx: bp.Context client: bp.Client }) => { const { state } = await client.getState({ type: 'integration', - name: States.webhookState, + name: 'webhookState', id: ctx.integrationId, }) @@ -54,7 +53,7 @@ const _ensureWebhookIsAuthenticated = async ({ } } -const _handleWebhookEvent = async (props: { parsedWebhookEvent: genericWebhookEvent; client: bp.Client }) => { +const _handleWebhookEvent = async (props: { parsedWebhookEvent: GenericWebhookEvent; client: bp.Client }) => { await Promise.allSettled([_handleCardComments(props), _publishEventToBotpress(props)]) } @@ -62,7 +61,7 @@ const _handleCardComments = async ({ parsedWebhookEvent, client, }: { - parsedWebhookEvent: genericWebhookEvent + parsedWebhookEvent: GenericWebhookEvent client: bp.Client }) => { if (!parsedWebhookEvent || parsedWebhookEvent.action.type !== TRELLO_EVENTS.commentCard) { @@ -77,7 +76,7 @@ const _publishEventToBotpress = async ({ parsedWebhookEvent, client, }: { - parsedWebhookEvent: genericWebhookEvent + parsedWebhookEvent: GenericWebhookEvent client: bp.Client }) => { if (!parsedWebhookEvent || !Reflect.has(TRELLO_EVENTS, parsedWebhookEvent.action.type)) { diff --git a/integrations/trello/src/webhook-events/handlers/card-comment.ts b/integrations/trello/src/webhook-events/handlers/card-comment.ts index bb1847f2a48..0e1347c5e4f 100644 --- a/integrations/trello/src/webhook-events/handlers/card-comment.ts +++ b/integrations/trello/src/webhook-events/handlers/card-comment.ts @@ -1,4 +1,4 @@ -import { CommentCardEvent } from 'definitions/schemas' +import { type CommentCardEvent } from 'definitions/events' import * as bp from '.botpress' type TrelloMessageData = { diff --git a/integrations/trello/src/webhook-events/webhook-lifecycle-manager.ts b/integrations/trello/src/webhook-events/webhook-lifecycle-manager.ts deleted file mode 100644 index c9678aba7f0..00000000000 --- a/integrations/trello/src/webhook-events/webhook-lifecycle-manager.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { States } from 'definitions/states' -import { integrationName } from '../../package.json' -import { TrelloClient } from '../trello-api/trello-client' -import * as bp from '.botpress' - -export namespace WebhookLifecycleManager { - export const registerTrelloWebhookIfNotExists = async ({ - ctx, - client, - logger, - webhookUrl, - }: { - ctx: bp.Context - client: bp.Client - logger: bp.Logger - webhookUrl: string - }): Promise => { - if (!ctx.configuration.trelloBoardId) { - logger.forBot().warn('No Trello board id provided. Skipping webhook registration...') - return - } - - if (await _getWebhookId(client, ctx)) { - logger.forBot().debug('Webhook already registered. Skipping registration...') - return - } - - await _registerTrelloWebhook(ctx, client, logger, webhookUrl) - } - - export const unregisterTrelloWebhookIfExists = async ({ - ctx, - client, - logger, - }: { - ctx: bp.Context - client: bp.Client - logger: bp.Logger - }): Promise => { - const webhookId = await _getWebhookId(client, ctx) - - if (!webhookId) { - logger.forBot().warn('No webhook is currently registered for this integration. Skipping unregistration...') - return - } - - await _unregisterTrelloWebhook(ctx, client, logger, webhookId) - } - - const _getWebhookId = async (client: bp.Client, ctx: bp.Context): Promise => { - try { - const webhookState = await client.getState({ - type: 'integration', - name: States.webhookState, - id: ctx.integrationId, - }) - - return webhookState.state.payload.trelloWebhookId ?? null - } catch { - return null - } - } - - const _registerTrelloWebhook = async ( - ctx: bp.Context, - client: bp.Client, - logger: bp.Logger, - webhookUrl: string - ): Promise => { - logger.forBot().info('Registering Trello webhook...') - - const trelloClient = new TrelloClient({ ctx }) - const webhookId = await trelloClient.createWebhook({ - description: integrationName + ctx.integrationId, - url: webhookUrl, - modelId: ctx.configuration.trelloBoardId as string, - }) - await _setWebhookId(client, ctx, webhookId) - } - - const _setWebhookId = async (client: bp.Client, ctx: bp.Context, webhookId: string): Promise => { - await client.setState({ - type: 'integration', - name: States.webhookState, - id: ctx.integrationId, - payload: { - trelloWebhookId: webhookId, - }, - }) - } - - const _unregisterTrelloWebhook = async ( - ctx: bp.Context, - client: bp.Client, - logger: bp.Logger, - webhookId: string - ): Promise => { - logger.forBot().info(`Unregistering webhook id ${webhookId} on Trello...`) - - const trelloClient = new TrelloClient({ ctx }) - await trelloClient.deleteWebhook({ id: webhookId }) - - logger.forBot().info(`Webhook id ${webhookId} unregistered`) - await _setWebhookId(client, ctx, webhookId) - } -} diff --git a/integrations/trello/src/webhook-lifecycle-utils.ts b/integrations/trello/src/webhook-lifecycle-utils.ts new file mode 100644 index 00000000000..59ab415433c --- /dev/null +++ b/integrations/trello/src/webhook-lifecycle-utils.ts @@ -0,0 +1,105 @@ +import { Webhook } from 'definitions/schemas' +import { WebhookIdState } from 'definitions/states' +import { integrationName } from '../package.json' +import { TrelloClient } from './trello-api/trello-client' +import * as bp from '.botpress' + +const _setWebhookId = async ({ ctx, client }: bp.CommonHandlerProps, webhookId: WebhookIdState): Promise => { + await client.setState({ + type: 'integration', + name: 'webhookState', + id: ctx.integrationId, + payload: { + trelloWebhookId: webhookId, + }, + }) +} + +const _clearWebhookId = (props: bp.CommonHandlerProps): Promise => _setWebhookId(props, null) + +const _registerWebhook = async ( + props: bp.CommonHandlerProps, + webhookUrl: string, + modelId: string, + trelloClient = new TrelloClient({ ctx: props.ctx }) +): Promise => { + const { ctx, logger } = props + logger.forBot().info('Registering Trello webhook...') + + const newWebhook = await trelloClient.createWebhook({ + description: integrationName + ctx.integrationId, + url: webhookUrl, + modelId, + }) + + await _setWebhookId(props, newWebhook.id) +} + +export const registerTrelloWebhookIfNotExists = async ( + props: bp.CommonHandlerProps, + webhookUrl: string, + trelloClient = new TrelloClient({ ctx: props.ctx }) +): Promise => { + const { ctx, logger } = props + const { trelloBoardId } = ctx.configuration + if (!trelloBoardId) { + logger.forBot().warn('No Trello board id provided. Skipping webhook registration...') + return + } + + const registeredWebhooks = await trelloClient.listWebhooks() + const isWebhookRegistered = registeredWebhooks.some( + (webhook) => webhook.callbackUrl === webhookUrl && webhook.modelId === trelloBoardId + ) + if (isWebhookRegistered) { + logger.forBot().debug('Webhook already registered. Skipping registration...') + return + } + + await _registerWebhook(props, webhookUrl, trelloBoardId, trelloClient) +} + +const _deleteWebhooksInList = async (trelloClient: TrelloClient, webhooks: Webhook[]): Promise => { + for (const webhook of webhooks) { + await trelloClient.deleteWebhook({ id: webhook.id }) + } +} + +/** Removes webhooks for models that we no longer want to track */ +export const cleanupStaleWebhooks = async ( + props: bp.CommonHandlerProps, + webhookUrl: string, + trelloClient = new TrelloClient({ ctx: props.ctx }) +): Promise => { + const registeredWebhooks = await trelloClient.listWebhooks() + + const { trelloBoardId } = props.ctx.configuration + const staleWebhooks = registeredWebhooks.filter((webhook) => { + return webhook.callbackUrl === webhookUrl && webhook.modelId !== trelloBoardId + }) + + await _deleteWebhooksInList(trelloClient, staleWebhooks) + + if (staleWebhooks.length > 0) { + props.logger.forBot().info(`Cleaned up ${staleWebhooks.length} stale Trello webhook(s).`) + } +} + +export const unregisterTrelloWebhooks = async ( + props: bp.CommonHandlerProps, + webhookUrl: string, + trelloClient = new TrelloClient({ ctx: props.ctx }) +) => { + const registeredWebhooks = await trelloClient.listWebhooks() + const webhooksToDelete = registeredWebhooks.filter((webhook) => { + return webhook.callbackUrl === webhookUrl + }) + + await _deleteWebhooksInList(trelloClient, webhooksToDelete) + + if (webhooksToDelete.length > 0) { + props.logger.forBot().info(`Deleted ${webhooksToDelete.length} Trello webhook(s).`) + } + + await _clearWebhookId(props) +} diff --git a/packages/common/src/error-handling/try-catch-wrapper.ts b/packages/common/src/error-handling/try-catch-wrapper.ts index 2ae29ac2081..a26230656df 100644 --- a/packages/common/src/error-handling/try-catch-wrapper.ts +++ b/packages/common/src/error-handling/try-catch-wrapper.ts @@ -119,7 +119,7 @@ export const createErrorHandlingDecorator = } catch (thrown: unknown) { await asyncFnWrapperWithErrorRedaction(async () => { throw thrown - }, errorMessage)() + }, `${errorMessage}: ${thrown}`)() } } return diff --git a/packages/llmz/package.json b/packages/llmz/package.json index f7d23319cfb..9a79534ec12 100644 --- a/packages/llmz/package.json +++ b/packages/llmz/package.json @@ -1,8 +1,8 @@ { "name": "llmz", "type": "module", - "description": "LLMz – An LLM-native Typescript VM built on top of Zui", - "version": "0.0.38", + "description": "LLMz - An LLM-native Typescript VM built on top of Zui", + "version": "0.0.39", "types": "./dist/index.d.ts", "main": "./dist/index.cjs", "module": "./dist/index.js", diff --git a/packages/llmz/src/llmz.ts b/packages/llmz/src/llmz.ts index 588e8588df9..5be15836e5c 100644 --- a/packages/llmz/src/llmz.ts +++ b/packages/llmz/src/llmz.ts @@ -272,7 +272,7 @@ export const _executeContext = async (props: ExecutionProps): Promise void)[] = [] const ctx = new Context({