kaneo (minimalist kanban) fork to experiment adding a tangled integration github.com/usekaneo/kaneo
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: implement task comment creation event handling and integrate with GitHub plugin

+144 -2
+19 -1
apps/api/src/activity/index.ts
··· 1 + import { eq } from "drizzle-orm"; 1 2 import { Hono } from "hono"; 2 3 import { describeRoute, resolver, validator } from "hono-openapi"; 3 4 import * as v from "valibot"; 4 - import { subscribeToEvent } from "../events"; 5 + import db from "../database"; 6 + import { taskTable } from "../database/schema"; 7 + import { publishEvent, subscribeToEvent } from "../events"; 5 8 import { activitySchema } from "../schemas"; 6 9 import { workspaceAccess } from "../utils/workspace-access-middleware"; 7 10 import createActivity from "./controllers/create-activity"; ··· 96 99 const { taskId, comment } = c.req.valid("json"); 97 100 const userId = c.get("userId"); 98 101 const newComment = await createComment(taskId, userId, comment); 102 + 103 + const [task] = await db 104 + .select({ projectId: taskTable.projectId }) 105 + .from(taskTable) 106 + .where(eq(taskTable.id, taskId)); 107 + 108 + if (task) { 109 + await publishEvent("task.comment_created", { 110 + taskId, 111 + userId, 112 + comment, 113 + projectId: task.projectId, 114 + }); 115 + } 116 + 99 117 return c.json(newComment); 100 118 }, 101 119 )
+26
apps/api/src/github-integration/controllers/create-github-integration.ts
··· 30 30 throw new HTTPException(404, { message: "Project not found" }); 31 31 } 32 32 33 + const allGitHubIntegrations = await db.query.integrationTable.findMany({ 34 + where: eq(integrationTable.type, "github"), 35 + }); 36 + 37 + for (const integration of allGitHubIntegrations) { 38 + if (integration.projectId === projectId) { 39 + continue; 40 + } 41 + 42 + try { 43 + const config = JSON.parse(integration.config); 44 + if ( 45 + config.repositoryOwner === repositoryOwner && 46 + config.repositoryName === repositoryName 47 + ) { 48 + throw new HTTPException(409, { 49 + message: `Repository ${repositoryOwner}/${repositoryName} is already linked to another project`, 50 + }); 51 + } 52 + } catch (error) { 53 + if (error instanceof HTTPException) { 54 + throw error; 55 + } 56 + } 57 + } 58 + 33 59 let installationId: number | null = null; 34 60 try { 35 61 const { data: installation } =
+50
apps/api/src/plugins/github/events/task-comment-created.ts
··· 1 + import type { PluginContext, TaskCommentCreatedEvent } from "../../types"; 2 + import type { GitHubConfig } from "../config"; 3 + import { findExternalLinkByTaskAndType } from "../services/link-manager"; 4 + import { getGithubApp, getInstallationIdForRepo } from "../utils/github-app"; 5 + 6 + export async function handleTaskCommentCreated( 7 + event: TaskCommentCreatedEvent, 8 + context: PluginContext, 9 + ): Promise<void> { 10 + const githubApp = getGithubApp(); 11 + if (!githubApp) { 12 + return; 13 + } 14 + 15 + const config = context.config as GitHubConfig; 16 + const { repositoryOwner, repositoryName } = config; 17 + 18 + const existingLink = await findExternalLinkByTaskAndType( 19 + event.taskId, 20 + context.integrationId, 21 + "issue", 22 + ); 23 + 24 + if (!existingLink) { 25 + return; 26 + } 27 + 28 + try { 29 + let installationId = config.installationId; 30 + if (!installationId) { 31 + installationId = await getInstallationIdForRepo( 32 + repositoryOwner, 33 + repositoryName, 34 + ); 35 + } 36 + 37 + const octokit = await githubApp.getInstallationOctokit(installationId); 38 + 39 + const issueNumber = Number.parseInt(existingLink.externalId, 10); 40 + 41 + await octokit.rest.issues.createComment({ 42 + owner: repositoryOwner, 43 + repo: repositoryName, 44 + issue_number: issueNumber, 45 + body: event.comment, 46 + }); 47 + } catch (error) { 48 + console.error("Failed to create GitHub comment:", error); 49 + } 50 + }
+2
apps/api/src/plugins/github/index.ts
··· 1 1 import type { IntegrationPlugin } from "../types"; 2 2 import { validateGitHubConfig } from "./config"; 3 + import { handleTaskCommentCreated } from "./events/task-comment-created"; 3 4 import { handleTaskCreated } from "./events/task-created"; 4 5 import { handleTaskDescriptionChanged } from "./events/task-description-changed"; 5 6 import { handleTaskPriorityChanged } from "./events/task-priority-changed"; ··· 15 16 onTaskPriorityChanged: handleTaskPriorityChanged, 16 17 onTaskTitleChanged: handleTaskTitleChanged, 17 18 onTaskDescriptionChanged: handleTaskDescriptionChanged, 19 + onTaskCommentCreated: handleTaskCommentCreated, 18 20 validateConfig: validateGitHubConfig, 19 21 }; 20 22
+37
apps/api/src/plugins/registry.ts
··· 5 5 import type { 6 6 IntegrationPlugin, 7 7 PluginContext, 8 + TaskCommentCreatedEvent, 8 9 TaskCreatedEvent, 9 10 TaskDescriptionChangedEvent, 10 11 TaskPriorityChangedEvent, ··· 115 116 userId: data.userId, 116 117 oldDescription: data.oldDescription, 117 118 newDescription: data.newDescription, 119 + }); 120 + }); 121 + 122 + subscribeToEvent<{ 123 + taskId: string; 124 + userId: string; 125 + comment: string; 126 + projectId: string; 127 + }>("task.comment_created", async (data) => { 128 + await broadcastTaskCommentCreated({ 129 + taskId: data.taskId, 130 + projectId: data.projectId, 131 + userId: data.userId, 132 + comment: data.comment, 118 133 }); 119 134 }); 120 135 ··· 260 275 } 261 276 } 262 277 } 278 + 279 + export async function broadcastTaskCommentCreated( 280 + event: TaskCommentCreatedEvent, 281 + ): Promise<void> { 282 + const integrations = await getActiveIntegrations(event.projectId); 283 + 284 + for (const integration of integrations) { 285 + const plugin = getPlugin(integration.type); 286 + if (!plugin?.onTaskCommentCreated) continue; 287 + 288 + const context = createContext(integration); 289 + 290 + try { 291 + await plugin.onTaskCommentCreated(event, context); 292 + } catch (error) { 293 + console.error( 294 + `Plugin ${plugin.type} error on task.comment_created:`, 295 + error, 296 + ); 297 + } 298 + } 299 + }
+10 -1
apps/api/src/plugins/types.ts
··· 49 49 newDescription: string | null; 50 50 }; 51 51 52 + export type TaskCommentCreatedEvent = { 53 + taskId: string; 54 + projectId: string; 55 + userId: string; 56 + comment: string; 57 + }; 58 + 52 59 export type TaskEvent = 53 60 | TaskCreatedEvent 54 61 | TaskStatusChangedEvent 55 62 | TaskPriorityChangedEvent 56 63 | TaskTitleChangedEvent 57 - | TaskDescriptionChangedEvent; 64 + | TaskDescriptionChangedEvent 65 + | TaskCommentCreatedEvent; 58 66 59 67 export type ExternalMetadata = { 60 68 type: "issue" | "pull_request" | "branch"; ··· 94 102 onTaskPriorityChanged?: TaskEventHandler<TaskPriorityChangedEvent>; 95 103 onTaskTitleChanged?: TaskEventHandler<TaskTitleChangedEvent>; 96 104 onTaskDescriptionChanged?: TaskEventHandler<TaskDescriptionChangedEvent>; 105 + onTaskCommentCreated?: TaskEventHandler<TaskCommentCreatedEvent>; 97 106 98 107 handleWebhook?: WebhookHandler; 99 108 getTaskMetadata?: MetadataProvider;