diff --git a/src/onebot11/action/group/GetGroupMemberList.ts b/src/onebot11/action/group/GetGroupMemberList.ts index 3a646bab..9a2e500b 100644 --- a/src/onebot11/action/group/GetGroupMemberList.ts +++ b/src/onebot11/action/group/GetGroupMemberList.ts @@ -7,6 +7,8 @@ import { napCatCore, NTQQGroupApi, NTQQUserApi, SignMiniApp } from '@/core'; import { WebApi } from '@/core/apis/webapi'; import { logDebug } from '@/common/utils/log'; import { FromSchema, JSONSchema } from 'json-schema-to-ts'; +import { getLastSentTimeAndJoinTime }from "./LastSendAndJoinRemberLRU" + const SchemaData = { type: 'object', properties: { @@ -56,6 +58,21 @@ class GetGroupMemberList extends BaseAction { } // 还原Map到Array const RetGroupMember: OB11GroupMember[] = Array.from(MemberMap.values()); + + // 无管理员权限通过本地记录获取发言时间 + const haveAdmin = RetGroupMember[0].last_sent_time !== 0; + if (!haveAdmin) { + logDebug('没有管理员权限,使用本地记录'); + const _sendAndJoinRember = await getLastSentTimeAndJoinTime(parseInt(group.groupCode)); + _sendAndJoinRember.forEach((rember) => { + const member = RetGroupMember.find(member=>member.user_id == rember.user_id); + if(member){ + member.last_sent_time = rember.last_sent_time; + member.join_time = rember.join_time; + } + }) + } + return RetGroupMember; } } diff --git a/src/onebot11/action/group/LRUCache.ts b/src/onebot11/action/group/LRUCache.ts new file mode 100644 index 00000000..d45bf8ba --- /dev/null +++ b/src/onebot11/action/group/LRUCache.ts @@ -0,0 +1,178 @@ +import { logError, logDebug } from "@/common/utils/log"; + +type group_id = number; +type user_id = number; + +class cacheNode { + value: T; + groupId: group_id; + userId: user_id; + prev: cacheNode | null; + next: cacheNode | null; + timestamp: number; + + constructor(groupId: group_id, userId: user_id, value: T) { + this.groupId = groupId; + this.userId = userId; + this.value = value; + this.prev = null; + this.next = null; + this.timestamp = Date.now(); + } +} + +type cache = { [key: group_id]: { [key: user_id]: cacheNode } }; +class LRU { + private maxAge: number; + private maxSize: number; + private currentSize: number; + private cache: cache; + private head: cacheNode | null = null; + private tail: cacheNode | null = null; + private onFuncs: ((node: cacheNode) => void)[] = []; + + constructor(maxAge: number = 2e4, maxSize: number = 5e3) { + this.maxAge = maxAge; + this.maxSize = maxSize; + this.cache = Object.create(null); + this.currentSize = 0; + + if (maxSize == 0) return; + setInterval(() => this.removeExpired(), this.maxAge); + } + + // 移除LRU节点 + private removeLRUNode(node: cacheNode) { + logDebug( + "removeLRUNode", + node.groupId, + node.userId, + node.value, + this.currentSize + ); + node.prev = node.next = null; + delete this.cache[node.groupId][node.userId]; + this.removeNode(node); + this.onFuncs.forEach((func) => func(node)); + this.currentSize--; + logDebug("removeLRUNode", "After", this.currentSize); + } + + public on(func: (node: cacheNode) => void) { + this.onFuncs.push(func); + } + + private removeExpired() { + console.log("remove expired LRU node", !!this.tail); + //console.log(`now current`, this.currentSize); + //const rCurrent = Object.values(this.cache) + // .map((group) => Object.values(group)) + // .flat().length; + //console.log(`realiy current`, rCurrent); + + const now = Date.now(); + let current = this.tail; + const nodesToRemove: cacheNode[] = []; + let removedCount = 0; + + // 收集需要删除的节点 + while (current && now - current.timestamp > this.maxAge) { + nodesToRemove.push(current); + current = current.prev; + removedCount++; + if (removedCount >= 100) break; + } + + // 更新链表指向 + if (nodesToRemove.length > 0) { + const newTail = nodesToRemove[nodesToRemove.length - 1].prev; + if (newTail) { + newTail.next = null; + } else { + this.head = null; + } + this.tail = newTail; + } + + // 删除收集到的节点 + // console.log(nodesToRemove) + nodesToRemove.forEach((node) => { + // console.log("node is null", node === null); + node.prev = node.next = null; + delete this.cache[node.groupId][node.userId]; + + this.currentSize--; + this.onFuncs.forEach((func) => func(node)); + }); + + console.log("after remove expired current", this.currentSize); + // console.log( + // "after remove expired realiy current", + // Object.values(this.cache) + // .map((group) => Object.values(group)) + // .flat().length + // ); + } + + private addNode(node: cacheNode) { + node.next = this.head; + if (this.head) this.head.prev = node; + if (!this.tail) this.tail = node; + this.head = node; + } + + private removeNode(node: cacheNode) { + if (node.prev) node.prev.next = node.next; + if (node.next) node.next.prev = node.prev; + if (node === this.head) this.head = node.next; + if (node === this.tail) this.tail = node.prev; + } + + private moveToHead(node: cacheNode) { + if (this.head === node) return; + + this.removeNode(node); + this.addNode(node); + node.prev = null; + + logDebug("moveToHead", node.groupId, node.userId, node.value); + } + + public set(groupId: group_id, userId: user_id, value: T) { + logDebug("set", groupId, userId, value, this.currentSize); + + if (!this.cache[groupId]) { + logDebug("set", "create group", groupId); + this.cache[groupId] = Object.create(null); + } + + const groupObject = this.cache[groupId]; + + if (groupObject[userId]) { + logDebug("update", groupId, userId, value); + const node = groupObject[userId]; + node.value = value; + node.timestamp = Date.now(); + this.moveToHead(node); + } else { + logDebug("add", groupId, userId, value); + const node = new cacheNode(groupId, userId, value); + groupObject[userId] = node; + this.currentSize++; + this.addNode(node); + if (this.currentSize > this.maxSize) { + const tail = this.tail!; + logDebug( + "remove expired LRU node", + tail.groupId, + tail.userId, + tail.value, + this.currentSize + ); + this.removeLRUNode(tail); + } + } + } +} + +export default LRU; diff --git a/src/onebot11/action/group/LastSendAndJoinRemberLRU.ts b/src/onebot11/action/group/LastSendAndJoinRemberLRU.ts new file mode 100644 index 00000000..2abf0124 --- /dev/null +++ b/src/onebot11/action/group/LastSendAndJoinRemberLRU.ts @@ -0,0 +1,188 @@ +import sqlite3 from "sqlite3"; +import { logError, logDebug } from "@/common/utils/log"; +import { selfInfo } from "@/core/data"; +import { ob11Config } from "@/onebot11/config"; +import LRU from "./LRUCache"; +import path from "path"; + +const dbPath = path.join( + ob11Config.getConfigDir(), + `lastSendAndJoinRember_${selfInfo.uin}.db` +); +const remberDb = new sqlite3.Database(dbPath); + +// 初始化全部的群到内存中 +const groupIds: number[] = []; +remberDb.serialize(() => { + const sql = `SELECT * FROM sqlite_master WHERE type='table'`; + remberDb.all(sql, [], (err, rows: { name: string }[]) => { + if (err) return logError(err); + rows.forEach((row) => groupIds.push(parseInt(row.name))); + logDebug(`已加载 ${groupIds.length} 个群`); + console.log(groupIds); + }); +}); + +const createTableSQL = (groupId: number) => + `CREATE TABLE IF NOT EXISTS "${groupId}" ( + user_id INTEGER, + last_sent_time INTEGER, + join_time INTEGER, + PRIMARY KEY (user_id) + );`; + +async function createTableIfNotExist(groupId: number) { + // 未开启本地记录 + if (!ob11Config.localDB) return; + + logDebug("检测数据表存在", groupId); + if (groupIds.includes(groupId)) { + logDebug("数据表已存在", groupId); + return; + } + + logDebug("创建数据表", groupId); + return new Promise((resolve, reject) => { + const sql = createTableSQL(groupId); + remberDb.all(sql, (err) => { + if (err) { + logError("数据表创建失败", err); + reject(err); + return; + } + groupIds.push(groupId); + logDebug("数据表创建成功", groupId); + resolve(true); + }); + }); +} + +// 入群记录 +export async function insertJoinTime( + groupId: number, + userId: number, + time: number +) { + // 未开启本地记录 + if (!ob11Config.localDB) return; + + logDebug("插入入群时间", userId, groupId); + await createTableIfNotExist(groupId); + remberDb.all( + `INSERT OR REPLACE INTO "${groupId}" (user_id, last_sent_time, join_time) VALUES (?,?,?)`, + [userId, time, time], + (err) => { + if (err) + logError(err), + Promise.reject(), + console.log("插入入群时间失败", userId, groupId); + } + ); +} + +// 发言记录 +const LURCache = new LRU(); +const LastSentCache = new (class { + private cache: { gid: number; uid: number }[] = []; + private maxSize: number; + + constructor(maxSize: number = 5000) { + this.maxSize = maxSize; + } + + get(gid: number, uid: number): boolean { + const exists = this.cache.some( + (entry) => entry.gid === gid && entry.uid === uid + ); + if (!exists) { + this.cache.push({ gid, uid }); + if (this.cache.length > this.maxSize) { + this.cache.shift(); + } + } + + return exists; + } +})(); + +LURCache.on(async (node) => { + const { value: time, groupId, userId } = node; + + logDebug("插入发言时间", userId, groupId); + await createTableIfNotExist(groupId); + + const method = await getDataSetMethod(groupId, userId); + logDebug("插入发言时间方法判断", userId, groupId, method); + + const sql = + method == "update" + ? `UPDATE "${groupId}" SET last_sent_time = ? WHERE user_id = ?` + : `INSERT INTO "${groupId}" (last_sent_time, user_id) VALUES (?, ?)`; + + remberDb.all(sql, [time, userId], (err) => { + if (err) { + return logError("插入/更新发言时间失败", userId, groupId); + } + logDebug("插入/更新发言时间成功", userId, groupId); + }); +}); + +async function getDataSetMethod(groupId: number, userId: number) { + // 缓存记录 + if (LastSentCache.get(groupId, userId)) { + logDebug("缓存命中", userId, groupId); + return "update"; + } + + // 数据库判断 + return new Promise<"insert" | "update">((resolve, reject) => { + remberDb.all( + `SELECT * FROM "${groupId}" WHERE user_id = ?`, + [userId], + (err, rows) => { + if (err) { + logError("查询发言时间存在失败", userId, groupId, err); + return logError("插入发言时间失败", userId, groupId, err); + } + + if (rows.length === 0) { + logDebug("查询发言时间不存在", userId, groupId); + return resolve("insert"); + } + + logDebug("查询发言时间存在", userId, groupId); + resolve("update"); + } + ); + }); +} + +interface IRember { + last_sent_time: number; + join_time: number; + user_id: number; +} +export async function getLastSentTimeAndJoinTime( + groupId: number +): Promise { + logDebug("读取发言时间", groupId); + return new Promise((resolve, reject) => { + remberDb.all(`SELECT * FROM "${groupId}" `, (err, rows: IRember[]) => { + if (err) { + logError("查询发言时间失败", groupId); + return resolve([]); + } + logDebug("查询发言时间成功", groupId, rows); + resolve(rows); + }); + }); +} + +export function insertLastSentTime( + groupId: number, + userId: number, + time: number +) { + if (!ob11Config.localDB) return; + LURCache.set(groupId, userId,time) +} diff --git a/src/onebot11/config.ts b/src/onebot11/config.ts index 01ad1de8..3992e443 100644 --- a/src/onebot11/config.ts +++ b/src/onebot11/config.ts @@ -33,6 +33,8 @@ export interface OB11Config { reportSelfMessage: boolean; token: string; + localDB: boolean; + read(): OB11Config; save(config: OB11Config): void; @@ -65,6 +67,8 @@ class Config extends ConfigBase implements OB11Config { reportSelfMessage = false; token = ''; + localDB = true; + getConfigPath() { return path.join(this.getConfigDir(), `onebot11_${selfInfo.uin}.json`); } diff --git a/src/onebot11/main.ts b/src/onebot11/main.ts index 8833a497..572d11c7 100644 --- a/src/onebot11/main.ts +++ b/src/onebot11/main.ts @@ -35,6 +35,7 @@ import { Data as SysData } from '@/proto/SysMessage'; import { Data as DeviceData } from '@/proto/SysMessage.DeviceChange'; import { OB11FriendPokeEvent, OB11GroupPokeEvent } from './event/notice/OB11PokeEvent'; import { isEqual } from '@/common/utils/helper'; +import { insertLastSentTime } from "./action/group/LastSendAndJoinRemberLRU" //下面几个其实应该移进Core-Data 缓存实现 但是现在在这里方便 // @@ -286,6 +287,9 @@ export class NapCatOnebot11 { } if (msg.post_type === 'message') { logMessage(msg as OB11Message).then().catch(logError); + if (msg.message_type == 'group' && msg.group_id) { + insertLastSentTime(msg.group_id, msg.user_id, msg.time) + } } else if (msg.post_type === 'notice') { logNotice(msg).then().catch(logError); } else if (msg.post_type === 'request') {