From 03b115b83ece7c0d9a0d80f80b1d4bad4b612a89 Mon Sep 17 00:00:00 2001 From: d0zingcat Date: Thu, 15 Jan 2026 20:51:37 +0800 Subject: [PATCH] feat: shorten user token Signed-off-by: d0zingcat --- apps/server/src/db/migrate-tokens.ts | 9 +++ apps/server/src/db/migrate.ts | 41 ++++++++++- apps/server/src/db/schema.ts | 2 +- apps/server/src/feishu.ts | 2 + apps/server/src/webhook.ts | 102 +++++++++++++++++++++++++-- 5 files changed, 146 insertions(+), 10 deletions(-) create mode 100644 apps/server/src/db/migrate-tokens.ts diff --git a/apps/server/src/db/migrate-tokens.ts b/apps/server/src/db/migrate-tokens.ts new file mode 100644 index 0000000..0919bfb --- /dev/null +++ b/apps/server/src/db/migrate-tokens.ts @@ -0,0 +1,9 @@ +import { db } from "./index"; +import { migrateUserTokens } from "./migrate"; + +async function main() { + await migrateUserTokens(db); + process.exit(0); +} + +main(); diff --git a/apps/server/src/db/migrate.ts b/apps/server/src/db/migrate.ts index 4d2851d..7dfbaa9 100644 --- a/apps/server/src/db/migrate.ts +++ b/apps/server/src/db/migrate.ts @@ -1,20 +1,52 @@ +import { eq } from "drizzle-orm"; import { drizzle } from "drizzle-orm/postgres-js"; import { migrate } from "drizzle-orm/postgres-js/migrator"; import postgres from "postgres"; import * as schema from "./schema"; +import { users } from "./schema"; const connectionString = process.env.DATABASE_URL || "postgres://postgres:password@localhost:5432/alert_message_center"; +export async function migrateUserTokens(db: any) { + console.log("⏳ Checking for user tokens that need shortening..."); + try { + const allUsers = await db.select().from(users); + let updatedCount = 0; + for (const user of allUsers) { + if (user.personalToken && user.personalToken.length > 8) { + const newToken = user.personalToken.substring(0, 8); + console.log( + `Updating user ${user.name}: ${user.personalToken} -> ${newToken}`, + ); + await db + .update(users) + .set({ personalToken: newToken }) + .where(eq(users.id, user.id)); + updatedCount++; + } + } + if (updatedCount > 0) { + console.log(`✅ Updated ${updatedCount} user tokens.`); + } else { + console.log("ℹ️ No tokens need shortening."); + } + } catch (error) { + console.error("❌ Failed to migrate user tokens:", error); + } +} + async function main() { - console.log("⏳ Running migrations..."); + console.log("⏳ Running database migrations..."); const sql = postgres(connectionString, { max: 1 }); const db = drizzle(sql, { schema }); try { await migrate(db, { migrationsFolder: "./drizzle" }); - console.log("✅ Migrations completed!"); + console.log("✅ Database migrations completed!"); + + await migrateUserTokens(db); } catch (error) { console.error("❌ Migration failed:", error); process.exit(1); @@ -23,4 +55,7 @@ async function main() { } } -main(); +// Only run main if this script is executed directly +if (import.meta.main) { + main(); +} diff --git a/apps/server/src/db/schema.ts b/apps/server/src/db/schema.ts index 36202a3..a503c7d 100644 --- a/apps/server/src/db/schema.ts +++ b/apps/server/src/db/schema.ts @@ -86,7 +86,7 @@ export const users = pgTable("users", { personalToken: text("personal_token") .notNull() .unique() - .$defaultFn(() => crypto.randomUUID().replace(/-/g, "")), + .$defaultFn(() => crypto.randomUUID().split("-")[0]), }); export const usersRelations = relations(users, ({ many }) => ({ diff --git a/apps/server/src/feishu.ts b/apps/server/src/feishu.ts index 86c8332..778fde5 100644 --- a/apps/server/src/feishu.ts +++ b/apps/server/src/feishu.ts @@ -33,6 +33,7 @@ export class FeishuClient { receiveIdType: "open_id" | "user_id" | "email" | "chat_id", msgType: string, content: Record | string, + uuid?: string, ) { // Content needs to be stringified for 'text' type in API, but SDK might handle it differently? // Actually SDK expects 'content' as string JSON for 'im.v1.messages.create' @@ -48,6 +49,7 @@ export class FeishuClient { receive_id: receiveId, msg_type: msgType, content: contentStr, + uuid: uuid, }, }); diff --git a/apps/server/src/webhook.ts b/apps/server/src/webhook.ts index 0c43cf9..c3bcb1c 100644 --- a/apps/server/src/webhook.ts +++ b/apps/server/src/webhook.ts @@ -4,6 +4,7 @@ import { db } from "./db"; import { alertLogs, alertTasks, topics, users } from "./db/schema"; import { feishuClient } from "./feishu"; import { logger } from "./lib/logger"; +import { uuid } from "zod/v4"; type FeishuReceiveIdType = "open_id" | "user_id" | "email" | "chat_id"; @@ -134,10 +135,38 @@ webhook.post("/:token/topic/:slug", async (c) => { let msgType = body.msg_type || "text"; let content = body.content; + // Special handling for incomplete payloads (missing 'content') if (!content) { - msgType = "text"; - content = { text: JSON.stringify(body, null, 2) }; - // Deep copy needed? usually content is new obj if we parsed body + // 1. Special case: Unwrap 'card' if provided (convenience for user) + if (body.card) { + content = body.card; + if (!msgType) msgType = "interactive"; + } else { + // 2. Pass-through strategy: Use rest of body as content + // Exclude keys that are definitely not part of content + // biome-ignore lint/performance/noDelete: usage is limited + const { msg_type, token, ...rest } = body; + content = rest; + + // 3. Infer msgType if missing + if (!msgType) { + if (body.post) msgType = "post"; + else if (body.image_key) msgType = "image"; + else if (body.file_key && body.image_key) msgType = "media"; // Media has both + else if (body.file_key) msgType = "file"; + else if (body.audio_key) msgType = "audio"; + else if (body.sticker_key) msgType = "sticker"; + else if (body.chat_id) msgType = "share_chat"; + else if (body.user_id) msgType = "share_user"; + else if (body.header || body.elements) msgType = "interactive"; // Unwrapped card + else { + // Fallback to text + msgType = "text"; + // For text, content must be simple or stringified + content = { text: JSON.stringify(body, null, 2) }; + } + } + } } else { // Deep clone content to avoid mutating shared object for parallel requests if we modify it content = JSON.parse(JSON.stringify(content)); @@ -145,7 +174,7 @@ webhook.post("/:token/topic/:slug", async (c) => { // Add metadata if (msgType === "text" && content.text) { - content.text = `[Topic: ${topic.name}]\n${content.text}`; + content.text = `[${topic.name}]\n${content.text}`; } if (msgType === "interactive" && content.header) { content.header.title.content = `[${topic.name}] ${content.header.title.content}`; @@ -156,6 +185,7 @@ webhook.post("/:token/topic/:slug", async (c) => { recipient.idType, msgType, content, + body.uuid, ); return { recipientId: recipient.id, status: "sent", error: null }; @@ -303,9 +333,67 @@ webhook.post("/:token/dm", async (c) => { let msgType = body.msg_type || "text"; let content = body.content; + // Special handling for incomplete payloads (missing 'content') if (!content) { - msgType = "text"; - content = { text: JSON.stringify(body, null, 2) }; + // 1. Interactive / Card + if ((msgType === "interactive" || !msgType) && body.card) { + msgType = "interactive"; + content = body.card; + } + // 2. Post (Rich Text) + else if ((msgType === "post" || !msgType) && body.post) { + msgType = "post"; + content = { post: body.post }; + } + // 3. Image + else if ((msgType === "image" || !msgType) && body.image_key) { + msgType = "image"; + content = { image_key: body.image_key }; + } + // 4. File + else if ((msgType === "file" || !msgType) && body.file_key) { + msgType = "file"; + content = { file_key: body.file_key }; + } + // 5. Audio + else if ((msgType === "audio" || !msgType) && body.audio_key) { + msgType = "audio"; + content = { file_key: body.audio_key }; + } + // 6. Media (Video) + else if ( + (msgType === "media" || !msgType) && + body.file_key && + body.image_key + ) { + msgType = "media"; + content = { file_key: body.file_key, image_key: body.image_key }; + } + // 7. Sticker + else if ((msgType === "sticker" || !msgType) && body.sticker_key) { + msgType = "sticker"; + content = { file_key: body.sticker_key }; + } + // 8. Share Chat + else if ((msgType === "share_chat" || !msgType) && body.chat_id) { + msgType = "share_chat"; + content = { chat_id: body.chat_id }; + } + // 9. Share User + else if ((msgType === "share_user" || !msgType) && body.user_id) { + msgType = "share_user"; + content = { user_id: body.user_id }; + } + // Fallback + else { + if (!msgType || msgType === "text") { + msgType = "text"; + content = { text: JSON.stringify(body, null, 2) }; + } + } + } else { + // Deep clone content to avoid mutating shared object for parallel requests if we modify it + content = JSON.parse(JSON.stringify(content)); } // Add metadata @@ -319,11 +407,13 @@ webhook.post("/:token/dm", async (c) => { const idType = user.feishuUserId.startsWith("ou_") ? "open_id" : "user_id"; + const uuid = body.uuid || crypto.randomUUID(); await feishuClient.sendMessage( user.feishuUserId, idType, msgType, content, + uuid, ); // Update Task