feat: shorten user token

Signed-off-by: d0zingcat <iamtangli42@gmail.com>
This commit is contained in:
2026-01-15 20:51:37 +08:00
parent f68a6a1c11
commit 03b115b83e
5 changed files with 146 additions and 10 deletions

View File

@@ -0,0 +1,9 @@
import { db } from "./index";
import { migrateUserTokens } from "./migrate";
async function main() {
await migrateUserTokens(db);
process.exit(0);
}
main();

View File

@@ -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();
}

View File

@@ -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 }) => ({

View File

@@ -33,6 +33,7 @@ export class FeishuClient {
receiveIdType: "open_id" | "user_id" | "email" | "chat_id",
msgType: string,
content: Record<string, unknown> | 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,
},
});

View File

@@ -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