chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:34:48 +08:00
commit 77bb5bf71f
3762 changed files with 353249 additions and 0 deletions
@@ -0,0 +1,49 @@
import { ActionDefinition, z } from '@botpress/sdk'
import { boardSchema } from 'definitions/schemas'
import { hasBoardId, noInput } 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: z.object({
board: boardSchema.title('Trello Board').describe('The details of the Trello board associated with the given 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: z.object({
boards: z
.array(boardSchema)
.title('Trello Boards')
.describe('A list of boards that match the given display 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: z.object({
boards: z.array(boardSchema).title('Trello Boards').describe('A list of Trello boards'),
}),
},
} as const satisfies ActionDefinition
@@ -0,0 +1,282 @@
import { ActionDefinition, z } from '@botpress/sdk'
import { cardSchema, listSchema, trelloIdSchema } from 'definitions/schemas'
import { hasCardId, hasListId, hasMessage } 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: z.object({
card: cardSchema.title('Trello Card').describe("The Trello card that's associated with the given card ID"),
}),
},
} as const satisfies ActionDefinition
export const getCardsByDisplayName = {
title: 'Find cards by name',
description: 'Find all cards 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: z.object({
cards: z.array(cardSchema).title('Trello Cards').describe('A list of cards that match the given card 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: z.object({
cards: z
.array(cardSchema)
.title('Trello Cards')
.describe('An array of cards that are contained within the given 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('The body text of the new card'),
memberIds: z
.array(trelloIdSchema)
.optional()
.title('Member IDs')
.describe('Members to add to the card (Optional). This should be a list of member IDs.'),
labelIds: z
.array(trelloIdSchema)
.optional()
.title('Label IDs')
.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).'),
completionStatus: z
.enum(['Complete', 'Incomplete'])
.default('Incomplete')
.title('Completion Status')
.describe(
'Whether the card should be marked as complete (Optional). Enter "Complete" or "Incomplete" (without quotes).'
),
})
.describe('Input schema for creating a new card'),
},
output: {
schema: z
.object({
message: hasMessage.shape.message
.title('Action message')
.describe('A message that says if the card was successfully created or not'),
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({
cardName: cardSchema.shape.name
.optional()
.title('Card Name')
.describe('The name of the card (Optional) (e.g. "My Test Card"). Leave empty to keep the current name.'),
cardBody: cardSchema.shape.description
.optional()
.title('Card Body')
.describe('The new body text of the card (Optional). Leave empty to keep the current body.'),
lifecycleStatus: z
.enum(['Open', 'Archived'])
.optional()
.title('Lifecycle Status')
.describe(
'Whether the card should be archived (Optional). Enter "Open", "Archived" (without quotes), or leave empty to keep the previous status.'
),
completionStatus: z
.enum(['Complete', 'Incomplete'])
.optional()
.title('Completion Status')
.describe(
'Whether the card should be marked as complete (Optional). Enter "Complete", "Incomplete" (without quotes), or leave empty to keep the previous status.'
),
memberIdsToAdd: z
.array(trelloIdSchema)
.optional()
.title('Member IDs 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.'
),
memberIdsToRemove: z
.array(trelloIdSchema)
.optional()
.title('Member IDs 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.'
),
labelIdsToAdd: z
.array(trelloIdSchema)
.optional()
.title('Label IDs 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.'
),
labelIdsToRemove: z
.array(trelloIdSchema)
.optional()
.title('Label IDs 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
.nullable()
.optional()
.title('Due Date')
.describe(
'The due date of the card in ISO 8601 format (Optional). Set to null to remove the due date or leave empty to keep the current due date.'
),
listId: listSchema.shape.id
.optional()
.title('List ID')
.describe('Unique identifier of the list in which the card will be moved to'),
/** Note: The validation for "verticalPosition" must be done in the action
* implementation, since studio does not support union types in inputs yet
* and the JSON schema generation does not support zod runtime validation
* like "refine" (at the time of writing, 2026-01-22). */
verticalPosition: z
.string()
.optional()
.title('Vertical Position')
.describe(
'The new position of the card in the list, either "top", "bottom", or a stringified float (Optional). Leave empty to keep the current position.'
),
})
.describe('Input schema for creating a new card'),
},
output: {
schema: hasMessage.describe('Output schema for updating a card'),
},
} as const satisfies ActionDefinition
export const deleteCard = {
title: 'Delete card',
description: 'Deletes a card by its unique identifier',
input: {
schema: z.object({
cardId: cardSchema.shape.id.title('Card ID').describe('ID of the card to delete'),
hardDelete: z
.boolean()
.default(false)
.title('Hard Delete')
.describe(
'Whether to perform a hard delete or a soft delete (archive). Set to true for hard delete, false for soft delete.'
),
}),
},
output: {
schema: z.object({}),
},
} 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: z.object({
message: z
.string()
.title('Action message')
.describe('A message that says if the comment was successfully created or not'),
newCommentId: trelloIdSchema.title('New Comment ID').describe('Unique identifier of the newly created comment'),
}),
},
} 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 to'),
/** Note: The validation for "newVerticalPosition" must be done in the action
* implementation, since studio does not support union types in inputs yet
* and the JSON schema generation does not support zod runtime validation
* like "refine" (at the time of writing, 2026-01-22). */
newVerticalPosition: z
.string()
.optional()
.title('New Vertical Position')
.describe(
'The new position of the card in the list, either "top", "bottom", or a stringified float (Optional). Leave empty to keep the current position.'
),
}),
},
output: {
schema: hasMessage.describe('Output schema for moving a card to a list'),
},
} as const satisfies ActionDefinition
@@ -0,0 +1,22 @@
import { z } from '@botpress/sdk'
import { boardSchema, listSchema, cardSchema } 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().title('Output message').describe('Output message'),
})
@@ -0,0 +1,48 @@
import { type IntegrationDefinitionProps } from '@botpress/sdk'
import { getAllBoards, getBoardById, getBoardsByDisplayName } from './board-actions'
import {
addCardComment,
createCard,
deleteCard,
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,
deleteCard,
addCardComment,
moveCardUp,
moveCardDown,
moveCardToList,
// === Member Actions ===
getMemberByIdOrUsername,
getBoardMembersByDisplayName,
getAllBoardMembers,
getAllCardMembers,
} as const satisfies NonNullable<IntegrationDefinitionProps['actions']>
@@ -0,0 +1,52 @@
import { ActionDefinition, z } from '@botpress/sdk'
import { listSchema } from 'definitions/schemas'
import { hasBoardId, hasListId } 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: z.object({
list: listSchema.title('Trello List').describe("The Trello list that's associated with the given list 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: z.object({
lists: z
.array(listSchema)
.title('Trello Lists')
.describe('A set of lists that are associated with the given board ID and match the given display 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: z.object({
lists: z
.array(listSchema)
.title('Trello Lists')
.describe('A set of all the lists that are associated with the given board ID'),
}),
},
} as const satisfies ActionDefinition
@@ -0,0 +1,69 @@
import { ActionDefinition, z } from '@botpress/sdk'
import { boardSchema, memberSchema } from 'definitions/schemas'
import { hasBoardId, hasCardId } 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: z.object({
member: memberSchema
.title('Trello Member')
.describe('The Trello member who is associated with the specified member 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: z.object({
members: z
.array(memberSchema)
.title('Card Members')
.describe('A list of members who have been assigned to the 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: z.object({
members: z.array(memberSchema).title('Board Members').describe('A list of members who have access to the 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'),
}),
},
output: {
schema: z.object({
members: z.array(memberSchema).title('Board Members').describe('A list of members that match the specified name'),
}),
},
} as const satisfies ActionDefinition
@@ -0,0 +1,43 @@
import { IntegrationDefinitionProps, messages } from '@botpress/sdk'
export const channels = {
cardComments: {
title: 'Card Comments',
description: 'Channel for managing comments on Trello cards',
messages: {
text: messages.defaults.text,
},
message: {
tags: {
commentId: {
title: 'Comment ID',
description: 'unique identifier of the comment',
},
},
},
conversation: {
tags: {
cardId: {
title: 'Card ID',
description: 'unique identifier of the card',
},
cardName: {
title: 'Card name',
description: 'display name of the card',
},
listId: {
title: 'List ID',
description: 'unique identifier of the list',
},
listName: {
title: 'Card ID',
description: 'display name of the list',
},
lastCommentId: {
title: 'Last comment ID',
description: 'unique identifier of the last comment sent by the integration in this conversation',
},
},
},
},
} as const satisfies NonNullable<IntegrationDefinitionProps['channels']>
@@ -0,0 +1,33 @@
import { IntegrationDefinitionProps, z } from '@botpress/sdk'
import { trelloIdRegex } from './schemas'
const _optionalTrelloIdRegex = new RegExp(`^$|${trelloIdRegex.source}`)
export const configuration = {
schema: z.object({
trelloApiKey: z
.string()
.title('Trello API Key')
.describe("Can be found in the app's settings within the Trello apps admin page")
.secret(),
trelloApiToken: z
.string()
.title('Trello API Token')
.describe('Can be obtained by granting access to the application on Trello')
.secret(),
trelloBoardId: z
.string()
.regex(_optionalTrelloIdRegex)
.optional()
.title('Trello Board ID')
.describe('Unique identifier of the board to watch for events on Trello'),
trelloApiSecret: z
.string()
.secret()
.optional()
.title('Trello API Secret')
.describe(
"Can be found in the app's settings within the Trello apps admin page. (Only used if the Trello Board ID is provided)"
),
}),
} as const satisfies NonNullable<IntegrationDefinitionProps['configuration']>
@@ -0,0 +1,30 @@
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,
},
list: {
title: 'List',
description: 'A list in a Trello board',
schema: listSchema,
},
board: {
title: 'Board',
description: 'A Trello board',
schema: boardSchema,
},
boardMember: {
title: 'Board Member',
description: 'A member of a Trello board',
schema: memberSchema,
},
cardMember: {
title: 'Card Member',
description: 'A member assigned to a Trello card',
schema: memberSchema,
},
} as const satisfies IntegrationDefinitionProps['entities']
@@ -0,0 +1,36 @@
import { z } from '@botpress/sdk'
import { boardSchema, cardSchema, listSchema, trelloIdSchema } from '../schemas'
import { botpressEventDataSchema, pickIdAndName } from './common'
export const attachmentAddedToCardEventSchema = botpressEventDataSchema.extend({
board: pickIdAndName(boardSchema).title('Board').describe('Board where the card was updated'),
list: pickIdAndName(listSchema).title('List').describe('List where the card was updated'),
card: pickIdAndName(cardSchema).title('Card').describe('Card that was updated'),
attachment: z
.object({
id: trelloIdSchema.title('Attachment ID').describe('Unique identifier of the attachment'),
name: z.string().title('Attachment Name').describe('Name of the attachment'),
url: z.string().url().title('Attachment URL').describe('URL of the attachment'),
previewUrl: z.string().url().optional().title('Attachment Preview URL').describe('URL of the attachment preview'),
previewUrl2x: z
.string()
.url()
.optional()
.title('Attachment Preview URL 2x')
.describe('URL of the attachment preview at up to 2x the resolution'),
})
.title('Attachment')
.describe('Attachment that was added to the card'),
})
export const attachmentRemovedFromCardEventSchema = botpressEventDataSchema.extend({
board: pickIdAndName(boardSchema).title('Board').describe('Board where the card was updated'),
card: pickIdAndName(cardSchema).title('Card').describe('Card that was updated'),
attachment: z
.object({
id: trelloIdSchema.title('Attachment ID').describe('Unique identifier of the attachment'),
name: z.string().title('Attachment Name').describe('Name of the attachment'),
})
.title('Attachment')
.describe('Attachment that was deleted from the card'),
})
@@ -0,0 +1,31 @@
import { z } from '@botpress/sdk'
import { boardSchema, cardSchema, listSchema, trelloIdSchema } from '../schemas'
import { botpressEventDataSchema, pickIdAndName } from './common'
const _cardCommentSchema = z.object({
id: trelloIdSchema.title('Comment ID').describe('Unique identifier of the comment'),
text: z.string().title('Comment Text').describe('Text of the comment'),
})
export const cardCommentCreatedEventSchema = botpressEventDataSchema.extend({
board: pickIdAndName(boardSchema).title('Board').describe('Board where the card was updated'),
list: pickIdAndName(listSchema).title('List').describe('List where the card was updated'),
card: pickIdAndName(cardSchema).title('Card').describe('Card that was updated'),
comment: _cardCommentSchema.title('New Comment').describe('Comment that was added to the card'),
})
export const cardCommentUpdatedEventSchema = botpressEventDataSchema.extend({
board: pickIdAndName(boardSchema).title('Board').describe('Board where the card was updated'),
card: pickIdAndName(cardSchema).title('Card').describe('Card that was updated'),
comment: _cardCommentSchema.title('Updated Comment').describe('Comment that was updated'),
old: _cardCommentSchema.omit({ id: true }).title('Old Comment').describe('The previous data of the comment'),
})
export const cardCommentDeletedEventSchema = botpressEventDataSchema.extend({
board: pickIdAndName(boardSchema).title('Board').describe('Board where the card was updated'),
card: pickIdAndName(cardSchema).title('Card').describe('Card that was updated'),
comment: _cardCommentSchema
.pick({ id: true })
.title('Deleted Comment')
.describe('Comment that was deleted from the card'),
})
@@ -0,0 +1,56 @@
import { z } from '@botpress/sdk'
import { boardSchema, cardSchema, listSchema, trelloIdSchema } from '../schemas'
import { botpressEventDataSchema, dueReminderSchema, pickIdAndName } from './common'
export const cardCreatedEventSchema = botpressEventDataSchema.extend({
board: pickIdAndName(boardSchema).title('Board').describe('Board where the card was created'),
list: pickIdAndName(listSchema).title('List').describe('List where the card was created'),
card: pickIdAndName(cardSchema).title('Card').describe('Card that was created'),
})
const _baseCardUpdateDataSchema = z
.object({
id: trelloIdSchema.title('Card ID').describe('Unique identifier of the card'),
name: z.string().title('Card Name').describe('Name of the card'),
description: z.string().title('Card Description').describe('Description of the card'),
listId: trelloIdSchema.title('List ID').describe('Unique identifier of the list where the card is located'),
labelIds: z.array(trelloIdSchema).title('Label IDs').describe('Labels attached to the card'),
verticalPosition: z.number().title('Card Position').describe('Position of the card within the list'),
startDate: z.string().datetime().nullable().title('Start Date').describe('Start date of the card'),
dueDate: z.string().datetime().nullable().title('Due Date').describe('Due date of the card'),
dueDateReminder: dueReminderSchema,
isCompleted: z.boolean().title('Is Completed').describe('Whether the card is completed'),
isArchived: z.boolean().title('Is Archived').describe('Whether the card is archived'),
})
.passthrough()
.partial()
export const cardUpdatedEventSchema = botpressEventDataSchema.extend({
board: pickIdAndName(boardSchema).title('Board').describe('Board where the card was updated'),
card: _baseCardUpdateDataSchema.required({ id: true, name: true }).title('Card').describe('Card that was updated'),
old: _baseCardUpdateDataSchema.omit({ id: true }).title('Old').describe('Previous state of the card'),
// Only excluded/optional when the card is moved between lists
list: pickIdAndName(listSchema).optional().title('List').describe('List where the card was updated'),
// Only included if the card was moved between lists
listBefore: pickIdAndName(listSchema)
.optional()
.title('List Before')
.describe('Previous list where the card was located'),
// Only included if the card was moved between lists
listAfter: pickIdAndName(listSchema)
.optional()
.title('List After')
.describe('New list where the card is now located'),
})
export const cardDeletedEventSchema = botpressEventDataSchema.extend({
board: pickIdAndName(boardSchema).title('Board').describe('Board where the card was deleted'),
list: pickIdAndName(listSchema).title('List').describe('List where the card was deleted'),
card: cardSchema.pick({ id: true }).title('Card').describe('Card that was deleted'),
})
export const cardVotesUpdatedEventSchema = botpressEventDataSchema.extend({
board: pickIdAndName(boardSchema).title('Board').describe('Board where the card was updated'),
card: pickIdAndName(cardSchema).title('Card').describe('Card that was updated'),
voted: z.boolean().title('Has Voted').describe('Whether the user voted on the card'),
})
@@ -0,0 +1,21 @@
import { z } from '@botpress/sdk'
import { boardSchema, cardSchema, trelloIdSchema } from '../schemas'
import { botpressEventDataSchema, pickIdAndName } from './common'
export const labelSchema = z.object({
id: trelloIdSchema.title('Label ID').describe('Unique identifier of the label'),
name: z.string().title('Label Name').describe('Name of the label'),
color: z.string().title('Label Color').describe('Color of the label'),
})
export const labelAddedToCardEventSchema = botpressEventDataSchema.extend({
board: pickIdAndName(boardSchema).title('Board').describe('Board where the card was modified'),
card: pickIdAndName(cardSchema).title('Card').describe('Card that was modified'),
label: labelSchema.title('Label').describe('Label that was added to the card'),
})
export const labelRemovedFromCardEventSchema = botpressEventDataSchema.extend({
board: pickIdAndName(boardSchema).title('Board').describe('Board where the card was modified'),
card: pickIdAndName(cardSchema).title('Card').describe('Card that was modified'),
label: labelSchema.title('Label').describe('Label that was removed from the card'),
})
@@ -0,0 +1,64 @@
import { z } from '@botpress/sdk'
import { boardSchema, cardSchema, trelloIdSchema } from '../schemas'
import { botpressEventDataSchema, dueReminderSchema, pickIdAndName } from './common'
export const checklistSchema = z.object({
id: trelloIdSchema.title('Checklist ID').describe('Unique identifier of the checklist'),
name: z.string().title('Checklist Name').describe('Name of the checklist'),
})
const _basicChecklistItemSchema = z.object({
id: trelloIdSchema.title('Checklist Item ID').describe('Unique identifier of the checklist item'),
name: z.string().title('Checklist Item Name').describe('Name of the checklist item'),
isCompleted: z.boolean().title('Is Completed').describe('Indicates if the checklist item is marked as completed'),
textData: z
.object({
emoji: z.object({}).title('Checklist Item Emoji').describe('Emoji of the checklist item'),
})
.title('Text data')
.describe('Text data of the checklist item'),
})
export const checklistAddedToCardEventSchema = botpressEventDataSchema.extend({
board: pickIdAndName(boardSchema).title('Board').describe('Board where the card was updated'),
card: pickIdAndName(cardSchema).title('Card').describe('Card that was updated'),
checklist: checklistSchema.title('Checklist').describe('Checklist that was added to the card'),
})
export const checklistItemCreatedEventSchema = botpressEventDataSchema.extend({
board: pickIdAndName(boardSchema).title('Board').describe('Board where the card was updated'),
card: pickIdAndName(cardSchema).title('Card').describe('Card that was updated'),
checklist: checklistSchema.title('Checklist').describe('Checklist where the item was added'),
checklistItem: _basicChecklistItemSchema.title('Checklist Item').describe('The checklist item that was added'),
})
const _baseChecklistItemUpdateDataSchema = _basicChecklistItemSchema.extend({
dueDateReminder: dueReminderSchema.optional(),
dueDate: z.string().datetime().nullable().optional().title('Due Date').describe('Due date of the checklist item'),
})
export const checklistItemUpdatedEventSchema = botpressEventDataSchema.extend({
board: pickIdAndName(boardSchema).title('Board').describe('Board where the card was updated'),
card: pickIdAndName(cardSchema).title('Card').describe('Card that was updated'),
checklist: checklistSchema.title('Checklist').describe('Checklist where the item was updated'),
checklistItem: _baseChecklistItemUpdateDataSchema.title('Checklist Item').describe('Checklist item that was updated'),
old: _baseChecklistItemUpdateDataSchema
.omit({ id: true })
.partial()
.title('Old Checklist Item')
.describe('The previous data of the checklist item'),
})
export const checklistItemStatusUpdatedEventSchema = botpressEventDataSchema.extend({
board: pickIdAndName(boardSchema).title('Board').describe('Board where the card was updated'),
card: pickIdAndName(cardSchema).title('Card').describe('Card that was updated'),
checklist: checklistSchema.title('Checklist').describe('Checklist where the item was updated'),
checklistItem: _basicChecklistItemSchema.title('Checklist Item').describe('Checklist item that was updated'),
})
export const checklistItemDeletedEventSchema = botpressEventDataSchema.extend({
board: pickIdAndName(boardSchema).title('Board').describe('Board where the card was updated'),
card: pickIdAndName(cardSchema).title('Card').describe('Card that was updated'),
checklist: checklistSchema.title('Checklist').describe('Checklist where the item was removed'),
checklistItem: _basicChecklistItemSchema.title('Checklist Item').describe('Checklist item that was removed'),
})
@@ -0,0 +1,69 @@
import { z } from '@botpress/sdk'
import { trelloIdSchema } from '../schemas'
export enum TrelloEventType {
// ---- Card Events ----
CARD_CREATED = 'createCard',
CARD_UPDATED = 'updateCard',
CARD_DELETED = 'deleteCard',
CARD_VOTES_UPDATED = 'voteOnCard',
// ---- Card Comment Events ----
CARD_COMMENT_CREATED = 'commentCard',
CARD_COMMENT_UPDATED = 'updateComment',
CARD_COMMENT_DELETED = 'deleteComment',
// ---- Card Label Events ----
LABEL_ADDED_TO_CARD = 'addLabelToCard',
LABEL_REMOVED_FROM_CARD = 'removeLabelFromCard',
// ---- Card Attachment Events ----
ATTACHMENT_ADDED_TO_CARD = 'addAttachmentToCard',
ATTACHMENT_REMOVED_FROM_CARD = 'deleteAttachmentFromCard',
// ---- Checklist Events ----
CHECKLIST_ADDED_TO_CARD = 'addChecklistToCard',
CHECKLIST_ITEM_CREATED = 'createCheckItem',
CHECKLIST_ITEM_UPDATED = 'updateCheckItem',
CHECKLIST_ITEM_DELETED = 'deleteCheckItem',
CHECKLIST_ITEM_STATUS_UPDATED = 'updateCheckItemStateOnCard',
// ---- Member Events ----
MEMBER_ADDED_TO_CARD = 'addMemberToCard',
MEMBER_REMOVED_FROM_CARD = 'removeMemberFromCard',
}
type IdAndNameSchema = z.ZodObject<{ id: z.ZodString; name: z.ZodString }>
export const pickIdAndName = <T extends IdAndNameSchema>(schema: T) => schema.pick({ id: true, name: true })
/** The number of minutes before the due date when a reminder will be sent.
*
* @remark When the value is "-1", it means no due date reminder is set. */
export const dueReminderSchema = z
.number()
.int('Due date reminder is not an integer')
.min(-1)
.title('Due Date Reminder')
.describe('The number of minutes before the due date when a reminder will be sent')
export const botpressEventDataSchema = z.object({
eventId: trelloIdSchema.title('Event ID').describe('Unique identifier of the event'),
actor: z
.union([
z.object({
id: trelloIdSchema.title('Actor ID').describe('Unique identifier of the actor who triggered the event'),
type: z
.literal('member')
.title('Actor Type')
.describe('The type of the actor (e.g. member or app) who triggered the event'),
name: z.string().title('Actor Name').describe('The name of the actor (e.g. member) who triggered the event'),
}),
z.object({
id: trelloIdSchema.title('Actor ID').describe('Unique identifier of the actor who triggered the event'),
type: z
.literal('app')
.title('Actor Type')
.describe('The type of the actor (e.g. member or app) who triggered the event'),
}),
])
.title('Actor')
.describe('The actor (e.g. member or app) who triggered the event'),
dateCreated: z.string().datetime().title('Date Created').describe('The datetime when the event was triggered'),
})
export type CommonEventData = z.infer<typeof botpressEventDataSchema>
@@ -0,0 +1,136 @@
import { type IntegrationDefinitionProps } from '@botpress/sdk'
import { attachmentAddedToCardEventSchema, attachmentRemovedFromCardEventSchema } from './card-attachment-events'
import {
cardCommentCreatedEventSchema,
cardCommentDeletedEventSchema,
cardCommentUpdatedEventSchema,
} from './card-comment-events'
import {
cardCreatedEventSchema,
cardDeletedEventSchema,
cardUpdatedEventSchema,
cardVotesUpdatedEventSchema,
} from './card-events'
import { labelAddedToCardEventSchema, labelRemovedFromCardEventSchema } from './card-label-events'
import {
checklistAddedToCardEventSchema,
checklistItemCreatedEventSchema,
checklistItemDeletedEventSchema,
checklistItemUpdatedEventSchema,
checklistItemStatusUpdatedEventSchema,
} from './checklist-events'
import { CommonEventData, TrelloEventType } from './common'
import { memberAddedToCardEventSchema, memberRemovedFromCardEventSchema } from './member-events'
export const events = {
// ===============================
// Card Events
// ===============================
[TrelloEventType.CARD_CREATED]: {
title: 'Card Created',
description: 'Triggered when a card is created',
schema: cardCreatedEventSchema,
},
[TrelloEventType.CARD_UPDATED]: {
title: 'Card Updated',
description: 'Triggered when a card is updated',
schema: cardUpdatedEventSchema,
},
[TrelloEventType.CARD_DELETED]: {
title: 'Card Deleted',
description: 'Triggered when a card is deleted',
schema: cardDeletedEventSchema,
},
[TrelloEventType.CARD_VOTES_UPDATED]: {
title: 'Card Votes Updated',
description: 'Triggered when a vote is added to or removed from a card',
schema: cardVotesUpdatedEventSchema,
},
// ===============================
// Card Comment Events
// ===============================
[TrelloEventType.CARD_COMMENT_CREATED]: {
title: 'Comment Created',
description: 'Triggered when a comment is added to a card',
schema: cardCommentCreatedEventSchema,
},
[TrelloEventType.CARD_COMMENT_UPDATED]: {
title: 'Comment Updated',
description: 'Triggered when a comment is updated on a card',
schema: cardCommentUpdatedEventSchema,
},
[TrelloEventType.CARD_COMMENT_DELETED]: {
title: 'Comment Deleted',
description: 'Triggered when a comment is deleted from a card',
schema: cardCommentDeletedEventSchema,
},
// ===============================
// Card Label Events
// ===============================
[TrelloEventType.LABEL_ADDED_TO_CARD]: {
title: 'Card Label Added',
description: 'Triggered when a label is added to a card',
schema: labelAddedToCardEventSchema,
},
[TrelloEventType.LABEL_REMOVED_FROM_CARD]: {
title: 'Card Label Removed',
description: 'Triggered when a label is removed from a card',
schema: labelRemovedFromCardEventSchema,
},
// ================================
// Card Attachment Events
// ================================
[TrelloEventType.ATTACHMENT_ADDED_TO_CARD]: {
title: 'Card Attachment Added',
description: 'Triggered when an attachment is added to a card',
schema: attachmentAddedToCardEventSchema,
},
[TrelloEventType.ATTACHMENT_REMOVED_FROM_CARD]: {
title: 'Card Attachment Removed',
description: 'Triggered when an attachment is removed from a card',
schema: attachmentRemovedFromCardEventSchema,
},
// ================================
// Checklist Events
// ================================
[TrelloEventType.CHECKLIST_ADDED_TO_CARD]: {
title: 'Checklist Added To Card',
description: 'Triggered when a checklist is added to a card',
schema: checklistAddedToCardEventSchema,
},
[TrelloEventType.CHECKLIST_ITEM_CREATED]: {
title: 'Checklist Item Created',
description: 'Triggered when a checklist item is added to a card',
schema: checklistItemCreatedEventSchema,
},
[TrelloEventType.CHECKLIST_ITEM_UPDATED]: {
title: 'Checklist Item Updated',
description: 'Triggered when a checklist item is modified on a card',
schema: checklistItemUpdatedEventSchema,
},
[TrelloEventType.CHECKLIST_ITEM_DELETED]: {
title: 'Checklist Item Deleted',
description: 'Triggered when a checklist item is removed from a card',
schema: checklistItemDeletedEventSchema,
},
[TrelloEventType.CHECKLIST_ITEM_STATUS_UPDATED]: {
title: 'Checklist Item Completion Updated',
description: 'Triggered when the completion status of a checklist item is updated',
schema: checklistItemStatusUpdatedEventSchema,
},
// ===============================
// Member Events
// ===============================
[TrelloEventType.MEMBER_ADDED_TO_CARD]: {
title: 'Member Added To Card',
description: 'Triggered when a member is added to a card',
schema: memberAddedToCardEventSchema,
},
[TrelloEventType.MEMBER_REMOVED_FROM_CARD]: {
title: 'Member Removed From Card',
description: 'Triggered when a member is removed from a card',
schema: memberRemovedFromCardEventSchema,
},
} as const satisfies NonNullable<IntegrationDefinitionProps['events']>
export { TrelloEventType, type CommonEventData }
@@ -0,0 +1,28 @@
import { z } from '@botpress/sdk'
import { boardSchema, cardSchema, trelloIdSchema } from '../schemas'
import { botpressEventDataSchema, pickIdAndName } from './common'
export const eventMemberSchema = z.object({
id: trelloIdSchema.title('Member ID').describe('Unique identifier of the member'),
name: z.string().title('Member Name').describe('Full name of the member'),
})
export const memberAddedToCardEventSchema = botpressEventDataSchema.extend({
board: pickIdAndName(boardSchema).title('Board').describe('Board where the card was updated'),
card: pickIdAndName(cardSchema).title('Card').describe('Card that the member was added to'),
member: eventMemberSchema.title('Member').describe('Member that was added to the card'),
})
export const memberRemovedFromCardEventSchema = botpressEventDataSchema.extend({
board: pickIdAndName(boardSchema).title('Board').describe('Board where the card was updated'),
card: pickIdAndName(cardSchema).title('Card').describe('Card that was updated'),
member: eventMemberSchema
.extend({
deactivated: z
.boolean()
.title('Deactivated')
.describe('Indicates if the member was deactivated at the time of removal'),
})
.title('Member')
.describe('Member that was removed from the card'),
})
+6
View File
@@ -0,0 +1,6 @@
export { actions } from './actions'
export { channels } from './channels'
export { configuration } from './configuration'
export { events } from './events'
export { user } from './user'
export { entities } from './entities'
@@ -0,0 +1,50 @@
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<typeof trelloIdSchema>
export const boardSchema = z.object({
id: trelloIdSchema.title('Board ID').describe('Unique identifier of the board'),
name: z.string().title('Board Name').describe('The name of the board'),
})
export type Board = z.infer<typeof boardSchema>
export const cardSchema = z.object({
id: trelloIdSchema.title('Card ID').describe('Unique identifier of the card'),
name: z.string().title('Card Name').describe('The preview name of the card'),
description: z.string().title('Card Description').describe('Detailed description of the card'),
listId: trelloIdSchema.title('List ID').describe('Identifier of the list the card belongs to'),
verticalPosition: z.number().title('Position').describe('Position of the card within the list'),
isClosed: z.boolean().title('Is Closed').describe('Indicates if the card is closed'),
isCompleted: z.boolean().title('Is Completed').describe('Indicates if the card is completed'),
dueDate: z.string().datetime().optional().title('Due Date').describe('The expected completed by date (Optional)'),
labelIds: z.array(trelloIdSchema).title('Label IDs').describe('A list of label IDs attached to the card'),
memberIds: z.array(trelloIdSchema).title('Member IDs').describe('A list of member IDs assigned to the card'),
})
export type Card = z.infer<typeof cardSchema>
export const listSchema = z.object({
id: trelloIdSchema.title('List ID').describe('Unique identifier of the list'),
name: z.string().title('List Name').describe('The name of the list'),
})
export type List = z.infer<typeof listSchema>
export const memberSchema = z.object({
id: trelloIdSchema.title('Member ID').describe('Unique identifier of the member'),
username: z.string().title('Username').describe('A public alias that represents the member'),
fullName: z.string().title('Full Name').describe('Full name of the member'),
})
export type Member = z.infer<typeof memberSchema>
export const webhookSchema = z.object({
id: trelloIdSchema.title('Webhook ID').describe('Unique identifier of the webhook'),
modelId: trelloIdSchema.title('Model ID').describe('ID of the Trello model the webhook watches for events'),
callbackUrl: z
.string()
.url()
.title('Callback URL')
.describe('The URL that Trello will call when a webhook event occurs'),
})
export type Webhook = z.infer<typeof webhookSchema>
+10
View File
@@ -0,0 +1,10 @@
import { IntegrationDefinitionProps } from '@botpress/sdk'
export const user = {
tags: {
userId: {
title: 'User ID',
description: 'Unique identifier of the Trello user',
},
},
} as const satisfies NonNullable<IntegrationDefinitionProps['user']>
+13
View File
@@ -0,0 +1,13 @@
import rootConfig from '../../eslint.config.mjs'
export default [
...rootConfig,
{
languageOptions: {
parserOptions: {
project: ['./tsconfig.json'],
tsconfigRootDir: import.meta.dirname,
},
},
},
]
+107
View File
@@ -0,0 +1,107 @@
This integration allows you to connect your Botpress chatbot with Trello, a
popular project management platform. With this integration, you can easily
manage your projects and tasks directly from your chatbot.
To set up the integration, you will need to provide your **Trello API key** and
**Token**. Once the integration is set up, you can use the built-in actions to
create and update cards, add comments to cards, and more.
For more detailed instructions on how to set up and use the Botpress Trello
integration, please refer to our documentation.
## Prerequisites
Before enabling the Botpress Trello Integration, please ensure that you have the
following:
- A Botpress cloud account.
- Access to a Trello workspace.
- API key generated from Trello.
- To generate an API key, you will need to create an application on Trello.
Follow the official instructions here: [Trello - API Introduction].
- API token generated from Trello.
- Once you have created your application, you may grant it access to one or
several of your Trello workspaces. Trello will then generate the API token
for you.
## Enable Integration
To enable the Trello integration in Botpress, follow these steps:
- Access your Botpress admin panel.
- Navigate to the “Integrations” section.
- Locate the Trello integration and click on “Enable” or “Configure.”
- Provide the required API key and API token.
- Save the configuration.
## Usage
Once the integration is enabled, you can start interacting with Trello from your
Botpress chatbot. The integration offers actions like `createCard`, `updateCard`,
`getMember`, `getBoardMembers` and `addComment` to manage tasks and users.
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.
- Some Trello paid features may not be available.
[Trello - API Introduction]: https://developer.atlassian.com/cloud/trello/guides/rest-api/api-introduction/
## Migration from 1.x.x to 2.x.x
- Replace "Move Card Up" action with "Move Card Down" actions (and vice versa) as the directions were reversed to match the visual displacement of the cards on Trello.
- Replace "Board List" action with "Get All Boards"
- Replace "Board Read" action with "Get Board By ID"
- Replace "List List" action with "Get Lists In Board"
- Replace "List Read" action with "Get List By ID"
- Replace "Card Create" action with "Create New Card"
- Replace "Card Update" action with "Update Card"
- Replace "Card Delete" action with "Delete Card"
- Replace "Card List" action with "Get Cards In List"
- Replace "Card Read" action with "Get Card By ID"
- Replace "Board Member List" action with "Get All Board Members"
- Replace "Board Member Read" action with "Get Member By ID Or Username"
- Replace "Card Member List" action with "Get All Card Members"
- Replace "Card Member Read" action with "Get Member By ID Or Username"
- Redefine the following properties in any "Create Card" actions
- Member IDs (formerly "Members")
- Label IDs (formerly "Labels")
- Redefine the following properties in any "Update Card" actions
- "Card Name" (formerly "Name")
- "Card Body" (formerly "Body Text")
- "Lifecycle Status" (formerly "Closed State")
- "Completion Status" (formerly "Complete State")
- "Member IDs To Add" (formerly "Members To Add")
- "Member IDs To Remove" (formerly "Members To Remove")
- "Label IDs To Add" (formerly "Labels To Add")
- "Label IDs To Remove" (formerly "Labels To Remove")
- Adjust the following events since their output data structure have changed
- "updateCard" event
- "commentCard" event
- "updateComment" event
- "deleteComment" event
- "createCheckItem" event
- "updateCheckItem" event
- "deleteCheckItem" event
- "updateCheckItemStateOnCard" event
+14
View File
@@ -0,0 +1,14 @@
<svg width="224" height="224" viewBox="0 0 224 224" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_356_36653)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M197.408 0H26.6277C11.9358 0 0.0202348 11.9073 0.000214633 26.6085V197.247C-0.0281939 204.333 2.76452 211.138 7.7612 216.158C12.7579 221.178 19.547 224 26.6277 224H197.408C204.483 223.99 211.263 221.164 216.251 216.145C221.24 211.127 224.028 204.327 224 197.247V26.6085C223.979 11.9212 212.085 0.0197841 197.408 0ZM96.6047 161.339C96.5959 163.707 95.6442 165.974 93.9603 167.638C92.2764 169.302 89.9998 170.227 87.6334 170.209H50.3123C45.431 170.189 41.4844 166.223 41.4844 161.339V50.2007C41.4844 45.3162 45.431 41.3509 50.3123 41.3312H87.6334C92.5207 41.3509 96.4777 45.3104 96.4972 50.2007L96.6047 161.339ZM182.731 110.312C182.731 112.683 181.782 114.956 180.097 116.622C178.411 118.289 176.129 119.211 173.759 119.182H136.438C131.551 119.162 127.594 115.202 127.574 110.312V50.2007C127.594 45.3104 131.551 41.3509 136.438 41.3312H173.759C178.64 41.3509 182.587 45.3162 182.587 50.2007L182.731 110.312Z" fill="url(#paint0_linear_356_36653)"/>
</g>
<defs>
<linearGradient id="paint0_linear_356_36653" x1="112.107" y1="224" x2="112.107" y2="0" gradientUnits="userSpaceOnUse">
<stop stop-color="#0052CC"/>
<stop offset="1" stop-color="#2684FF"/>
</linearGradient>
<clipPath id="clip0_356_36653">
<rect width="224" height="224" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,49 @@
import { posthogHelper } from '@botpress/common'
import * as sdk from '@botpress/sdk'
import { trelloIdSchema } from 'definitions/schemas'
import { events, actions, channels, user, configuration, entities } from './definitions'
export const INTEGRATION_NAME = 'trello'
export const INTEGRATION_VERSION = '2.1.3'
export default new sdk.IntegrationDefinition({
name: INTEGRATION_NAME,
title: 'Trello',
version: INTEGRATION_VERSION,
readme: 'hub.md',
description: 'Update cards, add comments, create new cards, and read board members from your chatbot.',
icon: 'icon.svg',
actions,
channels,
user,
configuration,
events,
entities,
secrets: {
...posthogHelper.COMMON_SECRET_NAMES,
},
/** The states are no longer being used, however, it is
* being left in, in order to prevent potential breaking changes.
*
* It should be removed next time we push a major release.
* @see https://github.com/botpress/botpress/pull/14849#pullrequestreview-3728680072 For more details. */
states: {
// TODO: Remove in next major release (v3.0.0)
webhook: {
type: 'integration',
schema: sdk.z.object({
trelloWebhookId: trelloIdSchema
.nullable()
.default(null)
.title('Trello Webhook ID')
.describe('Unique id of the webhook that is created by Trello upon integration registration'),
}),
},
},
attributes: {
category: 'Project Management',
guideSlug: 'trello',
repo: 'botpress',
},
})
+22
View File
@@ -0,0 +1,22 @@
{
"name": "@botpresshub/trello",
"scripts": {
"build": "bp add -y && bp build",
"check:type": "tsc --noEmit",
"check:bplint": "bp lint",
"test": "vitest --run"
},
"private": true,
"dependencies": {
"@botpress/client": "workspace:*",
"@botpress/common": "workspace:*",
"@botpress/sdk": "workspace:*",
"@botpress/sdk-addons": "workspace:*",
"trello.js": "^1.2.7"
},
"devDependencies": {
"@botpress/cli": "workspace:*",
"@botpress/common": "workspace:*",
"@botpress/sdk": "workspace:*"
}
}
@@ -0,0 +1,33 @@
import { nameCompare } from '../string-utils'
import { printActionTriggeredMsg, getTools } from './helpers'
import * as bp from '.botpress'
export const getAllBoards: bp.Integration['actions']['getAllBoards'] = async (props) => {
printActionTriggeredMsg(props)
const { trelloClient } = getTools(props)
const {} = props.input
const boards = await trelloClient.getAllBoards()
return { boards }
}
export const getBoardById: bp.Integration['actions']['getBoardById'] = async (props) => {
printActionTriggeredMsg(props)
const { trelloClient } = getTools(props)
const { boardId } = props.input
const board = await trelloClient.getBoardById({ boardId })
return { board }
}
export const getBoardsByDisplayName: bp.Integration['actions']['getBoardsByDisplayName'] = async (props) => {
printActionTriggeredMsg(props)
const { trelloClient } = getTools(props)
const { boardName } = props.input
const boards = await trelloClient.getAllBoards()
const matchingBoards = boards.filter((b) => nameCompare(b.name, boardName))
return { boards: matchingBoards }
}
@@ -0,0 +1,174 @@
import { z, RuntimeError } from '@botpress/sdk'
import { nameCompare } from '../string-utils'
import { CardPosition } from '../trello-api'
import { printActionTriggeredMsg, getTools } from './helpers'
import { moveCardVertically } from './move-card-helpers'
import * as bp from '.botpress'
export const getCardsInList: bp.Integration['actions']['getCardsInList'] = async (props) => {
printActionTriggeredMsg(props)
const { trelloClient } = getTools(props)
const { listId } = props.input
const matchingCards = await trelloClient.getCardsInList({ listId })
return { cards: matchingCards }
}
export const getCardById: bp.Integration['actions']['getCardById'] = async (props) => {
printActionTriggeredMsg(props)
const { trelloClient } = getTools(props)
const card = await trelloClient.getCardById({ cardId: props.input.cardId })
return { card }
}
export const getCardsByDisplayName: bp.Integration['actions']['getCardsByDisplayName'] = async (props) => {
printActionTriggeredMsg(props)
const { trelloClient } = getTools(props)
const { listId, cardName } = props.input
const cards = await trelloClient.getCardsInList({ listId })
const matchingCards = cards.filter((c) => nameCompare(c.name, cardName))
return { cards: matchingCards }
}
export const createCard: bp.Integration['actions']['createCard'] = async (props) => {
printActionTriggeredMsg(props)
const { trelloClient } = getTools(props)
const { listId, cardName, cardBody, memberIds, labelIds, dueDate, completionStatus } = props.input
const newCard = await trelloClient.createCard({
card: {
name: cardName,
description: cardBody ?? '',
listId,
memberIds,
labelIds,
dueDate,
isCompleted: completionStatus === 'Complete',
},
})
return { message: `Card created successfully. Card ID: ${newCard.id}`, newCardId: newCard.id }
}
const _verticalPositionSchema = z.union([z.literal('top'), z.literal('bottom'), z.coerce.number()]).optional()
const _validateVerticalPosition = (verticalPosition: string | undefined): CardPosition | undefined => {
const result = _verticalPositionSchema.safeParse(verticalPosition?.toLowerCase().trim())
if (!result.success) {
throw new RuntimeError(
`Invalid verticalPosition value. It must be either "top", "bottom", or a float. -> ${result.error.message}`
)
}
return result.data
}
export const updateCard: bp.Integration['actions']['updateCard'] = async (props) => {
printActionTriggeredMsg(props)
const { trelloClient } = getTools(props)
const {
cardId,
listId,
cardName,
cardBody,
lifecycleStatus,
completionStatus,
dueDate,
labelIdsToAdd,
labelIdsToRemove,
memberIdsToAdd,
memberIdsToRemove,
verticalPosition,
} = props.input
// When sending null for the due date, the Trello API ignores it. However, an empty string removes the due date.
// A blank string from a Botpress bot should be treated as undefined (no change), while null is converted to an
// empty string to should remove the due date.
let formattedDueDate = dueDate?.trim() !== '' ? dueDate : undefined
formattedDueDate = formattedDueDate !== null ? formattedDueDate : ''
const card = await trelloClient.getCardById({ cardId })
await trelloClient.updateCard({
partialCard: {
id: cardId,
listId,
name: cardName,
description: cardBody,
isClosed: lifecycleStatus ? lifecycleStatus === 'Archived' : undefined,
isCompleted: completionStatus ? completionStatus === 'Complete' : undefined,
dueDate: formattedDueDate,
labelIds: card.labelIds.concat(labelIdsToAdd ?? []).filter((labelId) => !labelIdsToRemove?.includes(labelId)),
memberIds: card.memberIds
.concat(memberIdsToAdd ?? [])
.filter((memberId) => !memberIdsToRemove?.includes(memberId)),
verticalPosition: _validateVerticalPosition(verticalPosition),
},
})
return { message: 'Card updated successfully.' }
}
export const deleteCard: bp.Integration['actions']['deleteCard'] = async (props) => {
printActionTriggeredMsg(props)
const { trelloClient } = getTools(props)
const { cardId, hardDelete = false } = props.input
if (hardDelete) {
await trelloClient.deleteCard(cardId)
} else {
await trelloClient.updateCard({
partialCard: {
id: cardId,
isClosed: true,
},
})
}
return {}
}
export const moveCardToList: bp.Integration['actions']['moveCardToList'] = async (props) => {
printActionTriggeredMsg(props)
const { trelloClient } = getTools(props)
const { cardId, newListId, newVerticalPosition } = props.input
const card = await trelloClient.getCardById({ cardId })
const newList = await trelloClient.getListById({ listId: newListId })
await trelloClient.updateCard({
partialCard: {
id: card.id,
listId: newList.id,
verticalPosition: _validateVerticalPosition(newVerticalPosition),
},
})
return { message: 'Card successfully moved to the new list' }
}
export const moveCardDown: bp.Integration['actions']['moveCardDown'] = async (props) => {
printActionTriggeredMsg(props)
const { trelloClient } = getTools(props)
const { cardId, moveDownByNSpaces } = props.input
const numOfPositions = moveDownByNSpaces ?? 1
await moveCardVertically({ trelloClient, cardId, numOfPositions })
return { message: 'Card successfully moved down' }
}
export const moveCardUp: bp.Integration['actions']['moveCardUp'] = async (props) => {
printActionTriggeredMsg(props)
const { trelloClient } = getTools(props)
const { cardId, moveUpByNSpaces } = props.input
const numOfPositions = -(moveUpByNSpaces ?? 1)
await moveCardVertically({ trelloClient, cardId, numOfPositions })
return { message: 'Card successfully moved up' }
}
@@ -0,0 +1,12 @@
import { printActionTriggeredMsg, getTools } from './helpers'
import * as bp from '.botpress'
export const addCardComment: bp.Integration['actions']['addCardComment'] = async (props) => {
printActionTriggeredMsg(props)
const { trelloClient } = getTools(props)
const { cardId, commentBody } = props.input
const newCommentId = await trelloClient.addCardComment({ cardId, commentBody })
return { message: 'Comment successfully added to the card', newCommentId }
}
@@ -0,0 +1,33 @@
import { TrelloClient } from '../trello-api/trello-client'
import * as bp from '.botpress'
export const printActionTriggeredMsg = ({ type, ctx, logger, input }: bp.AnyActionProps) => {
logger.forBot().debug(`Running action "${type}" [bot id: ${ctx.botId}]`, { input })
}
type ResolvedFactoryTools<T extends Record<string, (props: bp.CommonHandlerProps) => unknown>> = {
readonly [K in keyof T]: ReturnType<T[K]>
} & {} // <-- Empty object used for improving readability in IDE type previews
const _createToolFactory = <T extends Record<string, (props: bp.CommonHandlerProps) => unknown>>(toolBuilders: T) => {
return (props: bp.CommonHandlerProps) => {
// Of all the options I tested, Proxy had the best average
// performance, especially as more "toolBuilders" are added.
return new Proxy(toolBuilders, {
get(target, tool: string) {
const builder = target[tool]
if (!builder) {
// Sanity check, should never actually be thrown
throw new Error(`No tool builder found for key: ${String(tool)}`)
}
return builder(props)
},
}) as ResolvedFactoryTools<T>
}
}
export const getTools = _createToolFactory({
trelloClient({ ctx }: bp.CommonHandlerProps) {
return new TrelloClient({ ctx })
},
})
+48
View File
@@ -0,0 +1,48 @@
import { getAllBoards, getBoardsByDisplayName, getBoardById } from './board-actions'
import {
getCardsInList,
getCardsByDisplayName,
getCardById,
createCard,
updateCard,
moveCardToList,
moveCardUp,
moveCardDown,
deleteCard,
} from './card-actions'
import { addCardComment } from './card-comment-actions'
import { getListsInBoard, getListsByDisplayName, getListById } from './list-actions'
import {
getAllBoardMembers,
getAllCardMembers,
getMemberByIdOrUsername,
getBoardMembersByDisplayName,
} from './member-actions'
import * as bp from '.botpress'
export const actions = {
// === Board Actions ===
getBoardById,
getBoardsByDisplayName,
getAllBoards,
// === List Actions ===
getListById,
getListsByDisplayName,
getListsInBoard,
// === Card Actions ===
getCardById,
getCardsByDisplayName,
getCardsInList,
createCard,
updateCard,
deleteCard,
addCardComment,
moveCardUp,
moveCardDown,
moveCardToList,
// === Member Actions ===
getMemberByIdOrUsername,
getBoardMembersByDisplayName,
getAllBoardMembers,
getAllCardMembers,
} as const satisfies bp.IntegrationProps['actions']
@@ -0,0 +1,34 @@
import { nameCompare } from '../string-utils'
import { printActionTriggeredMsg, getTools } from './helpers'
import * as bp from '.botpress'
export const getListsInBoard: bp.Integration['actions']['getListsInBoard'] = async (props) => {
printActionTriggeredMsg(props)
const { trelloClient } = getTools(props)
const { boardId } = props.input
const matchingLists = await trelloClient.getListsInBoard({ boardId })
return { lists: matchingLists }
}
export const getListsByDisplayName: bp.Integration['actions']['getListsByDisplayName'] = async (props) => {
printActionTriggeredMsg(props)
const { trelloClient } = getTools(props)
const { boardId, listName } = props.input
const lists = await trelloClient.getListsInBoard({ boardId })
const matchingLists = lists.filter((l) => nameCompare(l.name, listName))
return { lists: matchingLists }
}
export const getListById: bp.Integration['actions']['getListById'] = async (props) => {
printActionTriggeredMsg(props)
const { trelloClient } = getTools(props)
const { listId } = props.input
const list = await trelloClient.getListById({ listId })
return { list }
}
@@ -0,0 +1,45 @@
import { nameCompare } from '../string-utils'
import { printActionTriggeredMsg, getTools } from './helpers'
import * as bp from '.botpress'
export const getAllBoardMembers: bp.Integration['actions']['getAllBoardMembers'] = async (props) => {
printActionTriggeredMsg(props)
const { trelloClient } = getTools(props)
const { boardId } = props.input
const members = await trelloClient.getBoardMembers({ boardId })
return { members }
}
export const getAllCardMembers: bp.Integration['actions']['getAllCardMembers'] = async (props) => {
printActionTriggeredMsg(props)
const { trelloClient } = getTools(props)
const { cardId } = props.input
return {
members: await trelloClient.getCardMembers({ cardId }),
}
}
export const getBoardMembersByDisplayName: bp.Integration['actions']['getBoardMembersByDisplayName'] = async (
props
) => {
printActionTriggeredMsg(props)
const { trelloClient } = getTools(props)
const { boardId, displayName } = props.input
const members = await trelloClient.getBoardMembers({ boardId })
const matchingMembers = members.filter((m) => nameCompare(m.fullName, displayName))
return { members: matchingMembers }
}
export const getMemberByIdOrUsername: bp.Integration['actions']['getMemberByIdOrUsername'] = async (props) => {
printActionTriggeredMsg(props)
const { trelloClient } = getTools(props)
const { memberIdOrUsername } = props.input
const member = await trelloClient.getMemberByIdOrUsername({ memberId: memberIdOrUsername })
return { member }
}
@@ -0,0 +1,59 @@
import * as sdk from '@botpress/sdk'
import { Card } from 'definitions/schemas'
import { TrelloClient, CardPosition } from '../trello-api'
export const moveCardVertically = async ({
trelloClient,
cardId,
numOfPositions,
}: {
trelloClient: TrelloClient
cardId: string
numOfPositions: number
}): Promise<void> => {
if (numOfPositions === 0) {
return
}
const card = await trelloClient.getCardById({ cardId })
const cardsInList = await trelloClient.getCardsInList({ listId: card.listId })
cardsInList.sort((a, b) => a.verticalPosition - b.verticalPosition)
const newPosition = _evaluateNewPosition(cardsInList, cardId, numOfPositions)
await trelloClient.updateCard({
partialCard: {
id: cardId,
verticalPosition: newPosition,
},
})
}
const _evaluateNewPosition = (cardsInList: Card[], cardIdToMove: string, numOfPositions: number): CardPosition => {
const cardIndex = cardsInList.findIndex((c) => c.id === cardIdToMove)
if (cardIndex === -1) {
throw new sdk.RuntimeError(`Card with id ${cardIdToMove} not found in the target list of cards`)
}
const newIndex = cardIndex + numOfPositions
if (newIndex <= 0) {
return 'top'
}
if (newIndex >= cardsInList.length - 1) {
return 'bottom'
}
const sibling = cardsInList[newIndex]
const otherSibling = cardsInList[newIndex + Math.sign(numOfPositions)]
if (!sibling || !otherSibling) {
// Sanity check, should never actually be called
throw new sdk.RuntimeError('Card must have a sibling card on each side to determine new position')
}
// This is supposed to be a float value. For reference, check the "pos" property in the "Update a Card" request
// parameters: https://developer.atlassian.com/cloud/trello/rest/api-group-cards/#api-cards-id-put-request
return (sibling.verticalPosition + otherSibling.verticalPosition) / 2
}
@@ -0,0 +1,9 @@
import { createChannelWrapper } from '@botpress/common'
import { TrelloClient } from 'src/trello-api/trello-client'
import * as bp from '.botpress'
export const wrapChannel = createChannelWrapper<bp.IntegrationProps>()({
toolFactories: {
trelloClient: ({ ctx }) => new TrelloClient({ ctx }),
},
})
@@ -0,0 +1,10 @@
import { CardCommentPublisher } from './publishers/card-comments'
import * as bp from '.botpress'
export const channels = {
cardComments: {
messages: {
text: CardCommentPublisher.publishTextMessage,
},
},
} as const satisfies bp.IntegrationProps['channels']
@@ -0,0 +1,27 @@
import * as sdk from '@botpress/sdk'
import { wrapChannel } from '../channel-wrapper'
export namespace CardCommentPublisher {
export const publishTextMessage = wrapChannel(
{ channelName: 'cardComments', messageType: 'text' },
async ({ trelloClient, conversation, ack, payload, client }) => {
if (!conversation.tags.cardId) {
throw new sdk.RuntimeError('Card id must be set')
}
const commentId = await trelloClient.addCardComment({
cardId: conversation.tags.cardId,
commentBody: payload.text,
})
await client.updateConversation({
id: conversation.id,
tags: {
lastCommentId: commentId,
},
})
await ack({ tags: { commentId } })
}
)
}
+24
View File
@@ -0,0 +1,24 @@
import { posthogHelper } from '@botpress/common'
import { INTEGRATION_NAME, INTEGRATION_VERSION } from 'integration.definition'
import { actions } from './actions'
import { channels } from './channels/publisher-dispatcher'
import { register, unregister } from './setup'
import { handler } from './webhook-events'
import * as bp from '.botpress'
const integrationConfig: bp.IntegrationProps = {
register,
unregister,
actions,
channels,
handler,
}
export default posthogHelper.wrapIntegration(
{
integrationName: INTEGRATION_NAME,
key: bp.secrets.POSTHOG_KEY,
integrationVersion: INTEGRATION_VERSION,
},
integrationConfig
)
+18
View File
@@ -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)
}
+3
View File
@@ -0,0 +1,3 @@
const _canonicalize = (identifier: string) => identifier.trim().toUpperCase().normalize()
export const nameCompare = (name1: string, name2: string) => _canonicalize(name1) === _canonicalize(name2)
@@ -0,0 +1,2 @@
export { TrelloClient } from './trello-client'
export * from './types'
@@ -0,0 +1,36 @@
import { Parameters } from 'trello.js'
import { UpdateCardPayload, CreateCardPayload } from '../types'
export namespace RequestMapping {
export const mapUpdateCard = (card: UpdateCardPayload): Parameters.UpdateCard =>
_keepOnlySetProperties({
id: card.id,
name: card.name,
desc: card.description,
idList: card.listId,
pos: card.verticalPosition,
closed: card.isClosed,
dueComplete: card.isCompleted,
due: card.dueDate,
idLabels: card.labelIds,
idMembers: card.memberIds,
})
export const mapCreateCard = (card: CreateCardPayload): Parameters.CreateCard =>
_keepOnlySetProperties({
name: card.name,
desc: card.description,
idList: card.listId,
due: card.dueDate,
idLabels: card.labelIds,
idMembers: card.memberIds,
pos: card.verticalPosition,
dueComplete: card.isCompleted,
})
}
export const _keepOnlySetProperties = <T extends Record<string, any>>(
obj: T
): {
[K in keyof T as undefined extends T[K] ? (T[K] extends undefined ? never : K) : K]: T[K]
} => Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined)) as any
@@ -0,0 +1,41 @@
import type { List, Member, Board, TrelloID, Card, Webhook } from 'definitions/schemas'
import { Models, type Models as TrelloJsModels } from 'trello.js'
export namespace ResponseMapping {
export const mapMember = (member: TrelloJsModels.Member): Member => ({
id: member.id ?? '',
fullName: member.fullName ?? '',
username: member.username ?? '',
})
export const mapBoard = (board: TrelloJsModels.Board): Board => ({
id: board.id,
name: board.name ?? '',
})
export const mapList = (list: TrelloJsModels.List): List => ({
id: list.id,
name: list.name,
})
export const mapTrelloId = (id?: Models.TrelloID): TrelloID => id ?? ''
export const mapCard = (card: TrelloJsModels.Card): Card => ({
id: card.id,
name: card.name,
description: card.desc,
listId: card.idList,
verticalPosition: card.pos,
isClosed: card.closed,
isCompleted: card.dueComplete,
dueDate: card.due ?? undefined,
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 ?? '',
})
}
@@ -0,0 +1,205 @@
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 { RequestMapping } from './mapping/request-mapping'
import { ResponseMapping } from './mapping/response-mapping'
import { UpdateCardPayload, CreateCardPayload } from './types'
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 })
}
public async getBoardMembers({ boardId }: { boardId: Board['id'] }): Promise<Member[]> {
const members = await this._trelloJs.boards
.getBoardMembers<TrelloJsModels.Member[]>({
id: boardId,
})
.catch(_useHandleCaughtError('Failed to retrieve board members'))
return members.map(ResponseMapping.mapMember)
}
public async getBoardById({ boardId }: { boardId: Board['id'] }): Promise<Board> {
const board = await this._trelloJs.boards
.getBoard({
id: boardId,
})
.catch(_useHandleCaughtError('Failed to retrieve board by id'))
return ResponseMapping.mapBoard(board)
}
public async getAllBoards(): Promise<Board[]> {
const boards = await this._trelloJs.members
.getMemberBoards({
id: 'me',
})
.catch(_useHandleCaughtError('Failed to retrieve all boards'))
return boards.map(ResponseMapping.mapBoard)
}
public async getListsInBoard({ boardId }: { boardId: Board['id'] }): Promise<List[]> {
const lists = await this._trelloJs.boards
.getBoardLists({
id: boardId,
})
.catch(_useHandleCaughtError('Failed to retrieve lists in board'))
return lists.map(ResponseMapping.mapList)
}
public async addCardComment({ cardId, commentBody }: { cardId: Card['id']; commentBody: string }): Promise<TrelloID> {
const comment = await this._trelloJs.cards
.addCardComment({
id: cardId,
text: commentBody,
})
.catch(_useHandleCaughtError('Failed to add comment to card'))
return ResponseMapping.mapTrelloId(comment.id)
}
public async getCardById({ cardId }: { cardId: Card['id'] }): Promise<Card> {
const card = await this._trelloJs.cards
.getCard({
id: cardId,
})
.catch(_useHandleCaughtError('Failed to get card by id'))
return ResponseMapping.mapCard(card)
}
public async createCard({ card }: { card: CreateCardPayload }): Promise<Card> {
const newCard = await this._trelloJs.cards
.createCard(RequestMapping.mapCreateCard(card))
.catch(_useHandleCaughtError('Failed to create card'))
return ResponseMapping.mapCard(newCard)
}
public async updateCard({ partialCard }: { partialCard: UpdateCardPayload }): Promise<Card> {
const updatedCard = await this._trelloJs.cards
.updateCard(RequestMapping.mapUpdateCard(partialCard))
.catch(_useHandleCaughtError('Failed to update card'))
return ResponseMapping.mapCard(updatedCard)
}
/** Hard deletes a Trello card.
*
* @remark For soft deletion use "updateCard" with "isClosed" as true */
public async deleteCard(cardId: Card['id']): Promise<void> {
await this._trelloJs.cards
.deleteCard({
id: cardId,
})
.catch(_useHandleCaughtError('Failed to delete card'))
}
public async getListById({ listId }: { listId: List['id'] }): Promise<List> {
const list = await this._trelloJs.lists
.getList<TrelloJsModels.List>({
id: listId,
})
.catch(_useHandleCaughtError('Failed to get list by id'))
return ResponseMapping.mapList(list)
}
public async getCardsInList({ listId }: { listId: List['id'] }): Promise<Card[]> {
const cards = await this._trelloJs.lists
.getListCards({
id: listId,
})
.catch(_useHandleCaughtError('Failed to get cards in list'))
return cards.map(ResponseMapping.mapCard)
}
public async getMemberByIdOrUsername({ memberId }: { memberId: Member['id'] | Member['username'] }): Promise<Member> {
const member = await this._trelloJs.members
.getMember({
id: memberId,
})
.catch(_useHandleCaughtError('Failed to get member by id or username'))
return ResponseMapping.mapMember(member)
}
public async listWebhooks(): Promise<Webhook[]> {
const rawWebhooks = await this._trelloJs.tokens
.getTokenWebhooks<TrelloJsModels.Webhook[]>({
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,
modelId,
}: {
description: string
url: string
modelId: string
}): Promise<Webhook> {
const webhook = await this._trelloJs.webhooks
.createWebhook({
description,
callbackURL: url,
idModel: modelId,
})
.catch(_useHandleCaughtError('Failed to create webhook'))
return ResponseMapping.mapWebhook(webhook)
}
public async deleteWebhook({ id }: { id: string }): Promise<void> {
await this._trelloJs.webhooks
.deleteWebhook({
id,
})
.catch(_useHandleCaughtError('Failed to delete webhook'))
}
public async getCardMembers({ cardId }: { cardId: Card['id'] }): Promise<Member[]> {
const members = await this._trelloJs.cards
.getCardMembers<TrelloJsModels.Member[]>({
id: cardId,
})
.catch(_useHandleCaughtError('Failed to get card members'))
return members.map(ResponseMapping.mapMember)
}
}
@@ -0,0 +1,12 @@
import type { Card } from 'definitions/schemas'
import { Merge } from 'src/types'
export type CardPosition = number | 'top' | 'bottom'
export type CreateCardPayload = Pick<Card, 'name' | 'description' | 'listId'> & Omit<Partial<Card>, 'id' | 'isClosed'>
export type UpdateCardPayload = Merge<
Pick<Card, 'id'> & Partial<Card>,
{
verticalPosition?: CardPosition
}
>
+3
View File
@@ -0,0 +1,3 @@
export type Merge<A, B> = Omit<A, keyof B> & B
export type Result<T, E extends Error = Error> = { success: true; data: T } | { success: false; error: E }
+26
View File
@@ -0,0 +1,26 @@
import { Result } from './types'
export const safeParseJson = (json: string): Result<object> => {
try {
return {
success: true,
data: JSON.parse(json),
} as const
} catch (thrown: unknown) {
return {
success: false,
error: thrown instanceof Error ? thrown : new Error(String(thrown)),
} as const
}
}
export const safeParseRequestBody = (body: string | undefined): Result<object> => {
if (!body?.trim()) {
return {
success: false,
error: new Error('Request body is empty'),
}
}
return safeParseJson(body)
}
@@ -0,0 +1,74 @@
import { CommentAddedWebhook } from '../schemas/card-comment-webhook-schemas'
import * as bp from '.botpress'
type Conversation = Awaited<ReturnType<bp.Client['getOrCreateConversation']>>['conversation']
type User = Awaited<ReturnType<bp.Client['getOrCreateUser']>>['user']
export const processInboundCommentChannelMessage = async (
client: bp.HandlerProps['client'],
webhookEvent: CommentAddedWebhook
): Promise<void> => {
const conversation = await _getOrCreateConversation(client, webhookEvent.data)
const user = await _getOrCreateUser(client, webhookEvent.memberCreator)
const comment = _extractCommentData(webhookEvent)
if (_checkIfMessageWasSentByOurselvesAndShouldBeIgnored(conversation, comment)) {
return
}
await _createMessage(client, conversation, user, comment)
}
const _extractCommentData = (event: CommentAddedWebhook) =>
({
id: event.id,
text: event.data.text,
}) as const
type CardComment = ReturnType<typeof _extractCommentData>
const _getOrCreateConversation = async (client: bp.HandlerProps['client'], eventData: CommentAddedWebhook['data']) => {
const { conversation } = await client.getOrCreateConversation({
channel: 'cardComments',
tags: {
listId: eventData.list.id,
listName: eventData.list.name,
cardId: eventData.card.id,
cardName: eventData.card.name,
},
})
return conversation
}
const _getOrCreateUser = async (
client: bp.HandlerProps['client'],
memberCreator: CommentAddedWebhook['memberCreator']
) => {
const { user } = await client.getOrCreateUser({
tags: {
userId: memberCreator.id,
},
name: memberCreator.fullName,
pictureUrl: `${memberCreator.avatarUrl}/50.png`,
})
return user
}
const _checkIfMessageWasSentByOurselvesAndShouldBeIgnored = (conversation: Conversation, comment: CardComment) =>
conversation.tags.lastCommentId === comment.id
const _createMessage = async (
client: bp.HandlerProps['client'],
conversation: Conversation,
user: User,
comment: CardComment
) => {
await client.createMessage({
conversationId: conversation.id,
userId: user.id,
type: 'text',
payload: { text: comment.text },
tags: { commentId: comment.id },
})
}
@@ -0,0 +1,40 @@
import { TrelloEventType } from 'definitions/events'
import { CardAttachmentAddedWebhook, CardAttachmentRemovedWebhook } from '../schemas/card-attachment-webhook-schemas'
import { extractCommonEventData, extractIdAndName } from './helpers'
import { Expect, IsWebhookHandler } from './types'
import * as bp from '.botpress'
export const handleAttachmentAddedEvent = async (
props: bp.HandlerProps,
eventType: TrelloEventType.ATTACHMENT_ADDED_TO_CARD,
webhookEvent: CardAttachmentAddedWebhook
) => {
return await props.client.createEvent({
type: eventType,
payload: {
...extractCommonEventData(webhookEvent),
board: extractIdAndName(webhookEvent.data.board),
list: extractIdAndName(webhookEvent.data.list),
card: extractIdAndName(webhookEvent.data.card),
attachment: webhookEvent.data.attachment,
},
})
}
type _HandleAttachmentAddedEventTest = Expect<IsWebhookHandler<typeof handleAttachmentAddedEvent>>
export const handleAttachmentRemovedEvent = async (
props: bp.HandlerProps,
eventType: TrelloEventType.ATTACHMENT_REMOVED_FROM_CARD,
webhookEvent: CardAttachmentRemovedWebhook
) => {
return await props.client.createEvent({
type: eventType,
payload: {
...extractCommonEventData(webhookEvent),
board: extractIdAndName(webhookEvent.data.board),
card: extractIdAndName(webhookEvent.data.card),
attachment: webhookEvent.data.attachment,
},
})
}
type _HandleAttachmentRemovedEventTest = Expect<IsWebhookHandler<typeof handleAttachmentRemovedEvent>>
@@ -0,0 +1,78 @@
import { TrelloEventType } from 'definitions/events'
import { processInboundCommentChannelMessage } from '../channel-handlers/comment-channel-handler'
import {
CommentAddedWebhook,
CommentDeletedWebhook,
CommentUpdatedWebhook,
} from '../schemas/card-comment-webhook-schemas'
import { extractCommonEventData, extractIdAndName } from './helpers'
import { Expect, IsWebhookHandler } from './types'
import * as bp from '.botpress'
export const handleCommentAddedEvent = async (
props: bp.HandlerProps,
eventType: TrelloEventType.CARD_COMMENT_CREATED,
webhookEvent: CommentAddedWebhook
) => {
const result = await Promise.allSettled([
processInboundCommentChannelMessage(props.client, webhookEvent),
props.client.createEvent({
type: eventType,
payload: {
...extractCommonEventData(webhookEvent),
board: extractIdAndName(webhookEvent.data.board),
list: extractIdAndName(webhookEvent.data.list),
card: extractIdAndName(webhookEvent.data.card),
comment: {
id: webhookEvent.id,
text: webhookEvent.data.text,
},
},
}),
])
return result[1].status === 'fulfilled' ? result[1].value : null
}
type _HandleCommentAddedEventTest = Expect<IsWebhookHandler<typeof handleCommentAddedEvent>>
export const handleCommentUpdatedEvent = async (
props: bp.HandlerProps,
eventType: TrelloEventType.CARD_COMMENT_UPDATED,
webhookEvent: CommentUpdatedWebhook
) => {
return await props.client.createEvent({
type: eventType,
payload: {
...extractCommonEventData(webhookEvent),
board: extractIdAndName(webhookEvent.data.board),
card: extractIdAndName(webhookEvent.data.card),
comment: {
id: webhookEvent.data.action.id,
text: webhookEvent.data.action.text,
},
old: {
text: webhookEvent.data.old.text,
},
},
})
}
type _HandleCommentUpdatedEventTest = Expect<IsWebhookHandler<typeof handleCommentUpdatedEvent>>
export const handleCommentDeletedEvent = async (
props: bp.HandlerProps,
eventType: TrelloEventType.CARD_COMMENT_DELETED,
webhookEvent: CommentDeletedWebhook
) => {
return await props.client.createEvent({
type: eventType,
payload: {
...extractCommonEventData(webhookEvent),
board: extractIdAndName(webhookEvent.data.board),
card: extractIdAndName(webhookEvent.data.card),
comment: {
id: webhookEvent.data.action.id,
},
},
})
}
type _HandleCommentDeletedEventTest = Expect<IsWebhookHandler<typeof handleCommentDeletedEvent>>
@@ -0,0 +1,83 @@
import { TrelloEventType } from 'definitions/events'
import {
CardCreatedWebhook,
CardDeletedWebhook,
CardUpdatedWebhook,
CardVotesUpdatedWebhook,
} from '../schemas/card-webhook-schemas'
import { extractCommonEventData, extractIdAndName, extractIdAndNameIfExists } from './helpers'
import { Expect, IsWebhookHandler } from './types'
import * as bp from '.botpress'
export const handleCardCreatedEvent = async (
props: bp.HandlerProps,
eventType: TrelloEventType.CARD_CREATED,
webhookEvent: CardCreatedWebhook
) => {
return await props.client.createEvent({
type: eventType,
payload: {
...extractCommonEventData(webhookEvent),
board: extractIdAndName(webhookEvent.data.board),
list: extractIdAndName(webhookEvent.data.list),
card: extractIdAndName(webhookEvent.data.card),
},
})
}
type _HandleCardCreatedEventTest = Expect<IsWebhookHandler<typeof handleCardCreatedEvent>>
export const handleCardUpdatedEvent = async (
props: bp.HandlerProps,
eventType: TrelloEventType.CARD_UPDATED,
webhookEvent: CardUpdatedWebhook
) => {
return await props.client.createEvent({
type: eventType,
payload: {
...extractCommonEventData(webhookEvent),
board: extractIdAndName(webhookEvent.data.board),
card: webhookEvent.data.card,
old: webhookEvent.data.old,
list: extractIdAndNameIfExists(webhookEvent.data.list),
listBefore: extractIdAndNameIfExists(webhookEvent.data.listBefore),
listAfter: extractIdAndNameIfExists(webhookEvent.data.listAfter),
},
})
}
type _HandleCardUpdatedEventTest = Expect<IsWebhookHandler<typeof handleCardUpdatedEvent>>
export const handleCardDeletedEvent = async (
props: bp.HandlerProps,
eventType: TrelloEventType.CARD_DELETED,
webhookEvent: CardDeletedWebhook
) => {
return await props.client.createEvent({
type: eventType,
payload: {
...extractCommonEventData(webhookEvent),
board: extractIdAndName(webhookEvent.data.board),
list: extractIdAndName(webhookEvent.data.list),
card: {
id: webhookEvent.data.card.id,
},
},
})
}
type _HandleCardDeletedEventTest = Expect<IsWebhookHandler<typeof handleCardDeletedEvent>>
export const handleCardVotesUpdatedEvent = async (
props: bp.HandlerProps,
eventType: TrelloEventType.CARD_VOTES_UPDATED,
webhookEvent: CardVotesUpdatedWebhook
) => {
return await props.client.createEvent({
type: eventType,
payload: {
...extractCommonEventData(webhookEvent),
board: extractIdAndName(webhookEvent.data.board),
card: extractIdAndName(webhookEvent.data.card),
voted: webhookEvent.data.voted,
},
})
}
type _HandleCardVotesUpdatedEventTest = Expect<IsWebhookHandler<typeof handleCardVotesUpdatedEvent>>
@@ -0,0 +1,39 @@
import { TrelloEventType } from 'definitions/events'
import { CardLabelAddedWebhook, CardLabelRemovedWebhook } from '../schemas/card-label-webhook-schemas'
import { extractCommonEventData, extractIdAndName } from './helpers'
import { Expect, IsWebhookHandler } from './types'
import * as bp from '.botpress'
const _handleLabelChangedEvent = async (
props: bp.HandlerProps,
eventType: TrelloEventType.LABEL_ADDED_TO_CARD | TrelloEventType.LABEL_REMOVED_FROM_CARD,
webhookEvent: CardLabelAddedWebhook | CardLabelRemovedWebhook
) => {
return await props.client.createEvent({
type: eventType,
payload: {
...extractCommonEventData(webhookEvent),
board: extractIdAndName(webhookEvent.data.board),
card: extractIdAndName(webhookEvent.data.card),
label: webhookEvent.data.label,
},
})
}
export const handleLabelAddedToCardEvent = async (
props: bp.HandlerProps,
eventType: TrelloEventType.LABEL_ADDED_TO_CARD,
webhookEvent: CardLabelAddedWebhook
) => {
return await _handleLabelChangedEvent(props, eventType, webhookEvent)
}
type _HandleLabelAddedToCardEventTest = Expect<IsWebhookHandler<typeof handleLabelAddedToCardEvent>>
export const handleLabelRemovedFromCardEvent = async (
props: bp.HandlerProps,
eventType: TrelloEventType.LABEL_REMOVED_FROM_CARD,
webhookEvent: CardLabelRemovedWebhook
) => {
return await _handleLabelChangedEvent(props, eventType, webhookEvent)
}
type _HandleLabelRemovedFromCardEventTest = Expect<IsWebhookHandler<typeof handleLabelRemovedFromCardEvent>>
@@ -0,0 +1,116 @@
import { TrelloEventType } from 'definitions/events'
import {
ChecklistAddedToCardWebhook,
ChecklistItemCreatedWebhook,
ChecklistItemDeletedWebhook,
ChecklistItemStatusUpdatedWebhook,
ChecklistItemUpdatedWebhook,
} from '../schemas/checklist-webhook-schemas'
import { extractCommonEventData, extractIdAndName } from './helpers'
import { Expect, IsWebhookHandler } from './types'
import * as bp from '.botpress'
const _extractCommonChecklistItemPayload = (
webhookEvent: ChecklistItemCreatedWebhook | ChecklistItemDeletedWebhook | ChecklistItemStatusUpdatedWebhook
) => ({
...extractCommonEventData(webhookEvent),
board: extractIdAndName(webhookEvent.data.board),
card: extractIdAndName(webhookEvent.data.card),
checklist: webhookEvent.data.checklist,
checklistItem: {
id: webhookEvent.data.checkItem.id,
name: webhookEvent.data.checkItem.name,
isCompleted: webhookEvent.data.checkItem.state === 'complete',
textData: webhookEvent.data.checkItem.textData,
},
})
export const handleChecklistAddedToCardEvent = async (
props: bp.HandlerProps,
eventType: TrelloEventType.CHECKLIST_ADDED_TO_CARD,
webhookEvent: ChecklistAddedToCardWebhook
) => {
return await props.client.createEvent({
type: eventType,
payload: {
...extractCommonEventData(webhookEvent),
board: extractIdAndName(webhookEvent.data.board),
card: extractIdAndName(webhookEvent.data.card),
checklist: webhookEvent.data.checklist,
},
})
}
type _HandleChecklistAddedToCardEventTest = Expect<IsWebhookHandler<typeof handleChecklistAddedToCardEvent>>
export const handleChecklistItemCreatedEvent = async (
props: bp.HandlerProps,
eventType: TrelloEventType.CHECKLIST_ITEM_CREATED,
webhookEvent: ChecklistItemCreatedWebhook
) => {
return await props.client.createEvent({
type: eventType,
payload: _extractCommonChecklistItemPayload(webhookEvent),
})
}
type _HandleChecklistItemCreatedEventTest = Expect<IsWebhookHandler<typeof handleChecklistItemCreatedEvent>>
const _mapOldChecklistItemData = (oldData: ChecklistItemUpdatedWebhook['data']['old']) => {
const { name, state, textData, dueReminder, due } = oldData
return {
name,
isCompleted: state !== undefined ? state === 'complete' : undefined,
textData,
dueDate: due,
dueDateReminder: dueReminder,
}
}
export const handleChecklistItemUpdatedEvent = async (
props: bp.HandlerProps,
eventType: TrelloEventType.CHECKLIST_ITEM_UPDATED,
webhookEvent: ChecklistItemUpdatedWebhook
) => {
return await props.client.createEvent({
type: eventType,
payload: {
...extractCommonEventData(webhookEvent),
board: extractIdAndName(webhookEvent.data.board),
card: extractIdAndName(webhookEvent.data.card),
checklist: webhookEvent.data.checklist,
checklistItem: {
id: webhookEvent.data.checkItem.id,
name: webhookEvent.data.checkItem.name,
isCompleted: webhookEvent.data.checkItem.state === 'complete',
textData: webhookEvent.data.checkItem.textData,
dueDate: webhookEvent.data.checkItem.due,
dueDateReminder: webhookEvent.data.checkItem.dueReminder,
},
old: _mapOldChecklistItemData(webhookEvent.data.old),
},
})
}
type _HandleChecklistItemUpdatedEventTest = Expect<IsWebhookHandler<typeof handleChecklistItemUpdatedEvent>>
export const handleChecklistItemDeletedEvent = async (
props: bp.HandlerProps,
eventType: TrelloEventType.CHECKLIST_ITEM_DELETED,
webhookEvent: ChecklistItemDeletedWebhook
) => {
return await props.client.createEvent({
type: eventType,
payload: _extractCommonChecklistItemPayload(webhookEvent),
})
}
type _HandleChecklistItemDeletedEventTest = Expect<IsWebhookHandler<typeof handleChecklistItemDeletedEvent>>
export const handleChecklistItemStatusUpdatedEvent = async (
props: bp.HandlerProps,
eventType: TrelloEventType.CHECKLIST_ITEM_STATUS_UPDATED,
webhookEvent: ChecklistItemStatusUpdatedWebhook
) => {
return await props.client.createEvent({
type: eventType,
payload: _extractCommonChecklistItemPayload(webhookEvent),
})
}
type _HandleChecklistItemStatusUpdatedEventTest = Expect<IsWebhookHandler<typeof handleChecklistItemStatusUpdatedEvent>>
@@ -0,0 +1,36 @@
import { CommonEventData } from 'definitions/events'
import { TrelloWebhook } from '../schemas/common'
export const extractIdAndName = (obj: { id: string; name: string }) => {
return {
id: obj.id,
name: obj.name,
}
}
export const extractIdAndNameIfExists = (obj: { id: string; name: string } | undefined) => {
return obj ? extractIdAndName(obj) : undefined
}
const _extractActorData = (webhookEvent: TrelloWebhook): CommonEventData['actor'] => {
if (webhookEvent.appCreator !== null) {
return {
id: webhookEvent.appCreator.id,
type: 'app' as const,
}
} else {
return {
id: webhookEvent.memberCreator.id,
type: 'member' as const,
name: webhookEvent.memberCreator.fullName,
}
}
}
export const extractCommonEventData = (webhookEvent: TrelloWebhook): CommonEventData => {
return {
eventId: webhookEvent.id,
actor: _extractActorData(webhookEvent),
dateCreated: webhookEvent.date.toISOString(),
}
}
@@ -0,0 +1,53 @@
import { TrelloEventType } from 'definitions/events'
import { WebhookEventPayload } from '../schemas'
import * as attachmentHandlers from './card-attachment-event-handlers'
import * as commentHandlers from './card-comment-event-handlers'
import * as cardHandlers from './card-event-handlers'
import * as labelHandlers from './card-label-event-handlers'
import * as checklistHandlers from './checklist-event-handlers'
import * as memberHandlers from './member-event-handlers'
import * as bp from '.botpress'
export const dispatchIntegrationEvent = async (props: bp.HandlerProps, eventPayload: WebhookEventPayload) => {
const eventType = eventPayload.action.type
switch (eventType) {
case TrelloEventType.CARD_CREATED:
return cardHandlers.handleCardCreatedEvent(props, eventType, eventPayload.action)
case TrelloEventType.CARD_UPDATED:
return cardHandlers.handleCardUpdatedEvent(props, eventType, eventPayload.action)
case TrelloEventType.CARD_DELETED:
return cardHandlers.handleCardDeletedEvent(props, eventType, eventPayload.action)
case TrelloEventType.CARD_VOTES_UPDATED:
return cardHandlers.handleCardVotesUpdatedEvent(props, eventType, eventPayload.action)
case TrelloEventType.CARD_COMMENT_CREATED:
return commentHandlers.handleCommentAddedEvent(props, eventType, eventPayload.action)
case TrelloEventType.CARD_COMMENT_UPDATED:
return commentHandlers.handleCommentUpdatedEvent(props, eventType, eventPayload.action)
case TrelloEventType.CARD_COMMENT_DELETED:
return commentHandlers.handleCommentDeletedEvent(props, eventType, eventPayload.action)
case TrelloEventType.LABEL_ADDED_TO_CARD:
return labelHandlers.handleLabelAddedToCardEvent(props, eventType, eventPayload.action)
case TrelloEventType.LABEL_REMOVED_FROM_CARD:
return labelHandlers.handleLabelRemovedFromCardEvent(props, eventType, eventPayload.action)
case TrelloEventType.ATTACHMENT_ADDED_TO_CARD:
return attachmentHandlers.handleAttachmentAddedEvent(props, eventType, eventPayload.action)
case TrelloEventType.ATTACHMENT_REMOVED_FROM_CARD:
return attachmentHandlers.handleAttachmentRemovedEvent(props, eventType, eventPayload.action)
case TrelloEventType.CHECKLIST_ADDED_TO_CARD:
return checklistHandlers.handleChecklistAddedToCardEvent(props, eventType, eventPayload.action)
case TrelloEventType.CHECKLIST_ITEM_CREATED:
return checklistHandlers.handleChecklistItemCreatedEvent(props, eventType, eventPayload.action)
case TrelloEventType.CHECKLIST_ITEM_UPDATED:
return checklistHandlers.handleChecklistItemUpdatedEvent(props, eventType, eventPayload.action)
case TrelloEventType.CHECKLIST_ITEM_DELETED:
return checklistHandlers.handleChecklistItemDeletedEvent(props, eventType, eventPayload.action)
case TrelloEventType.CHECKLIST_ITEM_STATUS_UPDATED:
return checklistHandlers.handleChecklistItemStatusUpdatedEvent(props, eventType, eventPayload.action)
case TrelloEventType.MEMBER_ADDED_TO_CARD:
return memberHandlers.handleMemberAddedToCardEvent(props, eventType, eventPayload.action)
case TrelloEventType.MEMBER_REMOVED_FROM_CARD:
return memberHandlers.handleMemberRemovedFromCardEvent(props, eventType, eventPayload.action)
default:
return null
}
}
@@ -0,0 +1,42 @@
import { TrelloEventType } from 'definitions/events'
import { MemberAddedToCardWebhook, MemberRemovedFromCardWebhook } from '../schemas/member-webhook-schemas'
import { extractCommonEventData, extractIdAndName } from './helpers'
import { Expect, IsWebhookHandler } from './types'
import * as bp from '.botpress'
export const handleMemberAddedToCardEvent = async (
props: bp.HandlerProps,
eventType: TrelloEventType.MEMBER_ADDED_TO_CARD,
webhookEvent: MemberAddedToCardWebhook
) => {
return await props.client.createEvent({
type: eventType,
payload: {
...extractCommonEventData(webhookEvent),
board: extractIdAndName(webhookEvent.data.board),
card: extractIdAndName(webhookEvent.data.card),
member: extractIdAndName(webhookEvent.data.member),
},
})
}
type _HandleMemberAddedToCardEventTest = Expect<IsWebhookHandler<typeof handleMemberAddedToCardEvent>>
export const handleMemberRemovedFromCardEvent = async (
props: bp.HandlerProps,
eventType: TrelloEventType.MEMBER_REMOVED_FROM_CARD,
webhookEvent: MemberRemovedFromCardWebhook
) => {
return await props.client.createEvent({
type: eventType,
payload: {
...extractCommonEventData(webhookEvent),
board: extractIdAndName(webhookEvent.data.board),
card: extractIdAndName(webhookEvent.data.card),
member: {
...extractIdAndName(webhookEvent.data.member),
deactivated: webhookEvent.data.deactivated,
},
},
})
}
type _HandleMemberRemovedFromCardEventTest = Expect<IsWebhookHandler<typeof handleMemberRemovedFromCardEvent>>
@@ -0,0 +1,18 @@
import { TrelloWebhook } from '../schemas/common'
import * as bp from '.botpress'
/** Used to enforce the signature of a webhook event handler */
type WebhookEventHandler = (
props: bp.HandlerProps,
eventType: keyof typeof bp.events,
eventPayload: Required<TrelloWebhook>
) => Promise<Awaited<ReturnType<bp.Client['createEvent']>> | null>
// Type testing utils
export type IsWebhookHandler<Handler extends (...args: any) => any> =
Parameters<Handler> extends Parameters<WebhookEventHandler>
? ReturnType<Handler> extends ReturnType<WebhookEventHandler>
? true
: false
: false
export type Expect<_T extends true> = void
@@ -0,0 +1,82 @@
import crypto from 'crypto'
import { TrelloEventType } from 'definitions/events'
import { Result } from '../types'
import { safeParseRequestBody } from '../utils'
import { dispatchIntegrationEvent } from './event-handlers'
import { fallbackEventPayloadSchema, WebhookEventPayload, webhookEventPayloadSchema } from './schemas'
import * as bp from '.botpress'
export const handler = async (props: bp.HandlerProps): Promise<void> => {
if (_verifyWebhookSignature(props)) {
props.logger.forBot().error('The provided webhook payload failed its signature validation')
return
}
const payloadResult = _parseWebhookPayload(props)
if (!payloadResult.success) {
const { error } = payloadResult
props.logger.forBot().error(error.message, error)
return
}
await dispatchIntegrationEvent(props, payloadResult.data)
}
const _isSupportedEventType = (type: string) => Object.values<string>(TrelloEventType).includes(type)
const _parseWebhookPayload = (props: bp.HandlerProps): Result<WebhookEventPayload> => {
const result = safeParseRequestBody(props.req.body)
if (!result.success) return result
const payloadResult = webhookEventPayloadSchema.safeParse(result.data)
if (payloadResult.success) return payloadResult
// Checks for payloads that don't match supported events, or if a supported
// event has a data structure that doesn't match the configured event schema
const fallbackPayloadResult = fallbackEventPayloadSchema.safeParse(result.data)
if (!fallbackPayloadResult.success) {
return {
success: false,
error: new Error(`The webhook payload has an unexpected format -> ${fallbackPayloadResult.error.message}`),
}
}
const eventType = fallbackPayloadResult.data.action.type
if (_isSupportedEventType(eventType)) {
return {
success: false,
error: new Error(
`The event data for the supported event type '${eventType}' has an unexpected format -> ${payloadResult.error.message}`
),
}
}
return {
success: false,
error: new Error(`Unsupported Trello event type: '${eventType}'`),
}
}
/** This only exists because for some reason `process.env.BP_WEBHOOK_URL` is not set */
const _getWebhookUrl = (ctx: bp.Context) =>
`${process.env.BP_API_URL}/${ctx.webhookId}`.replace(/\w+(?=\.botpress)/, 'webhook')
const _base64Digest = (secret: string, content: string) => {
return crypto.createHmac('sha1', secret).update(content).digest('base64')
}
const _verifyWebhookSignature = (props: bp.HandlerProps) => {
const { req, ctx } = props
const { trelloApiSecret } = ctx.configuration
if (!trelloApiSecret) {
// No secret configured, skip verification
return true
}
const callbackURL = _getWebhookUrl(ctx)
const content = (req.body ?? '') + callbackURL
const doubleHash = _base64Digest(trelloApiSecret, content)
const headerHash = req.headers['x-trello-webhook']
return doubleHash === headerHash
}
@@ -0,0 +1 @@
export { handler } from './handler-dispatcher'
@@ -0,0 +1,35 @@
import { z } from '@botpress/sdk'
import { TrelloEventType } from 'definitions/events'
import { pickIdAndName } from 'definitions/events/common'
import { boardSchema, cardSchema, listSchema, trelloIdSchema } from 'definitions/schemas'
import { trelloWebhookSchema } from './common'
export const cardAttachmentAddedWebhookSchema = trelloWebhookSchema.extend({
type: z.literal(TrelloEventType.ATTACHMENT_ADDED_TO_CARD),
data: z.object({
board: pickIdAndName(boardSchema),
list: pickIdAndName(listSchema),
card: pickIdAndName(cardSchema),
attachment: z.object({
id: trelloIdSchema,
name: z.string(),
url: z.string().url(),
previewUrl: z.string().url().optional(),
previewUrl2x: z.string().url().optional(),
}),
}),
})
export type CardAttachmentAddedWebhook = z.infer<typeof cardAttachmentAddedWebhookSchema>
export const cardAttachmentRemovedWebhookSchema = trelloWebhookSchema.extend({
type: z.literal(TrelloEventType.ATTACHMENT_REMOVED_FROM_CARD),
data: z.object({
board: pickIdAndName(boardSchema),
card: pickIdAndName(cardSchema),
attachment: z.object({
id: trelloIdSchema,
name: z.string(),
}),
}),
})
export type CardAttachmentRemovedWebhook = z.infer<typeof cardAttachmentRemovedWebhookSchema>
@@ -0,0 +1,48 @@
/** NOTE: The "brands" are only for documentation purposes, since Trello
* is very inconsistent with the data structures for the comment events */
import { z } from '@botpress/sdk'
import { TrelloEventType } from 'definitions/events'
import { pickIdAndName } from 'definitions/events/common'
import { boardSchema, cardSchema, listSchema, trelloIdSchema } from 'definitions/schemas'
import { trelloWebhookSchema } from './common'
export const commentAddedWebhookSchema = trelloWebhookSchema.extend({
/** @remark This is only the comment ID for the comment added event */
id: trelloIdSchema.brand('EventID').brand('CommentID'),
type: z.literal(TrelloEventType.CARD_COMMENT_CREATED),
data: z.object({
board: pickIdAndName(boardSchema),
list: pickIdAndName(listSchema),
card: pickIdAndName(cardSchema),
text: z.string().brand('CommentText'),
}),
})
export type CommentAddedWebhook = z.infer<typeof commentAddedWebhookSchema>
export const commentUpdatedWebhookSchema = trelloWebhookSchema.extend({
type: z.literal(TrelloEventType.CARD_COMMENT_UPDATED),
data: z.object({
board: pickIdAndName(boardSchema),
card: pickIdAndName(cardSchema),
action: z.object({
id: trelloIdSchema.brand('CommentID'),
text: z.string().brand('NewCommentText'),
}),
old: z.object({
text: z.string().brand('OldCommentText'),
}),
}),
})
export type CommentUpdatedWebhook = z.infer<typeof commentUpdatedWebhookSchema>
export const commentDeletedWebhookSchema = trelloWebhookSchema.extend({
type: z.literal(TrelloEventType.CARD_COMMENT_DELETED),
data: z.object({
board: pickIdAndName(boardSchema),
card: pickIdAndName(cardSchema),
action: z.object({
id: trelloIdSchema.brand('CommentID'),
}),
}),
})
export type CommentDeletedWebhook = z.infer<typeof commentDeletedWebhookSchema>
@@ -0,0 +1,26 @@
import { z } from '@botpress/sdk'
import { TrelloEventType } from 'definitions/events'
import { labelSchema } from 'definitions/events/card-label-events'
import { pickIdAndName } from 'definitions/events/common'
import { boardSchema, cardSchema } from 'definitions/schemas'
import { trelloWebhookSchema } from './common'
export const cardLabelAddedWebhookSchema = trelloWebhookSchema.extend({
type: z.literal(TrelloEventType.LABEL_ADDED_TO_CARD),
data: z.object({
board: pickIdAndName(boardSchema),
card: pickIdAndName(cardSchema),
label: labelSchema,
}),
})
export type CardLabelAddedWebhook = z.infer<typeof cardLabelAddedWebhookSchema>
export const cardLabelRemovedWebhookSchema = trelloWebhookSchema.extend({
type: z.literal(TrelloEventType.LABEL_REMOVED_FROM_CARD),
data: z.object({
board: pickIdAndName(boardSchema),
card: pickIdAndName(cardSchema),
label: labelSchema,
}),
})
export type CardLabelRemovedWebhook = z.infer<typeof cardLabelRemovedWebhookSchema>
@@ -0,0 +1,69 @@
import { z } from '@botpress/sdk'
import { dueReminderSchema, pickIdAndName, TrelloEventType } from 'definitions/events/common'
import { boardSchema, cardSchema, listSchema, trelloIdSchema } from 'definitions/schemas'
import { trelloWebhookSchema } from './common'
const _basicListSchema = pickIdAndName(listSchema)
const _basicCardSchema = pickIdAndName(cardSchema)
export const cardCreatedWebhookSchema = trelloWebhookSchema.extend({
type: z.literal(TrelloEventType.CARD_CREATED),
data: z.object({
board: pickIdAndName(boardSchema),
list: _basicListSchema,
card: _basicCardSchema,
}),
})
export type CardCreatedWebhook = z.infer<typeof cardCreatedWebhookSchema>
const _baseCardUpdateDataSchema = _basicCardSchema
.extend({
// These properties are only included if their values were modified
desc: z.string(),
idList: trelloIdSchema,
idLabels: z.array(trelloIdSchema),
pos: z.number(),
start: z.string().datetime().nullable(),
due: z.string().datetime().nullable(),
dueReminder: dueReminderSchema.nullable(),
dueComplete: z.boolean(),
closed: z.boolean(),
})
.passthrough()
.partial()
export const cardUpdatedWebhookSchema = trelloWebhookSchema.extend({
type: z.literal(TrelloEventType.CARD_UPDATED),
data: z.object({
board: pickIdAndName(boardSchema),
// Seemed to only be excluded/optional when the card is moved between lists
list: _basicListSchema.optional(),
card: _baseCardUpdateDataSchema.required({ id: true, name: true }),
old: _baseCardUpdateDataSchema.omit({ id: true }),
// Only included if the card was moved between lists
listBefore: _basicListSchema.optional(),
// Only included if the card was moved between lists
listAfter: _basicListSchema.optional(),
}),
})
export type CardUpdatedWebhook = z.infer<typeof cardUpdatedWebhookSchema>
export const cardDeletedWebhookSchema = trelloWebhookSchema.extend({
type: z.literal(TrelloEventType.CARD_DELETED),
data: z.object({
board: pickIdAndName(boardSchema),
list: _basicListSchema,
card: _basicCardSchema.pick({ id: true }),
}),
})
export type CardDeletedWebhook = z.infer<typeof cardDeletedWebhookSchema>
export const cardVotesUpdatedWebhookSchema = trelloWebhookSchema.extend({
type: z.literal(TrelloEventType.CARD_VOTES_UPDATED),
data: z.object({
board: pickIdAndName(boardSchema),
card: _basicCardSchema,
voted: z.boolean(),
}),
})
export type CardVotesUpdatedWebhook = z.infer<typeof cardVotesUpdatedWebhookSchema>
@@ -0,0 +1,80 @@
import { z } from '@botpress/sdk'
import { TrelloEventType } from 'definitions/events'
import { checklistSchema } from 'definitions/events/checklist-events'
import { dueReminderSchema, pickIdAndName } from 'definitions/events/common'
import { boardSchema, cardSchema, trelloIdSchema } from 'definitions/schemas'
import { trelloWebhookSchema } from './common'
const _checklistItemCompletionStateSchema = z.union([z.literal('complete'), z.literal('incomplete')])
const _basicChecklistItemSchema = z.object({
id: trelloIdSchema,
name: z.string(),
state: _checklistItemCompletionStateSchema,
textData: z.object({
emoji: z.object({}),
}),
})
export const checklistAddedToCardWebhookSchema = trelloWebhookSchema.extend({
type: z.literal(TrelloEventType.CHECKLIST_ADDED_TO_CARD),
data: z.object({
board: pickIdAndName(boardSchema),
card: pickIdAndName(cardSchema),
checklist: checklistSchema,
}),
})
export type ChecklistAddedToCardWebhook = z.infer<typeof checklistAddedToCardWebhookSchema>
export const checklistItemCreatedWebhookSchema = trelloWebhookSchema.extend({
type: z.literal(TrelloEventType.CHECKLIST_ITEM_CREATED),
data: z.object({
board: pickIdAndName(boardSchema),
card: pickIdAndName(cardSchema),
checklist: checklistSchema,
checkItem: _basicChecklistItemSchema,
}),
})
export type ChecklistItemCreatedWebhook = z.infer<typeof checklistItemCreatedWebhookSchema>
export const checklistItemUpdatedWebhookSchema = trelloWebhookSchema.extend({
type: z.literal(TrelloEventType.CHECKLIST_ITEM_UPDATED),
data: z.object({
board: pickIdAndName(boardSchema),
card: pickIdAndName(cardSchema),
checklist: checklistSchema,
checkItem: _basicChecklistItemSchema.extend({
dueReminder: dueReminderSchema.optional(),
// Technically optional, if I include the "updateCheckItemDue" event type. Otherwise, it isn't included in "CHECKLIST_ITEM_UPDATED" event
due: z.string().datetime().optional(),
}),
old: _basicChecklistItemSchema.omit({ id: true }).partial().extend({
dueReminder: dueReminderSchema.optional(),
// Technically optional, if I include the "updateCheckItemDue" event type. Otherwise, it isn't included in "CHECKLIST_ITEM_UPDATED" event
due: z.string().datetime().optional(),
}),
}),
})
export type ChecklistItemUpdatedWebhook = z.infer<typeof checklistItemUpdatedWebhookSchema>
export const checklistItemDeletedWebhookSchema = trelloWebhookSchema.extend({
type: z.literal(TrelloEventType.CHECKLIST_ITEM_DELETED),
data: z.object({
board: pickIdAndName(boardSchema),
card: pickIdAndName(cardSchema),
checklist: checklistSchema,
checkItem: _basicChecklistItemSchema,
}),
})
export type ChecklistItemDeletedWebhook = z.infer<typeof checklistItemDeletedWebhookSchema>
export const checklistItemStatusUpdatedWebhookSchema = trelloWebhookSchema.extend({
type: z.literal(TrelloEventType.CHECKLIST_ITEM_STATUS_UPDATED),
data: z.object({
board: pickIdAndName(boardSchema),
card: pickIdAndName(cardSchema),
checklist: checklistSchema,
checkItem: _basicChecklistItemSchema,
}),
})
export type ChecklistItemStatusUpdatedWebhook = z.infer<typeof checklistItemStatusUpdatedWebhookSchema>
@@ -0,0 +1,41 @@
import { z } from '@botpress/sdk'
import { TrelloEventType } from 'definitions/events'
import { trelloIdSchema } from 'definitions/schemas'
export const trelloWebhookSchema = z.object({
/** Action ID (aka Event ID) */
id: trelloIdSchema,
/** Event Triggered Date */
date: z.coerce.date(),
type: z.nativeEnum(TrelloEventType),
data: z.any(),
/** The Trello app that triggered the event. (e.g. Via http request using an API key & token)
*
* @remark This field is `null` when the event was not triggered by an app. */
appCreator: z
.object({
/** Some internal Trello identifier for the app that triggered the event.
*
* @remark At the time of writing (2026-01-15), this field cannot be linked back to any
* specific API key or token, as Trello does not expose an endpoint for that purpose.
* Also, through testing each of the endpoints, I have not been able to find it within
* other endpoint responses. */
id: trelloIdSchema,
})
.nullable(),
/** Member who initiated the action, or the member
* who created the app that initiated the action
* (if "appCreator" is not null).
*
* @remark This still appears to be present even if
* the action was initiated via an API key & token. */
memberCreator: z.object({
id: trelloIdSchema,
fullName: z.string(),
username: z.string(),
initials: z.string(),
avatarHash: z.string(),
avatarUrl: z.string(),
}),
})
export type TrelloWebhook = z.infer<typeof trelloWebhookSchema>
@@ -0,0 +1,68 @@
import { z } from '@botpress/sdk'
import { trelloIdSchema } from 'definitions/schemas'
import { cardAttachmentAddedWebhookSchema, cardAttachmentRemovedWebhookSchema } from './card-attachment-webhook-schemas'
import {
commentAddedWebhookSchema,
commentDeletedWebhookSchema,
commentUpdatedWebhookSchema,
} from './card-comment-webhook-schemas'
import { cardLabelAddedWebhookSchema, cardLabelRemovedWebhookSchema } from './card-label-webhook-schemas'
import {
cardCreatedWebhookSchema,
cardDeletedWebhookSchema,
cardUpdatedWebhookSchema,
cardVotesUpdatedWebhookSchema,
} from './card-webhook-schemas'
import {
checklistAddedToCardWebhookSchema,
checklistItemCreatedWebhookSchema,
checklistItemDeletedWebhookSchema,
checklistItemStatusUpdatedWebhookSchema,
checklistItemUpdatedWebhookSchema,
} from './checklist-webhook-schemas'
import { trelloWebhookSchema } from './common'
import { memberAddedToCardWebhookSchema, memberRemovedFromCardWebhookSchema } from './member-webhook-schemas'
const _webhookDetailsSchema = z.object({
id: trelloIdSchema,
idModel: trelloIdSchema,
active: z.boolean(),
consecutiveFailures: z.number().min(0),
})
export const webhookEventPayloadSchema = z.object({
action: z.union([
// ---- Card Events ----
cardCreatedWebhookSchema,
cardUpdatedWebhookSchema,
cardDeletedWebhookSchema,
cardVotesUpdatedWebhookSchema,
// ---- Card Comment Events ----
commentAddedWebhookSchema,
commentUpdatedWebhookSchema,
commentDeletedWebhookSchema,
// ---- Card Label Events ----
cardLabelAddedWebhookSchema,
cardLabelRemovedWebhookSchema,
// ---- Card Attachment Events ----
cardAttachmentAddedWebhookSchema,
cardAttachmentRemovedWebhookSchema,
// ---- Checklist Events ----
checklistAddedToCardWebhookSchema,
checklistItemCreatedWebhookSchema,
checklistItemUpdatedWebhookSchema,
checklistItemDeletedWebhookSchema,
checklistItemStatusUpdatedWebhookSchema,
// ---- Member Events ----
memberAddedToCardWebhookSchema,
memberRemovedFromCardWebhookSchema,
]),
webhook: _webhookDetailsSchema,
})
export type WebhookEventPayload = z.infer<typeof webhookEventPayloadSchema>
/** Fallback schema for unsupported event types */
export const fallbackEventPayloadSchema = z.object({
action: trelloWebhookSchema.extend({ type: z.string() }),
webhook: _webhookDetailsSchema,
})
@@ -0,0 +1,27 @@
import { z } from '@botpress/sdk'
import { TrelloEventType } from 'definitions/events'
import { pickIdAndName } from 'definitions/events/common'
import { eventMemberSchema } from 'definitions/events/member-events'
import { boardSchema, cardSchema } from 'definitions/schemas'
import { trelloWebhookSchema } from './common'
export const memberAddedToCardWebhookSchema = trelloWebhookSchema.extend({
type: z.literal(TrelloEventType.MEMBER_ADDED_TO_CARD),
data: z.object({
board: pickIdAndName(boardSchema),
card: pickIdAndName(cardSchema),
member: eventMemberSchema,
}),
})
export type MemberAddedToCardWebhook = z.infer<typeof memberAddedToCardWebhookSchema>
export const memberRemovedFromCardWebhookSchema = trelloWebhookSchema.extend({
type: z.literal(TrelloEventType.MEMBER_REMOVED_FROM_CARD),
data: z.object({
deactivated: z.boolean(),
board: pickIdAndName(boardSchema),
card: pickIdAndName(cardSchema),
member: eventMemberSchema,
}),
})
export type MemberRemovedFromCardWebhook = z.infer<typeof memberRemovedFromCardWebhookSchema>
@@ -0,0 +1,87 @@
import { Webhook } from 'definitions/schemas'
import { INTEGRATION_NAME } from 'integration.definition'
import { TrelloClient } from './trello-api/trello-client'
import * as bp from '.botpress'
const _registerWebhook = async (
props: bp.CommonHandlerProps,
webhookUrl: string,
modelId: string,
trelloClient = new TrelloClient({ ctx: props.ctx })
): Promise<void> => {
const { ctx, logger } = props
logger.forBot().info('Registering Trello webhook...')
await trelloClient.createWebhook({
description: INTEGRATION_NAME + ctx.integrationId,
url: webhookUrl,
modelId,
})
}
export const registerTrelloWebhookIfNotExists = async (
props: bp.CommonHandlerProps,
webhookUrl: string,
trelloClient = new TrelloClient({ ctx: props.ctx })
): Promise<void> => {
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<void> => {
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<void> => {
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).`)
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"paths": { "*": ["./*"] },
"outDir": "dist"
},
"include": [".botpress/**/*", "definitions/**/*", "src/**/*", "*.ts"]
}
+2
View File
@@ -0,0 +1,2 @@
import config from '../../vitest.config'
export default config