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 9a620ba2f31238f03cd28f1da5ef3838d67e4e8a 148 lines 4.1 kB view raw
1import { and, eq } from "drizzle-orm"; 2import { HTTPException } from "hono/http-exception"; 3import * as v from "valibot"; 4import db from "../../database"; 5import { integrationTable } from "../../database/schema"; 6import { 7 defaultTelegramEvents, 8 normalizeTelegramConfig, 9 type TelegramConfig, 10 telegramConfigSchema, 11 telegramEventsSchema, 12} from "../../plugins/telegram/config"; 13 14export const telegramIntegrationPatchBodySchema = v.object({ 15 botToken: v.optional(v.string()), 16 chatId: v.optional(v.string()), 17 threadId: v.optional(v.nullable(v.number())), 18 chatLabel: v.optional(v.nullable(v.string())), 19 isActive: v.optional(v.boolean()), 20 events: v.optional(telegramEventsSchema), 21}); 22 23export type TelegramIntegrationPatchBody = v.InferOutput< 24 typeof telegramIntegrationPatchBodySchema 25>; 26 27export function buildNextTelegramConfigFromPatch( 28 body: TelegramIntegrationPatchBody, 29 currentConfig: TelegramConfig, 30): TelegramConfig { 31 const nextBotToken = 32 "botToken" in body ? (body.botToken?.trim() ?? "") : currentConfig.botToken; 33 const nextChatId = 34 "chatId" in body ? (body.chatId?.trim() ?? "") : currentConfig.chatId; 35 return { 36 botToken: nextBotToken, 37 chatId: nextChatId, 38 threadId: 39 body.threadId === undefined 40 ? currentConfig.threadId 41 : (body.threadId ?? undefined), 42 chatLabel: 43 body.chatLabel === undefined 44 ? currentConfig.chatLabel 45 : (body.chatLabel ?? undefined), 46 events: { 47 ...(currentConfig.events ?? {}), 48 ...(body.events ?? {}), 49 }, 50 }; 51} 52 53function maskBotToken(value: string): string { 54 const [prefix, suffix = ""] = value.split(":", 2); 55 if (!suffix) { 56 return "Configured"; 57 } 58 59 const maskedSuffix = 60 suffix.length > 8 ? `${suffix.slice(0, 4)}${suffix.slice(-4)}` : "••••"; 61 return `${prefix}:${maskedSuffix}`; 62} 63 64function sanitizeTelegramConfigForLog(rawConfig: string): string { 65 try { 66 const parsed = JSON.parse(rawConfig) as Record<string, unknown>; 67 for (const key of [ 68 "botToken", 69 "chatId", 70 "threadId", 71 "chatLabel", 72 ] as const) { 73 if (key in parsed) { 74 parsed[key] = "[REDACTED]"; 75 } 76 } 77 return JSON.stringify(parsed); 78 } catch { 79 return "[UNPARSEABLE]"; 80 } 81} 82 83type TelegramIntegrationRecord = { 84 id: string; 85 projectId: string; 86 config: string; 87 isActive: boolean | null; 88 createdAt: Date; 89 updatedAt: Date; 90}; 91 92export function parseTelegramIntegrationConfig( 93 integration: Pick<TelegramIntegrationRecord, "config" | "id" | "projectId">, 94): TelegramConfig { 95 try { 96 const parsed = v.parse( 97 telegramConfigSchema, 98 JSON.parse(integration.config), 99 ); 100 return normalizeTelegramConfig(parsed); 101 } catch (error) { 102 console.error("Failed to parse Telegram integration config", { 103 error, 104 integrationId: integration.id, 105 projectId: integration.projectId, 106 sanitizedConfig: sanitizeTelegramConfigForLog(integration.config), 107 }); 108 throw new HTTPException(500, { 109 message: "Stored Telegram integration configuration is invalid", 110 }); 111 } 112} 113 114export function toResponse(integration: TelegramIntegrationRecord) { 115 const config = parseTelegramIntegrationConfig(integration); 116 117 return { 118 id: integration.id, 119 projectId: integration.projectId, 120 chatId: config.chatId, 121 threadId: config.threadId ?? null, 122 chatLabel: config.chatLabel ?? null, 123 botTokenConfigured: Boolean(config.botToken), 124 maskedBotToken: config.botToken ? maskBotToken(config.botToken) : "", 125 events: { 126 ...defaultTelegramEvents, 127 ...(config.events ?? {}), 128 }, 129 isActive: integration.isActive, 130 createdAt: integration.createdAt, 131 updatedAt: integration.updatedAt, 132 }; 133} 134 135export async function getTelegramIntegration(projectId: string) { 136 const integration = await db.query.integrationTable.findFirst({ 137 where: and( 138 eq(integrationTable.projectId, projectId), 139 eq(integrationTable.type, "telegram"), 140 ), 141 }); 142 143 if (!integration) { 144 return null; 145 } 146 147 return toResponse(integration); 148}