diff --git a/integrations/canny/hub.md b/integrations/canny/hub.md
new file mode 100644
index 00000000000..9023191b734
--- /dev/null
+++ b/integrations/canny/hub.md
@@ -0,0 +1,59 @@
+This integration provides comprehensive access to Canny's API for managing posts and comments in your feature request boards.
+
+# Features
+
+## Actions
+
+**Post Management:**
+
+- **Create Post**: Create new posts in your Canny boards
+- **Get Post**: Retrieve detailed information about a specific post
+- **List Posts**: List posts with filtering and pagination options
+- **Update Post**: Modify existing posts (title, details, ETA, etc.)
+- **Delete Post**: Remove posts from your boards
+
+**Comment Management:**
+
+- **Create Comment**: Add comments to posts
+- **Get Comment**: Retrieve comment details
+- **List Comments**: List comments with filtering options
+- **Delete Comment**: Remove comments
+
+## Channels
+
+**Posts Channel:**
+
+- Handles conversations where posts are the main topic and comments are replies
+- When users send messages in conversations, they're automatically created as comments on the associated post
+
+## Events
+
+**Webhook Support:**
+
+- Automatically creates conversations when new posts are created in Canny
+- Adds messages to conversations when new comments are posted
+- The first message in each conversation is the post content, and subsequent messages are comments
+
+# Configuration
+
+To use this integration, you'll need:
+
+1. **API Key**: Your Canny API key (get this from your Canny account settings)
+
+# Author ID
+
+Posts and comments will appear as "Botpress" unless you provide a specific `authorId`.
+
+To use a different author, provide an `authorId` of an existing user in your Canny workspace. You can find available user Ids using the "List Users" action.
+
+# Usage Examples
+
+## Setting up Webhooks
+
+Configure webhooks in your Canny account to point to your Botpress integration endpoint to automatically sync posts and comments as conversations.
+
+# Conversation Flow
+
+1. **Post Creation**: When a new post is created in Canny, a new conversation is started in Botpress
+2. **Comment Replies**: When users reply in the conversation, new comments are created on the original post
+3. **Thread Management**: Each post becomes a conversation thread with comments as messages
diff --git a/integrations/canny/icon.svg b/integrations/canny/icon.svg
new file mode 100644
index 00000000000..7990248f3b0
--- /dev/null
+++ b/integrations/canny/icon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/integrations/canny/integration.definition.ts b/integrations/canny/integration.definition.ts
new file mode 100644
index 00000000000..7d3ec2b0614
--- /dev/null
+++ b/integrations/canny/integration.definition.ts
@@ -0,0 +1,404 @@
+import { z, IntegrationDefinition } from '@botpress/sdk'
+
+export default new IntegrationDefinition({
+ name: 'canny',
+ version: '0.1.0',
+ title: 'Canny',
+ description: 'Connect your Botpress bot to Canny for feature request management and customer feedback collection',
+ readme: 'hub.md',
+ icon: 'icon.svg',
+ configuration: {
+ schema: z.object({
+ apiKey: z.string().title('API Key').describe('Your Canny API key'),
+ defaultAuthorId: z.string().title('Default Author Id').describe('Default author Id for system messages'),
+ }),
+ },
+ channels: {
+ posts: {
+ title: 'Posts',
+ description: 'Channel for Canny posts and comments',
+ messages: {
+ text: {
+ schema: z.object({
+ text: z.string(),
+ }),
+ },
+ image: {
+ schema: z.object({
+ imageUrl: z.string(),
+ caption: z.string().optional(),
+ }),
+ },
+ },
+ conversation: {
+ tags: {
+ cannyPostId: {
+ title: 'Post ID',
+ description: 'The Canny post ID',
+ },
+ cannyBoardId: {
+ title: 'Board ID',
+ description: 'The Canny board ID',
+ },
+ },
+ },
+ message: {
+ tags: {
+ cannyType: {
+ title: 'Type',
+ description: 'Whether this is a post or comment',
+ },
+ cannyPostId: {
+ title: 'Post ID',
+ description: 'The associated post ID',
+ },
+ cannyCommentId: {
+ title: 'Comment ID',
+ description: 'The comment ID (if applicable)',
+ },
+ },
+ },
+ },
+ },
+ actions: {
+ // Post actions
+ createPost: {
+ title: 'Create Post',
+ description: 'Create a new post in Canny',
+ input: {
+ schema: z.object({
+ authorId: z
+ .string()
+ .optional()
+ .title('Author Id')
+ .describe('The author Id (defaults to Botpress user if not provided)'),
+ boardId: z.string().title('Board Id').describe('The board Id'),
+ title: z.string().title('Post Title').describe('Post title'),
+ details: z.string().title('Post Details').describe('Post details'),
+ byId: z.string().optional().title('By Id').describe('Admin Id who created the post'),
+ categoryId: z.string().optional().title('Category Id').describe('Category Id'),
+ ownerId: z.string().optional().title('Owner Id').describe('Owner Id'),
+ imageURLs: z.array(z.string()).optional().title('Image URLs').describe('Image URLs'),
+ eta: z.string().optional().title('ETA').describe('ETA (MM/YYYY)'),
+ etaPublic: z.boolean().optional().title('ETA Public').describe('Make ETA public'),
+ customFields: z.record(z.any()).optional().title('Custom Fields').describe('Custom fields'),
+ }),
+ },
+ output: {
+ schema: z.object({
+ postId: z.string().title('Post ID').describe('The ID of the created post'),
+ }),
+ },
+ },
+ getPost: {
+ title: 'Get Post',
+ description: 'Retrieve a post by ID',
+ input: {
+ schema: z.object({
+ postId: z.string().title('Post Id').describe('The post Id'),
+ boardId: z.string().optional().title('Board Id').describe('The board Id'),
+ }),
+ },
+ output: {
+ schema: z.object({
+ post: z
+ .object({
+ id: z.string(),
+ title: z.string(),
+ details: z.string().optional(),
+ authorName: z.string().optional(),
+ authorEmail: z.string().optional(),
+ boardName: z.string(),
+ status: z.string(),
+ score: z.number(),
+ commentCount: z.number(),
+ created: z.string(),
+ url: z.string(),
+ })
+ .title('Post')
+ .describe('The retrieved post object'),
+ }),
+ },
+ },
+ listPosts: {
+ title: 'List Posts',
+ description: 'List posts with optional filters',
+ input: {
+ schema: z.object({
+ boardId: z.string().optional().title('Board Id').describe('Filter posts by board Id'),
+ authorId: z.string().optional().title('Author Id').describe('Filter posts by author Id'),
+ companyId: z.string().optional().title('Company Id').describe('Filter posts by company Id'),
+ tagIds: z.array(z.string()).optional().title('Tag Ids').describe('Filter posts by tag Ids'),
+ limit: z.number().optional().title('Limit').describe('Number of posts to return'),
+ nextToken: z.number().optional().title('Next Token').describe('Token for pagination - skip this many posts'),
+ search: z.string().optional().title('Search').describe('Search term to filter posts'),
+ sort: z
+ .enum(['newest', 'oldest', 'relevance', 'score', 'statusChanged', 'trending'])
+ .optional()
+ .title('Sort')
+ .describe('Sort order for posts'),
+ status: z.string().optional().title('Status').describe('Filter posts by status'),
+ }),
+ },
+ output: {
+ schema: z.object({
+ posts: z
+ .array(
+ z.object({
+ id: z.string(),
+ title: z.string(),
+ details: z.string().optional(),
+ authorName: z.string().optional(),
+ authorEmail: z.string().optional(),
+ boardName: z.string(),
+ status: z.string(),
+ score: z.number(),
+ commentCount: z.number(),
+ created: z.string(),
+ url: z.string(),
+ })
+ )
+ .title('Posts')
+ .describe('Array of posts'),
+ nextToken: z.number().optional().title('Next Token').describe('Token for next page if more posts available'),
+ }),
+ },
+ },
+ updatePost: {
+ title: 'Update Post',
+ description: 'Update an existing post',
+ input: {
+ schema: z.object({
+ postId: z.string().title('Post Id').describe('The post Id'),
+ title: z.string().optional().title('Title').describe('Updated post title'),
+ details: z.string().optional().title('Details').describe('Updated post details'),
+ eta: z.string().optional().title('ETA').describe('Updated ETA (MM/YYYY)'),
+ etaPublic: z.boolean().optional().title('ETA Public').describe('Make ETA public'),
+ imageURLs: z.array(z.string()).optional().title('Image URLs').describe('Updated image URLs'),
+ customFields: z.record(z.any()).optional().title('Custom Fields').describe('Updated custom fields'),
+ }),
+ },
+ output: {
+ schema: z.object({}),
+ },
+ },
+ deletePost: {
+ title: 'Delete Post',
+ description: 'Delete a post',
+ input: {
+ schema: z.object({
+ postId: z.string().title('Post Id').describe('The post Id to delete'),
+ }),
+ },
+ output: {
+ schema: z.object({}),
+ },
+ },
+
+ // Comment actions
+ createComment: {
+ title: 'Create Comment',
+ description: 'Create a new comment on a post',
+ input: {
+ schema: z.object({
+ authorId: z
+ .string()
+ .optional()
+ .title('Author Id')
+ .describe('The author Id (defaults to Botpress user if not provided)'),
+ postId: z.string().title('Post Id').describe('The post Id'),
+ value: z.string().title('Comment Text').describe('Comment text'),
+ parentId: z.string().optional().title('Parent Id').describe('Parent comment Id for replies'),
+ imageURLs: z.array(z.string()).optional().title('Image URLs').describe('Image URLs'),
+ internal: z.boolean().optional().title('Internal').describe('Whether this is an internal comment'),
+ shouldNotifyVoters: z.boolean().optional().title('Notify Voters').describe('Notify voters'),
+ createdAt: z.string().optional().title('Created At').describe('Created timestamp'),
+ }),
+ },
+ output: {
+ schema: z.object({
+ commentId: z.string().title('Comment ID').describe('The ID of the created comment'),
+ }),
+ },
+ },
+ getComment: {
+ title: 'Get Comment',
+ description: 'Retrieve a comment by ID',
+ input: {
+ schema: z.object({
+ commentId: z.string().title('Comment Id').describe('The comment Id'),
+ }),
+ },
+ output: {
+ schema: z.object({
+ comment: z
+ .object({
+ id: z.string(),
+ value: z.string(),
+ authorName: z.string(),
+ authorEmail: z.string(),
+ postTitle: z.string(),
+ postId: z.string(),
+ created: z.string(),
+ internal: z.boolean(),
+ likeCount: z.number(),
+ })
+ .title('Comment')
+ .describe('The retrieved comment object'),
+ }),
+ },
+ },
+ listComments: {
+ title: 'List Comments',
+ description: 'List comments with optional filters',
+ input: {
+ schema: z.object({
+ postId: z.string().optional().title('Post Id').describe('Filter comments by post Id'),
+ authorId: z.string().optional().title('Author Id').describe('Filter comments by author Id'),
+ boardId: z.string().optional().title('Board Id').describe('Filter comments by board Id'),
+ companyId: z.string().optional().title('Company Id').describe('Filter comments by company Id'),
+ limit: z.number().optional().title('Limit').describe('Number of comments to return'),
+ nextToken: z
+ .number()
+ .optional()
+ .title('Next Token')
+ .describe('Token for pagination - skip this many comments'),
+ }),
+ },
+ output: {
+ schema: z.object({
+ comments: z
+ .array(
+ z.object({
+ id: z.string(),
+ value: z.string(),
+ authorName: z.string(),
+ authorEmail: z.string(),
+ postTitle: z.string(),
+ postId: z.string(),
+ created: z.string(),
+ internal: z.boolean(),
+ likeCount: z.number(),
+ })
+ )
+ .title('Comments')
+ .describe('Array of comments'),
+ nextToken: z
+ .number()
+ .optional()
+ .title('Next Token')
+ .describe('Token for next page if more comments available'),
+ }),
+ },
+ },
+ deleteComment: {
+ title: 'Delete Comment',
+ description: 'Delete a comment',
+ input: {
+ schema: z.object({
+ commentId: z.string().title('Comment Id').describe('The comment Id to delete'),
+ }),
+ },
+ output: {
+ schema: z.object({}),
+ },
+ },
+
+ // User management actions
+ createOrUpdateUser: {
+ title: 'Create or Update User',
+ description: 'Create a new user or update an existing one in Canny',
+ input: {
+ schema: z.object({
+ name: z.string().min(1).max(50).title('Name').describe('User display name (required, 1-50 characters)'),
+ userId: z.string().optional().title('User Id').describe('Your internal user Id'),
+ email: z.string().optional().title('Email').describe('User email address'),
+ avatarURL: z.string().optional().title('Avatar URL').describe('User avatar URL'),
+ alias: z.string().optional().title('Alias').describe('User alias'),
+ created: z.string().optional().title('Created').describe('User creation timestamp'),
+ customFields: z.record(z.any()).optional().title('Custom Fields').describe('Custom fields'),
+ }),
+ },
+ output: {
+ schema: z.object({
+ user: z
+ .object({
+ id: z.string(),
+ name: z.string(),
+ email: z.string(),
+ avatarURL: z.string().optional(),
+ userId: z.string(),
+ isAdmin: z.boolean(),
+ created: z.string(),
+ })
+ .title('User')
+ .describe('The created or updated user object'),
+ }),
+ },
+ },
+ listUsers: {
+ title: 'List Users',
+ description: 'List all users in your Canny workspace with pagination',
+ input: {
+ schema: z.object({
+ limit: z
+ .number()
+ .min(1)
+ .max(100)
+ .optional()
+ .title('Limit')
+ .describe('Number of users to fetch (1-100, defaults to 10)'),
+ nextToken: z.string().optional().title('Next Token').describe('Token for pagination from previous request'),
+ }),
+ },
+ output: {
+ schema: z.object({
+ users: z
+ .array(
+ z.object({
+ id: z.string(),
+ name: z.string(),
+ email: z.string(),
+ avatarURL: z.string().optional(),
+ userId: z.string(), // Can be empty string if user has no userId
+ isAdmin: z.boolean(),
+ created: z.string(),
+ })
+ )
+ .title('Users')
+ .describe('Array of users'),
+ nextToken: z.string().optional().title('Next Token').describe('Token for next page if more users available'),
+ }),
+ },
+ },
+
+ // Board management actions
+ listBoards: {
+ title: 'List Boards',
+ description: 'List all boards in your Canny workspace',
+ input: {
+ schema: z.object({
+ limit: z.number().optional().title('Limit').describe('Number of boards to fetch'),
+ nextToken: z.number().optional().title('Next Token').describe('Token for pagination - skip this many boards'),
+ }),
+ },
+ output: {
+ schema: z.object({
+ boards: z
+ .array(
+ z.object({
+ id: z.string(),
+ name: z.string(),
+ postCount: z.number(),
+ url: z.string(),
+ created: z.string(),
+ })
+ )
+ .title('Boards')
+ .describe('Array of boards'),
+ nextToken: z.number().optional().title('Next Token').describe('Token for next page if more boards available'),
+ }),
+ },
+ },
+ },
+})
diff --git a/integrations/canny/package.json b/integrations/canny/package.json
new file mode 100644
index 00000000000..d8082a1cb4d
--- /dev/null
+++ b/integrations/canny/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "@botpresshub/canny",
+ "description": "Canny integration for Botpress",
+ "private": true,
+ "scripts": {
+ "build": "bp add -y && bp build",
+ "check:type": "tsc --noEmit",
+ "check:bplint": "bp lint"
+ },
+ "dependencies": {
+ "@botpress/client": "workspace:*",
+ "@botpress/sdk": "workspace:*",
+ "axios": "^1.6.0"
+ }
+}
diff --git a/integrations/canny/src/actions/boards.ts b/integrations/canny/src/actions/boards.ts
new file mode 100644
index 00000000000..1508a872eaa
--- /dev/null
+++ b/integrations/canny/src/actions/boards.ts
@@ -0,0 +1,49 @@
+import { CannyClient } from '../misc/canny-client'
+import { InvalidAPIKeyError, CannyAPIError } from '../misc/errors'
+import { IntegrationProps } from '.botpress'
+
+type ListBoardsAction = IntegrationProps['actions']['listBoards']
+
+const DEFAULT_BOARD_LIMIT = 25
+
+export const listBoards: ListBoardsAction = async ({ input, ctx }) => {
+ if (!ctx.configuration.apiKey) {
+ throw new InvalidAPIKeyError()
+ }
+
+ const client = CannyClient.create({
+ apiKey: ctx.configuration.apiKey,
+ })
+
+ try {
+ const result = await client.listBoards({
+ limit: input.limit,
+ skip: input.nextToken,
+ })
+
+ const boards = result.boards || []
+ const requestedLimit = input.limit || DEFAULT_BOARD_LIMIT
+ const hasMore = result.hasMore !== undefined ? result.hasMore : boards.length >= requestedLimit
+
+ const nextToken = hasMore ? (input.nextToken ?? 0) + (input.limit ?? DEFAULT_BOARD_LIMIT) : undefined
+
+ return {
+ boards: boards.map((board) => ({
+ id: board.id,
+ name: board.name,
+ postCount: board.postCount,
+ url: board.url,
+ created: board.created,
+ })),
+ nextToken,
+ }
+ } catch (error: any) {
+ console.error('Error listing boards:', error)
+
+ if (error.response?.status === 401) {
+ throw new InvalidAPIKeyError()
+ }
+
+ throw new CannyAPIError(error)
+ }
+}
diff --git a/integrations/canny/src/actions/comments.ts b/integrations/canny/src/actions/comments.ts
new file mode 100644
index 00000000000..61ef2dd4afb
--- /dev/null
+++ b/integrations/canny/src/actions/comments.ts
@@ -0,0 +1,133 @@
+import { RuntimeError } from '@botpress/sdk'
+import { CannyClient } from '../misc/canny-client'
+import { InvalidAPIKeyError, CannyAPIError } from '../misc/errors'
+import { IntegrationProps } from '.botpress'
+
+type CreateCommentAction = IntegrationProps['actions']['createComment']
+type GetCommentAction = IntegrationProps['actions']['getComment']
+type ListCommentsAction = IntegrationProps['actions']['listComments']
+type DeleteCommentAction = IntegrationProps['actions']['deleteComment']
+
+export const createComment: CreateCommentAction = async ({ input, ctx }) => {
+ if (!ctx.configuration.apiKey) {
+ throw new InvalidAPIKeyError()
+ }
+
+ const client = CannyClient.create({
+ apiKey: ctx.configuration.apiKey,
+ })
+
+ let authorId = input.authorId
+
+ if (!authorId) {
+ // Create the Botpress user for comments
+ const botUser = await client.createOrUpdateUser({
+ name: 'Botpress',
+ userId: 'botpress-user',
+ email: 'integration@botpress.com',
+ })
+ authorId = botUser.id
+ }
+ if (!input.postId) {
+ throw new RuntimeError('postId is required to create a comment')
+ }
+ if (!input.value) {
+ throw new RuntimeError('value (comment text) is required to create a comment')
+ }
+
+ try {
+ const result = await client.createComment({
+ authorId,
+ postId: input.postId,
+ value: input.value,
+ parentId: input.parentId,
+ imageURLs: input.imageURLs,
+ internal: input.internal,
+ shouldNotifyVoters: input.shouldNotifyVoters,
+ createdAt: input.createdAt,
+ })
+
+ return {
+ commentId: result.id,
+ }
+ } catch (error: any) {
+ if (
+ error.response?.data?.error?.includes('user') ||
+ error.response?.data?.error?.includes('author') ||
+ error.message?.includes('user')
+ ) {
+ if (!input.authorId) {
+ throw new RuntimeError(
+ 'Comment creation failed: Canny requires users to be "identified" through their SDK. Please provide an authorId of a user who has been identified in your Canny workspace, or ensure the default author from the integration configuration is properly identified.'
+ )
+ } else {
+ throw new RuntimeError(
+ `Comment creation failed: The authorId "${authorId}" is not valid or the user hasn't been identified through Canny's SDK.`
+ )
+ }
+ }
+ throw new CannyAPIError(error)
+ }
+}
+
+export const getComment: GetCommentAction = async ({ input, ctx }) => {
+ const client = CannyClient.create({
+ apiKey: ctx.configuration.apiKey,
+ })
+
+ const comment = await client.getComment(input.commentId)
+
+ return {
+ comment: {
+ id: comment.id,
+ value: comment.value,
+ authorName: comment.author.name,
+ authorEmail: comment.author.email,
+ postTitle: comment.post.title,
+ postId: comment.post.id,
+ created: comment.created,
+ internal: comment.internal,
+ likeCount: comment.likeCount,
+ },
+ }
+}
+
+export const listComments: ListCommentsAction = async ({ input, ctx }) => {
+ const client = CannyClient.create({
+ apiKey: ctx.configuration.apiKey,
+ })
+
+ const result = await client.listComments({
+ postId: input.postId,
+ authorId: input.authorId,
+ boardId: input.boardId,
+ companyId: input.companyId,
+ limit: input.limit,
+ skip: input.nextToken,
+ })
+
+ return {
+ comments: result.comments.map((comment) => ({
+ id: comment.id,
+ value: comment.value,
+ authorName: comment.author.name,
+ authorEmail: comment.author.email,
+ postTitle: comment.post.title,
+ postId: comment.post.id,
+ created: comment.created,
+ internal: comment.internal,
+ likeCount: comment.likeCount,
+ })),
+ nextToken: result.hasMore ? (input.nextToken || 0) + (input.limit || 10) : undefined,
+ }
+}
+
+export const deleteComment: DeleteCommentAction = async ({ input, ctx }) => {
+ const client = CannyClient.create({
+ apiKey: ctx.configuration.apiKey,
+ })
+
+ await client.deleteComment(input.commentId)
+
+ return {}
+}
diff --git a/integrations/canny/src/actions/posts.ts b/integrations/canny/src/actions/posts.ts
new file mode 100644
index 00000000000..f6fe2180b45
--- /dev/null
+++ b/integrations/canny/src/actions/posts.ts
@@ -0,0 +1,207 @@
+import { RuntimeError } from '@botpress/sdk'
+import { CannyClient } from '../misc/canny-client'
+import { InvalidAPIKeyError, CannyAPIError } from '../misc/errors'
+import { IntegrationProps } from '.botpress'
+
+type CreatePostAction = IntegrationProps['actions']['createPost']
+type GetPostAction = IntegrationProps['actions']['getPost']
+type ListPostsAction = IntegrationProps['actions']['listPosts']
+type UpdatePostAction = IntegrationProps['actions']['updatePost']
+type DeletePostAction = IntegrationProps['actions']['deletePost']
+
+export const createPost: CreatePostAction = async ({ input, ctx }) => {
+ if (!ctx.configuration.apiKey) {
+ throw new InvalidAPIKeyError()
+ }
+
+ const client = CannyClient.create({
+ apiKey: ctx.configuration.apiKey,
+ })
+
+ const authorId = input.authorId
+
+ if (!input.boardId) {
+ throw new RuntimeError('boardId is required to create a post')
+ }
+
+ if (!input.title) {
+ throw new RuntimeError('title is required to create a post')
+ }
+ if (!input.details) {
+ throw new RuntimeError('details is required to create a post')
+ }
+
+ if (!authorId) {
+ const botUser = await client.createOrUpdateUser({
+ name: 'Botpress',
+ userId: 'botpress-user',
+ email: 'integration@botpress.com',
+ })
+
+ // Try approach 1: Use the userId field
+ try {
+ const result = await client.createPost({
+ authorId: 'botpress-user',
+ boardId: input.boardId,
+ title: input.title,
+ details: input.details,
+ byId: input.byId,
+ categoryId: input.categoryId,
+ ownerId: input.ownerId,
+ imageURLs: input.imageURLs,
+ eta: input.eta,
+ etaPublic: input.etaPublic,
+ customFields: input.customFields,
+ })
+
+ return { postId: result.id }
+ } catch (error1: any) {
+ // Try approach 2: Use the Canny-generated id
+ try {
+ const result = await client.createPost({
+ authorId: botUser.id,
+ boardId: input.boardId,
+ title: input.title,
+ details: input.details,
+ byId: input.byId,
+ categoryId: input.categoryId,
+ ownerId: input.ownerId,
+ imageURLs: input.imageURLs,
+ eta: input.eta,
+ etaPublic: input.etaPublic,
+ customFields: input.customFields,
+ })
+
+ return { postId: result.id }
+ } catch (error2: any) {
+ throw new RuntimeError(
+ `Post creation failed: Canny requires users to be "identified" through their SDK. Both approaches failed: userId (${error1.response?.data?.error || error1.message}) and Canny id (${error2.response?.data?.error || error2.message}). Please provide an authorId of a user who has been identified in your Canny workspace!`
+ )
+ }
+ }
+ }
+
+ try {
+ const result = await client.createPost({
+ authorId,
+ boardId: input.boardId,
+ title: input.title,
+ details: input.details,
+ byId: input.byId,
+ categoryId: input.categoryId,
+ ownerId: input.ownerId,
+ imageURLs: input.imageURLs,
+ eta: input.eta,
+ etaPublic: input.etaPublic,
+ customFields: input.customFields,
+ })
+
+ return {
+ postId: result.id,
+ }
+ } catch (error: any) {
+ if (
+ error.response?.data?.error?.includes('user') ||
+ error.response?.data?.error?.includes('author') ||
+ error.message?.includes('user')
+ ) {
+ if (!input.authorId) {
+ throw new RuntimeError(
+ 'Post creation failed: Canny requires users to be "identified" through their SDK. Please provide an authorId of a user who has been identified in your Canny workspace, or ensure the default author from the integration configuration is properly identified.'
+ )
+ } else {
+ throw new RuntimeError(
+ `Post creation failed: The authorId "${authorId}" is not valid or the user hasn't been identified through Canny's SDK.`
+ )
+ }
+ }
+ throw new CannyAPIError(error)
+ }
+}
+
+export const getPost: GetPostAction = async ({ input, ctx }) => {
+ const client = CannyClient.create({
+ apiKey: ctx.configuration.apiKey,
+ })
+
+ const post = await client.getPost(input.postId, input.boardId)
+
+ return {
+ post: {
+ id: post.id,
+ title: post.title,
+ details: post.details,
+ authorName: post.author?.name,
+ authorEmail: post.author?.email,
+ boardName: post.board.name,
+ status: post.status,
+ score: post.score,
+ commentCount: post.commentCount,
+ created: post.created,
+ url: post.url,
+ },
+ }
+}
+
+export const listPosts: ListPostsAction = async ({ input, ctx }) => {
+ const client = CannyClient.create({
+ apiKey: ctx.configuration.apiKey,
+ })
+
+ const result = await client.listPosts({
+ boardId: input.boardId,
+ authorId: input.authorId,
+ companyId: input.companyId,
+ tagIds: input.tagIds,
+ limit: input.limit,
+ skip: input.nextToken,
+ search: input.search,
+ sort: input.sort,
+ status: input.status,
+ })
+
+ return {
+ posts: result.posts.map((post) => ({
+ id: post.id,
+ title: post.title,
+ details: post.details,
+ authorName: post.author?.name,
+ authorEmail: post.author?.email,
+ boardName: post.board.name,
+ status: post.status,
+ score: post.score,
+ commentCount: post.commentCount,
+ created: post.created,
+ url: post.url,
+ })),
+ nextToken: result.hasMore ? (input.nextToken || 0) + (input.limit || 10) : undefined,
+ }
+}
+
+export const updatePost: UpdatePostAction = async ({ input, ctx }) => {
+ const client = CannyClient.create({
+ apiKey: ctx.configuration.apiKey,
+ })
+
+ await client.updatePost({
+ postId: input.postId,
+ title: input.title,
+ details: input.details,
+ eta: input.eta,
+ etaPublic: input.etaPublic,
+ imageURLs: input.imageURLs,
+ customFields: input.customFields,
+ })
+
+ return {}
+}
+
+export const deletePost: DeletePostAction = async ({ input, ctx }) => {
+ const client = CannyClient.create({
+ apiKey: ctx.configuration.apiKey,
+ })
+
+ await client.deletePost(input.postId)
+
+ return {}
+}
diff --git a/integrations/canny/src/actions/users.ts b/integrations/canny/src/actions/users.ts
new file mode 100644
index 00000000000..cf957d524cc
--- /dev/null
+++ b/integrations/canny/src/actions/users.ts
@@ -0,0 +1,111 @@
+import { RuntimeError } from '@botpress/sdk'
+import { CannyClient } from '../misc/canny-client'
+import { InvalidAPIKeyError, InvalidRequestError, CannyAPIError } from '../misc/errors'
+import { IntegrationProps } from '.botpress'
+
+// User action types
+type CreateOrUpdateUserAction = IntegrationProps['actions']['createOrUpdateUser']
+type ListUsersAction = IntegrationProps['actions']['listUsers']
+
+export const createOrUpdateUser: CreateOrUpdateUserAction = async ({ input, ctx }) => {
+ if (!ctx.configuration.apiKey) {
+ throw new InvalidAPIKeyError()
+ }
+
+ const client = CannyClient.create({
+ apiKey: ctx.configuration.apiKey,
+ })
+
+ if (!input.name || input.name.trim().length === 0) {
+ throw new RuntimeError('name is required and must be between 1-50 characters')
+ }
+ if (input.name.length > 50) {
+ throw new RuntimeError('name must be 50 characters or less')
+ }
+
+ try {
+ const user = await client.createOrUpdateUser({
+ name: input.name,
+ userId: input.userId,
+ email: input.email,
+ avatarURL: input.avatarURL,
+ alias: input.alias,
+ created: input.created,
+ customFields: input.customFields,
+ })
+
+ return {
+ user: {
+ id: user.id,
+ name: user.name,
+ email: user.email,
+ avatarURL: user.url, // Canny returns avatar in 'url' field
+ userId: user.userId,
+ isAdmin: user.isAdmin,
+ created: user.created,
+ },
+ }
+ } catch (error: any) {
+ console.error('Error creating/updating user:', error)
+
+ if (error.response?.status === 401) {
+ throw new InvalidAPIKeyError()
+ }
+
+ if (error.response?.status === 400) {
+ throw new InvalidRequestError(error)
+ }
+
+ if (error.response?.data?.error?.includes('user') || error.message?.includes('user')) {
+ throw new RuntimeError(
+ `User identification failed: ${error.response?.data?.error || error.message}. Make sure the user is properly identified through Canny Identify SDK.`
+ )
+ }
+
+ throw new CannyAPIError(error)
+ }
+}
+
+export const listUsers: ListUsersAction = async ({ input, ctx }) => {
+ if (!ctx.configuration.apiKey) {
+ throw new InvalidAPIKeyError()
+ }
+
+ if (input.limit && (input.limit < 1 || input.limit > 100)) {
+ throw new RuntimeError('limit must be between 1 and 100')
+ }
+
+ const client = CannyClient.create({
+ apiKey: ctx.configuration.apiKey,
+ })
+
+ try {
+ const result = await client.listUsers({
+ limit: input.limit,
+ cursor: input.nextToken,
+ })
+
+ const response = {
+ users: result.users.map((user) => ({
+ id: user.id,
+ name: user.name,
+ email: user.email || '',
+ avatarURL: user.url || undefined,
+ userId: user.userId || '',
+ isAdmin: user.isAdmin,
+ created: user.created,
+ })),
+ nextToken: result.hasNextPage ? (result as any).cursor : undefined,
+ }
+
+ return response
+ } catch (error: any) {
+ console.error('Error listing users:', error)
+
+ if (error.response?.status === 401) {
+ throw new InvalidAPIKeyError()
+ }
+
+ throw new CannyAPIError(error)
+ }
+}
diff --git a/integrations/canny/src/channels/posts.ts b/integrations/canny/src/channels/posts.ts
new file mode 100644
index 00000000000..43ad2f50a3d
--- /dev/null
+++ b/integrations/canny/src/channels/posts.ts
@@ -0,0 +1,58 @@
+import { CannyClient } from '../misc/canny-client'
+
+export const posts = {
+ messages: {
+ text: async ({ payload, ctx, conversation, ack }: any) => {
+ const client = CannyClient.create({
+ apiKey: ctx.configuration.apiKey,
+ })
+
+ const postId = conversation.tags?.cannyPostId
+ if (!postId) {
+ throw new Error('No post ID found in conversation')
+ }
+
+ const botUser = await client.createOrUpdateUser({
+ name: 'Botpress',
+ userId: 'botpress-user',
+ email: 'integration@botpress.com',
+ })
+ const authorId = botUser.id
+
+ await client.createComment({
+ authorId,
+ postId,
+ value: payload.text,
+ })
+
+ await ack()
+ },
+
+ image: async ({ payload, ctx, conversation, ack }: any) => {
+ const client = CannyClient.create({
+ apiKey: ctx.configuration.apiKey,
+ })
+
+ const postId = conversation.tags?.cannyPostId
+ if (!postId) {
+ throw new Error('No post ID found in conversation')
+ }
+
+ const botUser = await client.createOrUpdateUser({
+ name: 'Botpress',
+ userId: 'botpress-user',
+ email: 'integration@botpress.com',
+ })
+ const authorId = botUser.id
+
+ await client.createComment({
+ authorId,
+ postId,
+ value: payload.caption || 'Image attached',
+ imageURLs: [payload.imageUrl],
+ })
+
+ await ack()
+ },
+ },
+}
diff --git a/integrations/canny/src/handlers/webhook.ts b/integrations/canny/src/handlers/webhook.ts
new file mode 100644
index 00000000000..a591b1ce18f
--- /dev/null
+++ b/integrations/canny/src/handlers/webhook.ts
@@ -0,0 +1,94 @@
+import { z } from '@botpress/sdk'
+import { IntegrationProps } from '.botpress'
+
+type WebhookHandler = IntegrationProps['handler']
+
+const CannyWebhookPayloadSchema = z.object({
+ type: z.enum(['post.created', 'comment.created']),
+ data: z.any(),
+})
+
+export const webhook: WebhookHandler = async ({ req, client }) => {
+ if (req.method !== 'POST') {
+ return { status: 405, body: 'Method not allowed' }
+ }
+
+ try {
+ const payload = CannyWebhookPayloadSchema.parse(req.body)
+
+ switch (payload.type) {
+ case 'post.created':
+ await handlePostCreated(client, payload.data)
+ break
+
+ case 'comment.created':
+ await handleCommentCreated(client, payload.data)
+ break
+
+ default:
+ }
+
+ return { status: 200, body: 'OK' }
+ } catch (error) {
+ console.error('Webhook processing error:', error)
+ return { status: 500, body: 'Internal server error' }
+ }
+}
+
+async function handlePostCreated(client: Parameters[0]['client'], postData: any) {
+ try {
+ const { conversation } = await client.createConversation({
+ channel: 'posts',
+ tags: {
+ cannyPostId: postData.id,
+ cannyBoardId: postData.board.id,
+ },
+ })
+
+ await client.createMessage({
+ conversationId: conversation.id,
+ type: 'text',
+ payload: {
+ text: `**${postData.title}**\n\n${postData.details || ''}\n\n*Posted by ${postData.author?.name || 'Unknown'}*`,
+ },
+ userId: postData.author?.id || 'canny-system',
+ tags: {
+ cannyType: 'post',
+ cannyPostId: postData.id,
+ },
+ })
+ } catch (error) {
+ console.error('Error handling post creation:', error)
+ }
+}
+
+async function handleCommentCreated(client: Parameters[0]['client'], commentData: any) {
+ try {
+ const { conversations } = await client.listConversations({
+ tags: {
+ cannyPostId: commentData.post.id,
+ },
+ })
+
+ const conversation = conversations[0]
+ if (!conversation) {
+ return
+ }
+
+ await client.createMessage({
+ conversationId: conversation.id,
+ type: 'text',
+ payload: {
+ text: commentData.value,
+ },
+ userId: commentData.author.id,
+ tags: {
+ cannyType: 'comment',
+ cannyCommentId: commentData.id,
+ cannyPostId: commentData.post.id,
+ },
+ })
+ } catch (error) {
+ console.error('Error handling comment creation:', error)
+ }
+}
diff --git a/integrations/canny/src/index.ts b/integrations/canny/src/index.ts
new file mode 100644
index 00000000000..ce8dc5b8df9
--- /dev/null
+++ b/integrations/canny/src/index.ts
@@ -0,0 +1,55 @@
+import { listBoards } from './actions/boards'
+import { createComment, getComment, listComments, deleteComment } from './actions/comments'
+import { createPost, getPost, listPosts, updatePost, deletePost } from './actions/posts'
+
+import { createOrUpdateUser, listUsers } from './actions/users'
+
+import { posts } from './channels/posts'
+import { webhook } from './handlers/webhook'
+import { CannyClient } from './misc/canny-client'
+
+import * as bp from '.botpress'
+
+export default new bp.Integration({
+ register: async ({ ctx }) => {
+ try {
+ if (!ctx.configuration.apiKey) {
+ throw new Error('Canny API key is not configured')
+ }
+
+ const client = CannyClient.create({
+ apiKey: ctx.configuration.apiKey,
+ })
+
+ if (!ctx.configuration.defaultAuthorId) {
+ throw new Error('Default author ID is required in integration configuration.')
+ }
+
+ const boardsResult = await client.listBoards()
+
+ boardsResult.boards.forEach((_board) => {})
+ } catch (error) {
+ console.error('Failed to register Canny integration:', error)
+ throw error
+ }
+ },
+ unregister: async () => {},
+ actions: {
+ createPost,
+ getPost,
+ listPosts,
+ updatePost,
+ deletePost,
+ createComment,
+ getComment,
+ listComments,
+ deleteComment,
+ createOrUpdateUser,
+ listUsers,
+ listBoards,
+ },
+ channels: {
+ posts,
+ },
+ handler: webhook,
+})
diff --git a/integrations/canny/src/misc/canny-client.ts b/integrations/canny/src/misc/canny-client.ts
new file mode 100644
index 00000000000..0e3625ee36a
--- /dev/null
+++ b/integrations/canny/src/misc/canny-client.ts
@@ -0,0 +1,291 @@
+import axios, { Axios } from 'axios'
+
+export type CannyClientConfiguration = {
+ apiKey: string
+}
+
+export type User = {
+ id: string
+ created: string
+ email: string
+ isAdmin: boolean
+ name: string
+ url: string
+ userId: string
+}
+
+export type Board = {
+ id: string
+ created: string
+ name: string
+ postCount: number
+ url: string
+}
+
+export type Category = {
+ id: string
+ name: string
+ postCount: number
+ url: string
+ parentId?: string
+}
+
+export type Tag = {
+ id: string
+ name: string
+ postCount: number
+ url: string
+}
+
+export type Post = {
+ id: string
+ author: User | null
+ board: Board
+ by?: User
+ category?: Category
+ commentCount: number
+ created: string
+ details?: string
+ eta?: string
+ imageURLs: string[]
+ jira?: {
+ linkedIssues: Array<{
+ id: string
+ key: string
+ url: string
+ }>
+ }
+ linear?: {
+ linkedIssueIds: string[]
+ }
+ owner?: User
+ score: number
+ status: string
+ statusChangedAt?: string
+ tags: Tag[]
+ title: string
+ url: string
+}
+
+export type Comment = {
+ id: string
+ author: User
+ board: Board
+ created: string
+ imageURLs: string[]
+ internal: boolean
+ likeCount: number
+ mentions: User[]
+ parentId?: string
+ post: Post
+ private: boolean
+ reactions: Record
+ value: string
+}
+
+export type CreatePostRequest = {
+ authorId: string
+ boardId: string
+ byId?: string
+ categoryId?: string
+ details: string
+ title: string
+ ownerId?: string
+ imageURLs?: string[]
+ createdAt?: string
+ eta?: string
+ etaPublic?: boolean
+ customFields?: Record
+}
+
+export type UpdatePostRequest = {
+ postId: string
+ title?: string
+ details?: string
+ eta?: string
+ etaPublic?: boolean
+ imageURLs?: string[]
+ customFields?: Record
+}
+
+export type CreateCommentRequest = {
+ authorId: string
+ postId: string
+ value: string
+ parentId?: string
+ imageURLs?: string[]
+ internal?: boolean
+ shouldNotifyVoters?: boolean
+ createdAt?: string
+}
+
+export type ListPostsRequest = {
+ boardId?: string
+ authorId?: string
+ companyId?: string
+ tagIds?: string[]
+ limit?: number
+ skip?: number
+ search?: string
+ sort?: 'newest' | 'oldest' | 'relevance' | 'score' | 'statusChanged' | 'trending'
+ status?: string
+}
+
+export type ListCommentsRequest = {
+ postId?: string
+ authorId?: string
+ boardId?: string
+ companyId?: string
+ limit?: number
+ skip?: number
+}
+
+export type ListPostsResponse = {
+ posts: Post[]
+ hasMore: boolean
+}
+
+export type ListCommentsResponse = {
+ comments: Comment[]
+ hasMore: boolean
+}
+
+export type CreateUserRequest = {
+ name: string
+ userId?: string
+ email?: string
+ avatarURL?: string
+ alias?: string
+ created?: string
+ customFields?: Record
+}
+
+export type ListUsersRequest = {
+ limit?: number
+ cursor?: string
+}
+
+export type ListUsersResponse = {
+ users: User[]
+ hasNextPage: boolean
+ cursor?: string
+}
+
+export type ListBoardsRequest = {
+ limit?: number
+ skip?: number
+}
+
+export type ListBoardsResponse = {
+ boards: Board[]
+ hasMore: boolean
+}
+
+export class CannyClient {
+ private constructor(
+ private readonly _client: Axios,
+ private readonly _apiKey: string
+ ) {}
+
+ public static create(config: CannyClientConfiguration) {
+ const client = axios.create({
+ baseURL: 'https://canny.io/api/v1',
+ timeout: 10_000,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ transformRequest: [
+ (data) => {
+ return JSON.stringify({ ...data, apiKey: config.apiKey })
+ },
+ ],
+ })
+
+ return new CannyClient(client, config.apiKey)
+ }
+
+ public async createPost(request: CreatePostRequest): Promise<{ id: string }> {
+ // Convert Id fields to ID for Canny API
+ const apiRequest = {
+ authorID: request.authorId,
+ boardID: request.boardId,
+ byID: request.byId,
+ categoryID: request.categoryId,
+ ownerID: request.ownerId,
+ details: request.details,
+ title: request.title,
+ imageURLs: request.imageURLs,
+ createdAt: request.createdAt,
+ eta: request.eta,
+ etaPublic: request.etaPublic,
+ customFields: request.customFields,
+ }
+ const response = await this._client.post('/posts/create', apiRequest)
+ return response.data
+ }
+
+ public async getPost(id: string, boardId?: string): Promise {
+ const response = await this._client.post('/posts/retrieve', { id, boardId })
+ return response.data
+ }
+
+ public async listPosts(request: ListPostsRequest = {}): Promise {
+ const response = await this._client.post('/posts/list', request)
+ return response.data
+ }
+
+ public async updatePost(request: UpdatePostRequest): Promise<{ success: boolean }> {
+ const response = await this._client.post('/posts/update', request)
+ return { success: response.data === 'success' }
+ }
+
+ public async deletePost(postId: string): Promise<{ success: boolean }> {
+ const response = await this._client.post('/posts/delete', { postId })
+ return { success: response.data === 'success' }
+ }
+
+ public async createComment(request: CreateCommentRequest): Promise<{ id: string }> {
+ const apiRequest = {
+ authorID: request.authorId,
+ postID: request.postId,
+ value: request.value,
+ parentID: request.parentId,
+ imageURLs: request.imageURLs,
+ internal: request.internal,
+ shouldNotifyVoters: request.shouldNotifyVoters,
+ createdAt: request.createdAt,
+ }
+ const response = await this._client.post('/comments/create', apiRequest)
+ return response.data
+ }
+
+ public async getComment(id: string): Promise {
+ const response = await this._client.post('/comments/retrieve', { id })
+ return response.data
+ }
+
+ public async listComments(request: ListCommentsRequest = {}): Promise {
+ const response = await this._client.post('/comments/list', request)
+ return response.data
+ }
+
+ public async deleteComment(commentId: string): Promise<{ success: boolean }> {
+ const response = await this._client.post('/comments/delete', { commentId })
+ return { success: response.data === 'success' }
+ }
+
+ public async createOrUpdateUser(request: CreateUserRequest): Promise {
+ const response = await this._client.post('/users/create_or_update', request)
+ return response.data
+ }
+
+ public async listUsers(request: ListUsersRequest = {}): Promise {
+ const response = await this._client.post('https://canny.io/api/v2/users/list', request)
+ return response.data
+ }
+
+ public async listBoards(request: ListBoardsRequest = {}): Promise {
+ const response = await this._client.post('/boards/list', request)
+ return response.data
+ }
+}
diff --git a/integrations/canny/src/misc/errors.ts b/integrations/canny/src/misc/errors.ts
new file mode 100644
index 00000000000..6e1479a74b8
--- /dev/null
+++ b/integrations/canny/src/misc/errors.ts
@@ -0,0 +1,32 @@
+import { RuntimeError } from '@botpress/sdk'
+import { isAxiosError } from 'axios'
+
+export class InvalidAPIKeyError extends RuntimeError {
+ public constructor() {
+ super('Invalid Canny API key. Please check your API key in the integration settings.')
+ }
+}
+
+export class InvalidRequestError extends RuntimeError {
+ public constructor(thrown: unknown) {
+ let message: string
+ if (isAxiosError(thrown)) {
+ message = thrown.response?.data?.error || thrown.message || 'Unknown error'
+ } else {
+ message = thrown instanceof Error ? thrown.message : String(thrown)
+ }
+ super(`Invalid request: ${message}`)
+ }
+}
+
+export class CannyAPIError extends RuntimeError {
+ public constructor(thrown: unknown) {
+ let message: string
+ if (isAxiosError(thrown)) {
+ message = thrown.response?.data?.error || thrown.message || 'Unknown error'
+ } else {
+ message = thrown instanceof Error ? thrown.message : String(thrown)
+ }
+ super(`Canny API error: ${message}`)
+ }
+}
diff --git a/integrations/canny/tsconfig.json b/integrations/canny/tsconfig.json
new file mode 100644
index 00000000000..c998946e1dd
--- /dev/null
+++ b/integrations/canny/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "baseUrl": ".",
+ "outDir": "dist",
+ "experimentalDecorators": true,
+ "emitDecoratorMetadata": true
+ },
+ "include": [".botpress/**/*", "definitions/**/*", "src/**/*", "integration.definition.ts"]
+}
diff --git a/integrations/webflow/definitions/sub-schemas.ts b/integrations/webflow/definitions/sub-schemas.ts
index 1a0269031c6..cd6d4063aae 100644
--- a/integrations/webflow/definitions/sub-schemas.ts
+++ b/integrations/webflow/definitions/sub-schemas.ts
@@ -3,14 +3,7 @@ import { z } from '@botpress/sdk'
export const formSchema = z.object({
name: z.string().optional(),
siteId: z.string(),
- data: z
- .object({
- 'First Name': z.string().optional(),
- 'Last Name': z.string().optional(),
- email: z.string().optional(),
- 'Phone Number': z.number().optional(),
- })
- .optional(),
+ data: z.record(z.string()).optional(),
schema: z
.array(
z.object({
diff --git a/integrations/webflow/src/client.ts b/integrations/webflow/src/client.ts
index 9278e0472b0..53af36be16a 100644
--- a/integrations/webflow/src/client.ts
+++ b/integrations/webflow/src/client.ts
@@ -107,36 +107,21 @@ export class WebflowClient {
await this._axiosClient.delete(path)
return {}
}
+ public listWebhooks = async (siteID: string): Promise<{ triggerType: string; id: string }[]> => {
+ const path = `/sites/${siteID}/webhooks`
+ const { data } = await this._axiosClient.get<{ webhooks: { triggerType: string; id: string }[] }>(path)
+ return data.webhooks
+ }
- public createWebhook = async (siteID: string, webhookUrl: string): Promise => {
+ public createWebhook = async (triggerType: string, siteID: string, webhookUrl: string): Promise => {
const path = `/sites/${siteID}/webhooks`
- const triggerTypes = [
- 'form_submission',
- 'site_publish',
- 'page_created',
- 'page_metadata_updated',
- 'page_deleted',
- 'collection_item_created',
- 'collection_item_changed',
- 'collection_item_deleted',
- 'collection_item_published',
- 'collection_item_unpublished',
- 'comment_created',
- ]
- for (const triggerType of triggerTypes) {
- await this._axiosClient.post(path, {
- triggerType,
- url: webhookUrl,
- })
- }
+ await this._axiosClient.post(path, {
+ triggerType,
+ url: webhookUrl,
+ })
}
- public deleteWebhooks = async (siteID: string): Promise => {
- const listPath = `/sites/${siteID}/webhooks`
- const listResp = await this._axiosClient.get(listPath)
- const webhookIDs = listResp.data.webhooks.map((w: { id: string }) => w.id)
- for (const webhookID of webhookIDs) {
- await this._axiosClient.delete(`/webhooks/${webhookID}`)
- }
+ public deleteWebhooks = async (webhookID: string): Promise => {
+ await this._axiosClient.delete(`/webhooks/${webhookID}`)
}
}
diff --git a/integrations/webflow/src/index.ts b/integrations/webflow/src/index.ts
index 79c58f56a37..70431c7bf40 100644
--- a/integrations/webflow/src/index.ts
+++ b/integrations/webflow/src/index.ts
@@ -32,13 +32,45 @@ const fireEvent = async (
export default new bp.Integration({
register: async (props) => {
const client = new WebflowClient(props.ctx.configuration.apiToken)
- await client
- .createWebhook(props.ctx.configuration.siteID, props.webhookUrl)
- .catch(_handleError('Failed to create webhooks'))
+ const triggerTypesToHook = [
+ 'form_submission',
+ 'site_publish',
+ 'page_created',
+ 'page_metadata_updated',
+ 'page_deleted',
+ 'collection_item_created',
+ 'collection_item_changed',
+ 'collection_item_deleted',
+ 'collection_item_published',
+ 'collection_item_unpublished',
+ 'comment_created',
+ ]
+
+ const already = await client
+ .listWebhooks(props.ctx.configuration.siteID)
+ .catch(_handleError('Failed to list webhooks'))
+
+ const existing = new Set(already.map((w: { triggerType: string }) => w.triggerType))
+ const missing = triggerTypesToHook.filter((t) => !existing.has(t))
+ await Promise.all(
+ missing.map((triggerType) =>
+ client
+ .createWebhook(triggerType, props.ctx.configuration.siteID, props.webhookUrl)
+ .catch(_handleError('Failed to create webhooks'))
+ )
+ )
},
unregister: async (props) => {
const client = new WebflowClient(props.ctx.configuration.apiToken)
- await client.deleteWebhooks(props.ctx.configuration.siteID).catch(_handleError('Failed to delete webhooks'))
+
+ const webhooks = await client
+ .listWebhooks(props.ctx.configuration.siteID)
+ .catch(_handleError('Failed to create webhooks'))
+
+ const webhookIDs = webhooks.map((w: { id: string }) => w.id)
+ await Promise.all(
+ webhookIDs.map((webhookID) => client.deleteWebhooks(webhookID).catch(_handleError('Failed to delete webhook')))
+ )
},
actions: {
async listCollections(props) {
@@ -139,6 +171,7 @@ export default new bp.Integration({
switch (data.triggerType) {
case 'form_submission':
+ console.log(data.payload)
await fireEvent(props, 'formSubmission', data.payload)
break
case 'site_publish':
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index eed6d767790..ce4dd892460 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -493,6 +493,18 @@ importers:
specifier: ^1.11.0
version: 1.11.0
+ integrations/canny:
+ dependencies:
+ '@botpress/client':
+ specifier: workspace:*
+ version: link:../../packages/client
+ '@botpress/sdk':
+ specifier: workspace:*
+ version: link:../../packages/sdk
+ axios:
+ specifier: ^1.6.0
+ version: 1.11.0
+
integrations/cerebras:
dependencies:
'@botpress/client':