diff --git a/src/common/utils/EventTask.ts b/src/common/utils/EventTask.ts index 57cf9daf..cf33e4ba 100644 --- a/src/common/utils/EventTask.ts +++ b/src/common/utils/EventTask.ts @@ -1,181 +1,182 @@ -import { NodeIKernelMsgListener } from "@/core"; -import { NodeIQQNTWrapperSession } from "@/core/wrapper"; -import { randomUUID } from "crypto"; - -interface Internal_MapKey { - timeout: number, - createtime: number, - func: Function -} - -export class ListenerClassBase { - [key: string]: string; -} - -export interface ListenerIBase { - // eslint-disable-next-line @typescript-eslint/no-misused-new - new(listener: any): ListenerClassBase; -} - -export class NTEventWrapper { - - private ListenerMap: { [key: string]: ListenerIBase } | undefined;//ListenerName-Unique -> Listener构造函数 - private WrapperSession: NodeIQQNTWrapperSession | undefined;//WrapperSession - private ListenerManger: Map = new Map(); //ListenerName-Unique -> Listener实例 - private EventTask = new Map>>();//tasks ListenerMainName -> ListenerSubName-> uuid -> {timeout,createtime,func} - constructor() { - - } - createProxyDispatch(ListenerMainName: string) { - let current = this; - return new Proxy({}, { - get(target: any, prop: any, receiver: any) { - // console.log('get', prop, typeof target[prop]); - if (typeof target[prop] === 'undefined') { - // 如果方法不存在,返回一个函数,这个函数调用existentMethod - return (...args: any[]) => { - current.DispatcherListener.apply(current, [ListenerMainName, prop, ...args]).then(); - }; - } - // 如果方法存在,正常返回 - return Reflect.get(target, prop, receiver); - } - }); - } - init({ ListenerMap, WrapperSession }: { ListenerMap: { [key: string]: typeof ListenerClassBase }, WrapperSession: NodeIQQNTWrapperSession }) { - this.ListenerMap = ListenerMap; - this.WrapperSession = WrapperSession; - } - CreatEventFunction any>(eventName: string): T | undefined { - let eventNameArr = eventName.split('/'); - type eventType = { - [key: string]: () => { [key: string]: (...params: Parameters) => Promise> } - } - if (eventNameArr.length > 1) { - let serviceName = 'get' + eventNameArr[0].replace('NodeIKernel', ''); - let eventName = eventNameArr[1]; - //getNodeIKernelGroupListener,GroupService - //console.log('2', eventName); - let services = (this.WrapperSession as unknown as eventType)[serviceName](); - let event = services[eventName]; - //重新绑定this - event = event.bind(services); - if (event) { - return event as T; - } - return undefined; - } - - } - CreatListenerFunction(listenerMainName: string, uniqueCode: string = ""): T { - let ListenerType = this.ListenerMap![listenerMainName]; - let Listener = this.ListenerManger.get(listenerMainName + uniqueCode); - if (!Listener && ListenerType) { - Listener = new ListenerType(this.createProxyDispatch(listenerMainName)); - let ServiceSubName = listenerMainName.match(/^NodeIKernel(.*?)Listener$/)![1]; - let Service = "NodeIKernel" + ServiceSubName + "Service/addKernel" + ServiceSubName + "Listener"; - let addfunc = this.CreatEventFunction<(listener: T) => number>(Service); - addfunc!(Listener as T); - //console.log(addfunc!(Listener as T)); - this.ListenerManger.set(listenerMainName + uniqueCode, Listener); - } - return Listener as T; - } - //统一回调清理事件 - async DispatcherListener(ListenerMainName: string, ListenerSubName: string, ...args: any[]) { - //console.log(ListenerMainName, this.EventTask.get(ListenerMainName), ListenerSubName, this.EventTask.get(ListenerMainName)?.get(ListenerSubName)); - this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.forEach((task, uuid) => { - //console.log(task.func, uuid, task.createtime, task.timeout); - if (task.createtime + task.timeout < Date.now()) { - this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.delete(uuid); - return; - } - task.func(...args); - }) - } - async CallNoListenerEvent Promise,>(EventName = '', timeout: number = 3000, ...args: Parameters) { - return new Promise>(async (resolve, reject) => { - let EventFunc = this.CreatEventFunction(EventName); - let complete = false; - let Timeouter = setTimeout(() => { - if (!complete) { - reject(new Error('NTEvent EventName:' + EventName + ' timeout')); - } - }, timeout); - let retData = await EventFunc!(...args); - complete = true; - resolve(retData); - }); - } - async CallNormalEvent Promise, ListenerType extends (...args: any[]) => void>(EventName = '', ListenerName = '', waitTimes = 1, timeout: number = 3000, ...args: Parameters) { - return new Promise<[EventRet: Awaited>, ...Parameters]>(async (resolve, reject) => { - const id = randomUUID(); - let complete = 0; - let retData: ArrayLike> | undefined = undefined; - let retEvent: any = {}; - let databack = () => { - if (complete < waitTimes) { - reject(new Error('NTEvent EventName:' + EventName + ' ListenerName:' + ListenerName + ' timeout')); - } else { - - resolve([retEvent as Awaited>, ...(retData as Parameters)]); - } - } - let Timeouter = setTimeout(databack, timeout); - - let ListenerNameList = ListenerName.split('/'); - let ListenerMainName = ListenerNameList[0]; - let ListenerSubName = ListenerNameList[1]; - let eventCallbak = { - timeout: timeout, - createtime: Date.now(), - func: (...args: any[]) => { - complete++; - //console.log('func', ...args); - retData = args as ArrayLike>; - if (complete >= waitTimes) { - clearTimeout(Timeouter); - databack(); - } - } - } - if (!this.EventTask.get(ListenerMainName)) { - this.EventTask.set(ListenerMainName, new Map()); - } - if (!(this.EventTask.get(ListenerMainName)?.get(ListenerSubName))) { - this.EventTask.get(ListenerMainName)?.set(ListenerSubName, new Map()); - } - this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.set(id, eventCallbak); - this.CreatListenerFunction(ListenerMainName); - let EventFunc = this.CreatEventFunction(EventName); - retEvent = await EventFunc!(...args); - }); - } -} -export const NTEventDispatch = new NTEventWrapper(); - -// 示例代码 快速创建事件 -// let NTEvent = new NTEventWrapper(); -// let TestEvent = NTEvent.CreatEventFunction<(force: boolean) => Promise>('NodeIKernelProfileLikeService/GetTest'); -// if (TestEvent) { -// TestEvent(true); -// } - -// 示例代码 快速创建监听Listener类 -// let NTEvent = new NTEventWrapper(); -// NTEvent.CreatListenerFunction('NodeIKernelMsgListener', 'core') - - -// 调用接口 -//let NTEvent = new NTEventWrapper(); -//let ret = await NTEvent.CallNormalEvent<(force: boolean) => Promise, (data1: string, data2: number) => void>('NodeIKernelProfileLikeService/GetTest', 'NodeIKernelMsgListener/onAddSendMsg', 1, 3000, true); - -// 注册监听 解除监听 -// NTEventDispatch.RigisterListener('NodeIKernelMsgListener/onAddSendMsg','core',cb); -// NTEventDispatch.UnRigisterListener('NodeIKernelMsgListener/onAddSendMsg','core'); - -// let GetTest = NTEventDispatch.CreatEvent('NodeIKernelProfileLikeService/GetTest','NodeIKernelMsgListener/onAddSendMsg',Mode); -// GetTest('test'); - -// always模式 +import { NodeIKernelMsgListener } from '@/core'; +import { NodeIQQNTWrapperSession } from '@/core/wrapper'; +import { randomUUID } from 'crypto'; + +interface Internal_MapKey { + timeout: number, + createtime: number, + func: (...arg: any[]) => any, +} + +export class ListenerClassBase { + [key: string]: string; +} + +export interface ListenerIBase { + // eslint-disable-next-line @typescript-eslint/no-misused-new + new(listener: any): ListenerClassBase; +} + +export class NTEventWrapper { + + private ListenerMap: { [key: string]: ListenerIBase } | undefined;//ListenerName-Unique -> Listener构造函数 + private WrapperSession: NodeIQQNTWrapperSession | undefined;//WrapperSession + private ListenerManger: Map = new Map(); //ListenerName-Unique -> Listener实例 + private EventTask = new Map>>();//tasks ListenerMainName -> ListenerSubName-> uuid -> {timeout,createtime,func} + constructor() { + + } + createProxyDispatch(ListenerMainName: string) { + // eslint-disable-next-line @typescript-eslint/no-this-alias + const current = this; + return new Proxy({}, { + get(target: any, prop: any, receiver: any) { + // console.log('get', prop, typeof target[prop]); + if (typeof target[prop] === 'undefined') { + // 如果方法不存在,返回一个函数,这个函数调用existentMethod + return (...args: any[]) => { + current.DispatcherListener.apply(current, [ListenerMainName, prop, ...args]).then(); + }; + } + // 如果方法存在,正常返回 + return Reflect.get(target, prop, receiver); + } + }); + } + init({ ListenerMap, WrapperSession }: { ListenerMap: { [key: string]: typeof ListenerClassBase }, WrapperSession: NodeIQQNTWrapperSession }) { + this.ListenerMap = ListenerMap; + this.WrapperSession = WrapperSession; + } + CreatEventFunction any>(eventName: string): T | undefined { + const eventNameArr = eventName.split('/'); + type eventType = { + [key: string]: () => { [key: string]: (...params: Parameters) => Promise> } + } + if (eventNameArr.length > 1) { + const serviceName = 'get' + eventNameArr[0].replace('NodeIKernel', ''); + const eventName = eventNameArr[1]; + //getNodeIKernelGroupListener,GroupService + //console.log('2', eventName); + const services = (this.WrapperSession as unknown as eventType)[serviceName](); + let event = services[eventName]; + //重新绑定this + event = event.bind(services); + if (event) { + return event as T; + } + return undefined; + } + + } + CreatListenerFunction(listenerMainName: string, uniqueCode: string = ''): T { + const ListenerType = this.ListenerMap![listenerMainName]; + let Listener = this.ListenerManger.get(listenerMainName + uniqueCode); + if (!Listener && ListenerType) { + Listener = new ListenerType(this.createProxyDispatch(listenerMainName)); + const ServiceSubName = listenerMainName.match(/^NodeIKernel(.*?)Listener$/)![1]; + const Service = 'NodeIKernel' + ServiceSubName + 'Service/addKernel' + ServiceSubName + 'Listener'; + const addfunc = this.CreatEventFunction<(listener: T) => number>(Service); + addfunc!(Listener as T); + //console.log(addfunc!(Listener as T)); + this.ListenerManger.set(listenerMainName + uniqueCode, Listener); + } + return Listener as T; + } + //统一回调清理事件 + async DispatcherListener(ListenerMainName: string, ListenerSubName: string, ...args: any[]) { + //console.log(ListenerMainName, this.EventTask.get(ListenerMainName), ListenerSubName, this.EventTask.get(ListenerMainName)?.get(ListenerSubName)); + this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.forEach((task, uuid) => { + //console.log(task.func, uuid, task.createtime, task.timeout); + if (task.createtime + task.timeout < Date.now()) { + this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.delete(uuid); + return; + } + task.func(...args); + }); + } + async CallNoListenerEvent Promise,>(EventName = '', timeout: number = 3000, ...args: Parameters) { + return new Promise>(async (resolve, reject) => { + const EventFunc = this.CreatEventFunction(EventName); + let complete = false; + const Timeouter = setTimeout(() => { + if (!complete) { + reject(new Error('NTEvent EventName:' + EventName + ' timeout')); + } + }, timeout); + const retData = await EventFunc!(...args); + complete = true; + resolve(retData); + }); + } + async CallNormalEvent Promise, ListenerType extends (...args: any[]) => void>(EventName = '', ListenerName = '', waitTimes = 1, timeout: number = 3000, ...args: Parameters) { + return new Promise<[EventRet: Awaited>, ...Parameters]>(async (resolve, reject) => { + const id = randomUUID(); + let complete = 0; + let retData: ArrayLike> | undefined = undefined; + let retEvent: any = {}; + const databack = () => { + if (complete < waitTimes) { + reject(new Error('NTEvent EventName:' + EventName + ' ListenerName:' + ListenerName + ' timeout')); + } else { + + resolve([retEvent as Awaited>, ...(retData as Parameters)]); + } + }; + const Timeouter = setTimeout(databack, timeout); + + const ListenerNameList = ListenerName.split('/'); + const ListenerMainName = ListenerNameList[0]; + const ListenerSubName = ListenerNameList[1]; + const eventCallbak = { + timeout: timeout, + createtime: Date.now(), + func: (...args: any[]) => { + complete++; + //console.log('func', ...args); + retData = args as ArrayLike>; + if (complete >= waitTimes) { + clearTimeout(Timeouter); + databack(); + } + } + }; + if (!this.EventTask.get(ListenerMainName)) { + this.EventTask.set(ListenerMainName, new Map()); + } + if (!(this.EventTask.get(ListenerMainName)?.get(ListenerSubName))) { + this.EventTask.get(ListenerMainName)?.set(ListenerSubName, new Map()); + } + this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.set(id, eventCallbak); + this.CreatListenerFunction(ListenerMainName); + const EventFunc = this.CreatEventFunction(EventName); + retEvent = await EventFunc!(...args); + }); + } +} +export const NTEventDispatch = new NTEventWrapper(); + +// 示例代码 快速创建事件 +// let NTEvent = new NTEventWrapper(); +// let TestEvent = NTEvent.CreatEventFunction<(force: boolean) => Promise>('NodeIKernelProfileLikeService/GetTest'); +// if (TestEvent) { +// TestEvent(true); +// } + +// 示例代码 快速创建监听Listener类 +// let NTEvent = new NTEventWrapper(); +// NTEvent.CreatListenerFunction('NodeIKernelMsgListener', 'core') + + +// 调用接口 +//let NTEvent = new NTEventWrapper(); +//let ret = await NTEvent.CallNormalEvent<(force: boolean) => Promise, (data1: string, data2: number) => void>('NodeIKernelProfileLikeService/GetTest', 'NodeIKernelMsgListener/onAddSendMsg', 1, 3000, true); + +// 注册监听 解除监听 +// NTEventDispatch.RigisterListener('NodeIKernelMsgListener/onAddSendMsg','core',cb); +// NTEventDispatch.UnRigisterListener('NodeIKernelMsgListener/onAddSendMsg','core'); + +// let GetTest = NTEventDispatch.CreatEvent('NodeIKernelProfileLikeService/GetTest','NodeIKernelMsgListener/onAddSendMsg',Mode); +// GetTest('test'); + +// always模式 // NTEventDispatch.CreatEvent('NodeIKernelProfileLikeService/GetTest','NodeIKernelMsgListener/onAddSendMsg',Mode,(...args:any[])=>{ console.log(args) }); \ No newline at end of file diff --git a/src/common/utils/LRUCache.ts b/src/common/utils/LRUCache.ts index 44f0bfa3..4240f298 100644 --- a/src/common/utils/LRUCache.ts +++ b/src/common/utils/LRUCache.ts @@ -1,145 +1,145 @@ -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--; - } - - public on(func: (node: cacheNode) => void) { - this.onFuncs.push(func); - } - - private removeExpired() { - 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; - } - - nodesToRemove.forEach((node) => { - node.prev = node.next = null; - delete this.cache[node.groupId][node.userId]; - - this.currentSize--; - this.onFuncs.forEach((func) => func(node)); - }); - } - - 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; - } - - public set(groupId: group_id, userId: user_id, value: T) { - if (!this.cache[groupId]) { - this.cache[groupId] = Object.create(null); - } - - const groupObject = this.cache[groupId]; - - if (groupObject[userId]) { - const node = groupObject[userId]; - node.value = value; - node.timestamp = Date.now(); - this.moveToHead(node); - } else { - const node = new cacheNode(groupId, userId, value); - groupObject[userId] = node; - this.currentSize++; - this.addNode(node); - if (this.currentSize > this.maxSize) { - const tail = this.tail!; - this.removeLRUNode(tail); - } - } - } -} - -export default LRU; +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--; + } + + public on(func: (node: cacheNode) => void) { + this.onFuncs.push(func); + } + + private removeExpired() { + 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; + } + + nodesToRemove.forEach((node) => { + node.prev = node.next = null; + delete this.cache[node.groupId][node.userId]; + + this.currentSize--; + this.onFuncs.forEach((func) => func(node)); + }); + } + + 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; + } + + public set(groupId: group_id, userId: user_id, value: T) { + if (!this.cache[groupId]) { + this.cache[groupId] = Object.create(null); + } + + const groupObject = this.cache[groupId]; + + if (groupObject[userId]) { + const node = groupObject[userId]; + node.value = value; + node.timestamp = Date.now(); + this.moveToHead(node); + } else { + const node = new cacheNode(groupId, userId, value); + groupObject[userId] = node; + this.currentSize++; + this.addNode(node); + if (this.currentSize > this.maxSize) { + const tail = this.tail!; + this.removeLRUNode(tail); + } + } + } +} + +export default LRU; diff --git a/src/common/utils/db.ts b/src/common/utils/db.ts index c5586609..d4a12c0d 100644 --- a/src/common/utils/db.ts +++ b/src/common/utils/db.ts @@ -1,509 +1,509 @@ -import { ElementType, FileElement, PicElement, PttElement, RawMessage, VideoElement } from '../../core/src/entities'; - -import sqlite3 from 'sqlite3'; -import { log, logDebug, logError } from '@/common/utils/log'; -import { NTQQMsgApi } from '@/core'; -import LRU from "@/common/utils/LRUCache"; - -export interface IRember { - last_sent_time: number; - join_time: number; - user_id: number; -} - - -type DBMsg = { - id: number, - shortId: number, - longId: string, - seq: number, - peerUid: string, - chatType: number, -} - -type DBFile = { - name: string; // 文件名 - path: string; - url: string; - size: number; - uuid: string; - msgId: string; - elementId: string; - element: PicElement | VideoElement | FileElement | PttElement; - elementType: ElementType.PIC | ElementType.VIDEO | ElementType.FILE | ElementType.PTT; -} - - -class DBUtilBase { - protected db: sqlite3.Database | undefined; - - async init(dbPath: string) { - if (this.db) { - return; - } - return new Promise((resolve, reject) => { - this.db = new sqlite3.Database(dbPath, sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE, (err) => { - if (err) { - logError('Could not connect to database', err); - reject(err); - return; - } - this.createTable(); - resolve(); - }); - }); - } - - protected createTable() { - throw new Error('Method not implemented.'); - } - - close() { - this.db?.close(); - } -} - -class DBUtil extends DBUtilBase { - private msgCache: Map = new Map(); - private globalMsgShortId = -2147483640; - private groupIds: number[] = []; - private LURCache = new LRU(); - private 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; - } - })(); - - constructor() { - super(); - const interval = 1000 * 60 * 10; // 10分钟清理一次缓存 - setInterval(() => { - logDebug('清理消息缓存'); - this.msgCache.forEach((msg, key) => { - if ((Date.now() - parseInt(msg.msgTime) * 1000) > interval) { - this.msgCache.delete(key); - } - }); - }, interval); - } - - async init(dbPath: string) { - await super.init(dbPath); - this.globalMsgShortId = await this.getCurrentMaxShortId(); - - - // 初始化群缓存列表 - this.db!.serialize(() => { - const sql = `SELECT * FROM sqlite_master WHERE type='table'`; - this.db!.all(sql, [], (err, rows: { name: string }[]) => { - if (err) return logError(err); - rows.forEach((row) => this.groupIds.push(parseInt(row.name))); - //logDebug(`已加载 ${groupIds.length} 个群`); - }); - }); - - - this.LURCache.on(async (node) => { - const { value: time, groupId, userId } = node; - - logDebug("插入发言时间", userId, groupId); - await this.createGroupInfoTimeTableIfNotExist(groupId); - - const method = await this.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 (?, ?)`; - - this.db!.all(sql, [time, userId], (err) => { - if (err) { - return logError("插入/更新发言时间失败", userId, groupId); - } - logDebug("插入/更新发言时间成功", userId, groupId); - }); - - }); - } - async getDataSetMethod(groupId: number, userId: number) { - // 缓存记录 - if (this.LastSentCache.get(groupId, userId)) { - logDebug("缓存命中", userId, groupId); - return "update"; - } - - // 数据库判断 - return new Promise<"insert" | "update">((resolve, reject) => { - this.db!.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"); - } - ); - }); - } - async createGroupInfoTimeTableIfNotExist(groupId: number) { - const createTableSQL = (groupId: number) => - `CREATE TABLE IF NOT EXISTS "${groupId}" ( - user_id INTEGER, - last_sent_time INTEGER, - join_time INTEGER, - PRIMARY KEY (user_id) - );`; - - if (this.groupIds.includes(groupId)) { - return; - } - return new Promise((resolve, reject) => { - const sql = createTableSQL(groupId); - this.db!.all(sql, (err) => { - if (err) { - reject(err); - return; - } - this.groupIds.push(groupId); - resolve(true); - }); - }); - } - protected createTable() { - // 消息记录 - const createTableSQL = ` - CREATE TABLE IF NOT EXISTS msgs ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - shortId INTEGER NOT NULL UNIQUE, - longId TEXT NOT NULL UNIQUE, - seq INTEGER NOT NULL, - peerUid TEXT NOT NULL, - chatType INTEGER NOT NULL - )`; - this.db!.run(createTableSQL, function (err) { - if (err) { - logError('Could not create table msgs', err.stack); - } - }); - - // 文件缓存 - const createFileTableSQL = ` - CREATE TABLE IF NOT EXISTS files ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - path TEXT NOT NULL, - url TEXT, - size INTEGER NOT NULL, - uuid TEXT, - elementType INTEGER, - element TEXT NOT NULL, - elementId TEXT NOT NULL, - msgId TEXT NOT NULL - )`; - this.db!.run(createFileTableSQL, function (err) { - if (err) { - logError('Could not create table files', err); - } - }); - - // 接收到的临时会话消息uid - const createTempUinTableSQL = ` - CREATE TABLE IF NOT EXISTS temp_uins ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - uid TEXT, - uin TEXT - )`; - this.db!.run(createTempUinTableSQL, function (err) { - if (err) { - logError('Could not create table temp_uins', err); - } - }); - } - - private async getCurrentMaxShortId() { - return new Promise((resolve, reject) => { - this.db!.get('SELECT MAX(shortId) as maxId FROM msgs', (err, row: { maxId: number }) => { - if (err) { - logDebug('Could not get max short id, Use default -2147483640', err); - return resolve(-2147483640); - } - logDebug('数据库中消息最大短id', row?.maxId); - resolve(row?.maxId ?? -2147483640); - }); - }); - } - - private async getMsg(query: string, params: any[]) { - const stmt = this.db!.prepare(query); - return new Promise((resolve, reject) => { - stmt.get(...params, (err: any, row: DBMsg) => { - // log("getMsg", row, err); - if (err) { - logError('Could not get msg', err, query, params); - return resolve(null); - } - if (!row) { - // logDebug('不存在数据库中的消息,不进行处理', query, params); - resolve(null); - return; - } - const msgId = row.longId; - NTQQMsgApi.getMsgsByMsgId({ peerUid: row.peerUid, chatType: row.chatType }, [msgId]).then(res => { - const msg = res.msgList[0]; - if (!msg) { - resolve(null); - return; - } - msg.id = row.shortId; - resolve(msg); - }).catch(e => { - resolve(null); - }); - }); - }); - } - - async getMsgByShortId(shortId: number): Promise { - if (this.msgCache.has(shortId)) { - return this.msgCache.get(shortId)!; - } - const getStmt = 'SELECT * FROM msgs WHERE shortId = ?'; - return this.getMsg(getStmt, [shortId]); - } - - async getMsgByLongId(longId: string): Promise { - if (this.msgCache.has(longId)) { - return this.msgCache.get(longId)!; - } - return this.getMsg('SELECT * FROM msgs WHERE longId = ?', [longId]); - } - - async getMsgBySeq(peerUid: string, seq: string): Promise { - const stmt = 'SELECT * FROM msgs WHERE peerUid = ? AND seq = ?'; - return this.getMsg(stmt, [peerUid, seq]); - } - - async addMsg(msg: RawMessage, update = true): Promise { - const existMsg = await this.getMsgByLongId(msg.msgId); - if (existMsg) { - // logDebug('消息已存在,更新数据库', msg.msgId); - if (update) this.updateMsg(msg).then(); - return existMsg.id!; - } - const stmt = this.db!.prepare('INSERT INTO msgs (shortId, longId, seq, peerUid, chatType) VALUES (?, ?, ?, ?, ?)'); - // const runAsync = promisify(stmt.run.bind(stmt)); - const shortId = ++this.globalMsgShortId; - msg.id = shortId; - //logDebug(`记录消息到数据库, 消息长id: ${msg.msgId}, 短id: ${msg.id}`); - this.msgCache.set(shortId, msg); - this.msgCache.set(msg.msgId, msg); - stmt.run(this.globalMsgShortId, msg.msgId, msg.msgSeq.toString(), msg.peerUid, msg.chatType, (err: any) => { - if (err) { - if (err.errno === 19) { - this.getMsgByLongId(msg.msgId).then((msg: RawMessage | null) => { - if (msg) { - this.msgCache.set(shortId, msg); - this.msgCache.set(msg.msgId, msg); - // logDebug('获取消息短id成功', msg.id); - } else { - logError('db could not get msg by long id', err); - } - }).catch(e => logError('db getMsgByLongId error', e)); - } else { - logError('db could not add msg', err); - } - } - }); - return shortId; - } - - async updateMsg(msg: RawMessage) { - const existMsg = this.msgCache.get(msg.msgId); - if (existMsg) { - Object.assign(existMsg, msg); - } - //logDebug(`更新消息, shortId:${msg.id}, seq: ${msg.msgSeq}, msgId: ${msg.msgId}`); - const stmt = this.db!.prepare('UPDATE msgs SET seq=? WHERE longId=?'); - stmt.run(msg.msgSeq, msg.msgId, (err: any) => { - if (err) { - logError('updateMsg db error', err); - } - }); - - } - - async addFileCache(file: DBFile) { - const stmt = this.db!.prepare('INSERT INTO files (name, path, url, size, uuid, elementType ,element, elementId, msgId) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)'); - return new Promise((resolve, reject) => { - stmt.run(file.name, file.path, file.url, file.size, file.uuid, - file.elementType, - JSON.stringify(file.element), - file.elementId, - file.msgId, - function (err: any) { - if (err) { - logError('db could not add file', err); - reject(err); - } - resolve(null); - }); - }); - } - - private async getFileCache(query: string, params: any[]) { - const stmt = this.db!.prepare(query); - return new Promise((resolve, reject) => { - stmt.get(...params, (err: any, row: DBFile & { element: string }) => { - if (err) { - logError('db could not get file cache', err); - reject(err); - } - if (row) { - row.element = JSON.parse(row.element); - } - resolve(row); - }); - }); - } - - async getFileCacheByName(name: string): Promise { - return this.getFileCache('SELECT * FROM files WHERE name = ?', [name]); - } - - async getFileCacheByUuid(uuid: string): Promise { - return this.getFileCache('SELECT * FROM files WHERE uuid = ?', [uuid]); - } - - // todo: 是否所有的文件都有uuid?语音消息有没有uuid? - async updateFileCache(file: DBFile) { - const stmt = this.db!.prepare('UPDATE files SET path = ?, url = ? WHERE uuid = ?'); - return new Promise((resolve, reject) => { - stmt.run(file.path, file.url, file.uuid, function (err: any) { - if (err) { - logError('db could not update file cache', err); - reject(err); - } - resolve(null); - }); - }); - } - - // 被动收到的临时会话消息uin->uid - async getReceivedTempUinMap() { - const stmt = 'SELECT * FROM temp_uins'; - return new Promise>((resolve, reject) => { - this.db!.all(stmt, (err, rows: { uin: string, uid: string }[]) => { - if (err) { - logError('db could not get temp uin map', err); - reject(err); - } - const map: Record = {}; - rows.forEach(row => { - map[row.uin] = row.uid; - }); - resolve(map); - }); - }); - } - - // 通过uin获取临时会话消息uid - async getUidByTempUin(uid: string) { - const stmt = 'SELECT * FROM temp_uins WHERE uin = ?'; - return new Promise((resolve, reject) => { - this.db!.get(stmt, [uid], (err, row: { uin: string, uid: string }) => { - if (err) { - logError('db could not get temp uin map', err); - reject(err); - } - resolve(row?.uid); - }); - }); - } - - async addTempUin(uin: string, uid: string) { - const existUid = await this.getUidByTempUin(uin); - if (!existUid) { - const stmt = this.db!.prepare('INSERT INTO temp_uins (uin, uid) VALUES (?, ?)'); - return new Promise((resolve, reject) => { - stmt.run(uin, uid, function (err: any) { - if (err) { - logError('db could not add temp uin', err); - reject(err); - } - resolve(null); - }); - }); - } - } - async getLastSentTimeAndJoinTime( - groupId: number - ): Promise { - logDebug("读取发言时间", groupId); - return new Promise((resolve, reject) => { - this.db!.all(`SELECT * FROM "${groupId}" `, (err, rows: IRember[]) => { - if (err) { - logError("查询发言时间失败", groupId); - return resolve([]); - } - logDebug("查询发言时间成功", groupId, rows); - resolve(rows); - }); - }); - } - - insertLastSentTime( - groupId: number, - userId: number, - time: number - ) { - this.LURCache.set(groupId, userId, time) - } - async insertJoinTime( - groupId: number, - userId: number, - time: number - ) { - await this.createGroupInfoTimeTableIfNotExist(groupId); - this.db!.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); - } - ); - - } -} - - -export const dbUtil = new DBUtil(); +import { ElementType, FileElement, PicElement, PttElement, RawMessage, VideoElement } from '../../core/src/entities'; + +import sqlite3 from 'sqlite3'; +import { log, logDebug, logError } from '@/common/utils/log'; +import { NTQQMsgApi } from '@/core'; +import LRU from '@/common/utils/LRUCache'; + +export interface IRember { + last_sent_time: number; + join_time: number; + user_id: number; +} + + +type DBMsg = { + id: number, + shortId: number, + longId: string, + seq: number, + peerUid: string, + chatType: number, +} + +type DBFile = { + name: string; // 文件名 + path: string; + url: string; + size: number; + uuid: string; + msgId: string; + elementId: string; + element: PicElement | VideoElement | FileElement | PttElement; + elementType: ElementType.PIC | ElementType.VIDEO | ElementType.FILE | ElementType.PTT; +} + + +class DBUtilBase { + protected db: sqlite3.Database | undefined; + + async init(dbPath: string) { + if (this.db) { + return; + } + return new Promise((resolve, reject) => { + this.db = new sqlite3.Database(dbPath, sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE, (err) => { + if (err) { + logError('Could not connect to database', err); + reject(err); + return; + } + this.createTable(); + resolve(); + }); + }); + } + + protected createTable() { + throw new Error('Method not implemented.'); + } + + close() { + this.db?.close(); + } +} + +class DBUtil extends DBUtilBase { + private msgCache: Map = new Map(); + private globalMsgShortId = -2147483640; + private groupIds: number[] = []; + private LURCache = new LRU(); + private 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; + } + })(); + + constructor() { + super(); + const interval = 1000 * 60 * 10; // 10分钟清理一次缓存 + setInterval(() => { + logDebug('清理消息缓存'); + this.msgCache.forEach((msg, key) => { + if ((Date.now() - parseInt(msg.msgTime) * 1000) > interval) { + this.msgCache.delete(key); + } + }); + }, interval); + } + + async init(dbPath: string) { + await super.init(dbPath); + this.globalMsgShortId = await this.getCurrentMaxShortId(); + + + // 初始化群缓存列表 + this.db!.serialize(() => { + const sql = 'SELECT * FROM sqlite_master WHERE type=\'table\''; + this.db!.all(sql, [], (err, rows: { name: string }[]) => { + if (err) return logError(err); + rows.forEach((row) => this.groupIds.push(parseInt(row.name))); + //logDebug(`已加载 ${groupIds.length} 个群`); + }); + }); + + + this.LURCache.on(async (node) => { + const { value: time, groupId, userId } = node; + + logDebug('插入发言时间', userId, groupId); + await this.createGroupInfoTimeTableIfNotExist(groupId); + + const method = await this.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 (?, ?)`; + + this.db!.all(sql, [time, userId], (err) => { + if (err) { + return logError('插入/更新发言时间失败', userId, groupId); + } + logDebug('插入/更新发言时间成功', userId, groupId); + }); + + }); + } + async getDataSetMethod(groupId: number, userId: number) { + // 缓存记录 + if (this.LastSentCache.get(groupId, userId)) { + logDebug('缓存命中', userId, groupId); + return 'update'; + } + + // 数据库判断 + return new Promise<'insert' | 'update'>((resolve, reject) => { + this.db!.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'); + } + ); + }); + } + async createGroupInfoTimeTableIfNotExist(groupId: number) { + const createTableSQL = (groupId: number) => + `CREATE TABLE IF NOT EXISTS "${groupId}" ( + user_id INTEGER, + last_sent_time INTEGER, + join_time INTEGER, + PRIMARY KEY (user_id) + );`; + + if (this.groupIds.includes(groupId)) { + return; + } + return new Promise((resolve, reject) => { + const sql = createTableSQL(groupId); + this.db!.all(sql, (err) => { + if (err) { + reject(err); + return; + } + this.groupIds.push(groupId); + resolve(true); + }); + }); + } + protected createTable() { + // 消息记录 + const createTableSQL = ` + CREATE TABLE IF NOT EXISTS msgs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + shortId INTEGER NOT NULL UNIQUE, + longId TEXT NOT NULL UNIQUE, + seq INTEGER NOT NULL, + peerUid TEXT NOT NULL, + chatType INTEGER NOT NULL + )`; + this.db!.run(createTableSQL, function (err) { + if (err) { + logError('Could not create table msgs', err.stack); + } + }); + + // 文件缓存 + const createFileTableSQL = ` + CREATE TABLE IF NOT EXISTS files ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + path TEXT NOT NULL, + url TEXT, + size INTEGER NOT NULL, + uuid TEXT, + elementType INTEGER, + element TEXT NOT NULL, + elementId TEXT NOT NULL, + msgId TEXT NOT NULL + )`; + this.db!.run(createFileTableSQL, function (err) { + if (err) { + logError('Could not create table files', err); + } + }); + + // 接收到的临时会话消息uid + const createTempUinTableSQL = ` + CREATE TABLE IF NOT EXISTS temp_uins ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + uid TEXT, + uin TEXT + )`; + this.db!.run(createTempUinTableSQL, function (err) { + if (err) { + logError('Could not create table temp_uins', err); + } + }); + } + + private async getCurrentMaxShortId() { + return new Promise((resolve, reject) => { + this.db!.get('SELECT MAX(shortId) as maxId FROM msgs', (err, row: { maxId: number }) => { + if (err) { + logDebug('Could not get max short id, Use default -2147483640', err); + return resolve(-2147483640); + } + logDebug('数据库中消息最大短id', row?.maxId); + resolve(row?.maxId ?? -2147483640); + }); + }); + } + + private async getMsg(query: string, params: any[]) { + const stmt = this.db!.prepare(query); + return new Promise((resolve, reject) => { + stmt.get(...params, (err: any, row: DBMsg) => { + // log("getMsg", row, err); + if (err) { + logError('Could not get msg', err, query, params); + return resolve(null); + } + if (!row) { + // logDebug('不存在数据库中的消息,不进行处理', query, params); + resolve(null); + return; + } + const msgId = row.longId; + NTQQMsgApi.getMsgsByMsgId({ peerUid: row.peerUid, chatType: row.chatType }, [msgId]).then(res => { + const msg = res.msgList[0]; + if (!msg) { + resolve(null); + return; + } + msg.id = row.shortId; + resolve(msg); + }).catch(e => { + resolve(null); + }); + }); + }); + } + + async getMsgByShortId(shortId: number): Promise { + if (this.msgCache.has(shortId)) { + return this.msgCache.get(shortId)!; + } + const getStmt = 'SELECT * FROM msgs WHERE shortId = ?'; + return this.getMsg(getStmt, [shortId]); + } + + async getMsgByLongId(longId: string): Promise { + if (this.msgCache.has(longId)) { + return this.msgCache.get(longId)!; + } + return this.getMsg('SELECT * FROM msgs WHERE longId = ?', [longId]); + } + + async getMsgBySeq(peerUid: string, seq: string): Promise { + const stmt = 'SELECT * FROM msgs WHERE peerUid = ? AND seq = ?'; + return this.getMsg(stmt, [peerUid, seq]); + } + + async addMsg(msg: RawMessage, update = true): Promise { + const existMsg = await this.getMsgByLongId(msg.msgId); + if (existMsg) { + // logDebug('消息已存在,更新数据库', msg.msgId); + if (update) this.updateMsg(msg).then(); + return existMsg.id!; + } + const stmt = this.db!.prepare('INSERT INTO msgs (shortId, longId, seq, peerUid, chatType) VALUES (?, ?, ?, ?, ?)'); + // const runAsync = promisify(stmt.run.bind(stmt)); + const shortId = ++this.globalMsgShortId; + msg.id = shortId; + //logDebug(`记录消息到数据库, 消息长id: ${msg.msgId}, 短id: ${msg.id}`); + this.msgCache.set(shortId, msg); + this.msgCache.set(msg.msgId, msg); + stmt.run(this.globalMsgShortId, msg.msgId, msg.msgSeq.toString(), msg.peerUid, msg.chatType, (err: any) => { + if (err) { + if (err.errno === 19) { + this.getMsgByLongId(msg.msgId).then((msg: RawMessage | null) => { + if (msg) { + this.msgCache.set(shortId, msg); + this.msgCache.set(msg.msgId, msg); + // logDebug('获取消息短id成功', msg.id); + } else { + logError('db could not get msg by long id', err); + } + }).catch(e => logError('db getMsgByLongId error', e)); + } else { + logError('db could not add msg', err); + } + } + }); + return shortId; + } + + async updateMsg(msg: RawMessage) { + const existMsg = this.msgCache.get(msg.msgId); + if (existMsg) { + Object.assign(existMsg, msg); + } + //logDebug(`更新消息, shortId:${msg.id}, seq: ${msg.msgSeq}, msgId: ${msg.msgId}`); + const stmt = this.db!.prepare('UPDATE msgs SET seq=? WHERE longId=?'); + stmt.run(msg.msgSeq, msg.msgId, (err: any) => { + if (err) { + logError('updateMsg db error', err); + } + }); + + } + + async addFileCache(file: DBFile) { + const stmt = this.db!.prepare('INSERT INTO files (name, path, url, size, uuid, elementType ,element, elementId, msgId) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)'); + return new Promise((resolve, reject) => { + stmt.run(file.name, file.path, file.url, file.size, file.uuid, + file.elementType, + JSON.stringify(file.element), + file.elementId, + file.msgId, + function (err: any) { + if (err) { + logError('db could not add file', err); + reject(err); + } + resolve(null); + }); + }); + } + + private async getFileCache(query: string, params: any[]) { + const stmt = this.db!.prepare(query); + return new Promise((resolve, reject) => { + stmt.get(...params, (err: any, row: DBFile & { element: string }) => { + if (err) { + logError('db could not get file cache', err); + reject(err); + } + if (row) { + row.element = JSON.parse(row.element); + } + resolve(row); + }); + }); + } + + async getFileCacheByName(name: string): Promise { + return this.getFileCache('SELECT * FROM files WHERE name = ?', [name]); + } + + async getFileCacheByUuid(uuid: string): Promise { + return this.getFileCache('SELECT * FROM files WHERE uuid = ?', [uuid]); + } + + // todo: 是否所有的文件都有uuid?语音消息有没有uuid? + async updateFileCache(file: DBFile) { + const stmt = this.db!.prepare('UPDATE files SET path = ?, url = ? WHERE uuid = ?'); + return new Promise((resolve, reject) => { + stmt.run(file.path, file.url, file.uuid, function (err: any) { + if (err) { + logError('db could not update file cache', err); + reject(err); + } + resolve(null); + }); + }); + } + + // 被动收到的临时会话消息uin->uid + async getReceivedTempUinMap() { + const stmt = 'SELECT * FROM temp_uins'; + return new Promise>((resolve, reject) => { + this.db!.all(stmt, (err, rows: { uin: string, uid: string }[]) => { + if (err) { + logError('db could not get temp uin map', err); + reject(err); + } + const map: Record = {}; + rows.forEach(row => { + map[row.uin] = row.uid; + }); + resolve(map); + }); + }); + } + + // 通过uin获取临时会话消息uid + async getUidByTempUin(uid: string) { + const stmt = 'SELECT * FROM temp_uins WHERE uin = ?'; + return new Promise((resolve, reject) => { + this.db!.get(stmt, [uid], (err, row: { uin: string, uid: string }) => { + if (err) { + logError('db could not get temp uin map', err); + reject(err); + } + resolve(row?.uid); + }); + }); + } + + async addTempUin(uin: string, uid: string) { + const existUid = await this.getUidByTempUin(uin); + if (!existUid) { + const stmt = this.db!.prepare('INSERT INTO temp_uins (uin, uid) VALUES (?, ?)'); + return new Promise((resolve, reject) => { + stmt.run(uin, uid, function (err: any) { + if (err) { + logError('db could not add temp uin', err); + reject(err); + } + resolve(null); + }); + }); + } + } + async getLastSentTimeAndJoinTime( + groupId: number + ): Promise { + logDebug('读取发言时间', groupId); + return new Promise((resolve, reject) => { + this.db!.all(`SELECT * FROM "${groupId}" `, (err, rows: IRember[]) => { + if (err) { + logError('查询发言时间失败', groupId); + return resolve([]); + } + logDebug('查询发言时间成功', groupId, rows); + resolve(rows); + }); + }); + } + + insertLastSentTime( + groupId: number, + userId: number, + time: number + ) { + this.LURCache.set(groupId, userId, time); + } + async insertJoinTime( + groupId: number, + userId: number, + time: number + ) { + await this.createGroupInfoTimeTableIfNotExist(groupId); + this.db!.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); + } + ); + + } +} + + +export const dbUtil = new DBUtil(); diff --git a/src/common/utils/system.ts b/src/common/utils/system.ts index 58cfd5a2..1e75a95e 100644 --- a/src/common/utils/system.ts +++ b/src/common/utils/system.ts @@ -21,6 +21,7 @@ const invalidMacAddresses = new Set([ ]); function validateMacAddress(candidate: string): boolean { + // eslint-disable-next-line no-useless-escape const tempCandidate = candidate.replace(/\-/g, ':').toLowerCase(); return !invalidMacAddresses.has(tempCandidate); } diff --git a/src/common/utils/version.ts b/src/common/utils/version.ts index fc8452af..cd164d5c 100644 --- a/src/common/utils/version.ts +++ b/src/common/utils/version.ts @@ -14,7 +14,7 @@ export async function checkVersion(): Promise { try { version = (await RequestUtil.HttpGetJson<{ version: string }>(url)).version; } catch (e) { - logDebug("检测更新异常",e); + logDebug('检测更新异常',e); } if (version) { resolve(version); diff --git a/src/onebot11/action/extends/OCRImage.ts b/src/onebot11/action/extends/OCRImage.ts index d6c206c6..d0cc6d05 100644 --- a/src/onebot11/action/extends/OCRImage.ts +++ b/src/onebot11/action/extends/OCRImage.ts @@ -1,46 +1,46 @@ -import { DeviceList } from '@/onebot11/main'; -import BaseAction from '../BaseAction'; -import { ActionName } from '../types'; -import { FromSchema, JSONSchema } from 'json-schema-to-ts'; -import { checkFileReceived, uri2local } from '@/common/utils/file'; -import { NTQQSystemApi } from '@/core'; -import fs from 'fs'; - -const SchemaData = { - type: 'object', - properties: { - image: { type: 'string' }, - }, - required: ['image'] -} as const satisfies JSONSchema; - -type Payload = FromSchema; - -export class OCRImage extends BaseAction { - actionName = ActionName.OCRImage; - PayloadSchema = SchemaData; - protected async _handle(payload: Payload) { - const { path, isLocal, errMsg } = (await uri2local(payload.image)); - if (errMsg) { - throw `OCR ${payload.image}失败,image字段可能格式不正确`; - } - if (path) { - await checkFileReceived(path, 5000); // 文件不存在QQ会崩溃,需要提前判断 - const ret = await NTQQSystemApi.ORCImage(path); - if (!isLocal) { - fs.unlink(path, () => { }); - } - if (!ret) { - throw `OCR ${payload.file}失败`; - } - return ret.result; - } - if (!isLocal) { - fs.unlink(path, () => { }); - } - throw `OCR ${payload.file}失败,文件可能不存在`; - } -} -export class IOCRImage extends OCRImage { - actionName = ActionName.IOCRImage; -} +import { DeviceList } from '@/onebot11/main'; +import BaseAction from '../BaseAction'; +import { ActionName } from '../types'; +import { FromSchema, JSONSchema } from 'json-schema-to-ts'; +import { checkFileReceived, uri2local } from '@/common/utils/file'; +import { NTQQSystemApi } from '@/core'; +import fs from 'fs'; + +const SchemaData = { + type: 'object', + properties: { + image: { type: 'string' }, + }, + required: ['image'] +} as const satisfies JSONSchema; + +type Payload = FromSchema; + +export class OCRImage extends BaseAction { + actionName = ActionName.OCRImage; + PayloadSchema = SchemaData; + protected async _handle(payload: Payload) { + const { path, isLocal, errMsg } = (await uri2local(payload.image)); + if (errMsg) { + throw `OCR ${payload.image}失败,image字段可能格式不正确`; + } + if (path) { + await checkFileReceived(path, 5000); // 文件不存在QQ会崩溃,需要提前判断 + const ret = await NTQQSystemApi.ORCImage(path); + if (!isLocal) { + fs.unlink(path, () => { }); + } + if (!ret) { + throw `OCR ${payload.file}失败`; + } + return ret.result; + } + if (!isLocal) { + fs.unlink(path, () => { }); + } + throw `OCR ${payload.file}失败,文件可能不存在`; + } +} +export class IOCRImage extends OCRImage { + actionName = ActionName.IOCRImage; +} diff --git a/src/onebot11/action/file/DelGroupFile.ts b/src/onebot11/action/file/DelGroupFile.ts index 2e05b429..498cfe13 100644 --- a/src/onebot11/action/file/DelGroupFile.ts +++ b/src/onebot11/action/file/DelGroupFile.ts @@ -1,23 +1,23 @@ -import { FromSchema, JSONSchema } from 'json-schema-to-ts'; -import BaseAction from '../BaseAction'; -import { ActionName } from '../types'; -import { NTQQGroupApi, NTQQMsgApi, NTQQUserApi } from '@/core/apis'; - -const SchemaData = { - type: 'object', - properties: { - group_id: { type: ['string', 'number'] }, - file_id: { type: 'string' }, - }, - required: ['group_id', 'file_id'] -} as const satisfies JSONSchema; - -type Payload = FromSchema; - -export class DelGroupFile extends BaseAction { - actionName = ActionName.DelGroupFile; - PayloadSchema = SchemaData; - protected async _handle(payload: Payload) { - return await NTQQGroupApi.DelGroupFile(payload.group_id.toString(), [payload.file_id]); - } -} +import { FromSchema, JSONSchema } from 'json-schema-to-ts'; +import BaseAction from '../BaseAction'; +import { ActionName } from '../types'; +import { NTQQGroupApi, NTQQMsgApi, NTQQUserApi } from '@/core/apis'; + +const SchemaData = { + type: 'object', + properties: { + group_id: { type: ['string', 'number'] }, + file_id: { type: 'string' }, + }, + required: ['group_id', 'file_id'] +} as const satisfies JSONSchema; + +type Payload = FromSchema; + +export class DelGroupFile extends BaseAction { + actionName = ActionName.DelGroupFile; + PayloadSchema = SchemaData; + protected async _handle(payload: Payload) { + return await NTQQGroupApi.DelGroupFile(payload.group_id.toString(), [payload.file_id]); + } +} diff --git a/src/onebot11/action/file/DelGroupFileFolder.ts b/src/onebot11/action/file/DelGroupFileFolder.ts index dd44fd9d..31907558 100644 --- a/src/onebot11/action/file/DelGroupFileFolder.ts +++ b/src/onebot11/action/file/DelGroupFileFolder.ts @@ -1,23 +1,23 @@ -import { FromSchema, JSONSchema } from 'json-schema-to-ts'; -import BaseAction from '../BaseAction'; -import { ActionName } from '../types'; -import { NTQQGroupApi, NTQQMsgApi, NTQQUserApi } from '@/core/apis'; - -const SchemaData = { - type: 'object', - properties: { - group_id: { type: ['string', 'number'] }, - folder_id: { type: 'string' }, - }, - required: ['group_id', 'folder_id'] -} as const satisfies JSONSchema; - -type Payload = FromSchema; - -export class DelGroupFileFolder extends BaseAction { - actionName = ActionName.DelGroupFileFolder; - PayloadSchema = SchemaData; - protected async _handle(payload: Payload) { - return (await NTQQGroupApi.DelGroupFileFolder(payload.group_id.toString(), payload.folder_id)).groupFileCommonResult; - } -} +import { FromSchema, JSONSchema } from 'json-schema-to-ts'; +import BaseAction from '../BaseAction'; +import { ActionName } from '../types'; +import { NTQQGroupApi, NTQQMsgApi, NTQQUserApi } from '@/core/apis'; + +const SchemaData = { + type: 'object', + properties: { + group_id: { type: ['string', 'number'] }, + folder_id: { type: 'string' }, + }, + required: ['group_id', 'folder_id'] +} as const satisfies JSONSchema; + +type Payload = FromSchema; + +export class DelGroupFileFolder extends BaseAction { + actionName = ActionName.DelGroupFileFolder; + PayloadSchema = SchemaData; + protected async _handle(payload: Payload) { + return (await NTQQGroupApi.DelGroupFileFolder(payload.group_id.toString(), payload.folder_id)).groupFileCommonResult; + } +} diff --git a/src/onebot11/action/file/GetGroupFileCount.ts b/src/onebot11/action/file/GetGroupFileCount.ts index 011c3bd6..2b84a2b6 100644 --- a/src/onebot11/action/file/GetGroupFileCount.ts +++ b/src/onebot11/action/file/GetGroupFileCount.ts @@ -1,23 +1,23 @@ -import { FromSchema, JSONSchema } from 'json-schema-to-ts'; -import BaseAction from '../BaseAction'; -import { ActionName } from '../types'; -import { NTQQGroupApi, NTQQUserApi } from '@/core/apis'; - -const SchemaData = { - type: 'object', - properties: { - group_id: { type: ['string', 'number'] }, - }, - required: ['group_id'] -} as const satisfies JSONSchema; - -type Payload = FromSchema; - -export class GetGroupFileCount extends BaseAction { - actionName = ActionName.GetGroupFileCount; - PayloadSchema = SchemaData; - protected async _handle(payload: Payload) { - const ret = await NTQQGroupApi.GetGroupFileCount([payload.group_id?.toString()]); - return { count: ret.groupFileCounts[0] }; - } -} +import { FromSchema, JSONSchema } from 'json-schema-to-ts'; +import BaseAction from '../BaseAction'; +import { ActionName } from '../types'; +import { NTQQGroupApi, NTQQUserApi } from '@/core/apis'; + +const SchemaData = { + type: 'object', + properties: { + group_id: { type: ['string', 'number'] }, + }, + required: ['group_id'] +} as const satisfies JSONSchema; + +type Payload = FromSchema; + +export class GetGroupFileCount extends BaseAction { + actionName = ActionName.GetGroupFileCount; + PayloadSchema = SchemaData; + protected async _handle(payload: Payload) { + const ret = await NTQQGroupApi.GetGroupFileCount([payload.group_id?.toString()]); + return { count: ret.groupFileCounts[0] }; + } +} diff --git a/src/onebot11/action/file/GetGroupFileList.ts b/src/onebot11/action/file/GetGroupFileList.ts index 903550ca..0d5a48e7 100644 --- a/src/onebot11/action/file/GetGroupFileList.ts +++ b/src/onebot11/action/file/GetGroupFileList.ts @@ -1,31 +1,31 @@ -import { FromSchema, JSONSchema } from 'json-schema-to-ts'; -import BaseAction from '../BaseAction'; -import { ActionName } from '../types'; -import { NTQQGroupApi, NTQQMsgApi, NTQQUserApi } from '@/core/apis'; - -const SchemaData = { - type: 'object', - properties: { - group_id: { type: ['string', 'number'] }, - start_index: { type: 'number' }, - file_count: { type: 'number' }, - }, - required: ['group_id', 'start_index', 'file_count'] -} as const satisfies JSONSchema; - -type Payload = FromSchema; - -export class GetGroupFileList extends BaseAction }> { - actionName = ActionName.GetGroupFileList; - PayloadSchema = SchemaData; - protected async _handle(payload: Payload) { - let ret = await NTQQMsgApi.getGroupFileList(payload.group_id.toString(), { - sortType: 1, - fileCount: payload.file_count, - startIndex: payload.start_index, - sortOrder: 2, - showOnlinedocFolder: 0 - }).catch((e) => { return []; }); - return { FileList: ret }; - } -} +import { FromSchema, JSONSchema } from 'json-schema-to-ts'; +import BaseAction from '../BaseAction'; +import { ActionName } from '../types'; +import { NTQQGroupApi, NTQQMsgApi, NTQQUserApi } from '@/core/apis'; + +const SchemaData = { + type: 'object', + properties: { + group_id: { type: ['string', 'number'] }, + start_index: { type: 'number' }, + file_count: { type: 'number' }, + }, + required: ['group_id', 'start_index', 'file_count'] +} as const satisfies JSONSchema; + +type Payload = FromSchema; + +export class GetGroupFileList extends BaseAction }> { + actionName = ActionName.GetGroupFileList; + PayloadSchema = SchemaData; + protected async _handle(payload: Payload) { + const ret = await NTQQMsgApi.getGroupFileList(payload.group_id.toString(), { + sortType: 1, + fileCount: payload.file_count, + startIndex: payload.start_index, + sortOrder: 2, + showOnlinedocFolder: 0 + }).catch((e) => { return []; }); + return { FileList: ret }; + } +} diff --git a/src/onebot11/action/file/SetGroupFileFolder.ts b/src/onebot11/action/file/SetGroupFileFolder.ts index 1961d506..856a2ba5 100644 --- a/src/onebot11/action/file/SetGroupFileFolder.ts +++ b/src/onebot11/action/file/SetGroupFileFolder.ts @@ -1,23 +1,23 @@ -import { FromSchema, JSONSchema } from 'json-schema-to-ts'; -import BaseAction from '../BaseAction'; -import { ActionName } from '../types'; -import { NTQQGroupApi, NTQQMsgApi, NTQQUserApi } from '@/core/apis'; - -const SchemaData = { - type: 'object', - properties: { - group_id: { type: ['string', 'number'] }, - folder_name: { type: 'string' }, - }, - required: ['group_id', 'folder_name'] -} as const satisfies JSONSchema; - -type Payload = FromSchema; - -export class SetGroupFileFolder extends BaseAction { - actionName = ActionName.SetGroupFileFolder; - PayloadSchema = SchemaData; - protected async _handle(payload: Payload) { - return (await NTQQGroupApi.CreatGroupFileFolder(payload.group_id.toString(), payload.folder_name)).resultWithGroupItem; - } -} +import { FromSchema, JSONSchema } from 'json-schema-to-ts'; +import BaseAction from '../BaseAction'; +import { ActionName } from '../types'; +import { NTQQGroupApi, NTQQMsgApi, NTQQUserApi } from '@/core/apis'; + +const SchemaData = { + type: 'object', + properties: { + group_id: { type: ['string', 'number'] }, + folder_name: { type: 'string' }, + }, + required: ['group_id', 'folder_name'] +} as const satisfies JSONSchema; + +type Payload = FromSchema; + +export class SetGroupFileFolder extends BaseAction { + actionName = ActionName.SetGroupFileFolder; + PayloadSchema = SchemaData; + protected async _handle(payload: Payload) { + return (await NTQQGroupApi.CreatGroupFileFolder(payload.group_id.toString(), payload.folder_name)).resultWithGroupItem; + } +} diff --git a/src/onebot11/action/go-cqhttp/GetStrangerInfo.ts b/src/onebot11/action/go-cqhttp/GetStrangerInfo.ts index 55df6b97..9174fd14 100644 --- a/src/onebot11/action/go-cqhttp/GetStrangerInfo.ts +++ b/src/onebot11/action/go-cqhttp/GetStrangerInfo.ts @@ -1,32 +1,32 @@ -import BaseAction from '../BaseAction'; -import { OB11User } from '../../types'; -import { getUidByUin, uid2UinMap } from '@/core/data'; -import { OB11Constructor } from '../../constructor'; -import { ActionName } from '../types'; -import { NTQQUserApi } from '@/core/apis/user'; -import { log, logDebug } from '@/common/utils/log'; -import { FromSchema, JSONSchema } from 'json-schema-to-ts'; - -const SchemaData = { - type: 'object', - properties: { - user_id: { type: [ 'number' , 'string' ] }, - }, - required: ['user_id'] -} as const satisfies JSONSchema; - -type Payload = FromSchema; - -export default class GoCQHTTPGetStrangerInfo extends BaseAction { - actionName = ActionName.GoCQHTTP_GetStrangerInfo; - - protected async _handle(payload: Payload): Promise { - const user_id = payload.user_id.toString(); - //logDebug('uidMaps', uidMaps); - const uid = getUidByUin(user_id); - if (!uid) { - throw new Error('查无此人'); - } - return OB11Constructor.stranger(await NTQQUserApi.getUserDetailInfo(uid)); - } -} +import BaseAction from '../BaseAction'; +import { OB11User } from '../../types'; +import { getUidByUin, uid2UinMap } from '@/core/data'; +import { OB11Constructor } from '../../constructor'; +import { ActionName } from '../types'; +import { NTQQUserApi } from '@/core/apis/user'; +import { log, logDebug } from '@/common/utils/log'; +import { FromSchema, JSONSchema } from 'json-schema-to-ts'; + +const SchemaData = { + type: 'object', + properties: { + user_id: { type: [ 'number' , 'string' ] }, + }, + required: ['user_id'] +} as const satisfies JSONSchema; + +type Payload = FromSchema; + +export default class GoCQHTTPGetStrangerInfo extends BaseAction { + actionName = ActionName.GoCQHTTP_GetStrangerInfo; + + protected async _handle(payload: Payload): Promise { + const user_id = payload.user_id.toString(); + //logDebug('uidMaps', uidMaps); + const uid = getUidByUin(user_id); + if (!uid) { + throw new Error('查无此人'); + } + return OB11Constructor.stranger(await NTQQUserApi.getUserDetailInfo(uid)); + } +} diff --git a/src/onebot11/action/group/GetGroupMemberInfo.ts b/src/onebot11/action/group/GetGroupMemberInfo.ts index 958272e6..fb0ddb54 100644 --- a/src/onebot11/action/group/GetGroupMemberInfo.ts +++ b/src/onebot11/action/group/GetGroupMemberInfo.ts @@ -1,65 +1,65 @@ -import { OB11GroupMember } from '../../types'; -import { getGroup, getGroupMember, groupMembers } from '@/core/data'; -import { OB11Constructor } from '../../constructor'; -import BaseAction from '../BaseAction'; -import { ActionName } from '../types'; -import { NTQQUserApi } from '@/core/apis/user'; -import { log, logDebug } from '@/common/utils/log'; -import { isNull } from '../../../common/utils/helper'; -import { WebApi } from '@/core/apis/webapi'; -import { NTQQGroupApi } from '@/core'; -import { FromSchema, JSONSchema } from 'json-schema-to-ts'; - -// no_cache get时传字符串 -const SchemaData = { - type: 'object', - properties: { - group_id: { type: ['number', 'string'] }, - user_id: { type: ['number', 'string'] }, - no_cache: { type: ['boolean', 'string'] }, - }, - required: ['group_id', 'user_id'] -} as const satisfies JSONSchema; - -type Payload = FromSchema; - -class GetGroupMemberInfo extends BaseAction { - actionName = ActionName.GetGroupMemberInfo; - PayloadSchema = SchemaData; - protected async _handle(payload: Payload) { - const group = await getGroup(payload.group_id.toString()); - if (!group) { - throw (`群(${payload.group_id})不存在`); - } - const webGroupMembers = await WebApi.getGroupMembers(payload.group_id.toString()); - if (payload.no_cache == true || payload.no_cache === 'true') { - groupMembers.set(group.groupCode, await NTQQGroupApi.getGroupMembers(payload.group_id.toString())); - } - const member = await getGroupMember(payload.group_id.toString(), payload.user_id.toString()); - // log(member); - if (member) { - logDebug('获取群成员详细信息'); - try { - const info = (await NTQQUserApi.getUserDetailInfo(member.uid)); - logDebug('群成员详细信息结果', info); - Object.assign(member, info); - } catch (e) { - logDebug('获取群成员详细信息失败, 只能返回基础信息', e); - } - const retMember = OB11Constructor.groupMember(payload.group_id.toString(), member); - for (let i = 0, len = webGroupMembers.length; i < len; i++) { - if (webGroupMembers[i]?.uin && webGroupMembers[i].uin === retMember.user_id) { - retMember.join_time = webGroupMembers[i]?.join_time; - retMember.last_sent_time = webGroupMembers[i]?.last_speak_time; - retMember.qage = webGroupMembers[i]?.qage; - retMember.level = webGroupMembers[i]?.lv.level.toString(); - } - } - return retMember; - } else { - throw (`群(${payload.group_id})成员${payload.user_id}不存在`); - } - } -} - -export default GetGroupMemberInfo; +import { OB11GroupMember } from '../../types'; +import { getGroup, getGroupMember, groupMembers } from '@/core/data'; +import { OB11Constructor } from '../../constructor'; +import BaseAction from '../BaseAction'; +import { ActionName } from '../types'; +import { NTQQUserApi } from '@/core/apis/user'; +import { log, logDebug } from '@/common/utils/log'; +import { isNull } from '../../../common/utils/helper'; +import { WebApi } from '@/core/apis/webapi'; +import { NTQQGroupApi } from '@/core'; +import { FromSchema, JSONSchema } from 'json-schema-to-ts'; + +// no_cache get时传字符串 +const SchemaData = { + type: 'object', + properties: { + group_id: { type: ['number', 'string'] }, + user_id: { type: ['number', 'string'] }, + no_cache: { type: ['boolean', 'string'] }, + }, + required: ['group_id', 'user_id'] +} as const satisfies JSONSchema; + +type Payload = FromSchema; + +class GetGroupMemberInfo extends BaseAction { + actionName = ActionName.GetGroupMemberInfo; + PayloadSchema = SchemaData; + protected async _handle(payload: Payload) { + const group = await getGroup(payload.group_id.toString()); + if (!group) { + throw (`群(${payload.group_id})不存在`); + } + const webGroupMembers = await WebApi.getGroupMembers(payload.group_id.toString()); + if (payload.no_cache == true || payload.no_cache === 'true') { + groupMembers.set(group.groupCode, await NTQQGroupApi.getGroupMembers(payload.group_id.toString())); + } + const member = await getGroupMember(payload.group_id.toString(), payload.user_id.toString()); + // log(member); + if (member) { + logDebug('获取群成员详细信息'); + try { + const info = (await NTQQUserApi.getUserDetailInfo(member.uid)); + logDebug('群成员详细信息结果', info); + Object.assign(member, info); + } catch (e) { + logDebug('获取群成员详细信息失败, 只能返回基础信息', e); + } + const retMember = OB11Constructor.groupMember(payload.group_id.toString(), member); + for (let i = 0, len = webGroupMembers.length; i < len; i++) { + if (webGroupMembers[i]?.uin && webGroupMembers[i].uin === retMember.user_id) { + retMember.join_time = webGroupMembers[i]?.join_time; + retMember.last_sent_time = webGroupMembers[i]?.last_speak_time; + retMember.qage = webGroupMembers[i]?.qage; + retMember.level = webGroupMembers[i]?.lv.level.toString(); + } + } + return retMember; + } else { + throw (`群(${payload.group_id})成员${payload.user_id}不存在`); + } + } +} + +export default GetGroupMemberInfo; diff --git a/src/onebot11/action/group/GetGroupMemberList.ts b/src/onebot11/action/group/GetGroupMemberList.ts index dbf39932..d33bda65 100644 --- a/src/onebot11/action/group/GetGroupMemberList.ts +++ b/src/onebot11/action/group/GetGroupMemberList.ts @@ -66,7 +66,7 @@ class GetGroupMemberList extends BaseAction { } else if (ob11Config.GroupLocalTime.Record && ob11Config.GroupLocalTime.RecordList[0] === '-1' || ob11Config.GroupLocalTime.RecordList.includes(payload.group_id.toString())) { const _sendAndJoinRember = await dbUtil.getLastSentTimeAndJoinTime(TypeConvert.toNumber(payload.group_id)); _sendAndJoinRember.forEach((element) => { - let MemberData = MemberMap.get(element.user_id); + const MemberData = MemberMap.get(element.user_id); if (MemberData) { MemberData.join_time = element.join_time; MemberData.last_sent_time = element.last_sent_time; diff --git a/src/onebot11/action/index.ts b/src/onebot11/action/index.ts index 3cbae143..a3d78b8c 100644 --- a/src/onebot11/action/index.ts +++ b/src/onebot11/action/index.ts @@ -1,155 +1,155 @@ -import GetMsg from './msg/GetMsg'; -import GetLoginInfo from './system/GetLoginInfo'; -import GetFriendList from './user/GetFriendList'; -import GetGroupList from './group/GetGroupList'; -import GetGroupInfo from './group/GetGroupInfo'; -import GetGroupMemberList from './group/GetGroupMemberList'; -import GetGroupMemberInfo from './group/GetGroupMemberInfo'; -import SendGroupMsg from './group/SendGroupMsg'; -import SendPrivateMsg from './msg/SendPrivateMsg'; -import SendMsg from './msg/SendMsg'; -import DeleteMsg from './msg/DeleteMsg'; -import BaseAction from './BaseAction'; -import GetVersionInfo from './system/GetVersionInfo'; -import CanSendRecord from './system/CanSendRecord'; -import CanSendImage from './system/CanSendImage'; -import GetStatus from './system/GetStatus'; -import { - GoCQHTTPSendForwardMsg, - GoCQHTTPSendGroupForwardMsg, - GoCQHTTPSendPrivateForwardMsg -} from './go-cqhttp/SendForwardMsg'; -import GoCQHTTPGetStrangerInfo from './go-cqhttp/GetStrangerInfo'; -import SendLike from './user/SendLike'; -import SetGroupAddRequest from './group/SetGroupAddRequest'; -import SetGroupLeave from './group/SetGroupLeave'; -import GetGuildList from './group/GetGuildList'; -import Debug from '@/onebot11/action/extends/Debug'; -import SetFriendAddRequest from './user/SetFriendAddRequest'; -import SetGroupWholeBan from './group/SetGroupWholeBan'; -import SetGroupName from './group/SetGroupName'; -import SetGroupBan from './group/SetGroupBan'; -import SetGroupKick from './group/SetGroupKick'; -import SetGroupAdmin from './group/SetGroupAdmin'; -import SetGroupCard from './group/SetGroupCard'; -import GetImage from './file/GetImage'; -import GetRecord from './file/GetRecord'; -import { GoCQHTTPMarkMsgAsRead, MarkGroupMsgAsRead, MarkPrivateMsgAsRead } from './msg/MarkMsgAsRead'; -import CleanCache from './system/CleanCache'; -import GoCQHTTPUploadGroupFile from './go-cqhttp/UploadGroupFile'; -import { GetConfigAction, SetConfigAction } from '@/onebot11/action/extends/Config'; -import GetGroupAddRequest from '@/onebot11/action/extends/GetGroupAddRequest'; -import SetQQAvatar from '@/onebot11/action/extends/SetQQAvatar'; -import GoCQHTTPDownloadFile from './go-cqhttp/DownloadFile'; -import GoCQHTTPGetGroupMsgHistory from './go-cqhttp/GetGroupMsgHistory'; -import GetFile from './file/GetFile'; -import { GoCQHTTPGetForwardMsgAction } from './go-cqhttp/GetForwardMsg'; -import GetFriendMsgHistory from './go-cqhttp/GetFriendMsgHistory'; -import { GetCookies } from './user/GetCookies'; -import { SetMsgEmojiLike } from '@/onebot11/action/msg/SetMsgEmojiLike'; -import { GetRobotUinRange } from './extends/GetRobotUinRange'; -import { SetOnlineStatus } from './extends/SetOnlineStatus'; -import { GetGroupNotice } from './group/GetGroupNotice'; -import { GetGroupEssence } from './group/GetGroupEssence'; -import { ForwardFriendSingleMsg, ForwardGroupSingleMsg } from '@/onebot11/action/msg/ForwardSingleMsg'; -import { GetFriendWithCategory } from './extends/GetFriendWithCategory'; -import { SendGroupNotice } from './go-cqhttp/SendGroupNotice'; -import { Reboot, RebootNormol } from './system/Reboot'; -import { GetGroupHonorInfo } from './go-cqhttp/GetGroupHonorInfo'; -import { GoCQHTTPHandleQuickAction } from './go-cqhttp/QuickAction'; -import { GetGroupSystemMsg } from './group/GetGroupSystemMsg'; -import { GetOnlineClient } from './go-cqhttp/GetOnlineClient'; -import { IOCRImage, OCRImage } from './extends/OCRImage'; -import { GetGroupFileCount } from './file/GetGroupFileCount'; -import { GetGroupFileList } from './file/GetGroupFileList'; -import { TranslateEnWordToZn } from './extends/TranslateEnWordToZn'; -import { SetGroupFileFolder } from './file/SetGroupFileFolder'; -import { DelGroupFile } from './file/DelGroupFile'; -import { DelGroupFileFolder } from './file/DelGroupFileFolder'; - -export const actionHandlers = [ - new RebootNormol(), - new GetFile(), - new Debug(), - new Reboot(), - // new GetConfigAction(), - // new SetConfigAction(), - // new GetGroupAddRequest(), - // TranslateEnWordToZn = "translate_en2zh", - new ForwardFriendSingleMsg(), - new ForwardGroupSingleMsg(), - new MarkGroupMsgAsRead(), - new MarkPrivateMsgAsRead(), - new SetQQAvatar(), - new TranslateEnWordToZn(), - new GetGroupFileCount(), - new GetGroupFileList(), - new SetGroupFileFolder(), - new DelGroupFile(), - new DelGroupFileFolder(), - // onebot11 - new SendLike(), - new GetMsg(), - new GetLoginInfo(), - new GetFriendList(), - new GetGroupList(), new GetGroupInfo(), - new GetGroupMemberList(), new GetGroupMemberInfo(), - new SendGroupMsg(), new SendPrivateMsg(), new SendMsg(), - new DeleteMsg(), - new SetGroupAddRequest(), - new SetFriendAddRequest(), - new SetGroupLeave(), - new GetVersionInfo(), - new CanSendRecord(), - new CanSendImage(), - new GetStatus(), - new SetGroupWholeBan(), - new SetGroupBan(), - new SetGroupKick(), - new SetGroupAdmin(), - new SetGroupName(), - new SetGroupCard(), - new GetImage(), - new GetRecord(), - new SetMsgEmojiLike(), - // new CleanCache(), - new GetCookies(), - // - new SetOnlineStatus(), - new GetRobotUinRange(), - new GetFriendWithCategory(), - //以下为go-cqhttp api - new GetOnlineClient(), - new OCRImage(), - new IOCRImage(), - new GetGroupHonorInfo(), - new SendGroupNotice(), - new GetGroupNotice(), - new GetGroupEssence(), - new GoCQHTTPSendForwardMsg(), - new GoCQHTTPSendGroupForwardMsg(), - new GoCQHTTPSendPrivateForwardMsg(), - new GoCQHTTPGetStrangerInfo(), - new GoCQHTTPDownloadFile(), - new GetGuildList(), - new GoCQHTTPMarkMsgAsRead(), - new GoCQHTTPUploadGroupFile(), - new GoCQHTTPGetGroupMsgHistory(), - new GoCQHTTPGetForwardMsgAction(), - new GetFriendMsgHistory(), - new GoCQHTTPHandleQuickAction(), - new GetGroupSystemMsg() -]; - -function initActionMap() { - const actionMap = new Map>(); - for (const action of actionHandlers) { - actionMap.set(action.actionName, action); - actionMap.set(action.actionName + '_async', action); - actionMap.set(action.actionName + '_rate_limited', action); - } - - return actionMap; -} - -export const actionMap = initActionMap(); +import GetMsg from './msg/GetMsg'; +import GetLoginInfo from './system/GetLoginInfo'; +import GetFriendList from './user/GetFriendList'; +import GetGroupList from './group/GetGroupList'; +import GetGroupInfo from './group/GetGroupInfo'; +import GetGroupMemberList from './group/GetGroupMemberList'; +import GetGroupMemberInfo from './group/GetGroupMemberInfo'; +import SendGroupMsg from './group/SendGroupMsg'; +import SendPrivateMsg from './msg/SendPrivateMsg'; +import SendMsg from './msg/SendMsg'; +import DeleteMsg from './msg/DeleteMsg'; +import BaseAction from './BaseAction'; +import GetVersionInfo from './system/GetVersionInfo'; +import CanSendRecord from './system/CanSendRecord'; +import CanSendImage from './system/CanSendImage'; +import GetStatus from './system/GetStatus'; +import { + GoCQHTTPSendForwardMsg, + GoCQHTTPSendGroupForwardMsg, + GoCQHTTPSendPrivateForwardMsg +} from './go-cqhttp/SendForwardMsg'; +import GoCQHTTPGetStrangerInfo from './go-cqhttp/GetStrangerInfo'; +import SendLike from './user/SendLike'; +import SetGroupAddRequest from './group/SetGroupAddRequest'; +import SetGroupLeave from './group/SetGroupLeave'; +import GetGuildList from './group/GetGuildList'; +import Debug from '@/onebot11/action/extends/Debug'; +import SetFriendAddRequest from './user/SetFriendAddRequest'; +import SetGroupWholeBan from './group/SetGroupWholeBan'; +import SetGroupName from './group/SetGroupName'; +import SetGroupBan from './group/SetGroupBan'; +import SetGroupKick from './group/SetGroupKick'; +import SetGroupAdmin from './group/SetGroupAdmin'; +import SetGroupCard from './group/SetGroupCard'; +import GetImage from './file/GetImage'; +import GetRecord from './file/GetRecord'; +import { GoCQHTTPMarkMsgAsRead, MarkGroupMsgAsRead, MarkPrivateMsgAsRead } from './msg/MarkMsgAsRead'; +import CleanCache from './system/CleanCache'; +import GoCQHTTPUploadGroupFile from './go-cqhttp/UploadGroupFile'; +import { GetConfigAction, SetConfigAction } from '@/onebot11/action/extends/Config'; +import GetGroupAddRequest from '@/onebot11/action/extends/GetGroupAddRequest'; +import SetQQAvatar from '@/onebot11/action/extends/SetQQAvatar'; +import GoCQHTTPDownloadFile from './go-cqhttp/DownloadFile'; +import GoCQHTTPGetGroupMsgHistory from './go-cqhttp/GetGroupMsgHistory'; +import GetFile from './file/GetFile'; +import { GoCQHTTPGetForwardMsgAction } from './go-cqhttp/GetForwardMsg'; +import GetFriendMsgHistory from './go-cqhttp/GetFriendMsgHistory'; +import { GetCookies } from './user/GetCookies'; +import { SetMsgEmojiLike } from '@/onebot11/action/msg/SetMsgEmojiLike'; +import { GetRobotUinRange } from './extends/GetRobotUinRange'; +import { SetOnlineStatus } from './extends/SetOnlineStatus'; +import { GetGroupNotice } from './group/GetGroupNotice'; +import { GetGroupEssence } from './group/GetGroupEssence'; +import { ForwardFriendSingleMsg, ForwardGroupSingleMsg } from '@/onebot11/action/msg/ForwardSingleMsg'; +import { GetFriendWithCategory } from './extends/GetFriendWithCategory'; +import { SendGroupNotice } from './go-cqhttp/SendGroupNotice'; +import { Reboot, RebootNormol } from './system/Reboot'; +import { GetGroupHonorInfo } from './go-cqhttp/GetGroupHonorInfo'; +import { GoCQHTTPHandleQuickAction } from './go-cqhttp/QuickAction'; +import { GetGroupSystemMsg } from './group/GetGroupSystemMsg'; +import { GetOnlineClient } from './go-cqhttp/GetOnlineClient'; +import { IOCRImage, OCRImage } from './extends/OCRImage'; +import { GetGroupFileCount } from './file/GetGroupFileCount'; +import { GetGroupFileList } from './file/GetGroupFileList'; +import { TranslateEnWordToZn } from './extends/TranslateEnWordToZn'; +import { SetGroupFileFolder } from './file/SetGroupFileFolder'; +import { DelGroupFile } from './file/DelGroupFile'; +import { DelGroupFileFolder } from './file/DelGroupFileFolder'; + +export const actionHandlers = [ + new RebootNormol(), + new GetFile(), + new Debug(), + new Reboot(), + // new GetConfigAction(), + // new SetConfigAction(), + // new GetGroupAddRequest(), + // TranslateEnWordToZn = "translate_en2zh", + new ForwardFriendSingleMsg(), + new ForwardGroupSingleMsg(), + new MarkGroupMsgAsRead(), + new MarkPrivateMsgAsRead(), + new SetQQAvatar(), + new TranslateEnWordToZn(), + new GetGroupFileCount(), + new GetGroupFileList(), + new SetGroupFileFolder(), + new DelGroupFile(), + new DelGroupFileFolder(), + // onebot11 + new SendLike(), + new GetMsg(), + new GetLoginInfo(), + new GetFriendList(), + new GetGroupList(), new GetGroupInfo(), + new GetGroupMemberList(), new GetGroupMemberInfo(), + new SendGroupMsg(), new SendPrivateMsg(), new SendMsg(), + new DeleteMsg(), + new SetGroupAddRequest(), + new SetFriendAddRequest(), + new SetGroupLeave(), + new GetVersionInfo(), + new CanSendRecord(), + new CanSendImage(), + new GetStatus(), + new SetGroupWholeBan(), + new SetGroupBan(), + new SetGroupKick(), + new SetGroupAdmin(), + new SetGroupName(), + new SetGroupCard(), + new GetImage(), + new GetRecord(), + new SetMsgEmojiLike(), + // new CleanCache(), + new GetCookies(), + // + new SetOnlineStatus(), + new GetRobotUinRange(), + new GetFriendWithCategory(), + //以下为go-cqhttp api + new GetOnlineClient(), + new OCRImage(), + new IOCRImage(), + new GetGroupHonorInfo(), + new SendGroupNotice(), + new GetGroupNotice(), + new GetGroupEssence(), + new GoCQHTTPSendForwardMsg(), + new GoCQHTTPSendGroupForwardMsg(), + new GoCQHTTPSendPrivateForwardMsg(), + new GoCQHTTPGetStrangerInfo(), + new GoCQHTTPDownloadFile(), + new GetGuildList(), + new GoCQHTTPMarkMsgAsRead(), + new GoCQHTTPUploadGroupFile(), + new GoCQHTTPGetGroupMsgHistory(), + new GoCQHTTPGetForwardMsgAction(), + new GetFriendMsgHistory(), + new GoCQHTTPHandleQuickAction(), + new GetGroupSystemMsg() +]; + +function initActionMap() { + const actionMap = new Map>(); + for (const action of actionHandlers) { + actionMap.set(action.actionName, action); + actionMap.set(action.actionName + '_async', action); + actionMap.set(action.actionName + '_rate_limited', action); + } + + return actionMap; +} + +export const actionMap = initActionMap(); diff --git a/src/onebot11/action/msg/ForwardSingleMsg.ts b/src/onebot11/action/msg/ForwardSingleMsg.ts index 693c3e2f..a00a03d5 100644 --- a/src/onebot11/action/msg/ForwardSingleMsg.ts +++ b/src/onebot11/action/msg/ForwardSingleMsg.ts @@ -1,62 +1,62 @@ -import BaseAction from '../BaseAction'; -import { NTQQMsgApi } from '@/core/apis'; -import { ChatType, Peer } from '@/core/entities'; -import { dbUtil } from '@/common/utils/db'; -import { getUidByUin } from '@/core/data'; -import { ActionName } from '../types'; -import { FromSchema, JSONSchema } from 'json-schema-to-ts'; - -const SchemaData = { - type: 'object', - properties: { - message_id: { type: 'number' }, - group_id: { type: [ 'number' , 'string' ] }, - user_id: { type: [ 'number' , 'string' ] } - }, - required: ['message_id'] -} as const satisfies JSONSchema; - -type Payload = FromSchema; - -class ForwardSingleMsg extends BaseAction { - protected async getTargetPeer(payload: Payload): Promise { - if (payload.user_id) { - const peerUid = getUidByUin(payload.user_id.toString()); - if (!peerUid) { - throw new Error(`无法找到私聊对象${payload.user_id}`); - } - return { chatType: ChatType.friend, peerUid }; - } - return { chatType: ChatType.group, peerUid: payload.group_id!.toString() }; - } - - protected async _handle(payload: Payload): Promise { - const msg = await dbUtil.getMsgByShortId(payload.message_id); - if (!msg) { - throw new Error(`无法找到消息${payload.message_id}`); - } - const peer = await this.getTargetPeer(payload); - const ret = await NTQQMsgApi.forwardMsg( - { - chatType: msg.chatType, - peerUid: msg.peerUid, - }, - peer, - [msg.msgId], - ); - if (ret.result !== 0) { - throw new Error(`转发消息失败 ${ret.errMsg}`); - } - return null; - } -} - -export class ForwardFriendSingleMsg extends ForwardSingleMsg { - PayloadSchema = SchemaData; - actionName = ActionName.ForwardFriendSingleMsg; -} - -export class ForwardGroupSingleMsg extends ForwardSingleMsg { - PayloadSchema = SchemaData; - actionName = ActionName.ForwardGroupSingleMsg; -} +import BaseAction from '../BaseAction'; +import { NTQQMsgApi } from '@/core/apis'; +import { ChatType, Peer } from '@/core/entities'; +import { dbUtil } from '@/common/utils/db'; +import { getUidByUin } from '@/core/data'; +import { ActionName } from '../types'; +import { FromSchema, JSONSchema } from 'json-schema-to-ts'; + +const SchemaData = { + type: 'object', + properties: { + message_id: { type: 'number' }, + group_id: { type: [ 'number' , 'string' ] }, + user_id: { type: [ 'number' , 'string' ] } + }, + required: ['message_id'] +} as const satisfies JSONSchema; + +type Payload = FromSchema; + +class ForwardSingleMsg extends BaseAction { + protected async getTargetPeer(payload: Payload): Promise { + if (payload.user_id) { + const peerUid = getUidByUin(payload.user_id.toString()); + if (!peerUid) { + throw new Error(`无法找到私聊对象${payload.user_id}`); + } + return { chatType: ChatType.friend, peerUid }; + } + return { chatType: ChatType.group, peerUid: payload.group_id!.toString() }; + } + + protected async _handle(payload: Payload): Promise { + const msg = await dbUtil.getMsgByShortId(payload.message_id); + if (!msg) { + throw new Error(`无法找到消息${payload.message_id}`); + } + const peer = await this.getTargetPeer(payload); + const ret = await NTQQMsgApi.forwardMsg( + { + chatType: msg.chatType, + peerUid: msg.peerUid, + }, + peer, + [msg.msgId], + ); + if (ret.result !== 0) { + throw new Error(`转发消息失败 ${ret.errMsg}`); + } + return null; + } +} + +export class ForwardFriendSingleMsg extends ForwardSingleMsg { + PayloadSchema = SchemaData; + actionName = ActionName.ForwardFriendSingleMsg; +} + +export class ForwardGroupSingleMsg extends ForwardSingleMsg { + PayloadSchema = SchemaData; + actionName = ActionName.ForwardGroupSingleMsg; +} diff --git a/src/onebot11/action/msg/SendMsg/create-send-elements.ts b/src/onebot11/action/msg/SendMsg/create-send-elements.ts index 46c4ffa7..4aae5273 100644 --- a/src/onebot11/action/msg/SendMsg/create-send-elements.ts +++ b/src/onebot11/action/msg/SendMsg/create-send-elements.ts @@ -1,252 +1,252 @@ -import { OB11MessageData, OB11MessageDataType, OB11MessageFileBase } from '@/onebot11/types'; -import { - AtType, - CustomMusicSignPostData, - Group, - IdMusicSignPostData, - NTQQFileApi, - SendArkElement, - SendMessageElement, - SendMsgElementConstructor -} from '@/core'; -import { getGroupMember } from '@/core/data'; -import { dbUtil } from '@/common/utils/db'; -import { logDebug, logError } from '@/common/utils/log'; -import { uri2local } from '@/common/utils/file'; -import { ob11Config } from '@/onebot11/config'; -import { RequestUtil } from '@/common/utils/request'; -import fs from 'node:fs'; - -export type MessageContext = { - group?: Group, - deleteAfterSentFiles: string[], -} - -async function handleOb11FileLikeMessage( - { data: { file, name: payloadFileName } }: OB11MessageFileBase, - { deleteAfterSentFiles }: MessageContext -) { - let uri = file; - - const cache = await dbUtil.getFileCacheByName(file); - if (cache) { - if (fs.existsSync(cache.path)) { - uri = 'file://' + cache.path; - } else if (cache.url) { - uri = cache.url; - } else { - const fileMsg = await dbUtil.getMsgByLongId(cache.msgId); - if (fileMsg) { - cache.path = await NTQQFileApi.downloadMedia( - fileMsg.msgId, fileMsg.chatType, fileMsg.peerUid, - cache.elementId, '', '' - ); - uri = 'file://' + cache.path; - dbUtil.updateFileCache(cache); - } - } - logDebug('找到文件缓存', uri); - } - - const { path, isLocal, fileName, errMsg } = (await uri2local(uri)); - - if (errMsg) { - logError('文件下载失败', errMsg); - throw Error('文件下载失败' + errMsg); - } - - if (!isLocal) { // 只删除http和base64转过来的文件 - deleteAfterSentFiles.push(path); - } - - return { path, fileName: payloadFileName || fileName }; -} - -const _handlers: { - [Key in OB11MessageDataType]: ( - sendMsg: Extract, - // This picks the correct message type out - // How great the type system of TypeScript is! - context: MessageContext - ) => SendMessageElement | undefined | Promise -} = { - [OB11MessageDataType.text]: ({ data: { text } }) => SendMsgElementConstructor.text(text), - - [OB11MessageDataType.at]: async ({ data: { qq: atQQ } }, context) => { - if (!context.group) return undefined; - - if (atQQ === 'all') return SendMsgElementConstructor.at(atQQ, atQQ, AtType.atAll, '全体成员'); - - // then the qq is a group member - const atMember = await getGroupMember(context.group.groupCode, atQQ); - return atMember ? - SendMsgElementConstructor.at(atQQ, atMember.uid, AtType.atUser, atMember.cardName || atMember.nick) : - undefined; - }, - - [OB11MessageDataType.reply]: async ({ data: { id } }) => { - const replyMsg = await dbUtil.getMsgByShortId(parseInt(id)); - return replyMsg ? - SendMsgElementConstructor.reply(replyMsg.msgSeq, replyMsg.msgId, replyMsg.senderUin!, replyMsg.senderUin!) : - undefined; - }, - - [OB11MessageDataType.face]: ({ data: { id } }) => SendMsgElementConstructor.face(parseInt(id)), - - [OB11MessageDataType.mface]: ({ - data: { - emoji_package_id, - emoji_id, - key, - summary - } - }) => SendMsgElementConstructor.mface(emoji_package_id, emoji_id, key, summary), - - // File service - - [OB11MessageDataType.image]: async (sendMsg, context) => { - const PicEle = await SendMsgElementConstructor.pic( - (await handleOb11FileLikeMessage(sendMsg, context)).path, - sendMsg.data.summary || '', - sendMsg.data.subType || 0 - ); - context.deleteAfterSentFiles.push(PicEle.picElement.sourcePath); - return PicEle; - } - , // currently not supported - - [OB11MessageDataType.file]: async (sendMsg, context) => { - const { path, fileName } = await handleOb11FileLikeMessage(sendMsg, context); - //logDebug('发送文件', path, fileName); - const FileEle = await SendMsgElementConstructor.file(path, fileName); - // 清除Upload的应该 - // context.deleteAfterSentFiles.push(fileName || FileEle.fileElement.filePath); - return FileEle; - }, - - [OB11MessageDataType.video]: async (sendMsg, context) => { - const { path, fileName } = await handleOb11FileLikeMessage(sendMsg, context); - - //logDebug('发送视频', path, fileName); - let thumb = sendMsg.data.thumb; - if (thumb) { - const uri2LocalRes = await uri2local(thumb); - if (uri2LocalRes.success) thumb = uri2LocalRes.path; - } - - return SendMsgElementConstructor.video(path, fileName, thumb); - }, - [OB11MessageDataType.miniapp]: async ({ data: any }) => SendMsgElementConstructor.miniapp(), - - [OB11MessageDataType.voice]: async (sendMsg, context) => - SendMsgElementConstructor.ptt((await handleOb11FileLikeMessage(sendMsg, context)).path), - - [OB11MessageDataType.json]: ({ data: { data } }) => SendMsgElementConstructor.ark(data), - - [OB11MessageDataType.dice]: ({ data: { result } }) => SendMsgElementConstructor.dice(result), - - [OB11MessageDataType.RPS]: ({ data: { result } }) => SendMsgElementConstructor.rps(result), - - [OB11MessageDataType.markdown]: ({ data: { content } }) => SendMsgElementConstructor.markdown(content), - - [OB11MessageDataType.music]: async ({ data }) => { - // 保留, 直到...找到更好的解决方案 - if (data.type === 'custom') { - if (!data.url) { - logError('自定义音卡缺少参数url'); - return undefined; - } - if (!data.audio) { - logError('自定义音卡缺少参数audio'); - return undefined; - } - if (!data.title) { - logError('自定义音卡缺少参数title'); - return undefined; - } - } else { - if (!['qq', '163'].includes(data.type)) { - logError('音乐卡片type错误, 只支持qq、163、custom,当前type:', data.type); - return undefined; - } - if (!data.id) { - logError('音乐卡片缺少参数id'); - return undefined; - } - } - - let postData: IdMusicSignPostData | CustomMusicSignPostData; - if (data.type === 'custom' && data.content) { - const { content, ...others } = data; - postData = { singer: content, ...others }; - } else { - postData = data; - } - - const signUrl = ob11Config.musicSignUrl; - if (!signUrl) { - throw Error('音乐消息签名地址未配置'); - } - try { - const musicJson = await RequestUtil.HttpGetJson(signUrl, 'POST', postData); - return SendMsgElementConstructor.ark(musicJson); - } catch (e) { - logError('生成音乐消息失败', e); - } - }, - - [OB11MessageDataType.node]: () => undefined, - - [OB11MessageDataType.forward]: () => undefined, - - [OB11MessageDataType.xml]: () => undefined, - - [OB11MessageDataType.poke]: () => undefined, -}; - -const handlers = <{ - [Key in OB11MessageDataType]: ( - sendMsg: OB11MessageData, - context: MessageContext - ) => SendMessageElement | undefined | Promise -}>_handlers; - -export default async function createSendElements( - messageData: OB11MessageData[], - group?: Group, - ignoreTypes: OB11MessageDataType[] = [] -) { - const sendElements: SendMessageElement[] = []; - const deleteAfterSentFiles: string[] = []; - for (const sendMsg of messageData) { - if (ignoreTypes.includes(sendMsg.type)) { - continue; - } - const callResult = await handlers[sendMsg.type]( - sendMsg, - { group, deleteAfterSentFiles } - ); - if (callResult) sendElements.push(callResult); - } - return { sendElements, deleteAfterSentFiles }; -} - -export async function createSendElementsParallel( - messageData: OB11MessageData[], - group?: Group, - ignoreTypes: OB11MessageDataType[] = [] -) { - const deleteAfterSentFiles: string[] = []; - const sendElements = ( - await Promise.all( - messageData.map(async sendMsg => ignoreTypes.includes(sendMsg.type) ? - undefined : - handlers[sendMsg.type](sendMsg, { group, deleteAfterSentFiles })) - ).then( - results => results.filter( - element => element !== undefined - ) - ) - ); - return { sendElements, deleteAfterSentFiles }; -} +import { OB11MessageData, OB11MessageDataType, OB11MessageFileBase } from '@/onebot11/types'; +import { + AtType, + CustomMusicSignPostData, + Group, + IdMusicSignPostData, + NTQQFileApi, + SendArkElement, + SendMessageElement, + SendMsgElementConstructor +} from '@/core'; +import { getGroupMember } from '@/core/data'; +import { dbUtil } from '@/common/utils/db'; +import { logDebug, logError } from '@/common/utils/log'; +import { uri2local } from '@/common/utils/file'; +import { ob11Config } from '@/onebot11/config'; +import { RequestUtil } from '@/common/utils/request'; +import fs from 'node:fs'; + +export type MessageContext = { + group?: Group, + deleteAfterSentFiles: string[], +} + +async function handleOb11FileLikeMessage( + { data: { file, name: payloadFileName } }: OB11MessageFileBase, + { deleteAfterSentFiles }: MessageContext +) { + let uri = file; + + const cache = await dbUtil.getFileCacheByName(file); + if (cache) { + if (fs.existsSync(cache.path)) { + uri = 'file://' + cache.path; + } else if (cache.url) { + uri = cache.url; + } else { + const fileMsg = await dbUtil.getMsgByLongId(cache.msgId); + if (fileMsg) { + cache.path = await NTQQFileApi.downloadMedia( + fileMsg.msgId, fileMsg.chatType, fileMsg.peerUid, + cache.elementId, '', '' + ); + uri = 'file://' + cache.path; + dbUtil.updateFileCache(cache); + } + } + logDebug('找到文件缓存', uri); + } + + const { path, isLocal, fileName, errMsg } = (await uri2local(uri)); + + if (errMsg) { + logError('文件下载失败', errMsg); + throw Error('文件下载失败' + errMsg); + } + + if (!isLocal) { // 只删除http和base64转过来的文件 + deleteAfterSentFiles.push(path); + } + + return { path, fileName: payloadFileName || fileName }; +} + +const _handlers: { + [Key in OB11MessageDataType]: ( + sendMsg: Extract, + // This picks the correct message type out + // How great the type system of TypeScript is! + context: MessageContext + ) => SendMessageElement | undefined | Promise +} = { + [OB11MessageDataType.text]: ({ data: { text } }) => SendMsgElementConstructor.text(text), + + [OB11MessageDataType.at]: async ({ data: { qq: atQQ } }, context) => { + if (!context.group) return undefined; + + if (atQQ === 'all') return SendMsgElementConstructor.at(atQQ, atQQ, AtType.atAll, '全体成员'); + + // then the qq is a group member + const atMember = await getGroupMember(context.group.groupCode, atQQ); + return atMember ? + SendMsgElementConstructor.at(atQQ, atMember.uid, AtType.atUser, atMember.cardName || atMember.nick) : + undefined; + }, + + [OB11MessageDataType.reply]: async ({ data: { id } }) => { + const replyMsg = await dbUtil.getMsgByShortId(parseInt(id)); + return replyMsg ? + SendMsgElementConstructor.reply(replyMsg.msgSeq, replyMsg.msgId, replyMsg.senderUin!, replyMsg.senderUin!) : + undefined; + }, + + [OB11MessageDataType.face]: ({ data: { id } }) => SendMsgElementConstructor.face(parseInt(id)), + + [OB11MessageDataType.mface]: ({ + data: { + emoji_package_id, + emoji_id, + key, + summary + } + }) => SendMsgElementConstructor.mface(emoji_package_id, emoji_id, key, summary), + + // File service + + [OB11MessageDataType.image]: async (sendMsg, context) => { + const PicEle = await SendMsgElementConstructor.pic( + (await handleOb11FileLikeMessage(sendMsg, context)).path, + sendMsg.data.summary || '', + sendMsg.data.subType || 0 + ); + context.deleteAfterSentFiles.push(PicEle.picElement.sourcePath); + return PicEle; + } + , // currently not supported + + [OB11MessageDataType.file]: async (sendMsg, context) => { + const { path, fileName } = await handleOb11FileLikeMessage(sendMsg, context); + //logDebug('发送文件', path, fileName); + const FileEle = await SendMsgElementConstructor.file(path, fileName); + // 清除Upload的应该 + // context.deleteAfterSentFiles.push(fileName || FileEle.fileElement.filePath); + return FileEle; + }, + + [OB11MessageDataType.video]: async (sendMsg, context) => { + const { path, fileName } = await handleOb11FileLikeMessage(sendMsg, context); + + //logDebug('发送视频', path, fileName); + let thumb = sendMsg.data.thumb; + if (thumb) { + const uri2LocalRes = await uri2local(thumb); + if (uri2LocalRes.success) thumb = uri2LocalRes.path; + } + + return SendMsgElementConstructor.video(path, fileName, thumb); + }, + [OB11MessageDataType.miniapp]: async ({ data: any }) => SendMsgElementConstructor.miniapp(), + + [OB11MessageDataType.voice]: async (sendMsg, context) => + SendMsgElementConstructor.ptt((await handleOb11FileLikeMessage(sendMsg, context)).path), + + [OB11MessageDataType.json]: ({ data: { data } }) => SendMsgElementConstructor.ark(data), + + [OB11MessageDataType.dice]: ({ data: { result } }) => SendMsgElementConstructor.dice(result), + + [OB11MessageDataType.RPS]: ({ data: { result } }) => SendMsgElementConstructor.rps(result), + + [OB11MessageDataType.markdown]: ({ data: { content } }) => SendMsgElementConstructor.markdown(content), + + [OB11MessageDataType.music]: async ({ data }) => { + // 保留, 直到...找到更好的解决方案 + if (data.type === 'custom') { + if (!data.url) { + logError('自定义音卡缺少参数url'); + return undefined; + } + if (!data.audio) { + logError('自定义音卡缺少参数audio'); + return undefined; + } + if (!data.title) { + logError('自定义音卡缺少参数title'); + return undefined; + } + } else { + if (!['qq', '163'].includes(data.type)) { + logError('音乐卡片type错误, 只支持qq、163、custom,当前type:', data.type); + return undefined; + } + if (!data.id) { + logError('音乐卡片缺少参数id'); + return undefined; + } + } + + let postData: IdMusicSignPostData | CustomMusicSignPostData; + if (data.type === 'custom' && data.content) { + const { content, ...others } = data; + postData = { singer: content, ...others }; + } else { + postData = data; + } + + const signUrl = ob11Config.musicSignUrl; + if (!signUrl) { + throw Error('音乐消息签名地址未配置'); + } + try { + const musicJson = await RequestUtil.HttpGetJson(signUrl, 'POST', postData); + return SendMsgElementConstructor.ark(musicJson); + } catch (e) { + logError('生成音乐消息失败', e); + } + }, + + [OB11MessageDataType.node]: () => undefined, + + [OB11MessageDataType.forward]: () => undefined, + + [OB11MessageDataType.xml]: () => undefined, + + [OB11MessageDataType.poke]: () => undefined, +}; + +const handlers = <{ + [Key in OB11MessageDataType]: ( + sendMsg: OB11MessageData, + context: MessageContext + ) => SendMessageElement | undefined | Promise +}>_handlers; + +export default async function createSendElements( + messageData: OB11MessageData[], + group?: Group, + ignoreTypes: OB11MessageDataType[] = [] +) { + const sendElements: SendMessageElement[] = []; + const deleteAfterSentFiles: string[] = []; + for (const sendMsg of messageData) { + if (ignoreTypes.includes(sendMsg.type)) { + continue; + } + const callResult = await handlers[sendMsg.type]( + sendMsg, + { group, deleteAfterSentFiles } + ); + if (callResult) sendElements.push(callResult); + } + return { sendElements, deleteAfterSentFiles }; +} + +export async function createSendElementsParallel( + messageData: OB11MessageData[], + group?: Group, + ignoreTypes: OB11MessageDataType[] = [] +) { + const deleteAfterSentFiles: string[] = []; + const sendElements = ( + await Promise.all( + messageData.map(async sendMsg => ignoreTypes.includes(sendMsg.type) ? + undefined : + handlers[sendMsg.type](sendMsg, { group, deleteAfterSentFiles })) + ).then( + results => results.filter( + element => element !== undefined + ) + ) + ); + return { sendElements, deleteAfterSentFiles }; +} diff --git a/src/onebot11/action/msg/SendMsg/handle-forward-node.ts b/src/onebot11/action/msg/SendMsg/handle-forward-node.ts index dff0bb42..85ea22fa 100644 --- a/src/onebot11/action/msg/SendMsg/handle-forward-node.ts +++ b/src/onebot11/action/msg/SendMsg/handle-forward-node.ts @@ -14,7 +14,7 @@ async function cloneMsg(msg: RawMessage): Promise { peerUid: selfInfo.uid }; - // logDebug('克隆的目标消息', msg); + // logDebug('克隆的目标消息', msg); const sendElements: SendMessageElement[] = []; diff --git a/src/onebot11/action/types.ts b/src/onebot11/action/types.ts index 939a96a4..ddda85a1 100644 --- a/src/onebot11/action/types.ts +++ b/src/onebot11/action/types.ts @@ -1,91 +1,91 @@ -export type BaseCheckResult = ValidCheckResult | InvalidCheckResult - -export interface ValidCheckResult { - valid: true - - [k: string | number]: any -} - -export interface InvalidCheckResult { - valid: false - message: string - - [k: string | number]: any -} - -export enum ActionName { - // 以下为扩展napcat扩展 - RebootNormol = 'reboot_normol',//无快速登录重新启动 - GetRobotUinRange = 'get_robot_uin_range', - SetOnlineStatus = 'set_online_status', - GetFriendsWithCategory = 'get_friends_with_category', - GetGroupIgnoreAddRequest = 'get_group_ignore_add_request', - SetQQAvatar = 'set_qq_avatar', - GetConfig = 'get_config', - SetConfig = 'set_config', - Debug = 'debug', - GetFile = 'get_file', - ForwardFriendSingleMsg = 'forward_friend_single_msg', - ForwardGroupSingleMsg = 'forward_group_single_msg', - TranslateEnWordToZn = "translate_en2zh", - GetGroupFileCount = "get_group_file_count", - GetGroupFileList = "get_group_file_list", - SetGroupFileFolder = "set_group_file_folder", - DelGroupFile = "del_group_file", - DelGroupFileFolder = "del_group_file_folder", - // onebot 11 - Reboot = 'set_restart', - SendLike = 'send_like', - GetLoginInfo = 'get_login_info', - GetFriendList = 'get_friend_list', - GetGroupInfo = 'get_group_info', - GetGroupList = 'get_group_list', - GetGroupMemberInfo = 'get_group_member_info', - GetGroupMemberList = 'get_group_member_list', - GetMsg = 'get_msg', - SendMsg = 'send_msg', - SendGroupMsg = 'send_group_msg', - SendPrivateMsg = 'send_private_msg', - DeleteMsg = 'delete_msg', - SetMsgEmojiLike = 'set_msg_emoji_like', - SetGroupAddRequest = 'set_group_add_request', - SetFriendAddRequest = 'set_friend_add_request', - SetGroupLeave = 'set_group_leave', - GetVersionInfo = 'get_version_info', - GetStatus = 'get_status', - CanSendRecord = 'can_send_record', - CanSendImage = 'can_send_image', - SetGroupKick = 'set_group_kick', - SetGroupBan = 'set_group_ban', - SetGroupWholeBan = 'set_group_whole_ban', - SetGroupAdmin = 'set_group_admin', - SetGroupCard = 'set_group_card', - SetGroupName = 'set_group_name', - GetImage = 'get_image', - GetRecord = 'get_record', - CleanCache = 'clean_cache', - GetCookies = 'get_cookies', - // 以下为go-cqhttp api - GoCQHTTP_HandleQuickAction = '.handle_quick_operation', - GetGroupHonorInfo = 'get_group_honor_info', - GoCQHTTP_GetEssenceMsg = 'get_essence_msg_list', - GoCQHTTP_SendGroupNotice = '_send_group_notice', - GoCQHTTP_GetGroupNotice = '_get_group_notice', - GoCQHTTP_SendForwardMsg = 'send_forward_msg', - GoCQHTTP_SendGroupForwardMsg = 'send_group_forward_msg', - GoCQHTTP_SendPrivateForwardMsg = 'send_private_forward_msg', - GoCQHTTP_GetStrangerInfo = 'get_stranger_info', - GoCQHTTP_MarkMsgAsRead = 'mark_msg_as_read', - GetGuildList = 'get_guild_list', - MarkPrivateMsgAsRead = 'mark_private_msg_as_read', - MarkGroupMsgAsRead = 'mark_group_msg_as_read', - GoCQHTTP_UploadGroupFile = 'upload_group_file', - GoCQHTTP_DownloadFile = 'download_file', - GoCQHTTP_GetGroupMsgHistory = 'get_group_msg_history', - GoCQHTTP_GetForwardMsg = 'get_forward_msg', - GetFriendMsgHistory = 'get_friend_msg_history', - GetGroupSystemMsg = 'get_group_system_msg', - GetOnlineClient = "get_online_clients", - OCRImage = "ocr_image", - IOCRImage = ".ocr_image" -} +export type BaseCheckResult = ValidCheckResult | InvalidCheckResult + +export interface ValidCheckResult { + valid: true + + [k: string | number]: any +} + +export interface InvalidCheckResult { + valid: false + message: string + + [k: string | number]: any +} + +export enum ActionName { + // 以下为扩展napcat扩展 + RebootNormol = 'reboot_normol',//无快速登录重新启动 + GetRobotUinRange = 'get_robot_uin_range', + SetOnlineStatus = 'set_online_status', + GetFriendsWithCategory = 'get_friends_with_category', + GetGroupIgnoreAddRequest = 'get_group_ignore_add_request', + SetQQAvatar = 'set_qq_avatar', + GetConfig = 'get_config', + SetConfig = 'set_config', + Debug = 'debug', + GetFile = 'get_file', + ForwardFriendSingleMsg = 'forward_friend_single_msg', + ForwardGroupSingleMsg = 'forward_group_single_msg', + TranslateEnWordToZn = 'translate_en2zh', + GetGroupFileCount = 'get_group_file_count', + GetGroupFileList = 'get_group_file_list', + SetGroupFileFolder = 'set_group_file_folder', + DelGroupFile = 'del_group_file', + DelGroupFileFolder = 'del_group_file_folder', + // onebot 11 + Reboot = 'set_restart', + SendLike = 'send_like', + GetLoginInfo = 'get_login_info', + GetFriendList = 'get_friend_list', + GetGroupInfo = 'get_group_info', + GetGroupList = 'get_group_list', + GetGroupMemberInfo = 'get_group_member_info', + GetGroupMemberList = 'get_group_member_list', + GetMsg = 'get_msg', + SendMsg = 'send_msg', + SendGroupMsg = 'send_group_msg', + SendPrivateMsg = 'send_private_msg', + DeleteMsg = 'delete_msg', + SetMsgEmojiLike = 'set_msg_emoji_like', + SetGroupAddRequest = 'set_group_add_request', + SetFriendAddRequest = 'set_friend_add_request', + SetGroupLeave = 'set_group_leave', + GetVersionInfo = 'get_version_info', + GetStatus = 'get_status', + CanSendRecord = 'can_send_record', + CanSendImage = 'can_send_image', + SetGroupKick = 'set_group_kick', + SetGroupBan = 'set_group_ban', + SetGroupWholeBan = 'set_group_whole_ban', + SetGroupAdmin = 'set_group_admin', + SetGroupCard = 'set_group_card', + SetGroupName = 'set_group_name', + GetImage = 'get_image', + GetRecord = 'get_record', + CleanCache = 'clean_cache', + GetCookies = 'get_cookies', + // 以下为go-cqhttp api + GoCQHTTP_HandleQuickAction = '.handle_quick_operation', + GetGroupHonorInfo = 'get_group_honor_info', + GoCQHTTP_GetEssenceMsg = 'get_essence_msg_list', + GoCQHTTP_SendGroupNotice = '_send_group_notice', + GoCQHTTP_GetGroupNotice = '_get_group_notice', + GoCQHTTP_SendForwardMsg = 'send_forward_msg', + GoCQHTTP_SendGroupForwardMsg = 'send_group_forward_msg', + GoCQHTTP_SendPrivateForwardMsg = 'send_private_forward_msg', + GoCQHTTP_GetStrangerInfo = 'get_stranger_info', + GoCQHTTP_MarkMsgAsRead = 'mark_msg_as_read', + GetGuildList = 'get_guild_list', + MarkPrivateMsgAsRead = 'mark_private_msg_as_read', + MarkGroupMsgAsRead = 'mark_group_msg_as_read', + GoCQHTTP_UploadGroupFile = 'upload_group_file', + GoCQHTTP_DownloadFile = 'download_file', + GoCQHTTP_GetGroupMsgHistory = 'get_group_msg_history', + GoCQHTTP_GetForwardMsg = 'get_forward_msg', + GetFriendMsgHistory = 'get_friend_msg_history', + GetGroupSystemMsg = 'get_group_system_msg', + GetOnlineClient = 'get_online_clients', + OCRImage = 'ocr_image', + IOCRImage = '.ocr_image' +} diff --git a/src/onebot11/action/user/GetCookies.ts b/src/onebot11/action/user/GetCookies.ts index 8ecf7554..8a22a9e4 100644 --- a/src/onebot11/action/user/GetCookies.ts +++ b/src/onebot11/action/user/GetCookies.ts @@ -25,10 +25,10 @@ export class GetCookies extends BaseAction { if (!payload.domain) { throw new Error('缺少参数 domain'); } - if (payload.domain.endsWith("qzone.qq.com")) { + if (payload.domain.endsWith('qzone.qq.com')) { const _Skey = await NTQQUserApi.getSkey() as string; // 兼容整个 *.qzone.qq.com - let data = (await NTQQUserApi.getQzoneCookies()); + const data = (await NTQQUserApi.getQzoneCookies()); const Bkn = WebApi.genBkn(data.p_skey); const CookieValue = 'p_skey=' + data.p_skey + '; skey=' + data.skey + '; p_uin=o' + selfInfo.uin + '; uin=o' + selfInfo.uin; return { cookies: CookieValue }; diff --git a/src/onebot11/event/notice/OB11GroupIncreaseEvent.ts b/src/onebot11/event/notice/OB11GroupIncreaseEvent.ts index 7e02a375..8a0dd831 100644 --- a/src/onebot11/event/notice/OB11GroupIncreaseEvent.ts +++ b/src/onebot11/event/notice/OB11GroupIncreaseEvent.ts @@ -15,6 +15,6 @@ export class OB11GroupIncreaseEvent extends OB11GroupNoticeEvent { this.sub_type = subType; if(ob11Config.GroupLocalTime.Record && (ob11Config.GroupLocalTime.RecordList[0] == '-1' || ob11Config.GroupLocalTime.RecordList.includes(groupId.toString()))) - dbUtil.insertJoinTime(groupId, userId, Math.floor(Date.now() / 1000)) + dbUtil.insertJoinTime(groupId, userId, Math.floor(Date.now() / 1000)); } } diff --git a/src/onebot11/main.ts b/src/onebot11/main.ts index 60438aa4..41a84774 100644 --- a/src/onebot11/main.ts +++ b/src/onebot11/main.ts @@ -152,7 +152,7 @@ export class NapCatOnebot11 { app_id: '0', device_name: device.deviceName, device_kind: 'none' - }) + }); // log('[设备列表] 设备名称: ' + device.deviceName); }); } diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 131891b3..d4d12c26 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -1,9 +1,9 @@ -/// - -interface ImportMetaEnv { - VITE_BUILD_TYPE: string -} - -interface ImportMeta { - readonly env: ImportMetaEnv +/// + +interface ImportMetaEnv { + VITE_BUILD_TYPE: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv } \ No newline at end of file diff --git a/src/webui/ui/NapCat.ts b/src/webui/ui/NapCat.ts index 36adda06..db2562d4 100644 --- a/src/webui/ui/NapCat.ts +++ b/src/webui/ui/NapCat.ts @@ -69,7 +69,7 @@ async function onSettingWindowCreated(view: Element) {
+}" placeholder="未设置" />