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.

at main 312 lines 9.0 kB view raw
1import { and, eq } from "drizzle-orm"; 2import { Hono } from "hono"; 3import { HTTPException } from "hono/http-exception"; 4import { describeRoute, resolver, validator } from "hono-openapi"; 5import * as v from "valibot"; 6import db from "../database"; 7import { integrationTable } from "../database/schema"; 8import { publishEvent } from "../events"; 9import { 10 normalizeTelegramConfig, 11 telegramEventsSchema, 12 validateTelegramConfig, 13} from "../plugins/telegram/config"; 14import { telegramIntegrationSchema } from "../schemas"; 15import { workspaceAccess } from "../utils/workspace-access-middleware"; 16import { 17 buildNextTelegramConfigFromPatch, 18 getTelegramIntegration, 19 parseTelegramIntegrationConfig, 20 telegramIntegrationPatchBodySchema, 21 toResponse, 22} from "./controllers/telegram-controller"; 23 24function safePublishIntegrationEvent( 25 eventName: 26 | "integration.created" 27 | "integration.updated" 28 | "integration.deleted", 29 data: { 30 projectId: string; 31 userId: string; 32 integrationType: "telegram"; 33 integrationId: string; 34 apiKeyId?: string; 35 }, 36) { 37 void publishEvent(eventName, data).catch((error) => { 38 console.error(`Failed to publish ${eventName}:`, error); 39 }); 40} 41 42const telegramIntegration = new Hono<{ 43 Variables: { 44 userId: string; 45 workspaceId: string; 46 apiKey?: { 47 id: string; 48 userId: string; 49 enabled: boolean; 50 }; 51 }; 52}>(); 53 54telegramIntegration 55 .get( 56 "/project/:projectId", 57 describeRoute({ 58 operationId: "getTelegramIntegration", 59 tags: ["Telegram"], 60 description: "Get Telegram integration for a project", 61 responses: { 62 200: { 63 description: "Telegram integration details", 64 content: { 65 "application/json": { schema: resolver(telegramIntegrationSchema) }, 66 }, 67 }, 68 404: { 69 description: "Telegram integration not found", 70 }, 71 }, 72 }), 73 validator("param", v.object({ projectId: v.string() })), 74 workspaceAccess.fromProject("projectId"), 75 async (c) => { 76 const { projectId } = c.req.valid("param"); 77 const integration = await getTelegramIntegration(projectId); 78 return c.json(integration); 79 }, 80 ) 81 .post( 82 "/project/:projectId", 83 describeRoute({ 84 operationId: "createTelegramIntegration", 85 tags: ["Telegram"], 86 description: "Create or replace a Telegram integration for a project", 87 responses: { 88 200: { 89 description: "Telegram integration created successfully", 90 content: { 91 "application/json": { schema: resolver(telegramIntegrationSchema) }, 92 }, 93 }, 94 }, 95 }), 96 validator("param", v.object({ projectId: v.string() })), 97 validator( 98 "json", 99 v.object({ 100 botToken: v.pipe(v.string(), v.minLength(1)), 101 chatId: v.pipe(v.string(), v.minLength(1)), 102 threadId: v.optional(v.number()), 103 chatLabel: v.optional(v.string()), 104 events: v.optional(telegramEventsSchema), 105 }), 106 ), 107 workspaceAccess.fromProject("projectId"), 108 async (c) => { 109 const { projectId } = c.req.valid("param"); 110 const body = c.req.valid("json"); 111 112 const config = normalizeTelegramConfig({ 113 botToken: body.botToken, 114 chatId: body.chatId, 115 threadId: body.threadId, 116 chatLabel: body.chatLabel, 117 events: body.events, 118 }); 119 120 const validation = validateTelegramConfig(config); 121 if (!validation.valid) { 122 throw new HTTPException(400, { 123 message: validation.errors?.join(", ") ?? "Invalid config", 124 }); 125 } 126 127 const priorIntegration = await db.query.integrationTable.findFirst({ 128 where: and( 129 eq(integrationTable.projectId, projectId), 130 eq(integrationTable.type, "telegram"), 131 ), 132 columns: { id: true }, 133 }); 134 135 await db 136 .insert(integrationTable) 137 .values({ 138 projectId, 139 type: "telegram", 140 config: JSON.stringify(config), 141 isActive: true, 142 }) 143 .onConflictDoUpdate({ 144 target: [integrationTable.projectId, integrationTable.type], 145 set: { 146 config: JSON.stringify(config), 147 updatedAt: new Date(), 148 }, 149 }); 150 151 const integration = await getTelegramIntegration(projectId); 152 if (!integration) { 153 throw new HTTPException(500, { 154 message: "Failed to load Telegram integration after save", 155 }); 156 } 157 158 const apiKey = c.get("apiKey"); 159 safePublishIntegrationEvent( 160 priorIntegration ? "integration.updated" : "integration.created", 161 { 162 projectId, 163 userId: c.get("userId"), 164 integrationType: "telegram", 165 integrationId: integration.id, 166 ...(apiKey?.id ? { apiKeyId: apiKey.id } : {}), 167 }, 168 ); 169 170 return c.json(integration); 171 }, 172 ) 173 .patch( 174 "/project/:projectId", 175 describeRoute({ 176 operationId: "updateTelegramIntegration", 177 tags: ["Telegram"], 178 description: "Update Telegram integration settings", 179 responses: { 180 200: { 181 description: "Telegram integration updated successfully", 182 content: { 183 "application/json": { schema: resolver(telegramIntegrationSchema) }, 184 }, 185 }, 186 }, 187 }), 188 validator("param", v.object({ projectId: v.string() })), 189 validator("json", telegramIntegrationPatchBodySchema), 190 workspaceAccess.fromProject("projectId"), 191 async (c) => { 192 const { projectId } = c.req.valid("param"); 193 const body = c.req.valid("json"); 194 195 const existing = await db.query.integrationTable.findFirst({ 196 where: and( 197 eq(integrationTable.projectId, projectId), 198 eq(integrationTable.type, "telegram"), 199 ), 200 }); 201 202 if (!existing) { 203 throw new HTTPException(404, { 204 message: "Telegram integration not found", 205 }); 206 } 207 208 const currentConfig = parseTelegramIntegrationConfig(existing); 209 const nextConfig = normalizeTelegramConfig( 210 buildNextTelegramConfigFromPatch(body, currentConfig), 211 ); 212 213 const resolvedIsActive = 214 body.isActive !== undefined 215 ? body.isActive 216 : (existing.isActive ?? true); 217 218 if ( 219 JSON.stringify(currentConfig) === JSON.stringify(nextConfig) && 220 resolvedIsActive === (existing.isActive ?? true) 221 ) { 222 return c.json(toResponse(existing)); 223 } 224 225 const validation = validateTelegramConfig(nextConfig); 226 if (!validation.valid) { 227 throw new HTTPException(400, { 228 message: validation.errors?.join(", ") ?? "Invalid config", 229 }); 230 } 231 232 await db 233 .update(integrationTable) 234 .set({ 235 config: JSON.stringify(nextConfig), 236 isActive: resolvedIsActive, 237 updatedAt: new Date(), 238 }) 239 .where(eq(integrationTable.id, existing.id)); 240 241 const integration = await getTelegramIntegration(projectId); 242 if (!integration) { 243 throw new HTTPException(500, { 244 message: "Failed to load Telegram integration after update", 245 }); 246 } 247 248 const apiKey = c.get("apiKey"); 249 safePublishIntegrationEvent("integration.updated", { 250 projectId, 251 userId: c.get("userId"), 252 integrationType: "telegram", 253 integrationId: integration.id, 254 ...(apiKey?.id ? { apiKeyId: apiKey.id } : {}), 255 }); 256 257 return c.json(integration); 258 }, 259 ) 260 .delete( 261 "/project/:projectId", 262 describeRoute({ 263 operationId: "deleteTelegramIntegration", 264 tags: ["Telegram"], 265 description: "Delete Telegram integration for a project", 266 responses: { 267 200: { 268 description: "Telegram integration deleted successfully", 269 content: { 270 "application/json": { 271 schema: resolver(v.object({ success: v.boolean() })), 272 }, 273 }, 274 }, 275 }, 276 }), 277 validator("param", v.object({ projectId: v.string() })), 278 workspaceAccess.fromProject("projectId"), 279 async (c) => { 280 const { projectId } = c.req.valid("param"); 281 282 const existing = await db.query.integrationTable.findFirst({ 283 where: and( 284 eq(integrationTable.projectId, projectId), 285 eq(integrationTable.type, "telegram"), 286 ), 287 }); 288 289 if (!existing) { 290 throw new HTTPException(404, { 291 message: "Telegram integration not found", 292 }); 293 } 294 295 await db 296 .delete(integrationTable) 297 .where(eq(integrationTable.id, existing.id)); 298 299 const apiKey = c.get("apiKey"); 300 safePublishIntegrationEvent("integration.deleted", { 301 projectId, 302 userId: c.get("userId"), 303 integrationType: "telegram", 304 integrationId: existing.id, 305 ...(apiKey?.id ? { apiKeyId: apiKey.id } : {}), 306 }); 307 308 return c.json({ success: true }); 309 }, 310 ); 311 312export default telegramIntegration;