diff --git a/server/api/UserApi.ts b/server/api/UserApi.ts index d4aa56c..a6c20e2 100644 --- a/server/api/UserApi.ts +++ b/server/api/UserApi.ts @@ -318,11 +318,11 @@ export default class UserApi extends BaseApi { const user = User.findById(token.author) as User const recentChats = user.getRecentChats() const recentChatsList: any[] = [] - for (const [chatId, content] of recentChats) { - const chat = Chat.findById(chatId) + for (const {chat_id, content} of recentChats) { + const chat = Chat.findById(chat_id) recentChatsList.push({ content, - id: chatId, + id: chat_id, title: chat?.getTitle(user) || "未知", avatar_file_hash: chat?.getAvatarFileHash(user) ? chat?.getAvatarFileHash(user) : undefined }) diff --git a/server/data/RecentChatBean.ts b/server/data/RecentChatBean.ts new file mode 100644 index 0000000..7180704 --- /dev/null +++ b/server/data/RecentChatBean.ts @@ -0,0 +1,10 @@ +export default class RecentChatBean { + declare count: number + /** 最近对话所关联的用户 */ + declare user_id: string + declare chat_id: string + declare content: string + declare updated_time: number + + [key: string]: unknown +} diff --git a/server/data/User.ts b/server/data/User.ts index 851e82d..817df8a 100644 --- a/server/data/User.ts +++ b/server/data/User.ts @@ -11,11 +11,10 @@ import UserBean from './UserBean.ts' import FileManager from './FileManager.ts' import { SQLInputValue } from "node:sqlite" import ChatPrivate from "./ChatPrivate.ts" -import Chat from "./Chat.ts" -import ChatBean from "./ChatBean.ts" -import MapJson from "../MapJson.ts" import DataWrongError from '../api/DataWrongError.ts' -import UserChatLinker from "./UserChatLinker.ts"; +import UserChatLinker from "./UserChatLinker.ts" +import UserFavouriteChatLinker from "./UserFavouriteChatLinker.ts" +import UserRecentChatLinker from "./UserRecentChatLinker.ts" type UserBeanKey = keyof UserBean @@ -38,8 +37,6 @@ export default class User { /* 用户名 */ username TEXT, /* 昵称 */ nickname TEXT NOT NULL, /* 头像, 可选 */ avatar_file_hash TEXT, - /* 对话列表 */ favourite_chats TEXT NOT NULL, - /* 最近对话 */ recent_chats TEXT NOT NULL, /* 设置 */ settings TEXT NOT NULL ); `) @@ -69,18 +66,14 @@ export default class User { username, nickname, avatar_file_hash, - favourite_chats, - recent_chats, settings - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);`).run( + ) VALUES (?, ?, ?, ?, ?, ?, ?);`).run( crypto.randomUUID(), password, Date.now(), userName, nickName, null, - '[]', - JSON.stringify(new Map(), MapJson.replacer), "{}" ).lastInsertRowid )[0] @@ -136,37 +129,25 @@ export default class User { this.setAttr("username", userName) } updateRecentChat(chatId: string, content: string) { - const map = JSON.parse(this.bean.recent_chats, MapJson.reviver) as Map - map.delete(chatId) - map.set(chatId, content) - this.setAttr("recent_chats", JSON.stringify(map, MapJson.replacer)) + UserRecentChatLinker.updateOrAddRecentChat(this.bean.id, chatId, content) } - getRecentChats(): Map { - try { - return JSON.parse(this.bean.recent_chats, MapJson.reviver) - } catch (e) { - console.log(chalk.yellow(`警告: 最近对话列表解析失敗: ${(e as Error).message}`)) - return new Map() - } + getRecentChats() { + return UserRecentChatLinker.getUserRecentChatBeans(this.bean.id) + } + + getFavouriteChats() { + return UserFavouriteChatLinker.getUserFavouriteChats(this.bean.id) + } + addFavouriteChats(chatIds: string[]) { + chatIds.forEach((v) => UserFavouriteChatLinker.linkUserAndChat(this.bean.id, v)) + } + removeFavouriteChats(chatIds: string[]) { + chatIds.forEach((v) => UserFavouriteChatLinker.unlinkUserAndChat(this.bean.id, v)) } addFavouriteChat(chatId: string) { - const ls = this.getFavouriteChats() - if (ls.indexOf(chatId) != -1 || ChatPrivate.getChatIdByUsersId(this.bean.id, this.bean.id) == chatId) return - ls.push(chatId) - this.setAttr("favourite_chats", JSON.stringify(ls)) - } - removeFavouriteChats(contacts: string[]) { - const ls = this.getFavouriteChats().filter((v) => !contacts.includes(v)) - this.setAttr("favourite_chats", JSON.stringify(ls)) - } - getFavouriteChats() { - try { - return [...(JSON.parse(this.bean.favourite_chats) as string[]), ChatPrivate.findOrCreateForPrivate(this, this).bean.id] - } catch (e) { - console.log(chalk.yellow(`警告: 收藏对话解析失败: ${(e as Error).message}`)) - return [] - } + this.addFavouriteChats([chatId]) } + getAllChatsList() { return UserChatLinker.getUserChats(this.bean.id) } diff --git a/server/data/UserFavouriteChatLinker.ts b/server/data/UserFavouriteChatLinker.ts new file mode 100644 index 0000000..ca9a397 --- /dev/null +++ b/server/data/UserFavouriteChatLinker.ts @@ -0,0 +1,57 @@ +import { DatabaseSync } from "node:sqlite" +import path from 'node:path' + +import config from "../config.ts" +import { SQLInputValue } from "node:sqlite" + +export default class UserFavouriteChatLinker { + static database: DatabaseSync = this.init() + + private static init(): DatabaseSync { + const db: DatabaseSync = new DatabaseSync(path.join(config.data_path, 'UserFavouriteChatLinker.db')) + db.exec(` + CREATE TABLE IF NOT EXISTS UserFavouriteChatLinker ( + /* 序号 */ count INTEGER PRIMARY KEY AUTOINCREMENT, + /* 用戶 ID */ user_id TEXT NOT NULL, + /* Chat ID */ chat_id TEXT NOT NULL + ); + `) + db.exec(`CREATE INDEX IF NOT EXISTS idx_user_id ON UserFavouriteChatLinker(user_id);`) + return db + } + + /** + * 若用户和对话未关联, 则进行关联 + */ + static linkUserAndChat(userId: string, chatId: string) { + if (!this.checkUserIsLinkedToChat(userId, chatId)) + this.database.prepare(`INSERT INTO UserFavouriteChatLinker ( + user_id, + chat_id + ) VALUES (?, ?);`).run( + userId, + chatId + ) + } + /** + * 解除用户和对话的关联 + */ + static unlinkUserAndChat(userId: string, chatId: string) { + this.database.prepare(`DELETE FROM UserFavouriteChatLinker WHERE user_id = ? AND chat_id = ?`).run(userId, chatId) + } + /** + * 检测用户和对话的关联 + */ + static checkUserIsLinkedToChat(userId: string, chatId: string) { + return this.findAllByCondition('user_id = ? AND chat_id = ?', userId, chatId).length != 0 + } + /** + * 获取用户所有关联的对话 + */ + static getUserFavouriteChats(userId: string) { + return this.findAllByCondition('user_id = ?', userId).map((v) => v.chat_id) as string[] + } + protected static findAllByCondition(condition: string, ...args: SQLInputValue[]) { + return this.database.prepare(`SELECT * FROM UserFavouriteChatLinker WHERE ${condition}`).all(...args) + } +} diff --git a/server/data/UserRecentChatLinker.ts b/server/data/UserRecentChatLinker.ts new file mode 100644 index 0000000..9c82171 --- /dev/null +++ b/server/data/UserRecentChatLinker.ts @@ -0,0 +1,71 @@ +import { DatabaseSync } from "node:sqlite" +import path from 'node:path' + +import RecentChatBean from './RecentChatBean.ts' +import config from "../config.ts" +import { SQLInputValue } from "node:sqlite" + +export default class UserRecentChatLinker { + static database: DatabaseSync = this.init() + + private static init(): DatabaseSync { + const db: DatabaseSync = new DatabaseSync(path.join(config.data_path, 'UserRecentChatLinker.db')) + db.exec(` + CREATE TABLE IF NOT EXISTS UserRecentChatLinker ( + /* 序号 */ count INTEGER PRIMARY KEY AUTOINCREMENT, + /* 用戶 ID */ user_id TEXT NOT NULL, + /* Chat ID */ chat_id TEXT NOT NULL, + /* Last Message Content */ content TEXT NOT NULL, + /* Last Update Time */ updated_time INT8 NOT NULL + ); + `) + db.exec(`CREATE INDEX IF NOT EXISTS idx_user_id ON UserRecentChatLinker(user_id);`) + return db + } + + /** + * 若用户和对话未关联, 则进行关联 + */ + static updateOrAddRecentChat(userId: string, chatId: string, content: string) { + if (!this.checkUserIsLinkedToChat(userId, chatId)) + this.database.prepare(`INSERT INTO UserRecentChatLinker ( + user_id, + chat_id, + content, + updated_time + ) VALUES (?, ?, ?, ?);`).run( + userId, + chatId, + content, + Date.now() + ) + else + this.database.prepare('UPDATE UserRecentChatLinker SET content = ?, updated_time = ? WHERE count = ?').run( + content, + Date.now(), + /* 既然已经 bind 了, 那么就不需要判断了? */ + this.findAllByCondition('user_id = ? AND chat_id = ?', userId, chatId)[0].count + ) + } + /** + * 解除用户和对话的关联 + */ + static removeRecentChat(userId: string, chatId: string) { + this.database.prepare(`DELETE FROM UserRecentChatLinker WHERE user_id = ? AND chat_id = ?`).run(userId, chatId) + } + /** + * 检测用户和对话的关联 + */ + static checkUserIsLinkedToChat(userId: string, chatId: string) { + return this.findAllByCondition('user_id = ? AND chat_id = ?', userId, chatId).length != 0 + } + /** + * 获取用户所有关联的对话 + */ + static getUserRecentChatBeans(userId: string) { + return this.findAllByCondition('user_id = ? ORDER BY updated_time DESC', userId) as unknown as RecentChatBean[] + } + protected static findAllByCondition(condition: string, ...args: SQLInputValue[]) { + return this.database.prepare(`SELECT * FROM UserRecentChatLinker WHERE ${condition}`).all(...args) + } +}