Compare commits

...

72 Commits

Author SHA1 Message Date
手瓜一十雪
18892379de fix: search file 2024-07-26 12:27:02 +08:00
手瓜一十雪
620d61c8dc docs: v1.6.8 2024-07-26 11:59:18 +08:00
手瓜一十雪
9f91398875 build: 再次优化发送速度 2024-07-26 11:30:04 +08:00
手瓜一十雪
34d19a471a refactor: 回滚 2024-07-26 10:58:56 +08:00
手瓜一十雪
2ef6477d7c build: log info 2024-07-25 20:22:03 +08:00
手瓜一十雪
26e6800836 fix: 退群推送 2024-07-25 18:08:49 +08:00
手瓜一十雪
9dbbcf3872 build: 1.6.8 - parse appid 2024-07-25 17:57:39 +08:00
手瓜一十雪
6b3343e1e4 build: 1.6.8 beta6 2024-07-25 10:59:06 +08:00
手瓜一十雪
ef48f754a5 docs: 整理当前进度 2024-07-25 10:44:53 +08:00
手瓜一十雪
3be1ede847 refactor: sendtime/join time 2024-07-25 10:32:44 +08:00
手瓜一十雪
7bff1b61e8 refactor: SendTime 2024-07-25 10:02:16 +08:00
手瓜一十雪
6affd0eb68 feat: GetSendTime 2024-07-24 17:49:03 +08:00
手瓜一十雪
0a112d15e0 fix: typo 2024-07-24 15:37:23 +08:00
手瓜一十雪
4e03f582bb fix: richmeida name 2024-07-24 14:43:10 +08:00
手瓜一十雪
8f186c1c5e chore: action clean 2024-07-24 14:37:48 +08:00
手瓜一十雪
cd1bae9a1f fix: setGroupAvatar 2024-07-24 14:35:12 +08:00
手瓜一十雪
60796c26ca Merge pull request #147 from serfend/default-config
fix[config]support overwrite by user #145
2024-07-24 14:28:42 +08:00
汉广
28927f950d fix[config]support overwrite by user 2024-07-24 14:25:58 +08:00
手瓜一十雪
95f16ebc8c Merge pull request #144 from Guation/main
feat: http与ws允许监听同一端口,快速登录允许自动选择QQ号,允许禁用webUI
2024-07-24 14:09:19 +08:00
挂神
25bca8385d feat: http与ws共站支持热重载 2024-07-24 13:14:35 +08:00
手瓜一十雪
965c7f23b4 feat: 群头像设置 2024-07-24 11:37:12 +08:00
手瓜一十雪
33082af9cc feat: searchFile 2024-07-24 11:23:27 +08:00
手瓜一十雪
1f7f3565b0 build: 1.6.8 beta05 2024-07-24 10:45:11 +08:00
手瓜一十雪
f784363696 refactor: UUID 2024-07-24 10:44:55 +08:00
手瓜一十雪
37bd51e138 refactor: getUserInfo 2024-07-24 10:42:22 +08:00
手瓜一十雪
c367728c43 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-07-24 10:23:48 +08:00
手瓜一十雪
61f0f5d884 refactor: 改造接口调用 2024-07-24 10:23:41 +08:00
手瓜一十雪
5f2ebeead7 docs: update 2024-07-23 18:32:07 +08:00
手瓜一十雪
7646037fc7 docs: 砍掉 2024-07-23 18:21:19 +08:00
手瓜一十雪
eac6d285ff chore: debug 2024-07-23 17:39:00 +08:00
手瓜一十雪
b921d5e734 refactor: downloadMedia 2024-07-23 16:15:23 +08:00
手瓜一十雪
831d808e63 chore: remove 2024-07-23 16:03:06 +08:00
手瓜一十雪
451b88d7e3 refactor: video type 2024-07-23 15:51:57 +08:00
手瓜一十雪
f916682a71 build: 1.6.8 beta07 2024-07-23 15:38:41 +08:00
手瓜一十雪
2d76bcf0cf refactor: message id 2024-07-23 15:10:39 +08:00
手瓜一十雪
8db294efe6 refactor: 转发消息修复 2024-07-23 14:54:05 +08:00
手瓜一十雪
5cc5149aed fix: 合并转发 2024-07-23 14:19:26 +08:00
手瓜一十雪
7ecb01dc9f docs: 规划 2024-07-23 12:34:20 +08:00
手瓜一十雪
8bf1a545d9 chore: remove debug 2024-07-23 10:12:20 +08:00
手瓜一十雪
efb2be2f94 fix: timeout 2024-07-23 09:50:31 +08:00
手瓜一十雪
bd2edda494 refactor: sendMsg 2024-07-23 09:45:00 +08:00
手瓜一十雪
991172eae4 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-07-23 09:21:36 +08:00
手瓜一十雪
fc7631f9aa refactor: sendmsg 2024-07-23 09:21:22 +08:00
手瓜一十雪
a21efb7d2f Merge pull request #145 from serfend/default-config
fix[default-config]config name check #138
2024-07-22 21:36:54 +08:00
汉广
03bc844ad0 fix[default-config]config name check 2024-07-22 20:12:24 +08:00
挂神
f7bdc35ed6 feat: http与ws允许监听同一端口,快速登录允许自动选择QQ号,允许禁用webUI 2024-07-22 19:47:23 +08:00
手瓜一十雪
0efdffd857 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-07-22 18:49:36 +08:00
手瓜一十雪
6a16a42d0c feat: refactor send 2024-07-22 18:49:25 +08:00
手瓜一十雪
e9c00c72b1 Merge pull request #138 from serfend/main
feat[config]support use default-template
2024-07-22 18:22:49 +08:00
手瓜一十雪
ab22f36b8a refactor: NTEvent Checker 2024-07-22 18:21:29 +08:00
手瓜一十雪
c57497cd91 feat: remove debug 2024-07-22 18:17:33 +08:00
手瓜一十雪
ba123236e5 feat:msgid generate 2024-07-22 18:17:03 +08:00
手瓜一十雪
338b6e4607 fix: QRCode 2024-07-22 15:46:48 +08:00
手瓜一十雪
f88c717560 build: 1.6.8-beta03 2024-07-22 15:40:41 +08:00
手瓜一十雪
f8ffc92db5 feat: remove LineDev&&Protobuf 2024-07-22 15:40:23 +08:00
手瓜一十雪
98c23c172c build: 1.6.8-无数据库版本 2024-07-22 15:13:38 +08:00
手瓜一十雪
781c107d8c feat: 拉取重启消息 2024-07-22 15:12:25 +08:00
手瓜一十雪
186668c075 fix: Login 2024-07-22 14:18:04 +08:00
手瓜一十雪
cf9f785193 style: lint 2024-07-22 14:12:03 +08:00
手瓜一十雪
72d2d3f224 feat: 破坏file/db相关接口 2024-07-22 14:09:37 +08:00
手瓜一十雪
087c76b394 refactor: msgId stage-2 2024-07-22 11:34:18 +08:00
手瓜一十雪
4f9fb2c8c3 Merge pull request #141 from cnxysoft/main
修复提交疏漏
2024-07-22 11:15:28 +08:00
手瓜一十雪
334e43e764 refactor: MsgId 2024-07-22 11:15:01 +08:00
Alen
7843256402 修复提交疏漏
修复变量类型未断言的问题
2024-07-22 11:07:33 +08:00
手瓜一十雪
0522ba35fe refactor: jest test 2024-07-22 10:24:55 +08:00
手瓜一十雪
24d3b52e0b refactor: Message Unique 2024-07-22 09:56:08 +08:00
手瓜一十雪
3177110f0f feat: RecentContact 2024-07-22 09:24:16 +08:00
手瓜一十雪
e1b8243a67 Merge pull request #140 from cnxysoft/main
BUG修复
2024-07-22 08:40:21 +08:00
Alen
b1c6ce3885 BUG修复
1.尝试让所有人能收到group_admin事件
2.修复请求API: delete_msg(POST请求网址传参)将负数判定为文本导致无法调用的问题
2024-07-22 01:22:38 +08:00
手瓜一十雪
0b4b25a11e feat: LineDev for Develop-0 2024-07-21 19:31:13 +08:00
手瓜一十雪
1176fe984a add: RecentListener 2024-07-21 19:01:47 +08:00
汉广
6ca768c3ee feat[config]support use default-template 2024-07-20 23:43:32 +08:00
86 changed files with 1621 additions and 1717 deletions

View File

@@ -1,4 +1,4 @@
# v1.6.7
# v1.6.8
QQ Version: Windows 9.9.12-26000 / Linux 3.2.9-26000
## 使用前警告
@@ -9,17 +9,12 @@ QQ Version: Windows 9.9.12-26000 / Linux 3.2.9-26000
启动方式: WayBoot.03 Electron Main进程为Node 直接注入代码 同理项目: LiteLoader
## 修复与优化
1. 尝试 修复 卡顿问题
2. 尝试 修复 精华消息被设置/一起听 接收时的报错
3. 优化 Uin与Uid 转换速度
4. 修复CQCode可能存在的解码问题
1. 移除数据库文件读写 ~ 优化性能
2. 重构消息发送 极限速度优化 ~ 优化性能
3. WebUi配置热重载优化 ~ 修复问题
4. 修复偶现崩溃问题 ~ 修复问题
## 新增与调整
1. 戳一戳上报raw
2. 精华消息设置通知事件
3. 新增设置/删除群精华API
4. 新增最近联系列表APIRAW 不稳定)
5. 新增设置所有消息已读API非标准
6. 新增获取点赞信息获取API非标准
1. 最后发言时间重构 入群时间失效 ~ 替换功能
新增的 API 详细见[API文档](https://napneko.github.io/zh-CN/develop/extends_api)

View File

@@ -1,3 +1,4 @@
public static final int C2C_PIC_DOWNLOAD = 1004;
public static final String C2C_PIC_DOWNLOAD_DOMAIN = "c2cpicdw.qpic.cn";
public static final String C2C_PIC_DOWNLOAD_QUIC_DOMAIN = "c2cpicdw.quic.qpic.cn";

View File

@@ -1 +1,8 @@
# Api方向
## getMsgUniqueId √ 已应用
getMsgUniqueId 传入时间 产出一个唯一ID 发送消息作为一个参数
# Native方向
## magic_load
## api_caller
## NodeMain

View File

@@ -2,7 +2,7 @@
"name": "napcat",
"private": true,
"type": "module",
"version": "1.6.7",
"version": "1.6.8",
"scripts": {
"watch:dev": "vite --mode development",
"watch:prod": "vite --mode production",
@@ -19,10 +19,9 @@
},
"devDependencies": {
"@babel/core": "^7.24.7",
"@babel/preset-typescript": "^7.24.7",
"vite-plugin-babel": "^1.2.0",
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/plugin-proposal-decorators": "^7.24.7",
"@babel/preset-typescript": "^7.24.7",
"@log4js-node/log4js-api": "^1.0.2",
"@protobuf-ts/plugin": "^2.9.4",
"@rollup/plugin-node-resolve": "^15.2.3",
@@ -31,9 +30,9 @@
"@types/express": "^4.17.21",
"@types/figlet": "^1.5.8",
"@types/fluent-ffmpeg": "^2.1.24",
"@types/jest": "^29.5.12",
"@types/node": "^20.11.30",
"@types/qrcode-terminal": "^0.12.2",
"@types/uuid": "^10.0.0",
"@types/ws": "^8.5.10",
"@typescript-eslint/eslint-plugin": "^7.4.0",
"@typescript-eslint/parser": "^7.4.0",
@@ -47,6 +46,7 @@
"rollup-plugin-obfuscator": "^1.1.0",
"typescript": "^5.3.3",
"vite": "^5.2.6",
"vite-plugin-babel": "^1.2.0",
"vite-plugin-cp": "^4.0.8",
"vite-plugin-dts": "^3.8.2",
"vite-tsconfig-paths": "^4.3.2"
@@ -65,8 +65,6 @@
"log4js": "^6.9.1",
"qrcode-terminal": "^0.12.0",
"silk-wasm": "^3.6.1",
"sqlite3": "^5.1.7",
"uuid": "^10.0.0",
"ws": "^8.16.0"
}
}

View File

@@ -9,7 +9,15 @@ type RegisterHandler = (res: Response, payload: any) => Promise<any>
export abstract class HttpServerBase {
name: string = 'NapCatQQ';
private readonly expressAPP: Express;
private server: http.Server | null = null;
private _server: http.Server | null = null;
public get server(): http.Server | null {
return this._server;
}
private set server(value: http.Server | null) {
this._server = value;
}
constructor() {
this.expressAPP = express();

View File

@@ -1,4 +1,5 @@
import { WebSocket, WebSocketServer } from 'ws';
import http from 'http';
import urlParse from 'url';
import { IncomingMessage } from 'node:http';
import { log } from '@/common/utils/log';
@@ -27,17 +28,36 @@ export class WebsocketServerBase {
constructor() {
}
start(port: number, host: string = '') {
try {
this.ws = new WebSocketServer({
port,
host: '',
maxPayload: 1024 * 1024 * 1024
}).on('error', () => {
});
log(`ws服务启动成功, ${host}:${port}`);
} catch (e: any) {
throw Error('ws服务启动失败, 请检查监听的ip和端口' + e.toString());
start(port: number | http.Server, host: string = '') {
if (port instanceof http.Server) {
try {
const wss = new WebSocketServer({
noServer: true,
maxPayload: 1024 * 1024 * 1024
}).on('error', () => {
});
this.ws = wss;
port.on('upgrade', function upgrade(request, socket, head) {
wss.handleUpgrade(request, socket, head, function done(ws) {
wss.emit('connection', ws, request);
});
});
log(`ws服务启动成功, 绑定到HTTP服务`);
} catch (e: any) {
throw Error('ws服务启动失败, 可能是绑定的HTTP服务异常' + e.toString());
}
} else {
try {
this.ws = new WebSocketServer({
port,
host: '',
maxPayload: 1024 * 1024 * 1024
}).on('error', () => {
});
log(`ws服务启动成功, ${host}:${port}`);
} catch (e: any) {
throw Error('ws服务启动失败, 请检查监听的ip和端口' + e.toString());
}
}
this.ws.on('connection', (wsClient, req) => {
const url: string = req.url!.split('?').shift() || '/';
@@ -50,10 +70,12 @@ export class WebsocketServerBase {
}
stop() {
this.ws && this.ws.close((err) => {
log('ws server close failed!', err);
});
this.ws = null;
if (this.ws) {
this.ws.close((err) => {
if (err) log('ws server close failed!', err);
});
this.ws = null;
}
}
restart(port: number) {

View File

@@ -1,36 +0,0 @@
import { sleep } from '@/common/utils/helper';
import { logError } from './log';
type AsyncQueueTask = (() => void) | (()=>Promise<void>);
// 2024.7.13 废弃
export class AsyncQueue {
private tasks: (AsyncQueueTask)[] = [];
public addTask(task: AsyncQueueTask) {
this.tasks.push(task);
// console.log('addTask', this.tasks.length);
if (this.tasks.length === 1) {
this.runQueue().then().catch(()=>{});
}
}
private async runQueue() {
// console.log('runQueue', this.tasks.length);
while (this.tasks.length > 0) {
const task = this.tasks[0];
// console.log('typeof task', typeof task);
try {
const taskRet = task();
// console.log('type of taskRet', typeof taskRet, taskRet);
if (taskRet instanceof Promise) {
await taskRet;
}
} catch (e) {
// console.error(e);
logError(e);
}
this.tasks.shift();
await sleep(100);
}
}
}

View File

@@ -3,6 +3,7 @@ import fs from 'node:fs';
import { log, logDebug, logError } from '@/common/utils/log';
import { dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { selfInfo } from '@/core/data';
const __filename = fileURLToPath(import.meta.url);
@@ -12,8 +13,9 @@ const configDir = path.resolve(__dirname, 'config');
fs.mkdirSync(configDir, { recursive: true });
export class ConfigBase<T>{
export class ConfigBase<T> {
public name: string = 'default_config'
private pathName: string | null = null // 本次读取的文件路径
constructor() {
}
@@ -22,19 +24,28 @@ export class ConfigBase<T>{
return null;
}
getConfigDir(){
getConfigDir() {
const configDir = path.resolve(__dirname, 'config');
fs.mkdirSync(configDir, { recursive: true });
return configDir;
}
getConfigPath(): string {
throw new Error('Method not implemented.');
getConfigPath(pathName: string | null): string {
const suffix = pathName ? `_${pathName}` : ''
const filename = `${this.name}${suffix}.json`
return path.join(this.getConfigDir(), filename);
}
read() {
const configPath = this.getConfigPath();
// 尝试加载当前账号配置
if (this.read_from_file(selfInfo.uin, false)) return this
// 尝试加载默认配置
return this.read_from_file('', true)
}
read_from_file(pathName: string, createIfNotExist: boolean) {
const configPath = this.getConfigPath(pathName);
if (!fs.existsSync(configPath)) {
try{
if (!createIfNotExist) return null
this.pathName = pathName // 记录有效的设置文件
try {
fs.writeFileSync(configPath, JSON.stringify(this, this.getKeys(), 2));
log(`配置文件${configPath}已创建\n如果修改此文件后需要重启 NapCat 生效`);
}
@@ -43,6 +54,7 @@ export class ConfigBase<T>{
}
return this;
}
try {
const data = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
logDebug(`配置文件${configPath}已加载`, data);
@@ -61,9 +73,13 @@ export class ConfigBase<T>{
}
}
save(config: T) {
save(config: T, overwrite: boolean = false) {
Object.assign(this, config);
const configPath = this.getConfigPath();
if (overwrite) {
// 用户要求强制写入,则变更当前文件为目标文件
this.pathName = `${selfInfo.uin}`
}
const configPath = this.getConfigPath(this.pathName);
try {
fs.writeFileSync(configPath, JSON.stringify(this, this.getKeys(), 2));
} catch (e: any) {

View File

@@ -5,6 +5,7 @@ interface Internal_MapKey {
timeout: number,
createtime: number,
func: (...arg: any[]) => any,
checker: ((...args: any[]) => boolean) | undefined,
}
export class ListenerClassBase {
@@ -83,17 +84,19 @@ export class NTEventWrapper {
}
//统一回调清理事件
async DispatcherListener(ListenerMainName: string, ListenerSubName: string, ...args: any[]) {
//console.log(ListenerMainName, this.EventTask.get(ListenerMainName), ListenerSubName, this.EventTask.get(ListenerMainName)?.get(ListenerSubName));
//console.log(ListenerMainName, ListenerSubName, ...args,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);
if (task.checker && task.checker(...args)) {
task.func(...args);
}
});
}
async CallNoListenerEvent<EventType extends (...args: any[]) => Promise<any>,>(EventName = '', timeout: number = 3000, ...args: Parameters<EventType>) {
async CallNoListenerEvent<EventType extends (...args: any[]) => Promise<any>>(EventName = '', timeout: number = 3000, ...args: Parameters<EventType>) {
return new Promise<Awaited<ReturnType<EventType>>>(async (resolve, reject) => {
const EventFunc = this.CreatEventFunction<EventType>(EventName);
let complete = false;
@@ -107,14 +110,15 @@ export class NTEventWrapper {
resolve(retData);
});
}
async CallNormalEvent<EventType extends (...args: any[]) => Promise<any>, ListenerType extends (...args: any[]) => void>(EventName = '', ListenerName = '', waitTimes = 1, timeout: number = 3000, ...args: Parameters<EventType>) {
async CallNormalEvent<EventType extends (...args: any[]) => Promise<any>, ListenerType extends (...args: any[]) => void>
(EventName = '', ListenerName = '', waitTimes = 1, timeout: number = 3000, checker: (...args: Parameters<ListenerType>) => boolean, ...args: Parameters<EventType>) {
return new Promise<[EventRet: Awaited<ReturnType<EventType>>, ...Parameters<ListenerType>]>(async (resolve, reject) => {
const id = randomUUID();
let complete = 0;
let retData: Parameters<ListenerType> | undefined = undefined;
let retEvent: any = {};
const databack = () => {
if (complete < waitTimes) {
if (complete == 0) {
reject(new Error('NTEvent EventName:' + EventName + ' ListenerName:' + ListenerName + ' timeout'));
} else {
resolve([retEvent as Awaited<ReturnType<EventType>>, ...retData!]);
@@ -128,6 +132,7 @@ export class NTEventWrapper {
const eventCallbak = {
timeout: timeout,
createtime: Date.now(),
checker: checker,
func: (...args: any[]) => {
complete++;
//console.log('func', ...args);

View File

@@ -1,161 +0,0 @@
import { logError, logDebug } from '@/common/utils/log';
type group_id = number;
type user_id = number;
class cacheNode<T> {
value: T;
groupId: group_id;
userId: user_id;
prev: cacheNode<T> | null;
next: cacheNode<T> | 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<T, K = { [key: user_id]: cacheNode<T> }> = { [key: group_id]: K };
type removeObject<T> = cache<T, { userId: user_id, value: T }[]>
class LRU<T> {
private maxAge: number;
private maxSize: number;
private currentSize: number;
private cache: cache<T>;
private head: cacheNode<T> | null = null;
private tail: cacheNode<T> | null = null;
private onFuncs: ((node: removeObject<T>) => void)[] = [];
constructor(maxAge: number = 6e4 * 3, maxSize: number = 1e4) {
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<T>) {
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.groupId]: [node] }));
this.currentSize--;
}
public on(func: (node: removeObject<T>) => void) {
this.onFuncs.push(func);
}
private removeExpired() {
const now = Date.now();
let current = this.tail;
let totalNodeNum = 0;
const removeObject: cache<T, { userId: user_id, value: T }[]> = {};
while (current && now - current.timestamp > this.maxAge) {
// 收集节点
if (!removeObject[current.groupId]) removeObject[current.groupId] = [];
removeObject[current.groupId].push({ userId: current.userId, value: current.value });
// 删除LRU节点
delete this.cache[current.groupId][current.userId];
current = current.prev;
totalNodeNum++;
this.currentSize--;
}
if (!totalNodeNum) return;
// 跟新链表指向
if (current) { current.next = null; } else { this.head = null; }
this.tail = current;
this.onFuncs.forEach(func => func(removeObject));
}
private addNode(node: cacheNode<T>) {
node.next = this.head;
if (this.head) this.head.prev = node;
if (!this.tail) this.tail = node;
this.head = node;
}
private removeNode(node: cacheNode<T>) {
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<T>) {
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);
}
}
}
public get(groupId: group_id): { userId: user_id; value: T }[];
public get(groupId: group_id, userId: user_id): null | { userId: user_id; value: T };
public get(groupId: group_id, userId?: user_id): any {
const groupObject = this.cache[groupId];
if (!groupObject) return userId === undefined ? [] : null;
if (userId === undefined) {
return Object.entries(groupObject).map(([userId, { value }]) => ({
userId: Number(userId),
value,
}));
}
if (groupObject[userId]) {
return { userId, value: groupObject[userId].value };
}
return null;
}
}
export default LRU;

View File

@@ -1,20 +1,40 @@
import crypto from 'crypto';
import { Peer } from '@/core';
import crypto, { randomInt, randomUUID } from 'crypto';
import { logError } from './log';
class LimitedHashTable<K, V> {
private keyToValue: Map<K, V> = new Map();
private valueToKey: Map<V, K> = new Map();
private maxSize: number;
private KeyQueneList: K[] = [];
private ValueQueneList: V[] = [];
constructor(maxSize: number) {
this.maxSize = maxSize;
}
resize(count: number) {
this.maxSize = count;
}
set(key: K, value: V): void {
const isExist = this.keyToValue.get(key);
if (isExist && isExist === value) {
return;
}
this.keyToValue.set(key, value);
this.valueToKey.set(value, key);
if (this.KeyQueneList.length >= this.maxSize || this.ValueQueneList.length >= this.maxSize) {
this.KeyQueneList.shift();
this.ValueQueneList.shift();
while (this.keyToValue.size !== this.valueToKey.size) {
console.log('keyToValue.size !== valueToKey.size Error Atom');
this.keyToValue.clear();
this.valueToKey.clear();
}
// console.log('---------------');
// console.log(this.keyToValue);
// console.log(this.valueToKey);
// console.log('---------------');
while (this.keyToValue.size > this.maxSize || this.valueToKey.size > this.maxSize) {
//console.log(this.keyToValue.size > this.maxSize, this.valueToKey.size > this.maxSize);
const oldestKey = this.keyToValue.keys().next().value;
this.valueToKey.delete(this.keyToValue.get(oldestKey)!);
this.keyToValue.delete(oldestKey);
}
}
@@ -26,7 +46,15 @@ class LimitedHashTable<K, V> {
return this.valueToKey.get(value);
}
delete(key: K): void {
deleteByValue(value: V): void {
const key = this.valueToKey.get(value);
if (key !== undefined) {
this.keyToValue.delete(key);
this.valueToKey.delete(value);
}
}
deleteByKey(key: K): void {
const value = this.keyToValue.get(key);
if (value !== undefined) {
this.keyToValue.delete(key);
@@ -36,18 +64,53 @@ class LimitedHashTable<K, V> {
}
class MessageUniqueWrapper {
private msgIdMap: LimitedHashTable<number, string> = new LimitedHashTable(1000);
createMsg(MsgId: string) {
const ShortId = parseInt(crypto.createHash('sha1').update('2345').digest('hex').slice(0, 8), 16);
this.msgIdMap.set(ShortId, MsgId);
return ShortId;
private msgDataMap: LimitedHashTable<string, number>;
private msgIdMap: LimitedHashTable<string, number>;
constructor(maxMap: number = 1000) {
this.msgIdMap = new LimitedHashTable<string, number>(maxMap);
this.msgDataMap = new LimitedHashTable<string, number>(maxMap);
}
getMsgIdByShortId(ShortId: number) {
return this.msgIdMap.getValue(ShortId);
createMsg(peer: Peer, msgId: string): number | undefined {
const key = `${msgId}|${peer.chatType}|${peer.peerUid}`;
const hash = crypto.createHash('sha1').update(key);
const shortId = parseInt(hash.digest('hex').slice(0, 8), 16);
const isExist = this.msgIdMap.getKey(shortId);
//console.log(`${peer.peerUid} ${msgId} ------- ${shortId}`);
if (isExist && isExist === msgId) {
return shortId;
}
this.msgIdMap.set(msgId, shortId);
this.msgDataMap.set(key, shortId);
return shortId;
}
getShortIdByMsgId(MsgId: string) {
return this.msgIdMap.getKey(MsgId);
getMsgIdAndPeerByShortId(shortId: number): { MsgId: string; Peer: Peer } | undefined {
const data = this.msgDataMap.getKey(shortId);
if (data) {
const [msgId, chatTypeStr, peerUid] = data.split('|');
const peer: Peer = {
chatType: parseInt(chatTypeStr),
peerUid,
guildId: '',
};
return { MsgId: msgId, Peer: peer };
}
return undefined;
}
getShortIdByMsgId(msgId: string): number | undefined {
return this.msgIdMap.getValue(msgId);
}
getPeerByMsgId(msgId: string) {
const shortId = this.msgIdMap.getValue(msgId);
if (!shortId) return undefined;
return this.getMsgIdAndPeerByShortId(shortId);
}
resize(maxSize: number): void {
this.msgIdMap.resize(maxSize);
this.msgDataMap.resize(maxSize);
}
}
export const MessageUnique = new MessageUniqueWrapper();
export const MessageUnique: MessageUniqueWrapper = new MessageUniqueWrapper();

View File

@@ -3,7 +3,7 @@ import { encode, getDuration, getWavFileInfo, isWav, isSilk } from 'silk-wasm';
import fsPromise from 'fs/promises';
import { log, logError } from './log';
import path from 'node:path';
import { v4 as uuidv4 } from 'uuid';
import { randomUUID } from 'crypto';
import { spawn } from 'node:child_process';
import { getTempDir } from '@/common/utils/file';
@@ -64,7 +64,7 @@ export async function encodeSilk(filePath: string) {
try {
const file = await fsPromise.readFile(filePath);
const pttPath = path.join(TEMP_DIR, uuidv4());
const pttPath = path.join(TEMP_DIR, randomUUID());
if (!isSilk(file)) {
log(`语音文件${filePath}需要转换成silk`);
const _isWav = isWav(file);

View File

@@ -1,477 +0,0 @@
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<void>((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<string | number, RawMessage> = new Map<string | number, RawMessage>();
private globalMsgShortId = -2147483640;
private groupIds: number[] = [];
private LURCache = new LRU<number>();
private LastSentCache = new (class {
private cache: { gid: number; uid: number }[] = [];
private maxSize: number;
constructor(maxSize: number = 50000) {
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 (nodeObject) => {
Object.entries(nodeObject).forEach(async ([_groupId, datas]) => {
const userIds = datas.map(v => v.userId);
const groupId = Number(_groupId);
logDebug('插入发言时间', _groupId);
await this.createGroupInfoTimeTableIfNotExist(groupId);
const needCreatUsers = await this.getNeedCreatList(groupId, userIds);
const updateList = needCreatUsers.length > 0 ? datas.filter(user => !needCreatUsers.includes(user.userId)) : datas;
const insertList = needCreatUsers.map(userId => datas.find(e => userId == e.userId)!);
logDebug('updateList', updateList);
logDebug('insertList', insertList);
if (insertList.length) {
const insertSql = `INSERT INTO "${groupId}" (last_sent_time, user_id) VALUES ${insertList.map(() => '(?, ?)').join(', ')};`;
this.db!.all(insertSql, insertList.map(v => [v.value, v.userId]).flat(), err => {
if (err) {
logError(`${groupId} 插入失败`);
logError(`更新Sql : ${insertSql}`);
}
});
}
if (updateList.length) {
const updateSql =
`UPDATE "${groupId}" SET last_sent_time = CASE ` +
updateList.map(v => `WHEN user_id = ${v.userId} THEN ${v.value}`).join(' ') +
' ELSE last_sent_time END WHERE user_id IN ' +
`(${updateList.map(v => v.userId).join(', ')});`;
this.db!.all(updateSql, [], err => {
if (err) {
logError(`${groupId} 跟新失败`);
logError(`更新Sql : ${updateSql}`);
}
});
}
});
});
}
async getNeedCreatList(groupId: number, userIds: number[]) {
// 获取缓存中没有的
const unhas = userIds.filter(userId => !this.LastSentCache.get(groupId, userId));
if (unhas.length == 0) {
logDebug('缓存全部命中');
return [];
}
logDebug('缓存未全部命中');
const sql = `SELECT * FROM "${groupId}" WHERE user_id IN (${unhas.map(() => '?').join(',')})`;
return new Promise<number[]>((resolve) => {
this.db!.all(sql, unhas, (err, rows: { user_id: number }[]) => {
const has = rows.map(v => v.user_id);
const needCreatUsers = unhas.filter(userId => !has.includes(userId));
if (needCreatUsers.length == 0) {
logDebug('数据库全部命中');
} else {
logDebug('数据库未全部命中');
}
resolve(needCreatUsers);
});
});
}
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);
}
});
}
private async getCurrentMaxShortId() {
return new Promise<number>((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<RawMessage | null>((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<RawMessage | null> {
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<RawMessage | null> {
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<RawMessage | null> {
const stmt = 'SELECT * FROM msgs WHERE peerUid = ? AND seq = ?';
return this.getMsg(stmt, [peerUid, seq]);
}
async addMsg(msg: RawMessage, update = true): Promise<number> {
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<DBFile | null>((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<DBFile | null> {
return this.getFileCache('SELECT * FROM files WHERE name = ?', [name]);
}
async getFileCacheByUuid(uuid: string): Promise<DBFile | null> {
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);
});
});
}
async getLastSentTimeAndJoinTime(
groupId: number
): Promise<IRember[]> {
logDebug('读取发言时间', groupId);
return new Promise<IRember[]>((resolve, reject) => {
this.db!.all(`SELECT * FROM "${groupId}" `, (err, rows: IRember[]) => {
const cache = this.LURCache.get(groupId).map(e => ({ user_id: e.userId, last_sent_time: e.value }));
if (err) {
logError('查询发言时间失败', groupId);
return resolve(cache.map(e => ({ ...e, join_time: 0 })));
}
Object.assign(rows, cache);
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(),
logError('插入入群时间失败', userId, groupId);
}
);
}
}
export const dbUtil = new DBUtil();

View File

@@ -4,9 +4,8 @@ import crypto from 'crypto';
import util from 'util';
import path from 'node:path';
import { log, logError } from './log';
import { dbUtil } from '@/common/utils/db';
import * as fileType from 'file-type';
import { v4 as uuidv4 } from 'uuid';
import { randomUUID } from 'crypto';
import { napCatCore } from '@/core';
export const getNapCatDir = () => {
@@ -192,7 +191,7 @@ export async function uri2local(uri: string, fileName: string | null = null): Pr
isLocal: false
};
if (!fileName) {
fileName = uuidv4();
fileName = randomUUID();
}
let filePath = path.join(getTempDir(), fileName);
let url = null;
@@ -235,7 +234,7 @@ export async function uri2local(uri: string, fileName: string | null = null): Pr
}
fileName = fileName.replace(/[/\\:*?"<>|]/g, '_');
res.fileName = fileName;
filePath = path.join(getTempDir(), uuidv4() + fileName);
filePath = path.join(getTempDir(), randomUUID() + fileName);
fs.writeFileSync(filePath, buffer);
} catch (e: any) {
res.errMsg = `${url}下载失败,` + e.toString();
@@ -251,13 +250,14 @@ export async function uri2local(uri: string, fileName: string | null = null): Pr
} else {
filePath = pathname;
}
} else {
const cache = await dbUtil.getFileCacheByName(uri);
if (cache) {
filePath = cache.path;
} else {
filePath = uri;
}
}
else {
// const cache = await dbUtil.getFileCacheByName(uri);
// if (cache) {
// filePath = cache.path;
// } else {
// filePath = uri;
// }
}
res.isLocal = true;

View File

@@ -12,6 +12,25 @@ export function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
export function PromiseTimer<T>(promise: Promise<T>, ms: number): Promise<T> {
const timeoutPromise = new Promise<T>((_, reject) =>
setTimeout(() => reject(new Error("PromiseTimer: Operation timed out")), ms)
);
return Promise.race([promise, timeoutPromise]);
}
export async function runAllWithTimeout<T>(tasks: Promise<T>[], timeout: number): Promise<T[]> {
const wrappedTasks = tasks.map(task =>
PromiseTimer(task, timeout).then(
result => ({ status: 'fulfilled', value: result }),
error => ({ status: 'rejected', reason: error })
)
);
const results = await Promise.all(wrappedTasks);
return results
.filter(result => result.status === 'fulfilled')
.map(result => (result as { status: 'fulfilled'; value: T }).value);
}
export function getMd5(s: string) {
const h = crypto.createHash('md5');

View File

@@ -91,8 +91,10 @@ export function enableConsoleLog(enable: boolean) {
function formatMsg(msg: any[]) {
let logMsg = '';
for (const msgItem of msg) {
// 判断是否是对象
if (typeof msgItem === 'object') {
if (msgItem instanceof Error) { // 判断是否是错误
logMsg += msgItem.stack + ' ';
continue;
} else if (typeof msgItem === 'object') { // 判断是否是对象
const obj = JSON.parse(JSON.stringify(msgItem, null, 2));
logMsg += JSON.stringify(truncateString(obj)) + ' ';
continue;

View File

@@ -1,7 +1,7 @@
import os from 'node:os';
import path from 'node:path';
import { networkInterfaces } from 'os';
import { v4 as uuidv4 } from 'uuid';
import { randomUUID } from 'crypto';
// 缓解Win7设备兼容性问题
let osName: string;
@@ -30,7 +30,7 @@ export async function getMachineId(): Promise<string> {
if (!machineId) {
machineId = (async () => {
const id = await getMacMachineId();
return id || uuidv4(); // fallback, generate a UUID
return id || randomUUID(); // fallback, generate a UUID
})();
}

File diff suppressed because one or more lines are too long

View File

@@ -1,9 +1,6 @@
import {
CacheFileList,
CacheFileListItem,
CacheFileType,
CacheScanResult,
ChatCacheList,
ChatCacheListItemBasic,
ChatType,
ElementType, IMAGE_HTTP_HOST, IMAGE_HTTP_HOST_NT, RawMessage
@@ -15,28 +12,13 @@ import { log, logDebug, logError } from '@/common/utils/log';
import { GeneralCallResult, napCatCore, OnRichMediaDownloadCompleteParams } from '@/core';
import { calculateFileMD5 } from '@/common/utils/file';
import * as fileType from 'file-type';
import { MsgListener } from '@/core/listeners';
import imageSize from 'image-size';
import { ISizeCalculationResult } from 'image-size/dist/types/interface';
import { sessionConfig } from '@/core/sessionConfig';
import { randomUUID } from 'crypto';
import { rkeyManager } from '../utils/rkey';
import { NTEventDispatch } from '@/common/utils/EventTask';
import { NodeIKernelSearchService } from '../services/NodeIKernelSearchService';
const downloadMediaTasks: Map<string, (arg: OnRichMediaDownloadCompleteParams) => void> = new Map<string, (arg: OnRichMediaDownloadCompleteParams) => void>();
const downloadMediaListener = new MsgListener();
downloadMediaListener.onRichMediaDownloadComplete = arg => {
for (const [uuid, cb] of downloadMediaTasks) {
cb(arg);
downloadMediaTasks.delete(uuid);
}
};
setTimeout(() => {
napCatCore.onLoginSuccess(() => {
napCatCore.addListener(downloadMediaListener);
});
}, 100);
export class NTQQFileApi {
static async getFileType(filePath: string) {
return fileType.fileTypeFromFile(filePath);
@@ -103,31 +85,33 @@ export class NTQQFileApi {
return sourcePath;
}
}
//logDebug('start downloadMedia', msgId, chatType, peerUid, elementId, thumbPath, sourcePath, timeout, force);
return new Promise<string>((resolve, reject) => {
let completed = false;
const cb = (arg: OnRichMediaDownloadCompleteParams) => {
//logDebug('downloadMedia complete', arg, msgId);
let data = await NTEventDispatch.CallNormalEvent<
(
params: {
fileModelId: string,
downloadSourceType: number,
triggerType: number,
msgId: string,
chatType: ChatType,
peerUid: string,
elementId: string,
thumbSize: number,
downloadType: number,
filePath: string
}) => Promise<unknown>,
(fileTransNotifyInfo: OnRichMediaDownloadCompleteParams) => void
>(
'NodeIKernelMsgService/downloadRichMedia',
'NodeIKernelMsgListener/onRichMediaDownloadComplete',
1,
timeout,
(arg: OnRichMediaDownloadCompleteParams) => {
if (arg.msgId === msgId) {
completed = true;
let filePath = arg.filePath;
if (filePath.startsWith('\\')) {
// log('filePath start with \\');
const downloadPath = sessionConfig.defaultFileDownloadPath;
//logDebug('downloadPath', downloadPath);
filePath = path.join(downloadPath, filePath);
// 下载路径是下载文件夹的相对路径
}
resolve(filePath);
return true;
}
};
downloadMediaTasks.set(randomUUID(), cb);
setTimeout(() => {
if (!completed) {
reject('下载超时');
}
}, timeout);
napCatCore.session.getMsgService().downloadRichMedia({
return false;
},
{
fileModelId: '0',
downloadSourceType: 0,
triggerType: 1,
@@ -137,9 +121,18 @@ export class NTQQFileApi {
elementId: elementId,
thumbSize: 0,
downloadType: 1,
filePath: thumbPath,
});
});
filePath: thumbPath
}
);
let filePath = data[1].filePath;
if (filePath.startsWith('\\')) {
// log('filePath start with \\');
const downloadPath = sessionConfig.defaultFileDownloadPath;
//logDebug('downloadPath', downloadPath);
filePath = path.join(downloadPath, filePath);
// 下载路径是下载文件夹的相对路径
}
return filePath;
}
static async getImageSize(filePath: string): Promise<ISizeCalculationResult | undefined> {
@@ -153,27 +146,79 @@ export class NTQQFileApi {
});
});
}
static async searchfile(keys: string[]) {
static async getImageUrl(element: { originImageUrl: any; md5HexStr?: any; fileUuid: any; }, isPrivateImage: boolean) {
type EventType = NodeIKernelSearchService['searchFileWithKeywords'];
interface OnListener {
searchId: string,
hasMore: boolean,
resultItems: {
chatType: ChatType,
buddyChatInfo: any[],
discussChatInfo: any[],
groupChatInfo:
{
groupCode: string,
isConf: boolean,
hasModifyConfGroupFace: boolean,
hasModifyConfGroupName: boolean,
groupName: string,
remark: string
}[]
,
dataLineChatInfo: any[],
tmpChatInfo: any[],
msgId: string,
msgSeq: string,
msgTime: string,
senderUid: string,
senderNick: string,
senderRemark: string,
senderCard: string,
elemId: string,
elemType: number,
fileSize: string,
filePath: string,
fileName: string,
hits:
{
start: number,
end: number
}[]
}[]
};
const [id, data] = await NTEventDispatch.CallNormalEvent<EventType, (params: OnListener) => void>(
'NodeIKernelSearchService/searchFileWithKeywords',
'NodeIKernelSearchListener/onSearchFileKeywordsResult',
1,
10000,
(arg): boolean => { return id == data.searchId },
keys,
12
);
return data.resultItems[0];
}
static async getImageUrl(element: { originImageUrl: any; md5HexStr?: any; fileUuid: any; }) {
if (!element) {
return '';
}
const url = element.originImageUrl; // 没有域名
const url: string = element.originImageUrl; // 没有域名
const md5HexStr = element.md5HexStr;
const fileMd5 = element.md5HexStr;
const fileUuid = element.fileUuid;
if (url) {
if (url.startsWith('/download')) {
if (url.includes('&rkey=')) {
let UrlParse = new URL(IMAGE_HTTP_HOST + url);//临时解析拼接
let imageAppid = UrlParse.searchParams.get('appid');
let isNewPic = imageAppid && ['1406', '1407'].includes(imageAppid);
if (isNewPic) {
let UrlRkey = UrlParse.searchParams.get('rkey');
if (UrlRkey) {
return IMAGE_HTTP_HOST_NT + url;
}
const rkeyData = await rkeyManager.getRkey();
const existsRKey = isPrivateImage ? rkeyData.private_rkey : rkeyData.group_rkey;
return IMAGE_HTTP_HOST_NT + url + `${existsRKey}`;
UrlRkey = imageAppid === '1406' ? rkeyData.private_rkey : rkeyData.group_rkey;
return IMAGE_HTTP_HOST_NT + url + `${UrlRkey}`;
} else {
// 老的图片url不需要rkey
return IMAGE_HTTP_HOST + url;

View File

@@ -14,6 +14,7 @@ export class NTQQFriendApi {
'NodeIKernelBuddyListener/onBuddyListChange',
1,
5000,
() => true,
forced
);
const friends: User[] = [];

View File

@@ -1,12 +1,13 @@
import { GroupMember, GroupRequestOperateTypes, GroupMemberRole, GroupNotify, Group, MemberExtSourceType, GroupNotifyTypes } from '../entities';
import { GroupMember, GroupRequestOperateTypes, GroupMemberRole, GroupNotify, Group, MemberExtSourceType, GroupNotifyTypes, ChatType } from '../entities';
import { GeneralCallResult, NTQQUserApi, napCatCore } from '@/core';
import { NTEventDispatch } from '@/common/utils/EventTask';
import { logDebug } from '@/common/utils/log';
// console.log(process.pid);
// setTimeout(async () => {
// console.log(JSON.stringify(await NTQQGroupApi.getMemberExtInfo(), null, 2));
// }, 20000);
import { log } from '@/common/utils/log';
import { groupMembers } from '../data';
import { CacheClassFuncAsyncExtend, runAllWithTimeout } from '@/common/utils/helper';
export class NTQQGroupApi {
static async setGroupAvatar(gc: string, filePath: string) {
return napCatCore.session.getGroupService().setHeader(gc, filePath);
}
static async getGroups(forced = false) {
let [_retData, _updateType, groupList] = await NTEventDispatch.CallNormalEvent
<(force: boolean) => Promise<any>, (updateType: number, groupList: Group[]) => void>
@@ -15,10 +16,99 @@ export class NTQQGroupApi {
'NodeIKernelGroupListener/onGroupListUpdate',
1,
5000,
() => true,
forced
);
return groupList;
}
@CacheClassFuncAsyncExtend(600, "LastestSendTime", () => true)
static async getGroupMemberLastestSendTimeCache(GroupCode: string) {
return NTQQGroupApi.getGroupMemberLastestSendTime(GroupCode);
}
/**
* 通过QQ自带数据库获取群成员最后发言时间(仅返回有效数据 且消耗延迟大 需要进行缓存)
* @param GroupCode 群号
* @returns Map<string, string> key: uin value: sendTime
* @example
* let ret = await NTQQGroupApi.getGroupMemberLastestSendTime('123456');
* for (let [uin, sendTime] of ret) {
* console.log(uin, sendTime);
* }
*/
static async getGroupMemberLastestSendTime(GroupCode: string) {
async function getdata(uid: string) {
let NTRet = await NTQQGroupApi.getLastestMsgByUids(GroupCode, [uid]);
if (NTRet.result != 0 && NTRet.msgList.length < 1) {
return undefined;
}
return { sendUin: NTRet.msgList[0].senderUin, sendTime: NTRet.msgList[0].msgTime }
}
let currentGroupMembers = groupMembers.get(GroupCode);
let PromiseData: Promise<({
sendUin: string;
sendTime: string;
} | undefined)>[] = [];
let ret: Map<string, string> = new Map();
if (!currentGroupMembers) {
return ret;
}
for (let member of currentGroupMembers.values()) {
PromiseData.push(getdata(member.uid).catch(() => undefined));
}
let allRet = await runAllWithTimeout(PromiseData, 2500);
for (let PromiseDo of allRet) {
if (PromiseDo) {
ret.set(PromiseDo.sendUin, PromiseDo.sendTime);
}
}
return ret;
}
static async getLastestMsgByUids(GroupCode: string, uids: string[]) {
let ret = await napCatCore.session.getMsgService().queryMsgsWithFilterEx('0', '0', '0', {
chatInfo: {
peerUid: GroupCode,
chatType: ChatType.group,
},
filterMsgType: [],
filterSendersUid: uids,
filterMsgToTime: '0',
filterMsgFromTime: '0',
isReverseOrder: false,
isIncludeCurrent: true,
pageLimit: 1,
});
return ret;
}
static async getLastestMsg(GroupCode: string, uins: string[]) {
let uids: Array<string> = [];
for (let uin of uins) {
try {
let uid = await NTQQUserApi.getUidByUin(uin)
if (uid) {
uids.push(uid);
}
} catch (error) {
log("getLastestMsg--->", error);
return undefined;
}
}
let ret = await napCatCore.session.getMsgService().queryMsgsWithFilterEx('0', '0', '0', {
chatInfo: {
peerUid: GroupCode,
chatType: ChatType.group,
},
filterMsgType: [],
filterSendersUid: uids,
filterMsgToTime: '0',
filterMsgFromTime: '0',
isReverseOrder: false,
isIncludeCurrent: true,
pageLimit: 1,
});
return ret;
}
static async getGroupRecommendContactArkJson(GroupCode: string) {
return napCatCore.session.getGroupService().getGroupRecommendContactArkJson(GroupCode);
}
@@ -63,6 +153,7 @@ export class NTQQGroupApi {
'NodeIKernelGroupListener/onGroupSingleScreenNotifies',
1,
5000,
() => true,
false,
'',
num

View File

@@ -1,83 +1,56 @@
import { GetFileListParam, Peer, RawMessage, SendMessageElement } from '@/core/entities';
import { selfInfo } from '@/core/data';
import { log, logError } from '@/common/utils/log';
import { friends, groups, selfInfo } from '@/core/data';
import { log, logWarn } from '@/common/utils/log';
import { sleep } from '@/common/utils/helper';
import { napCatCore } from '@/core';
import { MsgListener, onGroupFileInfoUpdateParamType } from '@/core/listeners';
import { napCatCore, NTQQUserApi } from '@/core';
import { onGroupFileInfoUpdateParamType } from '@/core/listeners';
import { GeneralCallResult } from '@/core/services/common';
import { randomUUID } from 'crypto';
const sendMessagePool: Record<string, ((sendSuccessMsg: RawMessage) => void | Promise<void>) | null> = {};// peerUid: callbackFunc
const sendSuccessCBMap: Record<string, ((sendSuccessMsg: RawMessage) => boolean | Promise<boolean>) | null> = {};// uuid: callbackFunc
const GroupFileInfoUpdateTasks: Map<string, ((groupFileListResult: onGroupFileInfoUpdateParamType) => void)> = new Map();
const sentMsgTasks: Map<string, (msg: RawMessage) => void> = new Map();
const msgListener = new MsgListener();
msgListener.onGroupFileInfoUpdate = (groupFileListResult: onGroupFileInfoUpdateParamType) => {
for (const [uuid, cb] of GroupFileInfoUpdateTasks) {
cb(groupFileListResult);
GroupFileInfoUpdateTasks.delete(uuid);
import { MessageUnique } from '../../../common/utils/MessageUnique';
import { NTEventDispatch } from '@/common/utils/EventTask';
async function LoadMessageIdList(Peer: Peer, msgId: string) {
let msgList = await NTQQMsgApi.getMsgHistory(Peer, msgId, 50);
for (let j = 0; j < msgList.msgList.length; j++) {
let shortId = MessageUnique.createMsg(Peer, msgList.msgList[j].msgId);
}
};
msgListener.onAddSendMsg = (msgRecord: RawMessage) => {
// console.log("sent msg", msgRecord, sendMessagePool);
for (const [uuid, cb] of sentMsgTasks) {
cb(msgRecord);
sentMsgTasks.delete(uuid);
}
async function loadMessageUnique() {
if (groups.size > 100) {
logWarn('群数量大于100可能会导致性能问题');
}
if (sendMessagePool[msgRecord.peerUid]) {
const r = sendMessagePool[msgRecord.peerUid]?.(msgRecord);
if (r instanceof Promise) {
r.then().catch(logError);
let predict = (groups.size + friends.size) / 5;
predict = predict < 20 ? 20 : predict;
predict = predict > 50 ? 50 : predict;
//let waitpromise: Array<Promise<{ msgList: RawMessage[]; }>> = [];
MessageUnique.resize(predict * 50);
let RecentContact = await NTQQUserApi.getRecentContactListSnapShot(predict);
let LoadMessageIdDo: Array<Promise<void>> = new Array<Promise<void>>();
if (RecentContact?.info?.changedList && RecentContact?.info?.changedList?.length > 0) {
for (let i = 0; i < RecentContact.info.changedList.length; i++) {
let Peer: Peer = { chatType: RecentContact.info.changedList[i].chatType, peerUid: RecentContact.info.changedList[i].peerUid, guildId: '' };
LoadMessageIdDo.push(LoadMessageIdList(Peer, RecentContact.info.changedList[i].msgId));
}
}
};
msgListener.onMsgInfoListUpdate = (msgInfoList: RawMessage[]) => {
msgInfoList.forEach(msg => {
new Promise((resolve, reject) => {
for (const cbId in sendSuccessCBMap) {
const cb = sendSuccessCBMap[cbId]!;
const cbResult = cb(msg);
const checkResult = (result: boolean) => {
if (result) {
delete sendSuccessCBMap[cbId];
}
};
if (cbResult instanceof Promise) {
cbResult.then(checkResult);
} else {
checkResult(cbResult);
}
}
}).then().catch(log);
await Promise.all(LoadMessageIdDo).then(() => {
log(`[消息序列] 加载 ${predict * 50} 条历史消息记录完成`);
});
};
}
setTimeout(() => {
napCatCore.onLoginSuccess(() => {
napCatCore.addListener(msgListener);
napCatCore.onLoginSuccess(async () => {
await sleep(100);
loadMessageUnique().then().catch();
});
}, 100);
export class NTQQMsgApi {
// static napCatCore: NapCatCore | null = null;
// enum BaseEmojiType {
// NORMAL_EMOJI,
// SUPER_EMOJI,
// RANDOM_SUPER_EMOJI,
// CHAIN_SUPER_EMOJI,
// EMOJI_EMOJI
// }
// enum BaseEmojiType {
// NORMAL_EMOJI,
// SUPER_EMOJI,
// RANDOM_SUPER_EMOJI,
// CHAIN_SUPER_EMOJI,
// EMOJI_EMOJI
// }
static async setEmojiLike(peer: Peer, msgSeq: string, emojiId: string, set: boolean = true) {
// nt_qq//global//nt_data//Emoji//emoji-resource//sysface_res/apng/ 下可以看到所有QQ表情预览
// nt_qq\global\nt_data\Emoji\emoji-resource\face_config.json 里面有所有表情的id, 自带表情id是QSid, 标准emoji表情id是QCid
@@ -97,136 +70,119 @@ export class NTQQMsgApi {
static async getMsgsBySeqAndCount(peer: Peer, seq: string, count: number, desc: boolean, z: boolean) {
return await napCatCore.session.getMsgService().getMsgsBySeqAndCount(peer, seq, count, desc, z);
}
static async activateChat(peer: Peer) {
// await this.fetchRecentContact();
// await sleep(500);
}
static async activateChatAndGetHistory(peer: Peer) {
}
static async setMsgRead(peer: Peer) {
return napCatCore.session.getMsgService().setMsgRead(peer);
}
static async getGroupFileList(GroupCode: string, params: GetFileListParam) {
return new Promise<Array<any>>(async (resolve, reject) => {
let complete = false;
setTimeout(() => {
if (!complete) {
reject('获取群文件列表超时');
}
}, 5000);
const GroupFileInfoUpdateCB = (groupFileListResult: onGroupFileInfoUpdateParamType) => {
complete = true;
resolve(groupFileListResult.item);
};
GroupFileInfoUpdateTasks.set(randomUUID(), GroupFileInfoUpdateCB);
await napCatCore.session.getRichMediaService().getGroupFileList(GroupCode, params);
});
let data = await NTEventDispatch.CallNormalEvent<
(GroupCode: string, params: GetFileListParam) => Promise<unknown>,
(groupFileListResult: onGroupFileInfoUpdateParamType) => void
>(
'NodeIKernelRichMediaService/sendMsg',
'NodeIKernelMsgListener/onGroupFileInfoUpdate',
1,
5000,
(groupFileListResult: onGroupFileInfoUpdateParamType) => {
return true;
},
GroupCode,
params
);
return data[1].item;
}
static async getMsgHistory(peer: Peer, msgId: string, count: number) {
// 消息时间从旧到新
return napCatCore.session.getMsgService().getMsgsIncludeSelf(peer, msgId, count, true);
}
static async fetchRecentContact() {
}
static async recallMsg(peer: Peer, msgIds: string[]) {
await napCatCore.session.getMsgService().recallMsg({
chatType: peer.chatType,
peerUid: peer.peerUid
}, msgIds);
}
static async sendMsg(peer: Peer, msgElements: SendMessageElement[], waitComplete = true, timeout = 10000): Promise<RawMessage> {
const peerUid = peer.peerUid;
// 等待上一个相同的peer发送完
let checkLastSendUsingTime = 0;
const waitLastSend: () => Promise<void> = async () => {
if (checkLastSendUsingTime > timeout) {
throw ('发送超时');
}
const lastSending = sendMessagePool[peer.peerUid];
if (lastSending) {
// log("有正在发送的消息,等待中...")
await sleep(500);
checkLastSendUsingTime += 500;
return await waitLastSend();
} else {
return;
}
};
await waitLastSend();
return new Promise((resolve, reject) => {
let completed = false;
let sentMessage: RawMessage | null = null;
const sendSuccessCBId = randomUUID() as string;
sendSuccessCBMap[sendSuccessCBId] = (msgRecord: RawMessage) => {
if (msgRecord.msgId === sentMessage?.msgId) {
if (msgRecord.sendStatus === 2) {
delete sendSuccessCBMap[sendSuccessCBId];
completed = true;
resolve(msgRecord);
static async sendMsg(peer: Peer, msgElements: SendMessageElement[], waitComplete = true, timeout = 10000) {
let msgId = await NTQQMsgApi.getMsgUnique(await NTQQMsgApi.getServerTime());
let data = await NTEventDispatch.CallNormalEvent<
(msgId: string, peer: Peer, msgElements: SendMessageElement[], map: Map<any, any>) => Promise<unknown>,
(msgList: RawMessage[]) => void
>(
'NodeIKernelMsgService/sendMsg',
'NodeIKernelMsgListener/onMsgInfoListUpdate',
1,
timeout,
(msgRecords: RawMessage[]) => {
for (let msgRecord of msgRecords) {
if (msgRecord.msgId === msgId && msgRecord.sendStatus === 2) {
return true;
}
return false;
}
return false;
};
sendMessagePool[peerUid] = async (rawMessage: RawMessage) => {
// console.log('收到sent 消息', rawMessage.msgId);
delete sendMessagePool[peerUid];
sentMessage = rawMessage;
};
setTimeout(() => {
if (completed) return;
delete sendMessagePool[peerUid];
delete sendSuccessCBMap[sendSuccessCBId];
reject('发送超时');
}, timeout);
const result = napCatCore.session.getMsgService().sendMsg('0', peer, msgElements, new Map());
},
msgId,
peer,
msgElements,
new Map()
);
let retMsg = data[1].find(msgRecord => {
if (msgRecord.msgId === msgId) {
return true;
}
});
return retMsg;
}
static async getMsgUniqueEx(){
let msgId = await NTQQMsgApi.getMsgUnique(await NTQQMsgApi.getServerTime());
return msgId;
}
static async getMsgUnique(time: string) {
return napCatCore.session.getMsgService().getMsgUniqueId(time);
}
static async getServerTime() {
return napCatCore.session.getMSFService().getServerTime();
}
static async forwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) {
return napCatCore.session.getMsgService().forwardMsg(msgIds, srcPeer, [destPeer], new Map());
}
static async multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]): Promise<RawMessage> {
const msgInfos = msgIds.map(id => {
return { msgId: id, senderShowName: selfInfo.nick };
});
return new Promise((resolve, reject) => {
let complete = false;
const onSentCB = (msg: RawMessage) => {
const arkElement = msg.elements.find(ele => ele.arkElement);
if (!arkElement) {
// log("收到的不是转发消息")
return;
let data = await NTEventDispatch.CallNormalEvent<
(msgInfo: typeof msgInfos, srcPeer: Peer, destPeer: Peer, comment: Array<any>, attr: Map<any, any>,) => Promise<unknown>,
(msgList: RawMessage[]) => void
>(
'NodeIKernelMsgService/multiForwardMsgWithComment',
'NodeIKernelMsgListener/onMsgInfoListUpdate',
1,
5000,
(msgRecords: RawMessage[]) => {
for (let msgRecord of msgRecords) {
if (msgRecord.peerUid == destPeer.peerUid && msgRecord.senderUid == selfInfo.uid) {
return true;
}
}
const forwardData: any = JSON.parse(arkElement.arkElement.bytesData);
if (forwardData.app != 'com.tencent.multimsg') {
return;
}
if (msg.peerUid == destPeer.peerUid && msg.senderUid == selfInfo.uid) {
complete = true;
resolve(msg);
}
};
sentMsgTasks.set(randomUUID(), onSentCB);
setTimeout(() => {
if (!complete) {
reject('转发消息超时');
}
}, 5000);
napCatCore.session.getMsgService().multiForwardMsgWithComment(msgInfos, srcPeer, destPeer, [], new Map());
}
return false;
},
msgInfos,
srcPeer,
destPeer,
[],
new Map()
);
for (let msg of data[1]) {
const arkElement = msg.elements.find(ele => ele.arkElement);
if (!arkElement) {
continue;
}
const forwardData: any = JSON.parse(arkElement.arkElement.bytesData);
if (forwardData.app != 'com.tencent.multimsg') {
continue;
}
if (msg.peerUid == destPeer.peerUid && msg.senderUid == selfInfo.uid) {
return msg;
}
}
throw new Error('转发消息超时');
}
static async markallMsgAsRead() {
return napCatCore.session.getMsgService().setAllC2CAndGroupMsgRead();

View File

@@ -16,7 +16,7 @@ export class NTQQSystemApi {
static async translateEnWordToZn(words: string[]) {
return napCatCore.session.getRichMediaService().translateEnWordToZn(words);
}
//调用会超时 没灯用
//调用会超时 没灯用 (好像是通知listener的) onLineDev
static async getOnlineDev() {
return napCatCore.session.getMsgService().getOnLineDev();
}
@@ -33,7 +33,7 @@ export class NTQQSystemApi {
static async BootMiniApp(appfile: string, params: string) {
await napCatCore.session.getNodeMiscService().setMiniAppVersion('2.16.4');
let c = await napCatCore.session.getNodeMiscService().getMiniAppPath();
console.log(c);
return napCatCore.session.getNodeMiscService().startNewMiniApp(appfile, params);
}
}

View File

@@ -1,52 +1,26 @@
import { ModifyProfileParams, SelfInfo, User, UserDetailInfoByUin } from '@/core/entities';
import { friends, selfInfo } from '@/core/data';
import { CacheClassFuncAsync, CacheClassFuncAsyncExtend } from '@/common/utils/helper';
import { GeneralCallResult, napCatCore, NTQQFriendApi } from '@/core';
import { ProfileListener } from '@/core/listeners';
import { rejects } from 'assert';
import { randomUUID } from 'crypto';
import { napCatCore } from '@/core';
import { NodeIKernelProfileListener, ProfileListener } from '@/core/listeners';
import { RequestUtil } from '@/common/utils/request';
import { log, logDebug, logError, logWarn } from '@/common/utils/log';
import { logWarn } from '@/common/utils/log';
import { NTEventDispatch } from '@/common/utils/EventTask';
const userInfoCache: Record<string, User> = {}; // uid: User
import { NodeIKernelProfileService } from '@/core/services';
const profileListener = new ProfileListener();
const userDetailHandlers: Map<string, ((profile: User) => void)> = new Map();
profileListener.onProfileDetailInfoChanged = (profile) => {
userInfoCache[profile.uid] = profile;
userDetailHandlers.forEach(handler => handler(profile));
};
setTimeout(() => {
napCatCore.onLoginSuccess(() => {
napCatCore.addListener(profileListener);
});
}, 100);
// 老版本逻辑现已移除
// console.log('onProfileDetailInfoChanged', profile);
// recevCount++;
// firstProfile = profile;
// if (recevCount === 2) {
// profileService.removeKernelProfileListener(listenerId);
// // if (!completed) {
// completed = true;
// resolve(profile);
// // }
// }
// };
export class NTQQUserApi {
static async getProfileLike(uid: string) {
return napCatCore.session.getProfileLikeService().getBuddyProfileLike({
"friendUids": [
friendUids: [
uid
],
"basic": 1,
"vote": 1,
"favorite": 0,
"userProfile": 1,
"type": 2,
"start": 0,
"limit": 20
basic: 1,
vote: 1,
favorite: 0,
userProfile: 1,
type: 2,
start: 0,
limit: 20
});
}
static async setLongNick(longNick: string) {
@@ -72,6 +46,9 @@ export class NTQQUserApi {
const ret = await napCatCore.session.getProfileService().setHeader(filePath) as setQQAvatarRet;
return { result: ret?.result, errMsg: ret?.errMsg };
}
static async setGroupAvatar(gc: string, filePath: string) {
return napCatCore.session.getGroupService().setHeader(gc, filePath);
}
static async getSelfInfo() {
@@ -87,45 +64,26 @@ export class NTQQUserApi {
// KQZONE,
// KOTHER
// }
static async getUserDetailInfo(uid: string): Promise<User> {
// const existUser = userInfoCache[uid];
// if (existUser) {
// return existUser;
// }
const profileService = napCatCore.session.getProfileService();
// console.log('getUserDetailInfo', result);
return new Promise((resolve, reject) => {
const uuid = randomUUID();
let completed = false;
let retData: User | undefined = undefined;
let isFirst = true;
// 不管返回几次 超时有数据就该返回 兼容就好了
setTimeout(() => {
if (!completed) {
if (retData) {
resolve(retData);
} else {
reject('getUserDetailInfo timeout');
static async getUserDetailInfo(uid: string) {
type EventService = NodeIKernelProfileService['getUserDetailInfoWithBizInfo'];
type EventListener = NodeIKernelProfileListener['onProfileDetailInfoChanged'];
let [_retData, profile] = await NTEventDispatch.CallNormalEvent
<EventService, EventListener>
(
'NodeIKernelProfileService/getUserDetailInfoWithBizInfo',
'NodeIKernelProfileListener/onProfileDetailInfoChanged',
2,
5000,
(profile: User) => {
if (profile.uid === uid) {
return true;
}
}
userDetailHandlers.delete(uuid);
}, 5000);
userDetailHandlers.set(uuid, (profile) => {
if (profile.uid === uid) {
if (isFirst) {
retData = profile;
isFirst = false;
// console.log('getUserDetailInfo', profile);
} else {
completed = true;
resolve(profile);
}
}
});
profileService.getUserDetailInfoWithBizInfo(uid, [0]).then(result => {
// console.log('getUserDetailInfo', result);
});
});
return false;
},
uid,
[0]
);
return profile;
}
static async modifySelfProfile(param: ModifyProfileParams) {
return napCatCore.session.getProfileService().modifyDesktopMiniProfile(param);
@@ -255,6 +213,15 @@ export class NTQQUserApi {
// }
return uin;
}
static async getRecentContactListSnapShot(count: number) {
return await napCatCore.session.getRecentContactService().getRecentContactListSnapShot(count);
}
static async getRecentContactListSyncLimit(count: number) {
return await napCatCore.session.getRecentContactService().getRecentContactListSyncLimit(count);
}
static async getRecentContactListSync() {
return await napCatCore.session.getRecentContactService().getRecentContactListSync();
}
static async getRecentContactList() {
return await napCatCore.session.getRecentContactService().getRecentContactList();
}

View File

@@ -1,4 +1,5 @@
import QQWrapper, { NodeIQQNTWrapperEngine, NodeIQQNTWrapperSession, NodeQQNTWrapperUtil } from '@/core/wrapper';
import { DeviceList } from '@/onebot11/main';
import {
NodeIKernelLoginService,
NodeIKernelBuddyService,
@@ -17,11 +18,10 @@ import fs from 'node:fs';
import { appid, qqVersionConfigInfo } from '@/common/utils/QQBasicInfo';
import { hostname, systemVersion } from '@/common/utils/system';
import { genSessionConfig } from '@/core/sessionConfig';
import { dbUtil } from '@/common/utils/db';
import { sleep } from '@/common/utils/helper';
import crypto from 'node:crypto';
import { rawFriends, friends, groupMembers, groups, selfInfo, stat } from '@/core/data';
import { RawMessage } from '@/core/entities';
import { GroupMember, RawMessage } from '@/core/entities';
import { NTEventDispatch } from '@/common/utils/EventTask';
import {
enableConsoleLog,
@@ -84,20 +84,21 @@ export class NapCatCore {
const dataPath = path.resolve(this.dataPath, './NapCat/data');
fs.mkdirSync(dataPath, { recursive: true });
logDebug('本账号数据/缓存目录:', dataPath);
dbUtil.init(path.resolve(dataPath, `./${arg.uin}-v2.db`)).then(() => {
this.initDataListener();
this.onLoginSuccessFuncList.map(cb => {
new Promise((resolve, reject) => {
const result = cb(arg.uin, arg.uid);
if (result instanceof Promise) {
result.then(resolve).catch(reject);
}
}).then();
});
}).catch((e) => {
logError('数据库初始化失败', e);
// dbUtil.init(path.resolve(dataPath, `./${arg.uin}-v2.db`)).then(() => {
this.initDataListener();
this.onLoginSuccessFuncList.map(cb => {
new Promise((resolve, reject) => {
const result = cb(arg.uin, arg.uid);
if (result instanceof Promise) {
result.then(resolve).catch(reject);
}
}).then();
});
// }).catch((e) => {
// logError('数据库初始化失败', e);
// });
// this.initDataListener();
}).catch((e) => {
logError('initSession failed', e);
throw new Error(`启动失败: ${JSON.stringify(e)}`);
@@ -108,8 +109,8 @@ export class NapCatCore {
logError('登录失败(onQRCodeSessionFailed)', errMsg);
if (errType == 1 && errCode == 3) {
// 二维码过期刷新
this.loginService.getQRCodePicture();
}
this.loginService.getQRCodePicture();
};
this.loginListener.onLoginFailed = (args) => {
logError('登录失败(onLoginFailed)', args);
@@ -189,7 +190,6 @@ export class NapCatCore {
}
});
}
private initDataListener() {
// 消息相关
interface LineDevice {
@@ -208,10 +208,17 @@ export class NapCatCore {
}
const msgListener = new MsgListener();
msgListener.onLineDev = (Devices: LineDevice[]) => {
DeviceList.splice(0, DeviceList.length);
Devices.map((Device: LineDevice) => {
if (Device.clientType === 2) {
log('账号设备(' + Device.devUid + ') 在线状态变更');
}
let DeviceData = {
app_id: Device.devUid,
device_name: Device.clientType.toString(),
device_kind: Device.clientType.toString(),
};
DeviceList.push(DeviceData);
// if (Device.clientType === 2) {
// log('账号设备(' + Device.devUid + ') 在线状态变更');
// }
});
};
msgListener.onKickedOffLine = (Info: KickedOffLineInfo) => {
@@ -242,6 +249,7 @@ export class NapCatCore {
stat.last_message_time = Math.floor(Date.now() / 1000);
};
msgListener.onRecvMsg = (msgList: RawMessage[]) => {
// console.log(JSON.stringify(msgList[0],null,2));
stat.packet_received += 1;
stat.message_received += msgList.length;
stat.last_message_time = Math.floor(Date.now() / 1000);
@@ -346,7 +354,7 @@ export class NapCatCore {
// console.log('onMemberListChange', groupCode, arg);
};
groupListener.onMemberInfoChange = (groupCode, changeType, members) => {
// console.log('onMemberInfoChange', arg);
//console.log('onMemberInfoChange', groupCode, changeType, members);
if (changeType === 0 && members.get(selfInfo.uid)?.isDelete) {
// 自身退群或者被踢退群 5s用于Api操作 之后不再出现
setTimeout(() => {
@@ -359,6 +367,9 @@ export class NapCatCore {
members.forEach((member, uid) => {
const existMember = existMembers.get(uid);
if (existMember) {
// 检查管理变动
member.isChangeRole = this.checkAdminEvent(groupCode, member, existMember);
// 更新成员信息
Object.assign(existMember, member);
}
else {
@@ -471,6 +482,13 @@ export class NapCatCore {
const loginList = await this.loginService.getLoginList();
return loginList;
}
checkAdminEvent(groupCode: string, memberNew: GroupMember, memberOld: GroupMember | undefined): boolean {
if (memberNew.role !== memberOld?.role) {
log(`${groupCode} ${memberNew.nick} 角色变更为 ${memberNew.role === 3 ? '管理员' : '群员'}`);
return true;
}
return false;
}
}
export const napCatCore = new NapCatCore();

View File

@@ -10,7 +10,8 @@ import {
SendReplyElement,
sendShareLocationElement,
SendTextElement,
SendVideoElement
SendVideoElement,
viedo_type
} from './index';
import { promises as fs } from 'node:fs';
import ffmpeg from 'fluent-ffmpeg';
@@ -127,7 +128,7 @@ export class SendMsgElementConstructor {
return element;
}
static async video(filePath: string, fileName: string = '', diyThumbPath: string = ''): Promise<SendVideoElement> {
static async video(filePath: string, fileName: string = '', diyThumbPath: string = '', videotype: viedo_type = viedo_type.VIDEO_FORMAT_MP4): Promise<SendVideoElement> {
const { fileName: _fileName, path, fileSize, md5 } = await NTQQFileApi.uploadFile(filePath, ElementType.VIDEO);
if (fileSize === 0) {
throw '文件异常大小为0';
@@ -195,6 +196,7 @@ export class SendMsgElementConstructor {
thumbWidth: videoInfo.width,
thumbHeight: videoInfo.height,
fileSize: '' + fileSize,
//fileFormat: videotype
// fileUuid: "",
// transferStatus: 0,
// progress: 0,
@@ -246,7 +248,28 @@ export class SendMsgElementConstructor {
}
};
}
// NodeIQQNTWrapperSession sendMsg [
// "0",
// {
// "peerUid": "u_e_RIxgTs2NaJ68h0PwOPSg",
// "chatType": 1,
// "guildId": ""
// },
// [
// {
// "elementId": "0",
// "elementType": 6,
// "faceElement": {
// "faceIndex": 0,
// "faceType": 5,
// "msgType": 0,
// "pokeType": 1,
// "pokeStrength": 0
// }
// }
// ],
// {}
// ]
static face(faceId: number): SendFaceElement {
// 从face_config.json中获取表情名称
const sysFaces = faceConfig.sysface;

View File

@@ -52,4 +52,5 @@ export interface GroupMember {
isRobot: boolean;
sex?: Sex
qqLevel?: QQLevel
isChangeRole: boolean;
}

View File

@@ -526,7 +526,7 @@ export interface VideoElement {
thumbMd5?: string
fileTime?: number; // second
thumbSize?: number; // byte
fileFormat?: number; // 2表示mp4 参考下面条目
fileFormat?: viedo_type; // 2表示mp4 参考下面条目
fileSize?: string; // byte
thumbWidth?: number;
thumbHeight?: number;

View File

@@ -40,14 +40,14 @@ export interface onGroupFileInfoUpdateParamType {
// fromNick: '拾xxxx,
// sig: '0x'
// }
export interface TempOnRecvParams{
sessionType: number,//1
chatType: ChatType,//100
peerUid: string,//uid
groupCode: string,//gc
fromNick: string,//gc name
sig: string,
export interface TempOnRecvParams {
sessionType: number,//1
chatType: ChatType,//100
peerUid: string,//uid
groupCode: string,//gc
fromNick: string,//gc name
sig: string,
}
export interface IKernelMsgListener {
onAddSendMsg(msgRecord: RawMessage): void;
@@ -158,7 +158,41 @@ export interface IKernelMsgListener {
onRichMediaUploadComplete(fileTransNotifyInfo: unknown): void;
onSearchGroupFileInfoUpdate(searchGroupFileResult: unknown): void;
onSearchGroupFileInfoUpdate(searchGroupFileResult:
{
result: {
retCode: number,
retMsg: string,
clientWording: string
},
syncCookie: string,
totalMatchCount: number,
ownerMatchCount: number,
isEnd: boolean,
reqId: number,
item: Array<{
groupCode: string,
groupName: string,
uploaderUin: string,
uploaderName: string,
matchUin: string,
matchWords: Array<unknown>,
fileNameHits: Array<{
start: number,
end: number
}>,
fileModelId: string,
fileId: string,
fileName: string,
fileSize: string,
busId: number,
uploadTime: number,
modifyTime: number,
deadTime: number,
downloadTimes: number,
localPath: string
}>
}): void;
onSendMsgError(j2: unknown, contact: unknown, i2: unknown, str: unknown): void;

View File

@@ -0,0 +1,44 @@
interface IKernelRecentContactListener {
onDeletedContactsNotify(...args: unknown[]): unknown;
onRecentContactNotification(...args: unknown[]): unknown;
onMsgUnreadCountUpdate(...args: unknown[]): unknown;
onGuildDisplayRecentContactListChanged(...args: unknown[]): unknown;
onRecentContactListChanged(...args: unknown[]): unknown;
onRecentContactListChangedVer2(...args: unknown[]): unknown;
}
export interface NodeIKernelRecentContactListener extends IKernelRecentContactListener {
// eslint-disable-next-line @typescript-eslint/no-misused-new
new(listener: IKernelRecentContactListener): NodeIKernelRecentContactListener;
}
export class KernelRecentContactListener implements IKernelRecentContactListener {
onDeletedContactsNotify(...args: unknown[]) {
}
onRecentContactNotification(...args: unknown[]) {
}
onMsgUnreadCountUpdate(...args: unknown[]) {
}
onGuildDisplayRecentContactListChanged(...args: unknown[]) {
}
onRecentContactListChanged(...args: unknown[]) {
}
onRecentContactListChangedVer2(...args: unknown[]) {
}
}

View File

@@ -9,6 +9,7 @@ import {
import { GeneralCallResult } from '@/core/services/common';
export interface NodeIKernelGroupService {
setHeader(uid:string,path:string): unknown;
addKernelGroupListener(listener: NodeIKernelGroupListener): number;

View File

@@ -0,0 +1,3 @@
export interface NodeIKernelMSFService {
getServerTime(): string;
}

View File

@@ -1,4 +1,4 @@
import { ChatType, ElementType, Peer, RawMessage, SendMessageElement } from '@/core/entities';
import { ElementType, Peer, RawMessage, SendMessageElement } from '@/core/entities';
import { NodeIKernelMsgListener } from '@/core/listeners/NodeIKernelMsgListener';
import { GeneralCallResult } from '@/core/services/common';
@@ -21,7 +21,7 @@ export interface NodeIKernelMsgService {
getAutoReplyTextList(...args: unknown[]): unknown;
getOnLineDev(): Promise<any>;
getOnLineDev(): void;
kickOffLine(DevInfo: Object): unknown;
@@ -115,7 +115,7 @@ export interface NodeIKernelMsgService {
addLocalTofuRecordMsg(...args: unknown[]): unknown;
addLocalRecordMsg(...args: unknown[]): unknown;
addLocalRecordMsg(Peer: Peer, msgId: string, rawMessage: RawMessage, attr: Array<any> | number, front: boolean): Promise<unknown>;
deleteMsg(...args: unknown[]): unknown;
@@ -141,7 +141,7 @@ export interface NodeIKernelMsgService {
getLastMessageList(peer: Peer[]): Promise<unknown>;
getAioFirstViewLatestMsgs(...args: unknown[]): unknown;
getAioFirstViewLatestMsgs(peer: Peer, unknown: number): unknown;
getMsgs(peer: Peer, msgId: string, count: unknown, queryOrder: boolean): Promise<unknown>;
@@ -186,27 +186,48 @@ export interface NodeIKernelMsgService {
getSourceOfReplyMsgByClientSeqAndTime(...args: unknown[]): unknown;
getMsgsByTypeFilter(peer: Peer, msgId: string, cnt: unknown, queryOrder: boolean, typeFilters: unknown): unknown;
getMsgsByTypeFilter(peer: Peer, msgId: string, cnt: unknown, queryOrder: boolean, typeFilter: { type: number, subtype: Array<number> }): unknown;
getMsgsByTypeFilters(...args: unknown[]): unknown;
getMsgsByTypeFilters(peer: Peer, msgId: string, cnt: unknown, queryOrder: boolean, typeFilters: Array<{ type: number, subtype: Array<number> }>): unknown;
getMsgWithAbstractByFilterParam(...args: unknown[]): unknown;
queryMsgsWithFilter(...args: unknown[]): unknown;
/**
* @deprecated 该函数已被标记为废弃,请使用新的替代方法。
* 使用过滤条件查询消息列表的版本2接口。
*
* 该函数通过一系列过滤条件来查询特定聊天中的消息列表。这些条件包括消息类型、发送者、时间范围等。
* 函数返回一个Promise解析为查询结果的未知类型对象。
*
* @param MsgId 消息ID用于特定消息的查询。
* @param MsgTime 消息时间,用于指定消息的时间范围。
* @param param 查询参数对象,包含详细的过滤条件和分页信息。
* @param param.chatInfo 聊天信息包括聊天类型和对方用户ID。
* @param param.filterMsgType 需要过滤的消息类型数组,留空表示不过滤。
* @param param.filterSendersUid 需要过滤的发送者用户ID数组。
* @param param.filterMsgFromTime 查询消息的起始时间。
* @param param.filterMsgToTime 查询消息的结束时间。
* @param param.pageLimit 每页的消息数量限制。
* @param param.isReverseOrder 是否按时间顺序倒序返回消息。
* @param param.isIncludeCurrent 是否包含当前页码。
* @returns 返回一个Promise解析为查询结果的未知类型对象。
*/
queryMsgsWithFilterVer2(MsgId: string, MsgTime: string, param: {
chatInfo: {
chatType: number,
peerUid: string
},
filterMsgType: [],
filterSendersUid: [],
filterSendersUid: Array<string>,
filterMsgFromTime: string,
filterMsgToTime: string,
pageLimit: number,
isReverseOrder: boolean,
isIncludeCurrent: boolean
}): Promise<unknown>;
// this.chatType = i2;
// this.peerUid = str;
@@ -228,14 +249,15 @@ export interface NodeIKernelMsgService {
peerUid: string
},
filterMsgType: [],
filterSendersUid: [],
filterSendersUid: string[],
filterMsgFromTime: string,
filterMsgToTime: string,
pageLimit: number,
isReverseOrder: boolean,
isIncludeCurrent: boolean
}): Promise<unknown>;
}): Promise<GeneralCallResult & {
msgList: RawMessage[]
}>;
//queryMsgsWithFilterEx(this.$msgId, this.$msgTime, this.$msgSeq, this.$param)
queryFileMsgsDesktop(...args: unknown[]): unknown;
@@ -261,7 +283,19 @@ export interface NodeIKernelMsgService {
queryTroopEmoticonMsgs(...args: unknown[]): unknown;
queryMsgsAndAbstractsWithFilter(...args: unknown[]): unknown;
queryMsgsAndAbstractsWithFilter(msgId: string, msgTime: string, megSeq: string, param: {
chatInfo: {
chatType: number,
peerUid: string
},
filterMsgType: [],
filterSendersUid: [],
filterMsgFromTime: string,
filterMsgToTime: string,
pageLimit: number,
isReverseOrder: boolean,
isIncludeCurrent: boolean
}): unknown;
setFocusOnGuild(...args: unknown[]): unknown;
@@ -430,7 +464,10 @@ export interface NodeIKernelMsgService {
downloadRichMedia(...args: unknown[]): unknown;
getFirstUnreadMsgSeq(...args: unknown[]): unknown;
getFirstUnreadMsgSeq(args: {
peerUid: string
guildId: string
}): unknown;
getFirstUnreadCommonMsg(...args: unknown[]): unknown;

View File

@@ -1,5 +1,28 @@
import { Peer } from "../entities";
import { NodeIKernelRecentContactListener } from "../listeners/NodeIKernelRecentContactListener";
import { GeneralCallResult } from "./common";
export interface FSABRecentContactParams {
anchorPointContact: {
contactId: string;
sortField: string;
pos: number;
},
relativeMoveCount: number;
listType: number;
count: number;
fetchOld: boolean;
}
// {
// "anchorPointContact": {
// "contactId": "",
// "sortField": "",
// "pos": 0
// },
// "relativeMoveCount": 0,
// "listType": 1,
// "count": 200,
// "fetchOld": true
// }
export interface NodeIKernelRecentContactService {
setGuildDisplayStatus(...args: unknown[]): unknown; // 2 arguments
@@ -11,7 +34,14 @@ export interface NodeIKernelRecentContactService {
enterOrExitMsgList(...args: unknown[]): unknown; // 1 arguments
getRecentContactListSnapShot(...args: unknown[]): unknown; // 1 arguments
/*!---!*/getRecentContactListSnapShot(count: number): Promise<GeneralCallResult & {
info: {
errCode: number,
errMsg: string,
sortedContactList: Array<number>,
changedList: Array<any>
}
}>; // 1 arguments
clearMsgUnreadCount(...args: unknown[]): unknown; // 1 arguments
@@ -19,7 +49,7 @@ export interface NodeIKernelRecentContactService {
jumpToSpecifyRecentContact(...args: unknown[]): unknown; // 1 arguments
fetchAndSubscribeABatchOfRecentContact(...args: unknown[]): unknown; // 1 arguments
/*!---!*/fetchAndSubscribeABatchOfRecentContact(params: FSABRecentContactParams): unknown; // 1 arguments
addRecentContact(peer: Peer): unknown;
@@ -31,9 +61,9 @@ export interface NodeIKernelRecentContactService {
updateGameMsgConfigs(...args: unknown[]): unknown; // 1 arguments
removeKernelRecentContactListener(...args: unknown[]): unknown; // 1 arguments
removeKernelRecentContactListener(listenerid: number): unknown; // 1 arguments
addKernelRecentContactListener(...args: unknown[]): unknown; // 1 arguments
addKernelRecentContactListener(listener: NodeIKernelRecentContactListener): void;
clearRecentContactsByChatType(...args: unknown[]): unknown; // 1 arguments

View File

@@ -85,23 +85,23 @@ export interface NodeIKernelRichMediaService {
downloadFileForFileUuid(peer: Peer, arg1: string, arg3: unknown[]): unknown;
downloadFileByUrlListtransgroupfile(arg1: unknown, arg2: unknown): unknown;
downloadFileByUrlList(arg1: unknown, arg2: unknown): unknown;
downloadFileForFileInfotransgroupfile(arg1: unknown, arg2: unknown): unknown;
downloadFileForFileInfo(arg1: unknown, arg2: unknown): unknown;
createGroupFolder(GroupCode: string, FolderName: string): Promise<GeneralCallResult & { resultWithGroupItem: { result: any, groupItem: Array<any> } }>
downloadFile(arg1: unknown, arg2: unknown, arg3: unknown, arg4: unknown): unknown;
createGroupFoldertransgroupfile(arg1: unknown, arg2: unknown): unknown;
createGroupFolder(arg1: unknown, arg2: unknown): unknown;
downloadGroupFolder(arg1: unknown, arg2: unknown, arg3: unknown): unknown;
renameGroupFolder(arg1: unknown, arg2: unknown, arg3: unknown): unknown;
deleteGroupFoldertransgroupfile(arg1: unknown, arg2: unknown): unknown;
deleteGroupFolder(arg1: unknown, arg2: unknown): unknown;
deleteTransferInfotransgroupfile(arg1: unknown, arg2: unknown): unknown;
deleteTransferInfo(arg1: unknown, arg2: unknown): unknown;
cancelTransferTask(arg1: unknown, arg2: unknown, arg3: unknown): unknown;
@@ -122,18 +122,28 @@ export interface NodeIKernelRichMediaService {
}
}>;
getGroupFileInfotransgroupfile(arg1: unknown, arg2: unknown): unknown;
getGroupFileInfo(arg1: unknown, arg2: unknown): unknown;
getGroupFileListtransgroupfile(arg1: unknown, arg2: unknown): unknown;
getGroupFileList(arg1: unknown, arg2: unknown): unknown;
getGroupTransferListtransgroupfile(arg1: unknown, arg2: unknown): unknown;
getGroupTransferList(arg1: unknown, arg2: unknown): unknown;
renameGroupFile(arg1: unknown, arg2: unknown, arg3: unknown, arg4: unknown, arg5: unknown): unknown;
moveGroupFile(arg1: unknown, arg2: unknown, arg3: unknown, arg4: unknown, arg5: unknown): unknown;
transGroupFile(arg1: unknown, arg2: unknown): unknown;
searchGroupFile(
keywords: Array<string>,
param: {
groupIds: Array<string>,
fileType: number,
context: string,
count: number,
sortType: number,
groupNames: Array<string>
}): Promise<unknown>;
searchGroupFileByWord(arg1: unknown, arg2: unknown, arg3: unknown, arg4: unknown, arg5: unknown): unknown;
deleteGroupFile(GroupCode: string, params: Array<number>, Files: Array<string>): Promise<GeneralCallResult & {
@@ -152,7 +162,7 @@ export interface NodeIKernelRichMediaService {
queryPicDownloadSize(arg: unknown): unknown;
searchGroupFiletransgroupfile(arg1: unknown, arg2: unknown): unknown;
searchGroupFile(arg1: unknown, arg2: unknown): unknown;
searchMoreGroupFile(arg: unknown): unknown;
@@ -160,7 +170,7 @@ export interface NodeIKernelRichMediaService {
onlyDownloadFile(arg1: unknown, arg2: unknown, arg3: unknown): unknown;
onlyUploadFiletransgroupfile(arg1: unknown, arg2: unknown): unknown;
onlyUploadFile(arg1: unknown, arg2: unknown): unknown;
isExtraLargePic(arg1: unknown, arg2: unknown, arg3: unknown): unknown;

View File

@@ -1,4 +1,4 @@
export interface NodeIKernelSearchService{
export interface NodeIKernelSearchService {
addKernelSearchListenerr(...args: any[]): unknown;// needs 1 arguments
removeKernelSearchListenerr(...args: any[]): unknown;// needs 1 arguments
@@ -48,10 +48,10 @@ export interface NodeIKernelSearchService{
searchMsgWithKeywordsr(...args: any[]): unknown;// needs 2 arguments
searchMoreMsgWithKeywordsr(...args: any[]): unknown;// needs 1 arguments
cancelSearchMsgWithKeywordsr(...args: any[]): unknown;// needs 3 arguments
searchFileWithKeywordsr(...args: any[]): unknown;// needs 2 arguments
cancelSearchMsgWithKeywords(...args: any[]): unknown;// needs 3 arguments
searchFileWithKeywords(keywords: string[], source: number): Promise<unknown>;// needs 2 arguments
searchMoreFileWithKeywordsr(...args: any[]): unknown;// needs 1 arguments

View File

@@ -12,18 +12,16 @@ export interface NapCatConfig {
consoleLogLevel: LogLevel,
}
class Config extends ConfigBase<NapCatConfig> implements NapCatConfig{
class Config extends ConfigBase<NapCatConfig> implements NapCatConfig {
name: string = 'napcat'
fileLog = true;
consoleLog = true;
fileLogLevel = LogLevel.DEBUG;
consoleLogLevel = LogLevel.INFO;
fileLogLevel = LogLevel.DEBUG;
consoleLogLevel = LogLevel.INFO;
constructor() {
super();
}
getConfigPath() {
return path.join(this.getConfigDir(), `napcat_${selfInfo.uin}.json`);
}
}
export const napCatConfig = new Config();

View File

@@ -40,6 +40,7 @@ import { NodeIKernelUnitedConfigService } from './services/NodeIKernelUnitedConf
import { NodeIKernelSearchService } from './services/NodeIKernelSearchService';
import { NodeIKernelCollectionService } from './services/NodeIKernelCollectionService';
import { NodeIKernelRecentContactService } from './services/NodeIKernelRecentContactService';
import { NodeIKernelMSFService } from './services/NodeIKernelMSFService';
const __filename = fileURLToPath(import.meta.url);
@@ -231,7 +232,7 @@ export interface NodeIQQNTWrapperSession {
getLockService(): unknown;
getMSFService(): unknown
getMSFService(): NodeIKernelMSFService;
getGuildHotUpdateService(): unknown;

View File

@@ -13,13 +13,15 @@ import { deleteOldFiles, UpdateConfig } from './common/utils/helper';
import { dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import chalk from 'chalk';
import { randomInt } from 'crypto';
import { MessageUnique } from './common/utils/MessageUnique';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const tagColor = chalk.cyan;
program
.option('-q, --qq <type>', 'QQ号')
.option('-q, --qq [type]', 'QQ号')
.parse(process.argv);
//deleteOldFiles(path.join(__dirname, 'logs'), 3).then().catch();
@@ -67,7 +69,18 @@ const showQRCode = async (url: string, base64: string, buffer: Buffer) => {
});
});
};
const quickLoginQQ = cmdOptions.qq;
let quickLoginQQ = cmdOptions.qq; // undefine、true、string
const QuickLoginList = await napCatCore.getQuickLoginList();
if (quickLoginQQ == true) {
if (QuickLoginList.LocalLoginInfoList.length > 0) {
quickLoginQQ = QuickLoginList.LocalLoginInfoList[0].uin;
log('-q 指令指定使用最近的QQ进行快速登录');
} else {
quickLoginQQ = '';
}
}
// napCatCore.on('system.login.error', (result) => {
// console.error('登录失败', result);
// napCatCore.qrLogin().then().catch(console.error);
@@ -105,11 +118,14 @@ if (quickLoginQQ) {
logError('快速登录错误:', res.loginErrorInfo.errMsg);
}
}).catch((e) => {
logError(e);
logError('快速登录错误:', e);
napCatCore.qrLogin(showQRCode);
});
} else {
log('没有 -q 参数指定快速登录的QQ,将使用二维码登录方式');
log('没有 -q 指令指定快速登录,将使用二维码登录方式');
if (QuickLoginList.LocalLoginInfoList.length > 0) {
log(`可用于快速登录的QQ${QuickLoginList.LocalLoginInfoList.map((u, index) => `\n${index}: ${u.uin} ${u.nickName}`)}`);
}
napCatCore.qrLogin(showQRCode);
}

View File

@@ -15,6 +15,6 @@ export class SetConfigAction extends BaseAction<OB11Config, void> {
actionName = ActionName.SetConfig;
protected async _handle(payload: OB11Config): Promise<void> {
ob11Config.save(payload);
ob11Config.save(payload, true);
}
}

View File

@@ -4,13 +4,13 @@ import { ActionName } from '../types';
import { NTQQUserApi } from '@/core/apis';
export class GetProfileLike extends BaseAction<void, any> {
actionName = ActionName.GetProfileLike;
protected async _handle(payload: void) {
let ret = await NTQQUserApi.getProfileLike(selfInfo.uid);
let listdata: any[] = ret.info.userLikeInfos[0].favoriteInfo.userInfos;
for (let i = 0; i < listdata.length; i++) {
listdata[i].uin = parseInt((await NTQQUserApi.getUinByUid(listdata[i].uid)) || '');
}
return listdata;
actionName = ActionName.GetProfileLike;
protected async _handle(payload: void) {
const ret = await NTQQUserApi.getProfileLike(selfInfo.uid);
const listdata: any[] = ret.info.userLikeInfos[0].favoriteInfo.userInfos;
for (let i = 0; i < listdata.length; i++) {
listdata[i].uin = parseInt((await NTQQUserApi.getUinByUid(listdata[i].uid)) || '');
}
return listdata;
}
}

View File

@@ -0,0 +1,57 @@
import BaseAction from '../BaseAction';
import { ActionName, BaseCheckResult } from '../types';
import * as fs from 'node:fs';
import { NTQQUserApi } from '@/core/apis/user';
import { checkFileReceived, uri2local } from '@/common/utils/file';
import { NTQQGroupApi } from '@/core';
// import { log } from "../../../common/utils";
interface Payload {
file: string,
groupCode: string
}
export default class SetGroupHeader extends BaseAction<Payload, any> {
actionName = ActionName.SetGroupHeader;
// 用不着复杂检测
protected async check(payload: Payload): Promise<BaseCheckResult> {
if (!payload.file || typeof payload.file != 'string' || !payload.groupCode || typeof payload.groupCode != 'string') {
return {
valid: false,
message: 'file和groupCode字段不能为空或者类型错误',
};
}
return {
valid: true,
};
}
protected async _handle(payload: Payload): Promise<any> {
const { path, isLocal, errMsg } = (await uri2local(payload.file));
if (errMsg) {
throw `头像${payload.file}设置失败,file字段可能格式不正确`;
}
if (path) {
await checkFileReceived(path, 5000); // 文件不存在QQ会崩溃需要提前判断
const ret = await NTQQGroupApi.setGroupAvatar(payload.groupCode,path);
if (!isLocal) {
fs.unlink(path, () => { });
}
if (!ret) {
throw `头像${payload.file}设置失败,api无返回`;
}
// log(`头像设置返回:${JSON.stringify(ret)}`)
// if (ret['result'] == 1004022) {
// throw `头像${payload.file}设置失败,文件可能不是图片格式`;
// } else if (ret['result'] != 0) {
// throw `头像${payload.file}设置失败,未知的错误,${ret['result']}:${ret['errMsg']}`;
// }
return ret;
} else {
if (!isLocal) {
fs.unlink(path, () => { });
}
throw `头像${payload.file}设置失败,无法获取头像,文件可能不存在`;
}
return null;
}
}

View File

@@ -1,15 +1,15 @@
import BaseAction from '../BaseAction';
import fs from 'fs/promises';
import { dbUtil } from '@/common/utils/db';
import { ob11Config } from '@/onebot11/config';
import { log, logDebug } from '@/common/utils/log';
import { sleep } from '@/common/utils/helper';
import { uri2local } from '@/common/utils/file';
import { ActionName, BaseCheckResult } from '../types';
import { FileElement, RawMessage, VideoElement } from '@/core/entities';
import { NTQQFileApi } from '@/core/apis';
import { NTQQFileApi, NTQQMsgApi } from '@/core/apis';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import Ajv from 'ajv';
import { MessageUnique } from '@/common/utils/MessageUnique';
export interface GetFilePayload {
file: string; // 文件名或者fileUuid
@@ -45,69 +45,67 @@ export class GetFileBase extends BaseAction<GetFilePayload, GetFileResponse> {
return { id: element.elementId, element: element.fileElement };
}
protected async _handle(payload: GetFilePayload): Promise<GetFileResponse> {
let cache = await dbUtil.getFileCacheByName(payload.file);
if (!cache) {
cache = await dbUtil.getFileCacheByUuid(payload.file);
}
if (!cache) {
throw new Error('file not found');
}
const { enableLocalFile2Url } = ob11Config;
try {
await fs.access(cache.path, fs.constants.F_OK);
} catch (e) {
logDebug('local file not found, start download...');
// if (cache.url) {
// const downloadResult = await uri2local(cache.url);
// if (downloadResult.success) {
// cache.path = downloadResult.path;
// dbUtil.updateFileCache(cache).then();
// } else {
// throw new Error('file download failed. ' + downloadResult.errMsg);
// }
// } else {
// // 没有url的可能是私聊文件或者群文件需要自己下载
// log('需要调用 NTQQ 下载文件api');
let msg = await dbUtil.getMsgByLongId(cache.msgId);
// log('文件 msg', msg);
if (msg) {
// 构建下载函数
const downloadPath = await NTQQFileApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid,
cache.elementId, '', '');
// await sleep(1000);
// log('download result', downloadPath);
msg = await dbUtil.getMsgByLongId(cache.msgId);
// log('下载完成后的msg', msg);
cache.path = downloadPath!;
dbUtil.updateFileCache(cache).then();
// log('下载完成后的msg', msg);
// }
}
}
// log('file found', cache);
const res: GetFileResponse = {
file: cache.path,
url: cache.url,
file_size: cache.size.toString(),
file_name: cache.name
};
if (enableLocalFile2Url) {
if (!cache.url) {
try {
res.base64 = await fs.readFile(cache.path, 'base64');
} catch (e) {
throw new Error('文件下载失败. ' + e);
}
}
}
// if (autoDeleteFile) {
// setTimeout(() => {
// fs.unlink(cache.filePath)
// }, autoDeleteFileSecond * 1000)
throw new Error('file not found');
// let cache = await dbUtil.getFileCacheByName(payload.file);
// if (!cache) {
// cache = await dbUtil.getFileCacheByUuid(payload.file);
// }
return res;
// if (!cache) {
// throw new Error('file not found');
// }
// const { enableLocalFile2Url } = ob11Config;
// try {
// await fs.access(cache.path, fs.constants.F_OK);
// } catch (e) {
// logDebug('local file not found, start download...');
// // if (cache.url) {
// // const downloadResult = await uri2local(cache.url);
// // if (downloadResult.success) {
// // cache.path = downloadResult.path;
// // dbUtil.updateFileCache(cache).then();
// // } else {
// // throw new Error('file download failed. ' + downloadResult.errMsg);
// // }
// // } else {
// // // 没有url的可能是私聊文件或者群文件需要自己下载
// // log('需要调用 NTQQ 下载文件api');
// let peer = MessageUnique.getPeerByMsgId(cache.msgId);
// let msg = await NTQQMsgApi.getMsgsByMsgId(peer?.Peer!,cache.msgId);
// // log('文件 msg', msg);
// if (msg) {
// // 构建下载函数
// const downloadPath = await NTQQFileApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid,
// cache.elementId, '', '');
// // await sleep(1000);
// // log('download result', downloadPath);
// let peer = MessageUnique.getPeerByMsgId(cache.msgId);
// msg = await NTQQMsgApi.getMsgsByMsgId(peer?.Peer!,cache.msgId);
// // log('下载完成后的msg', msg);
// cache.path = downloadPath!;
// dbUtil.updateFileCache(cache).then();
// // log('下载完成后的msg', msg);
// // }
// }
// }
// // log('file found', cache);
// const res: GetFileResponse = {
// file: cache.path,
// url: cache.url,
// file_size: cache.size.toString(),
// file_name: cache.name
// };
// if (enableLocalFile2Url) {
// if (!cache.url) {
// try {
// res.base64 = await fs.readFile(cache.path, 'base64');
// } catch (e) {
// throw new Error('文件下载失败. ' + e);
// }
// }
// }
//return res;
}
}

View File

@@ -1,11 +1,10 @@
import BaseAction from '../BaseAction';
import { ActionName, BaseCheckResult } from '../types';
import { ActionName } from '../types';
import fs from 'fs';
import { join as joinPath } from 'node:path';
import { calculateFileMD5, getTempDir, httpDownload } from '@/common/utils/file';
import { v4 as uuid4 } from 'uuid';
import { randomUUID } from 'crypto';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import Ajv from 'ajv';
interface FileResponse {
file: string;
}
@@ -32,7 +31,7 @@ export default class GoCQHTTPDownloadFile extends BaseAction<Payload, FileRespon
PayloadSchema = SchemaData;
protected async _handle(payload: Payload): Promise<FileResponse> {
const isRandomName = !payload.name;
const name = payload.name || uuid4();
const name = payload.name || randomUUID();
const filePath = joinPath(getTempDir(), name);
if (payload.base64) {

View File

@@ -1,11 +1,10 @@
import BaseAction from '../BaseAction';
import { OB11ForwardMessage, OB11Message, OB11MessageData } from '../../types';
import { NTQQMsgApi } from '@/core/apis';
import { dbUtil } from '@/common/utils/db';
import { OB11Constructor } from '../../constructor';
import { ActionName, BaseCheckResult } from '../types';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import Ajv from 'ajv';
import { MessageUnique } from '@/common/utils/MessageUnique';
const SchemaData = {
type: 'object',
@@ -29,24 +28,19 @@ export class GoCQHTTPGetForwardMsgAction extends BaseAction<Payload, any> {
if (!msgId) {
throw Error('message_id is required');
}
let rootMsg = await dbUtil.getMsgByLongId(msgId);
const rootMsgId = MessageUnique.getShortIdByMsgId(msgId);
const rootMsg = MessageUnique.getMsgIdAndPeerByShortId(rootMsgId || parseInt(msgId));
if (!rootMsg) {
rootMsg = await dbUtil.getMsgByShortId(parseInt(msgId));
if (!rootMsg) {
throw Error('msg not found');
}
throw Error('msg not found');
}
const data = await NTQQMsgApi.getMultiMsg({
chatType: rootMsg.chatType,
peerUid: rootMsg.peerUid
}, rootMsg.msgId, rootMsg.msgId);
const data = await NTQQMsgApi.getMultiMsg(rootMsg.Peer, rootMsg.MsgId, rootMsg.MsgId);
if (!data || data.result !== 0) {
throw Error('找不到相关的聊天记录' + data?.errMsg);
}
const msgList = data.msgList;
const messages = await Promise.all(msgList.map(async msg => {
const resMsg = await OB11Constructor.message(msg);
resMsg.message_id = await dbUtil.addMsg(msg);
resMsg.message_id = await MessageUnique.createMsg({ guildId:'',chatType:msg.chatType,peerUid:msg.peerUid },msg.msgId)!;
return resMsg;
}));
messages.map(msg => {

View File

@@ -2,11 +2,11 @@ import BaseAction from '../BaseAction';
import { OB11Message, OB11User } from '../../types';
import { ActionName } from '../types';
import { ChatType } from '@/core/entities';
import { dbUtil } from '@/common/utils/db';
import { NTQQMsgApi } from '@/core/apis/msg';
import { OB11Constructor } from '../../constructor';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { NTQQFriendApi, NTQQUserApi } from '@/core';
import { MessageUnique } from '@/common/utils/MessageUnique';
interface Response {
messages: OB11Message[];
@@ -32,7 +32,7 @@ export default class GetFriendMsgHistory extends BaseAction<Payload, Response> {
if (!uid) {
throw `记录${payload.user_id}不存在`;
}
const startMsgId = (await dbUtil.getMsgByShortId(payload.message_seq))?.msgId || '0';
const startMsgId = (await MessageUnique.getMsgIdAndPeerByShortId(payload.message_seq))?.MsgId || '0';
const friend = await NTQQFriendApi.isBuddy(uid);
const historyResult = (await NTQQMsgApi.getMsgHistory({
chatType: friend ? ChatType.friend : ChatType.temp,
@@ -41,7 +41,7 @@ export default class GetFriendMsgHistory extends BaseAction<Payload, Response> {
//logDebug(historyResult);
const msgList = historyResult.msgList;
await Promise.all(msgList.map(async msg => {
msg.id = await dbUtil.addMsg(msg);
msg.id = await MessageUnique.createMsg({ guildId: '', chatType: msg.chatType, peerUid: msg.peerUid }, msg.msgId);
}));
const ob11MsgList = await Promise.all(msgList.map(msg => OB11Constructor.message(msg)));
return { 'messages': ob11MsgList };

View File

@@ -3,11 +3,10 @@ import { OB11Message, OB11User } from '../../types';
import { getGroup, groups } from '@/core/data';
import { ActionName } from '../types';
import { ChatType } from '@/core/entities';
import { dbUtil } from '@/common/utils/db';
import { NTQQMsgApi } from '@/core/apis/msg';
import { OB11Constructor } from '../../constructor';
import { logDebug } from '@/common/utils/log';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { MessageUnique } from '@/common/utils/MessageUnique';
interface Response {
messages: OB11Message[];
}
@@ -15,7 +14,7 @@ interface Response {
const SchemaData = {
type: 'object',
properties: {
group_id: { type: [ 'number' , 'string' ] },
group_id: { type: ['number', 'string'] },
message_seq: { type: 'number' },
count: { type: 'number' }
},
@@ -32,7 +31,7 @@ export default class GoCQHTTPGetGroupMsgHistory extends BaseAction<Payload, Resp
if (!group) {
throw `${payload.group_id}不存在`;
}
const startMsgId = (await dbUtil.getMsgByShortId(payload.message_seq))?.msgId || '0';
const startMsgId = (await MessageUnique.getMsgIdAndPeerByShortId(payload.message_seq))?.MsgId || '0';
// log("startMsgId", startMsgId)
const historyResult = (await NTQQMsgApi.getMsgHistory({
chatType: ChatType.group,
@@ -41,7 +40,7 @@ export default class GoCQHTTPGetGroupMsgHistory extends BaseAction<Payload, Resp
//logDebug(historyResult);
const msgList = historyResult.msgList;
await Promise.all(msgList.map(async msg => {
msg.id = await dbUtil.addMsg(msg);
msg.id = await MessageUnique.createMsg({ guildId: '', chatType: msg.chatType, peerUid: msg.peerUid }, msg.msgId);
}));
const ob11MsgList = await Promise.all(msgList.map(msg => OB11Constructor.message(msg)));
return { 'messages': ob11MsgList };

View File

@@ -3,6 +3,7 @@ import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { JSONSchema } from 'json-schema-to-ts';
import { NTQQSystemApi } from '@/core';
import { sleep } from '@/common/utils/helper';
const SchemaData = {
type: 'object',
@@ -15,7 +16,8 @@ export class GetOnlineClient extends BaseAction<void, Array<any>> {
actionName = ActionName.GetOnlineClient;
protected async _handle(payload: void) {
//console.log(await NTQQSystemApi.getOnlineDev());
NTQQSystemApi.getOnlineDev();
await sleep(500);
return DeviceList;
}
}

View File

@@ -1,31 +1,31 @@
import { dbUtil } from '@/common/utils/db';
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { NTQQGroupApi } from '@/core';
import { MessageUnique } from '@/common/utils/MessageUnique';
const SchemaData = {
type: 'object',
properties: {
message_id: { type: ['number', 'string'] }
},
required: ['message_id']
type: 'object',
properties: {
message_id: { type: ['number', 'string'] }
},
required: ['message_id']
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export default class DelEssenceMsg extends BaseAction<Payload, any> {
actionName = ActionName.DelEssenceMsg;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload): Promise<any> {
const msg = await dbUtil.getMsgByShortId(parseInt(payload.message_id.toString()));
if (!msg) {
throw new Error('msg not found');
}
return await NTQQGroupApi.removeGroupEssence(
msg.peerUin,
msg.msgId
);
actionName = ActionName.DelEssenceMsg;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload): Promise<any> {
const msg = await MessageUnique.getMsgIdAndPeerByShortId(parseInt(payload.message_id.toString()));
if (!msg) {
throw new Error('msg not found');
}
return await NTQQGroupApi.removeGroupEssence(
msg.Peer.peerUid,
msg.MsgId
);
}
}

View File

@@ -1,11 +1,10 @@
import { OB11GroupMember } from '../../types';
import { getGroup, getGroupMember, groupMembers } from '@/core/data';
import { getGroup, getGroupMember, groupMembers, selfInfo } 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 { logDebug } from '@/common/utils/log';
import { WebApi } from '@/core/apis/webapi';
import { NTQQGroupApi } from '@/core';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
@@ -28,25 +27,29 @@ class GetGroupMemberInfo extends BaseAction<Payload, OB11GroupMember> {
PayloadSchema = SchemaData;
protected async _handle(payload: Payload) {
const group = await getGroup(payload.group_id.toString());
const role = (await getGroupMember(payload.group_id, selfInfo.uin))?.role;
const isPrivilege = role === 3 || role === 4;
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);
//早返回
if (!member) {
throw (`群(${payload.group_id})成员${payload.user_id}不存在`);
}
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);
if (isPrivilege) {
const webGroupMembers = await WebApi.getGroupMembers(payload.group_id.toString());
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;
@@ -55,11 +58,17 @@ class GetGroupMemberInfo extends BaseAction<Payload, OB11GroupMember> {
retMember.level = webGroupMembers[i]?.lv.level.toString();
}
}
return retMember;
} else {
throw (`群(${payload.group_id})成员${payload.user_id}不存在`);
let LastestMsgList = await NTQQGroupApi.getLastestMsg(payload.group_id.toString(), [payload.user_id.toString()]);
if (LastestMsgList?.msgList?.length && LastestMsgList?.msgList?.length > 0) {
let last_send_time = LastestMsgList.msgList[0].msgTime;
if (last_send_time && last_send_time != '0' && last_send_time != '') {
retMember.last_sent_time = parseInt(last_send_time);
retMember.join_time = Math.round(Date.now() / 1000);//兜底数据 防止群管乱杀
}
}
}
return retMember;
}
}
export default GetGroupMemberInfo;
export default GetGroupMemberInfo;

View File

@@ -3,13 +3,9 @@ import { OB11GroupMember } from '../../types';
import { OB11Constructor } from '../../constructor';
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { napCatCore, NTQQGroupApi, NTQQUserApi } from '@/core';
import { NTQQGroupApi } from '@/core';
import { WebApi } from '@/core/apis/webapi';
import { logDebug } from '@/common/utils/log';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { ob11Config } from '@/onebot11/config';
import { dbUtil } from '@/common/utils/db';
import { TypeConvert } from '@/common/utils/type';
const SchemaData = {
type: 'object',
@@ -43,7 +39,11 @@ class GetGroupMemberList extends BaseAction<Payload, OB11GroupMember[]> {
const MemberMap: Map<number, OB11GroupMember> = new Map<number, OB11GroupMember>();
// 转为Map 方便索引
let date = Math.round(Date.now() / 1000);
for (let i = 0, len = _groupMembers.length; i < len; i++) {
// 保证基础数据有这个 同时避免群管插件过于依赖这个杀了
_groupMembers[i].join_time = date;
_groupMembers[i].last_sent_time = date;
MemberMap.set(_groupMembers[i].user_id, _groupMembers[i]);
}
@@ -63,15 +63,15 @@ class GetGroupMemberList extends BaseAction<Payload, OB11GroupMember[]> {
MemberMap.set(webGroupMembers[i]?.uin, MemberData);
}
}
} 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) => {
const MemberData = MemberMap.get(element.user_id);
} else {
const DateMap = await NTQQGroupApi.getGroupMemberLastestSendTimeCache(payload.group_id.toString());//开始从本地拉取
for (let DateUin of DateMap.keys()) {
const MemberData = MemberMap.get(parseInt(DateUin));
if (MemberData) {
MemberData.join_time = element.join_time;
MemberData.last_sent_time = element.last_sent_time;
MemberData.last_sent_time = parseInt(DateMap.get(DateUin)!);
//join_time 有基础数据兜底
}
});
}
}
// 还原索引到Array 一同返回
@@ -82,7 +82,7 @@ class GetGroupMemberList extends BaseAction<Payload, OB11GroupMember[]> {
// }
// _groupMembers = Array.from(retData);
_groupMembers = Array.from(MemberMap.values());
return _groupMembers;
}

View File

@@ -1,8 +1,8 @@
import { dbUtil } from '@/common/utils/db';
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { NTQQGroupApi, NTQQMsgApi } from '@/core';
import { MessageUnique } from '@/common/utils/MessageUnique';
const SchemaData = {
type: 'object',
@@ -18,13 +18,13 @@ export default class SetEssenceMsg extends BaseAction<Payload, any> {
actionName = ActionName.SetEssenceMsg;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload): Promise<any> {
const msg = await dbUtil.getMsgByShortId(parseInt(payload.message_id.toString()));
const msg = await MessageUnique.getMsgIdAndPeerByShortId(parseInt(payload.message_id.toString()));
if (!msg) {
throw new Error('msg not found');
}
return await NTQQGroupApi.addGroupEssence(
msg.peerUin,
msg.msgId
msg.Peer.peerUid,
msg.MsgId
);
}
}

View File

@@ -74,6 +74,7 @@ import DelEssenceMsg from './group/DelEssenceMsg';
import SetEssenceMsg from './group/SetEssenceMsg';
import GetRecentContact from './user/GetRecentContact';
import { GetProfileLike } from './extends/GetProfileLike';
import SetGroupHeader from './extends/SetGroupHeader';
export const actionHandlers = [
new RebootNormal(),
@@ -85,10 +86,6 @@ export const actionHandlers = [
new sharePeer(),
new CreateCollection(),
new SetLongNick(),
// new GetConfigAction(),
// new SetConfigAction(),
// new GetGroupAddRequest(),
// TranslateEnWordToZn = "translate_en2zh",
new ForwardFriendSingleMsg(),
new ForwardGroupSingleMsg(),
new MarkGroupMsgAsRead(),
@@ -105,9 +102,13 @@ export const actionHandlers = [
new GetMsg(),
new GetLoginInfo(),
new GetFriendList(),
new GetGroupList(), new GetGroupInfo(),
new GetGroupMemberList(), new GetGroupMemberInfo(),
new SendGroupMsg(), new SendPrivateMsg(), new SendMsg(),
new GetGroupList(),
new GetGroupInfo(),
new GetGroupMemberList(),
new GetGroupMemberInfo(),
new SendGroupMsg(),
new SendPrivateMsg(),
new SendMsg(),
new DeleteMsg(),
new SetGroupAddRequest(),
new SetFriendAddRequest(),
@@ -125,9 +126,7 @@ export const actionHandlers = [
new GetImage(),
new GetRecord(),
new SetMsgEmojiLike(),
// new CleanCache(),
new GetCookies(),
//
new SetOnlineStatus(),
new GetRobotUinRange(),
new GetFriendWithCategory(),
@@ -156,7 +155,8 @@ export const actionHandlers = [
new SetEssenceMsg(),
new GetRecentContact(),
new MarkAllMsgAsRead(),
new GetProfileLike()
new GetProfileLike(),
new SetGroupHeader()
];
function initActionMap() {

View File

@@ -1,13 +1,18 @@
import { NTQQMsgApi } from '@/core/apis';
import { ActionName } from '../types';
import BaseAction from '../BaseAction';
import { dbUtil } from '@/common/utils/db';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { MessageUnique } from '@/common/utils/MessageUnique';
const SchemaData = {
type: 'object',
properties: {
message_id: { type: 'number' },
message_id: {
oneOf:[
{ type: 'number' },
{ type: 'string' }
]
}
},
required: ['message_id']
} as const satisfies JSONSchema;
@@ -18,9 +23,9 @@ class DeleteMsg extends BaseAction<Payload, void> {
actionName = ActionName.DeleteMsg;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload) {
const msg = await dbUtil.getMsgByShortId(payload.message_id);
const msg = await MessageUnique.getMsgIdAndPeerByShortId(Number(payload.message_id));
if (msg) {
await NTQQMsgApi.recallMsg({ peerUid: msg.peerUid, chatType: msg.chatType }, [msg.msgId]);
await NTQQMsgApi.recallMsg(msg.Peer, [msg.MsgId]);
}
}
}

View File

@@ -1,16 +1,16 @@
import BaseAction from '../BaseAction';
import { NTQQMsgApi, NTQQUserApi } from '@/core/apis';
import { ChatType, Peer } from '@/core/entities';
import { dbUtil } from '@/common/utils/db';
import { ActionName } from '../types';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { MessageUnique } from '@/common/utils/MessageUnique';
const SchemaData = {
type: 'object',
properties: {
message_id: { type: 'number' },
group_id: { type: [ 'number' , 'string' ] },
user_id: { type: [ 'number' , 'string' ] }
group_id: { type: ['number', 'string'] },
user_id: { type: ['number', 'string'] }
},
required: ['message_id']
} as const satisfies JSONSchema;
@@ -30,18 +30,14 @@ class ForwardSingleMsg extends BaseAction<Payload, null> {
}
protected async _handle(payload: Payload): Promise<null> {
const msg = await dbUtil.getMsgByShortId(payload.message_id);
const msg = await MessageUnique.getMsgIdAndPeerByShortId(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,
},
const ret = await NTQQMsgApi.forwardMsg(msg.Peer,
peer,
[msg.msgId],
[msg.MsgId],
);
if (ret.result !== 0) {
throw new Error(`转发消息失败 ${ret.errMsg}`);

View File

@@ -2,8 +2,9 @@ import { OB11Message } from '../../types';
import { OB11Constructor } from '../../constructor';
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { dbUtil } from '@/common/utils/db';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { MessageUnique } from '@/common/utils/MessageUnique';
import { NTQQMsgApi } from '@/core';
export type ReturnDataType = OB11Message
@@ -11,7 +12,7 @@ export type ReturnDataType = OB11Message
const SchemaData = {
type: 'object',
properties: {
message_id: { type: ['number','string'] },
message_id: { type: ['number', 'string'] },
},
required: ['message_id']
} as const satisfies JSONSchema;
@@ -26,14 +27,15 @@ class GetMsg extends BaseAction<Payload, OB11Message> {
if (!payload.message_id) {
throw Error('参数message_id不能为空');
}
let msg = await dbUtil.getMsgByShortId(parseInt(payload.message_id.toString()));
if (!msg) {
msg = await dbUtil.getMsgByLongId(payload.message_id.toString());
}
if (!msg) {
const MsgShortId = await MessageUnique.getShortIdByMsgId(payload.message_id.toString());
const msgIdWithPeer = await MessageUnique.getMsgIdAndPeerByShortId(MsgShortId || parseInt(payload.message_id.toString()));
if (!msgIdWithPeer) {
throw ('消息不存在');
}
return await OB11Constructor.message(msg);
const msg = await NTQQMsgApi.getMsgsByMsgId(
{ guildId: '', peerUid: msgIdWithPeer?.Peer.peerUid, chatType: msgIdWithPeer.Peer.chatType },
[msgIdWithPeer?.MsgId || payload.message_id.toString()]);
return await OB11Constructor.message(msg.msgList[0]);
}
}

View File

@@ -5,18 +5,19 @@ import {
Group,
IdMusicSignPostData,
NTQQFileApi,
NTQQMsgApi,
SendArkElement,
SendMessageElement,
SendMsgElementConstructor,
SignMusicWrapper
} 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';
import { MessageUnique } from '@/common/utils/MessageUnique';
export type MessageContext = {
group?: Group,
@@ -27,27 +28,27 @@ async function handleOb11FileLikeMessage(
{ data: { file, name: payloadFileName } }: OB11MessageFileBase,
{ deleteAfterSentFiles }: MessageContext
) {
let uri = file;
const 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 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 fileMsgPeer = MessageUnique.getPeerByMsgId(cache.msgId);
// if (fileMsgPeer) {
// cache.path = await NTQQFileApi.downloadMedia(
// fileMsgPeer.MsgId, fileMsgPeer.Peer.chatType, fileMsgPeer.Peer.peerUid,
// cache.elementId, '', ''
// );
// uri = 'file://' + cache.path;
// dbUtil.updateFileCache(cache);
// }
// }
// logDebug('找到文件缓存', uri);
// }
const { path, isLocal, fileName, errMsg } = (await uri2local(uri));
@@ -69,9 +70,9 @@ const _handlers: {
// This picks the correct message type out
// How great the type system of TypeScript is!
context: MessageContext
) => SendMessageElement | undefined | Promise<SendMessageElement | undefined>
) => Promise<SendMessageElement | undefined>
} = {
[OB11MessageDataType.text]: ({ data: { text } }) => SendMsgElementConstructor.text(text),
[OB11MessageDataType.text]: async ({ data: { text } }) => SendMsgElementConstructor.text(text),
[OB11MessageDataType.at]: async ({ data: { qq: atQQ } }, context) => {
if (!context.group) return undefined;
@@ -86,15 +87,16 @@ const _handlers: {
},
[OB11MessageDataType.reply]: async ({ data: { id } }) => {
const replyMsg = await dbUtil.getMsgByShortId(parseInt(id));
const replyMsgM = MessageUnique.getMsgIdAndPeerByShortId(parseInt(id));
const replyMsg = (await NTQQMsgApi.getMsgsByMsgId(replyMsgM?.Peer!, [replyMsgM?.MsgId!])).msgList[0];
return replyMsg ?
SendMsgElementConstructor.reply(replyMsg.msgSeq, replyMsg.msgId, replyMsg.senderUin!, replyMsg.senderUin!) :
undefined;
},
[OB11MessageDataType.face]: ({ data: { id } }) => SendMsgElementConstructor.face(parseInt(id)),
[OB11MessageDataType.face]: async ({ data: { id } }) => SendMsgElementConstructor.face(parseInt(id)),
[OB11MessageDataType.mface]: ({
[OB11MessageDataType.mface]: async ({
data: {
emoji_package_id,
emoji_id,
@@ -144,13 +146,13 @@ const _handlers: {
[OB11MessageDataType.voice]: async (sendMsg, context) =>
SendMsgElementConstructor.ptt((await handleOb11FileLikeMessage(sendMsg, context)).path),
[OB11MessageDataType.json]: ({ data: { data } }) => SendMsgElementConstructor.ark(data),
[OB11MessageDataType.json]: async ({ data: { data } }) => SendMsgElementConstructor.ark(data),
[OB11MessageDataType.dice]: ({ data: { result } }) => SendMsgElementConstructor.dice(result),
[OB11MessageDataType.dice]: async ({ data: { result } }) => SendMsgElementConstructor.dice(result),
[OB11MessageDataType.RPS]: ({ data: { result } }) => SendMsgElementConstructor.rps(result),
[OB11MessageDataType.RPS]: async ({ data: { result } }) => SendMsgElementConstructor.rps(result),
[OB11MessageDataType.markdown]: ({ data: { content } }) => SendMsgElementConstructor.markdown(content),
[OB11MessageDataType.markdown]: async ({ data: { content } }) => SendMsgElementConstructor.markdown(content),
[OB11MessageDataType.music]: async ({ data }) => {
// 保留, 直到...找到更好的解决方案
@@ -202,13 +204,13 @@ const _handlers: {
}
},
[OB11MessageDataType.node]: () => undefined,
[OB11MessageDataType.node]: async () => undefined,
[OB11MessageDataType.forward]: () => undefined,
[OB11MessageDataType.forward]: async () => undefined,
[OB11MessageDataType.xml]: () => undefined,
[OB11MessageDataType.xml]: async () => undefined,
[OB11MessageDataType.poke]: () => undefined,
[OB11MessageDataType.poke]: async () => undefined,
[OB11MessageDataType.Location]: async () => {
return SendMsgElementConstructor.location();
@@ -219,7 +221,7 @@ const handlers = <{
[Key in OB11MessageDataType]: (
sendMsg: OB11MessageData,
context: MessageContext
) => SendMessageElement | undefined | Promise<SendMessageElement | undefined>
) => Promise<SendMessageElement | undefined>
}>_handlers;
export default async function createSendElements(
@@ -227,18 +229,20 @@ export default async function createSendElements(
group?: Group,
ignoreTypes: OB11MessageDataType[] = []
) {
const sendElements: SendMessageElement[] = [];
const deleteAfterSentFiles: string[] = [];
let callResultList: Array<Promise<SendMessageElement | undefined>> = [];
for (const sendMsg of messageData) {
if (ignoreTypes.includes(sendMsg.type)) {
continue;
}
const callResult = await handlers[sendMsg.type](
const callResult = handlers[sendMsg.type](
sendMsg,
{ group, deleteAfterSentFiles }
);
if (callResult) sendElements.push(callResult);
)?.catch(undefined);
callResultList.push(callResult);
}
let ret = await Promise.all(callResultList);
const sendElements: SendMessageElement[] = ret.filter(ele => ele) as SendMessageElement[];
return { sendElements, deleteAfterSentFiles };
}

View File

@@ -1,12 +1,11 @@
import { ChatType, ElementType, Group, NTQQMsgApi, Peer, RawMessage, SendMessageElement } from '@/core';
import { OB11MessageNode } from '@/onebot11/types';
import { selfInfo } from '@/core/data';
import { dbUtil } from '@/common/utils/db';
import createSendElements from '@/onebot11/action/msg/SendMsg/create-send-elements';
import { logDebug, logError } from '@/common/utils/log';
import { sleep } from '@/common/utils/helper';
import fs from 'node:fs';
import { normalize, sendMsg } from '@/onebot11/action/msg/SendMsg/index';
import { MessageUnique } from '@/common/utils/MessageUnique';
async function cloneMsg(msg: RawMessage): Promise<RawMessage | undefined> {
const selfPeer = {
@@ -14,7 +13,7 @@ async function cloneMsg(msg: RawMessage): Promise<RawMessage | undefined> {
peerUid: selfInfo.uid
};
// logDebug('克隆的目标消息', msg);
//logDebug('克隆的目标消息', msg);
const sendElements: SendMessageElement[] = [];
@@ -54,13 +53,14 @@ export async function handleForwardNode(destPeer: Peer, messageNodes: OB11Messag
const nodeId = messageNode.data.id;
// 有nodeId表示一个子转发消息卡片
if (nodeId) {
const nodeMsg = await dbUtil.getMsgByShortId(parseInt(nodeId));
const nodeMsg = MessageUnique.getMsgIdAndPeerByShortId(parseInt(nodeId));
if (!needClone) {
nodeMsgIds.push(nodeMsg!.msgId);
nodeMsgIds.push(nodeMsg!.MsgId);
} else {
if (nodeMsg!.peerUid !== selfInfo.uid) {
if (nodeMsg!.Peer.peerUid !== selfInfo.uid) {
// need cloning
const clonedMsg = await cloneMsg(nodeMsg!);
const rawClone = await NTQQMsgApi.getMsgsByMsgId(nodeMsg?.Peer!, [nodeMsg?.MsgId!]);
const clonedMsg = await cloneMsg(rawClone.msgList[0]);
if (clonedMsg) {
nodeMsgIds.push(clonedMsg.msgId);
}
@@ -91,7 +91,7 @@ export async function handleForwardNode(destPeer: Peer, messageNodes: OB11Messag
//logDebug(sendElementsSplit);
}
// log("分割后的转发节点", sendElementsSplit)
const MsgNodeList: Promise<RawMessage>[] = [];
const MsgNodeList: Promise<RawMessage | undefined>[] = [];
for (const sendElementsSplitElement of sendElementsSplit) {
MsgNodeList.push(sendMsg(selfPeer, sendElementsSplitElement, [], true));
await sleep(Math.trunc(sendElementsSplit.length / 10) * 100);
@@ -99,7 +99,9 @@ export async function handleForwardNode(destPeer: Peer, messageNodes: OB11Messag
}
for (const msgNode of MsgNodeList) {
const result = await msgNode;
nodeMsgIds.push(result.msgId);
if (result) {
nodeMsgIds.push(result.msgId);
}
//logDebug('转发节点生成成功', result.msgId);
}
} catch (e) {
@@ -107,13 +109,14 @@ export async function handleForwardNode(destPeer: Peer, messageNodes: OB11Messag
}
}
}
// 检查srcPeer是否一致不一致则需要克隆成自己的消息, 让所有srcPeer都变成自己的使其保持一致才能够转发
const nodeMsgArray: Array<RawMessage> = [];
let srcPeer: Peer | undefined = undefined;
let needSendSelf = false;
for (const msgId of nodeMsgIds) {
const nodeMsg = await dbUtil.getMsgByLongId(msgId);
const nodeMsgPeer = await MessageUnique.getPeerByMsgId(msgId);
const nodeMsg = (await NTQQMsgApi.getMsgsByMsgId(nodeMsgPeer?.Peer!, [msgId])).msgList[0];
if (nodeMsg) {
nodeMsgArray.push(nodeMsg);
if (!srcPeer) {
@@ -124,7 +127,7 @@ export async function handleForwardNode(destPeer: Peer, messageNodes: OB11Messag
}
}
}
//logDebug('nodeMsgArray', nodeMsgArray);
// logDebug('nodeMsgArray', nodeMsgArray);
nodeMsgIds = nodeMsgArray.map(msg => msg.msgId);
if (needSendSelf) {
//logDebug('需要克隆转发消息');

View File

@@ -8,13 +8,13 @@ import {
} from '@/onebot11/types';
import { ActionName, BaseCheckResult } from '@/onebot11/action/types';
import { getGroup } from '@/core/data';
import { dbUtil } from '@/common/utils/db';
import { ChatType, ElementType, Group, NTQQFileApi, NTQQFriendApi, NTQQMsgApi, NTQQUserApi, Peer, SendMessageElement, } from '@/core';
import fs from 'node:fs';
import { logDebug, logError } from '@/common/utils/log';
import { decodeCQCode } from '@/onebot11/cqcode';
import createSendElements from './create-send-elements';
import { handleForwardNode } from '@/onebot11/action/msg/SendMsg/handle-forward-node';
import { MessageUnique } from '@/common/utils/MessageUnique';
export interface ReturnDataType {
message_id: number;
@@ -66,10 +66,10 @@ export async function sendMsg(peer: Peer, sendElements: SendMessageElement[], de
}
const returnMsg = await NTQQMsgApi.sendMsg(peer, sendElements, waitComplete, timeout);
try {
returnMsg.id = await dbUtil.addMsg(returnMsg, false);
returnMsg!.id = await MessageUnique.createMsg({ chatType: peer.chatType, guildId: '', peerUid: peer.peerUid }, returnMsg!.msgId);
} catch (e: any) {
logDebug('发送消息id获取失败', e);
returnMsg.id = 0;
returnMsg!.id = 0;
}
deleteAfterSentFiles.map((f) => {
@@ -155,8 +155,8 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
if (getSpecialMsgNum(payload, OB11MessageDataType.node)) {
const returnMsg = await handleForwardNode(peer, messages as OB11MessageNode[], group);
if (returnMsg) {
const msgShortId = await dbUtil.addMsg(returnMsg!, false);
return { message_id: msgShortId };
const msgShortId = await MessageUnique.createMsg({ guildId: '', peerUid: peer.peerUid, chatType: peer.chatType }, returnMsg!.msgId);
return { message_id: msgShortId! };
} else {
throw Error('发送转发消息失败');
}
@@ -172,7 +172,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
const { sendElements, deleteAfterSentFiles } = await createSendElements(messages, group);
//console.log(peer, JSON.stringify(sendElements,null,2));
const returnMsg = await sendMsg(peer, sendElements, deleteAfterSentFiles);
return { message_id: returnMsg.id! };
return { message_id: returnMsg!.id! };
}
}

View File

@@ -1,8 +1,8 @@
import { ActionName } from '../types';
import BaseAction from '../BaseAction';
import { dbUtil } from '@/common/utils/db';
import { NTQQMsgApi } from '@/core/apis';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { MessageUnique } from '@/common/utils/MessageUnique';
const SchemaData = {
type: 'object',
@@ -19,16 +19,14 @@ export class SetMsgEmojiLike extends BaseAction<Payload, any> {
actionName = ActionName.SetMsgEmojiLike;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload) {
const msg = await dbUtil.getMsgByShortId(parseInt(payload.message_id.toString()));
const msg = MessageUnique.getMsgIdAndPeerByShortId(parseInt(payload.message_id.toString()));
if (!msg) {
throw new Error('msg not found');
}
if (!payload.emoji_id){
throw new Error('emojiId not found');
}
return await NTQQMsgApi.setEmojiLike({
chatType: msg.chatType,
peerUid: msg.peerUid
}, msg.msgSeq, payload.emoji_id.toString(), true);
const msgSeq = (await NTQQMsgApi.getMsgsByMsgId(msg.Peer, [msg.MsgId])).msgList[0].msgSeq;
return await NTQQMsgApi.setEmojiLike(msg.Peer, msgSeq, payload.emoji_id.toString(), true);
}
}

View File

@@ -7,7 +7,6 @@ import {
ChatCacheListItemBasic,
CacheFileType
} from '@/core/entities';
import { dbUtil } from '@/common/utils/db';
import { NTQQFileApi, NTQQFileCacheApi } from '@/core/apis/file';
import { logError } from '@/common/utils/log';

View File

@@ -94,9 +94,10 @@ export enum ActionName {
CreateCollection = 'create_collection',
GetCollectionList = 'get_collection_list',
SetLongNick = 'set_self_longnick',
SetEssenceMsg = "set_essence_msg",
DelEssenceMsg = "delete_essence_msg",
GetRecentContact = "get_recent_contact",
_MarkAllMsgAsRead = "_mark_all_as_read",
GetProfileLike = "get_profile_like"
SetEssenceMsg = 'set_essence_msg',
DelEssenceMsg = 'delete_essence_msg',
GetRecentContact = 'get_recent_contact',
_MarkAllMsgAsRead = '_mark_all_as_read',
GetProfileLike = 'get_profile_like',
SetGroupHeader = "set_group_head"
}

View File

@@ -6,6 +6,7 @@ import { NTQQUserApi } from '@/core';
export default class GetRecentContact extends BaseAction<void, any> {
actionName = ActionName.GetRecentContact;
protected async _handle(payload: void) {
return await NTQQUserApi.getRecentContactList()
//没有效果
return await NTQQUserApi.getRecentContactListSnapShot(10);
}
}

View File

@@ -42,6 +42,7 @@ export interface OB11Config {
}
class Config extends ConfigBase<OB11Config> implements OB11Config {
name: string = 'onebot11'
http = {
enable: false,
host: '',
@@ -72,10 +73,6 @@ class Config extends ConfigBase<OB11Config> implements OB11Config {
RecordList: [] as Array<string>
};
getConfigPath() {
return path.join(this.getConfigDir(), `onebot11_${selfInfo.uin}.json`);
}
protected getKeys(): string[] | null {
return null;
}

View File

@@ -27,7 +27,6 @@ import {
} from '@/core/entities';
import { EventType } from './event/OB11BaseEvent';
import { encodeCQCode } from './cqcode';
import { dbUtil } from '@/common/utils/db';
import { OB11GroupIncreaseEvent } from './event/notice/OB11GroupIncreaseEvent';
import { OB11GroupBanEvent } from './event/notice/OB11GroupBanEvent';
import { OB11GroupUploadNoticeEvent } from './event/notice/OB11GroupUploadNoticeEvent';
@@ -43,10 +42,10 @@ import { ob11Config } from '@/onebot11/config';
import { deleteGroup, getGroupMember, groupMembers, selfInfo, tempGroupCodeMap } from '@/core/data';
import { NTQQFileApi, NTQQGroupApi, NTQQMsgApi, NTQQUserApi } from '@/core/apis';
import { OB11GroupMsgEmojiLikeEvent } from '@/onebot11/event/notice/OB11MsgEmojiLikeEvent';
import { napCatCore } from '@/core';
import { OB11FriendPokeEvent, OB11GroupPokeEvent } from './event/notice/OB11PokeEvent';
import { OB11BaseNoticeEvent } from './event/notice/OB11BaseNoticeEvent';
import { OB11GroupEssenceEvent } from './event/notice/OB11GroupEssenceEvent';
import { MessageUnique } from '@/common/utils/MessageUnique';
export class OB11Constructor {
@@ -137,26 +136,26 @@ export class OB11Constructor {
message_data['type'] = 'reply';
// log("收到回复消息", element.replyElement.replayMsgSeq)
try {
// let retData = await NTQQMsgApi.getMsgsBySeqAndCount(
// {
// chatType: msg.chatType,
// peerUid: msg.peerUid,
// guildId: '',
// },
// element.replyElement.replayMsgSeq,
// 1,
// false,
// true
// );
let replyMsg = await NTQQMsgApi.getMsgsBySeqAndCount(
{
chatType: msg.chatType,
peerUid: msg.peerUid,
guildId: '',
},
element.replyElement.replayMsgSeq,
1,
true,
true
);
// console.log(JSON.stringify(retData, null, 2));
const replyMsg = await dbUtil.getMsgBySeq(msg.peerUid, element.replyElement.replayMsgSeq);
// log("找到回复消息", replyMsg.msgShortId, replyMsg.msgId)
if (replyMsg && replyMsg.id) {
message_data['data']['id'] = replyMsg.id!.toString();
// const replyMsg = await NTQQMsgApi.getMsgsBySeqAndCount({ peerUid: msg.peerUid, guildId: '', chatType: msg.chatType }, element.replyElement.replayMsgSeq, 1, true, true);
if (replyMsg) {
message_data['data']['id'] = MessageUnique.createMsg({ peerUid: msg.peerUid, guildId: '', chatType: msg.chatType }, replyMsg.msgList[0].msgId)?.toString();
}
else {
continue;
}
//log("找到回复消息", message_data['data']['id'], replyMsg.msgList[0].msgId)
} catch (e: any) {
logError('获取不到引用的消息', e.stack, element.replyElement.replayMsgSeq);
}
@@ -171,24 +170,24 @@ export class OB11Constructor {
// let currentRKey = "CAQSKAB6JWENi5LMk0kc62l8Pm3Jn1dsLZHyRLAnNmHGoZ3y_gDZPqZt-64"
try {
message_data['data']['url'] = await NTQQFileApi.getImageUrl(element.picElement, msg.chatType !== ChatType.group);
message_data['data']['url'] = await NTQQFileApi.getImageUrl(element.picElement);
} catch (e: any) {
logError('获取图片url失败', e.stack);
}
//console.log(message_data['data']['url'])
// message_data["data"]["file_id"] = element.picElement.fileUuid
message_data['data']['file_size'] = element.picElement.fileSize;
dbUtil.addFileCache({
name: element.picElement.fileName,
path: element.picElement.sourcePath,
size: element.picElement.fileSize,
url: message_data['data']['url'],
uuid: element.picElement.fileUuid || '',
msgId: msg.msgId,
element: element.picElement,
elementType: ElementType.PIC,
elementId: element.elementId
}).then();
// dbUtil.addFileCache({
// name: element.picElement.fileName,
// path: element.picElement.sourcePath,
// size: element.picElement.fileSize,
// url: message_data['data']['url'],
// uuid: element.picElement.fileUuid || '',
// msgId: msg.msgId,
// element: element.picElement,
// elementType: ElementType.PIC,
// elementId: element.elementId
// }).then();
// 不自动下载图片
}
@@ -203,17 +202,18 @@ export class OB11Constructor {
message_data['data']['file_id'] = videoOrFileElement.fileUuid;
message_data['data']['file_size'] = videoOrFileElement.fileSize;
if (!element.videoElement) {
dbUtil.addFileCache({
msgId: msg.msgId,
name: videoOrFileElement.fileName,
path: videoOrFileElement.filePath,
size: parseInt(videoOrFileElement.fileSize || '0'),
uuid: videoOrFileElement.fileUuid || '',
url: '',
element: element.videoElement || element.fileElement,
elementType: element.videoElement ? ElementType.VIDEO : ElementType.FILE,
elementId: element.elementId
}).then();
// dbUtil.addFileCache({
// msgId: msg.msgId,
// name: videoOrFileElement.fileName,
// path: videoOrFileElement.filePath,
// size: parseInt(videoOrFileElement.fileSize || '0'),
// uuid: videoOrFileElement.fileUuid || '',
// url: '',
// element: element.videoElement || element.fileElement,
// elementType: element.videoElement ? ElementType.VIDEO : ElementType.FILE,
// elementId: element.elementId
// }).then();
// }
}
}
else if (element.pttElement) {
@@ -222,17 +222,17 @@ export class OB11Constructor {
message_data['data']['path'] = element.pttElement.filePath;
// message_data["data"]["file_id"] = element.pttElement.fileUuid
message_data['data']['file_size'] = element.pttElement.fileSize;
dbUtil.addFileCache({
name: element.pttElement.fileName,
path: element.pttElement.filePath,
size: parseInt(element.pttElement.fileSize) || 0,
url: '',
uuid: element.pttElement.fileUuid || '',
msgId: msg.msgId,
element: element.pttElement,
elementType: ElementType.PTT,
elementId: element.elementId
}).then();
// dbUtil.addFileCache({
// name: element.pttElement.fileName,
// path: element.pttElement.filePath,
// size: parseInt(element.pttElement.fileSize) || 0,
// url: '',
// uuid: element.pttElement.fileUuid || '',
// msgId: msg.msgId,
// element: element.pttElement,
// elementType: ElementType.PTT,
// elementId: element.elementId
// }).then();
}
else if (element.arkElement) {
@@ -285,6 +285,7 @@ export class OB11Constructor {
else (resMsg.message as OB11MessageData[]).push(message_data);
resMsg.raw_message += cqCode;
}
}
resMsg.raw_message = resMsg.raw_message.trim();
return resMsg;
@@ -408,11 +409,12 @@ export class OB11Constructor {
const senderUin = emojiLikeData.gtip.qq.jp;
const msgSeq = emojiLikeData.gtip.url.msgseq;
const emojiId = emojiLikeData.gtip.face.id;
const replyMsg = await dbUtil.getMsgBySeq(msg.peerUid, msgSeq);
if (!replyMsg) {
const replyMsgList = (await NTQQMsgApi.getMsgsBySeqAndCount({ chatType: ChatType.group, guildId: '', peerUid: msg.peerUid }, msgSeq, 1, true, true)).msgList;
if (replyMsgList.length < 1) {
return;
}
return new OB11GroupMsgEmojiLikeEvent(parseInt(msg.peerUid), parseInt(senderUin), replyMsg.id!, [{
const replyMsg = replyMsgList[0];
return new OB11GroupMsgEmojiLikeEvent(parseInt(msg.peerUid), parseInt(senderUin), MessageUnique.getShortIdByMsgId(replyMsg?.msgId!)!, [{
emoji_id: emojiId,
count: 1
}]);
@@ -453,17 +455,17 @@ export class OB11Constructor {
}
}
if (grayTipElement.jsonGrayTipElement.busiId == 2401) {
let searchParams = new URL(json.items[0].jp).searchParams;
let msgSeq = searchParams.get('msgSeq')!;
let Group = searchParams.get('groupCode');
let Businessid = searchParams.get('businessid');
let Peer: Peer = {
const searchParams = new URL(json.items[0].jp).searchParams;
const msgSeq = searchParams.get('msgSeq')!;
const Group = searchParams.get('groupCode');
const Businessid = searchParams.get('businessid');
const Peer: Peer = {
guildId: '',
chatType: ChatType.group,
peerUid: Group!
};
let msgData = await NTQQMsgApi.getMsgsBySeqAndCount(Peer, msgSeq.toString(), 1, true, true);
return new OB11GroupEssenceEvent(parseInt(msg.peerUid), await dbUtil.addMsg(msgData.msgList[0]), parseInt(msgData.msgList[0].senderUin));
const msgData = await NTQQMsgApi.getMsgsBySeqAndCount(Peer, msgSeq.toString(), 1, true, true);
return new OB11GroupEssenceEvent(parseInt(msg.peerUid), MessageUnique.getShortIdByMsgId(msgData.msgList[0].msgId)!, parseInt(msgData.msgList[0].senderUin));
// 获取MsgSeq+Peer可获取具体消息
}
if (grayTipElement.jsonGrayTipElement.busiId == 2407) {

View File

@@ -1,14 +1,14 @@
import { OB11GroupNoticeEvent } from './OB11GroupNoticeEvent';
export class OB11GroupEssenceEvent extends OB11GroupNoticeEvent {
notice_type = 'essence';
message_id: number;
sender_id: number;
sub_type: 'add' | 'delete' = "add";
notice_type = 'essence';
message_id: number;
sender_id: number;
sub_type: 'add' | 'delete' = 'add';
constructor(groupId: number, message_id: number, sender_id: number) {
super();
this.group_id = groupId;
this.message_id = message_id;
this.sender_id = sender_id;
}
constructor(groupId: number, message_id: number, sender_id: number) {
super();
this.group_id = groupId;
this.message_id = message_id;
this.sender_id = sender_id;
}
}

View File

@@ -1,6 +1,4 @@
import { OB11GroupNoticeEvent } from './OB11GroupNoticeEvent';
import { dbUtil } from '@/common/utils/db';
import { ob11Config } from '@/onebot11/config';
type GroupIncreaseSubType = 'approve' | 'invite';
export class OB11GroupIncreaseEvent extends OB11GroupNoticeEvent {
@@ -13,8 +11,5 @@ export class OB11GroupIncreaseEvent extends OB11GroupNoticeEvent {
this.operator_id = operatorId;
this.user_id = userId;
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));
}
}

View File

@@ -1,5 +1,5 @@
import { napCatCore } from '@/core';
import { MsgListener, TempOnRecvParams } from '@/core/listeners';
import { DebugGroupListener, MsgListener, TempOnRecvParams } from '@/core/listeners';
import { OB11Constructor } from '@/onebot11/constructor';
import { postOB11Event } from '@/onebot11/server/postOB11Event';
import {
@@ -19,11 +19,10 @@ import { OB11Config, ob11Config } from '@/onebot11/config';
import { httpHeart, ob11HTTPServer } from '@/onebot11/server/http';
import { ob11WebsocketServer } from '@/onebot11/server/ws/WebsocketServer';
import { ob11ReverseWebsockets } from '@/onebot11/server/ws/ReverseWebsocket';
import { getGroup, getGroupMember, selfInfo, tempGroupCodeMap } from '@/core/data';
import { dbUtil } from '@/common/utils/db';
import { getGroup, getGroupMember, groupMembers, selfInfo, tempGroupCodeMap } from '@/core/data';
import { BuddyListener, GroupListener, NodeIKernelBuddyListener } from '@/core/listeners';
import { OB11FriendRequestEvent } from '@/onebot11/event/request/OB11FriendRequest';
import { NTQQGroupApi, NTQQUserApi, SignMusicWrapper } from '@/core/apis';
import { NTQQGroupApi, NTQQUserApi } from '@/core/apis';
import { log, logDebug, logError, setLogSelfInfo } from '@/common/utils/log';
import { OB11GroupRequestEvent } from '@/onebot11/event/request/OB11GroupRequest';
import { OB11GroupAdminNoticeEvent } from '@/onebot11/event/notice/OB11GroupAdminNoticeEvent';
@@ -32,11 +31,8 @@ import { OB11FriendRecallNoticeEvent } from '@/onebot11/event/notice/OB11FriendR
import { OB11GroupRecallNoticeEvent } from '@/onebot11/event/notice/OB11GroupRecallNoticeEvent';
import { logMessage, logNotice, logRequest } from '@/onebot11/log';
import { OB11Message } from '@/onebot11/types';
import { OB11LifeCycleEvent } from './event/meta/OB11LifeCycleEvent';
import { Data as SysData } from '@/proto/SysMessage';
import { Data as DeviceData } from '@/proto/SysMessage.DeviceChange';
import { OB11FriendPokeEvent, OB11GroupPokeEvent } from './event/notice/OB11PokeEvent';
import { isEqual } from '@/common/utils/helper';
import { MessageUnique } from '@/common/utils/MessageUnique';
//下面几个其实应该移进Core-Data 缓存实现 但是现在在这里方便
//
@@ -50,6 +46,10 @@ export let DeviceList = new Array<LineDevice>();
//peer->cached(boolen)
// const PokeCache = new Map<string, boolean>();
function check_http_ws_equal(conf: any) { // 放在NapCatOnebot11里能被onReady调用却不能被SetConfig调用 不知道为什么 只能放这里了
return isEqual(conf.http.port, conf.ws.port) && isEqual(conf.http.host, conf.ws.host);
}
export class NapCatOnebot11 {
private bootTime: number = Date.now() / 1000; // 秒
@@ -77,7 +77,11 @@ export class NapCatOnebot11 {
ob11HTTPServer.start(ob11Config.http.port, ob11Config.http.host);
}
if (ob11Config.ws.enable) {
ob11WebsocketServer.start(ob11Config.ws.port, ob11Config.ws.host);
if (check_http_ws_equal(ob11Config) && ob11HTTPServer.server) { // ob11HTTPServer.server != null 隐含了 ob11Config.http.enable == true 的条件
ob11WebsocketServer.start(ob11HTTPServer.server);
} else {
ob11WebsocketServer.start(ob11Config.ws.port, ob11Config.ws.host);
}
}
if (ob11Config.reverseWs.enable) {
ob11ReverseWebsockets.start();
@@ -89,109 +93,109 @@ export class NapCatOnebot11 {
// Create MsgListener
const msgListener = new MsgListener();
msgListener.onRecvSysMsg = async (protobufData: number[]) => {
function buf2hex(buffer: Buffer) {
return [...new Uint8Array(buffer)]
.map(x => x.toString(16).padStart(2, '0'))
.join('');
}
// let Data: Data = {
// header: {
// GroupNumber: 0,
// GroupString: '',
// QQ: 0,
// Uid: '',
// },
// Body: {
// MsgType: 0,
// SubType_0: 0,
// SubType_1: 0,
// MsgSeq: 0,
// Time: 0,
// MsgID: 0,
// Other: 0,
// function buf2hex(buffer: Buffer) {
// return [...new Uint8Array(buffer)]
// .map(x => x.toString(16).padStart(2, '0'))
// .join('');
// }
// // let Data: Data = {
// // header: {
// // GroupNumber: 0,
// // GroupString: '',
// // QQ: 0,
// // Uid: '',
// // },
// // Body: {
// // MsgType: 0,
// // SubType_0: 0,
// // SubType_1: 0,
// // MsgSeq: 0,
// // Time: 0,
// // MsgID: 0,
// // Other: 0,
// // }
// // };
// try {
// // 生产环境会自己去掉
// const hex = buf2hex(Buffer.from(protobufData));
// //console.log(hex);
// const sysMsg = SysData.fromBinary(Buffer.from(protobufData));
// const peeruin = sysMsg.header[0].peerNumber;
// const peeruid = sysMsg.header[0].peerString;
// const MsgType = sysMsg.body[0].msgType;
// const subType0 = sysMsg.body[0].subType0;
// const subType1 = sysMsg.body[0].subType1;
// // let pokeEvent: OB11FriendPokeEvent | OB11GroupPokeEvent;
// // //console.log(peeruid);
// // if (MsgType == 528 && subType0 == 290 && hex.length < 250 && hex.endsWith('04')) {
// // // 防止上报两次 私聊戳一戳
// // if (PokeCache.has(peeruid)) {
// // //log('[私聊] 用户 ', peeruin, ' 对你戳一戳');
// // pokeEvent = new OB11FriendPokeEvent(parseInt(selfInfo.uin), peeruin);
// // postOB11Event(pokeEvent);
// // }
// // PokeCache.set(peeruid, false);
// // setTimeout(() => {
// // PokeCache.delete(peeruid);
// // }, 1000);
// // }
// // if (MsgType == 732 && subType0 == 20 && hex.length < 150 && hex.endsWith('04')) {
// // // 防止上报两次 群聊戳一戳
// // if (PokeCache.has(peeruid)) {
// // log('[群聊] 群组 ', peeruin, ' 戳一戳');
// // pokeEvent = new OB11GroupPokeEvent(peeruin);
// // postOB11Event(pokeEvent);
// // }
// // PokeCache.set(peeruid, false);
// // setTimeout(() => {
// // PokeCache.delete(peeruid);
// // }, 1000);
// // }
// if (MsgType == 528 && subType0 == 349) {
// const sysDeviceMsg = DeviceData.fromBinary(Buffer.from(protobufData));
// DeviceList = [];
// sysDeviceMsg.event[0].content[0].devices.forEach(device => {
// DeviceList.push({
// app_id: '0',
// device_name: device.deviceName,
// device_kind: 'none'
// });
// // log('[设备列表] 设备名称: ' + device.deviceName);
// });
// }
// };
try {
// 生产环境会自己去掉
const hex = buf2hex(Buffer.from(protobufData));
//console.log(hex);
const sysMsg = SysData.fromBinary(Buffer.from(protobufData));
const peeruin = sysMsg.header[0].peerNumber;
const peeruid = sysMsg.header[0].peerString;
const MsgType = sysMsg.body[0].msgType;
const subType0 = sysMsg.body[0].subType0;
const subType1 = sysMsg.body[0].subType1;
// let pokeEvent: OB11FriendPokeEvent | OB11GroupPokeEvent;
// //console.log(peeruid);
// if (MsgType == 528 && subType0 == 290 && hex.length < 250 && hex.endsWith('04')) {
// // 防止上报两次 私聊戳一戳
// if (PokeCache.has(peeruid)) {
// //log('[私聊] 用户 ', peeruin, ' 对你戳一戳');
// pokeEvent = new OB11FriendPokeEvent(parseInt(selfInfo.uin), peeruin);
// postOB11Event(pokeEvent);
// }
// PokeCache.set(peeruid, false);
// setTimeout(() => {
// PokeCache.delete(peeruid);
// }, 1000);
// }
// if (MsgType == 732 && subType0 == 20 && hex.length < 150 && hex.endsWith('04')) {
// // 防止上报两次 群聊戳一戳
// if (PokeCache.has(peeruid)) {
// log('[群聊] 群组 ', peeruin, ' 戳一戳');
// pokeEvent = new OB11GroupPokeEvent(peeruin);
// postOB11Event(pokeEvent);
// }
// PokeCache.set(peeruid, false);
// setTimeout(() => {
// PokeCache.delete(peeruid);
// }, 1000);
// }
if (MsgType == 528 && subType0 == 349) {
const sysDeviceMsg = DeviceData.fromBinary(Buffer.from(protobufData));
DeviceList = [];
sysDeviceMsg.event[0].content[0].devices.forEach(device => {
DeviceList.push({
app_id: '0',
device_name: device.deviceName,
device_kind: 'none'
});
// log('[设备列表] 设备名称: ' + device.deviceName);
});
}
// 未区分增加与减少
// if (MsgType == 34 && subType0 == 0) {
// const role = (await getGroupMember(peeruin, selfInfo.uin))?.role;
// const isPrivilege = role === 3 || role === 4;
// if (!isPrivilege) {
// const leaveUin =
// log('[群聊] 群组 ', peeruin, ' 成员' + leaveUin + '退出');
// const groupDecreaseEvent = new OB11GroupDecreaseEvent(peeruin, parseInt(leaveUin), 0, 'leave');// 不知道怎么出去的
// postOB11Event(groupDecreaseEvent, true);
// }
// }
// MsgType (SubType) EventName
// 33 GroupMemIncreased
// 34 GroupMemberDecreased
// 44 GroupAdminChange
// 82 GroupMessage
// 84 GroupApply
// 87 InviteGroup
// 528 35 FriendApply
// 528 39 CardChange
// 528 68 GroupApply
// 528 138 C2CRecall
// 528 290 C2CPoke
// 528 349 DeviceChange
// 732 12 GroupBan
// 732 16 GroupUniqueTitleChange
// 732 17 GroupRecall
// 732 20 GroupCommonTips
// 732 21 EssenceMessage
} catch (e) {
log('解析SysMsg异常', e);
// console.log(e);
}
// // 未区分增加与减少
// // if (MsgType == 34 && subType0 == 0) {
// // const role = (await getGroupMember(peeruin, selfInfo.uin))?.role;
// // const isPrivilege = role === 3 || role === 4;
// // if (!isPrivilege) {
// // const leaveUin =
// // log('[群聊] 群组 ', peeruin, ' 成员' + leaveUin + '退出');
// // const groupDecreaseEvent = new OB11GroupDecreaseEvent(peeruin, parseInt(leaveUin), 0, 'leave');// 不知道怎么出去的
// // postOB11Event(groupDecreaseEvent, true);
// // }
// // }
// // MsgType (SubType) EventName
// // 33 GroupMemIncreased
// // 34 GroupMemberDecreased
// // 44 GroupAdminChange
// // 82 GroupMessage
// // 84 GroupApply
// // 87 InviteGroup
// // 528 35 FriendApply
// // 528 39 CardChange
// // 528 68 GroupApply
// // 528 138 C2CRecall
// // 528 290 C2CPoke
// // 528 349 DeviceChange
// // 732 12 GroupBan
// // 732 16 GroupUniqueTitleChange
// // 732 17 GroupRecall
// // 732 20 GroupCommonTips
// // 732 21 EssenceMessage
// } catch (e) {
// log('解析SysMsg异常', e);
// // console.log(e);
// }
};
msgListener.onKickedOffLine = (Info: KickedOffLineInfo) => {
// 下线通知
@@ -229,10 +233,8 @@ export class NapCatOnebot11 {
// });
// console.log(ret);
new Promise((resolve) => {
dbUtil.addMsg(m).then(msgShortId => {
m.id = msgShortId;
this.postReceiveMsg([m]).then().catch(logError);
}).catch(logError);
m.id = MessageUnique.createMsg({ chatType: m.chatType, peerUid: m.peerUid, guildId: '' }, m.msgId);
this.postReceiveMsg([m]).then().catch(logError);
}).then();
}
};
@@ -245,10 +247,8 @@ export class NapCatOnebot11 {
logMessage(_msg as OB11Message).then().catch(logError);
}).catch(logError);
if (ob11Config.reportSelfMessage) {
dbUtil.addMsg(msg).then(id => {
msg.id = id;
this.postReceiveMsg([msg]).then().catch(logError);
});
msg.id = MessageUnique.createMsg({ chatType: msg.chatType, peerUid: msg.peerUid, guildId: '' }, msg.msgId);
this.postReceiveMsg([msg]).then().catch(logError);
}
};
napCatCore.addListener(msgListener);
@@ -265,11 +265,59 @@ export class NapCatOnebot11 {
// GroupListener
const groupListener = new GroupListener();
// groupListener.onMemberListChange = async (arg: {
// sceneId: string,
// ids: string[],
// infos: Map<string, GroupMember>, // uid -> GroupMember
// finish: boolean,
// hasRobot: boolean
// }) => {
// }
groupListener.onGroupNotifiesUpdated = async (doubt, notifies) => {
//console.log('ob11 onGroupNotifiesUpdated', notifies[0]);
this.postGroupNotifies(notifies).then().catch(e => logError('postGroupNotifies error: ', e));
if (![GroupNotifyTypes.ADMIN_SET, GroupNotifyTypes.ADMIN_UNSET, GroupNotifyTypes.ADMIN_UNSET_OTHER].includes(notifies[0].type)) {
this.postGroupNotifies(notifies).then().catch(e => logError('postGroupNotifies error: ', e));
}
};
groupListener.onMemberInfoChange = async (groupCode: string, changeType: number, members: Map<string, GroupMember>) => {
//console.log("ob11 onMemberInfoChange", groupCode, changeType, members)
if (changeType === 1) {
let member;
for (const [key, value] of members) {
member = value;
break;
}
if (member) {
const existMembers = groupMembers.get(groupCode);
if (existMembers) {
const existMember = existMembers.get(member.uid);
if (existMember) {
if (existMember.isChangeRole) {
//console.log("ob11 onMemberInfoChange:eventMember:localMember", member, existMember)
const notify: GroupNotify[] = [
{
time: Date.now(),
seq: (Date.now() * 1000 * 1000).toString(),
type: member.role === GroupMemberRole.admin ? GroupNotifyTypes.ADMIN_SET : GroupNotifyTypes.ADMIN_UNSET_OTHER, // 8 设置; 13 取消
status: 0,
group: { groupCode: groupCode, groupName: '' },
user1: { uid: member.uid, nickName: member.nick },
user2: { uid: member.uid, nickName: member.nick },
actionUser: { uid: '', nickName: '' },
actionTime: '0',
invitationExt: { srcType: 0, groupCode: '0', waitStatus: 0 },
postscript: '',
repeatSeqs: [],
warningTips: ''
}
];
this.postGroupNotifies(notify).then().catch(e => logError('postGroupNotifies error: ', e));
}
}
}
}
}
// 如果自身是非管理员也许要从这里获取Delete 成员变动 待测试与验证
const role = (await getGroupMember(groupCode, selfInfo.uin))?.role;
const isPrivilege = role === 3 || role === 4;
@@ -298,9 +346,6 @@ export class NapCatOnebot11 {
const { debug, reportSelfMessage } = ob11Config;
for (const message of msgList) {
logDebug('收到新消息', message);
// if (message.senderUin !== selfInfo.uin){
// message.msgShortId = await dbUtil.addMsg(message);
// }
OB11Constructor.message(message).then((msg) => {
logDebug('收到消息: ', msg);
if (debug) {
@@ -312,10 +357,6 @@ export class NapCatOnebot11 {
}
if (msg.post_type === 'message') {
logMessage(msg as OB11Message).then().catch(logError);
// 大概测试了一下10000个以内 includes 和 find 性能差距不大
if (msg.message_type == 'group' && msg.group_id && ob11Config.GroupLocalTime.Record && (ob11Config.GroupLocalTime.RecordList[0] === '-1' || ob11Config.GroupLocalTime.RecordList.find(gid => gid == msg.group_id?.toString()))) {
dbUtil.insertLastSentTime(msg.group_id, msg.user_id, msg.time);
}
} else if (msg.post_type === 'notice') {
logNotice(msg).then().catch(logError);
} else if (msg.post_type === 'request') {
@@ -353,51 +394,68 @@ export class NapCatOnebot11 {
// throw new Error('Invalid configuration object');
// }
const OldConfig = JSON.parse(JSON.stringify(ob11Config)); //进行深拷贝
ob11Config.save(NewOb11);//保存新配置
ob11Config.save(NewOb11, true);//保存新配置
const isHttpChanged = !isEqual(NewOb11.http.port, OldConfig.http.port);
const isHttpEnableChanged = !isEqual(NewOb11.http.enable, OldConfig.http.enable);
const isHttpChanged = !isEqual(NewOb11.http.enable, OldConfig.http.enable) ||
!isEqual(NewOb11.http.host, OldConfig.http.host) ||
!isEqual(NewOb11.http.port, OldConfig.http.port);
// const isHttpPostChanged = !isEqual(NewOb11.http.postUrls, OldConfig.http.postUrls);
// const isEnanleHttpPostChanged = !isEqual(NewOb11.http.enablePost, OldConfig.http.enablePost);
const isWsChanged = !isEqual(NewOb11.ws.port, OldConfig.ws.port);
const isEnableWsChanged = !isEqual(NewOb11.ws.enable, OldConfig.ws.enable);
const isWsChanged = !isEqual(NewOb11.ws.enable, OldConfig.ws.enable) ||
!isEqual(NewOb11.ws.host, OldConfig.ws.host) ||
!isEqual(NewOb11.ws.port, OldConfig.ws.port);
const isEnableWsReverseChanged = !isEqual(NewOb11.reverseWs.enable, OldConfig.reverseWs.enable);
const isWsReverseUrlsChanged = !isEqual(NewOb11.reverseWs.urls, OldConfig.reverseWs.urls);
const isWsReverseChanged = !isEqual(NewOb11.reverseWs.enable, OldConfig.reverseWs.enable) ||
!isEqual(NewOb11.reverseWs.urls, OldConfig.reverseWs.urls);
//const isEnableHeartBeatChanged = !isEqual(NewOb11.heartInterval, OldConfig.heartInterval);
// http重启逻辑
// console.log(isHttpEnableChanged, isHttpChanged, NewOb11.http.enable);
if ((isHttpEnableChanged || isHttpChanged) && NewOb11.http.enable) {
if (OldConfig.http.enable) {
ob11HTTPServer.stop();
}
ob11HTTPServer.start(NewOb11.http.port, NewOb11.http.host);
} else if (isHttpEnableChanged && !NewOb11.http.enable) {
ob11HTTPServer.stop();
}
// ws重启逻辑
if ((isEnableWsChanged || isWsChanged) && NewOb11.ws.enable) {
if (OldConfig.ws.enable) {
if (check_http_ws_equal(NewOb11) || check_http_ws_equal(OldConfig)) {
// http与ws共站 需要同步重启
if (isHttpChanged || isWsChanged) {
log("http与ws进行热重载")
ob11WebsocketServer.stop();
ob11HTTPServer.stop();
if (NewOb11.http.enable) {
ob11HTTPServer.start(NewOb11.http.port, NewOb11.http.host);
}
if (NewOb11.ws.enable) {
if (check_http_ws_equal(NewOb11) && ob11HTTPServer.server) {
ob11WebsocketServer.start(ob11HTTPServer.server);
} else {
ob11WebsocketServer.start(NewOb11.ws.port, NewOb11.ws.host);
}
}
}
} else {
// http重启逻辑
if (isHttpChanged) {
log("http进行热重载")
ob11HTTPServer.stop();
if (NewOb11.http.enable) {
ob11HTTPServer.start(NewOb11.http.port, NewOb11.http.host);
}
}
// ws重启逻辑
if (isWsChanged) {
log("ws进行热重载")
ob11WebsocketServer.stop();
if (NewOb11.ws.enable) {
ob11WebsocketServer.start(NewOb11.ws.port, NewOb11.ws.host);
}
}
ob11WebsocketServer.start(NewOb11.ws.port, NewOb11.ws.host);
} else if (isHttpEnableChanged && !NewOb11.http.enable) {
ob11WebsocketServer.stop();
}
// 反向ws重启逻辑
if ((isEnableWsReverseChanged || isWsReverseUrlsChanged) && NewOb11.reverseWs.enable) {
if (OldConfig.reverseWs.enable) {
ob11ReverseWebsockets.stop();
}
ob11ReverseWebsockets.start();
} else if (isHttpEnableChanged && !NewOb11.http.enable) {
if (isWsReverseChanged) {
log("反向ws进行热重载")
ob11ReverseWebsockets.stop();
if (NewOb11.reverseWs.enable) {
ob11ReverseWebsockets.start();
}
}
} catch (e) {
@@ -476,7 +534,7 @@ export class NapCatOnebot11 {
groupRequestEvent.flag = flag;
postOB11Event(groupRequestEvent);
} else if (notify.type == GroupNotifyTypes.INVITE_ME) {
logDebug('收到邀请我加群通知');
logDebug(`收到邀请我加群通知:${notify}`);
const groupInviteEvent = new OB11GroupRequestEvent();
groupInviteEvent.group_id = parseInt(notify.group.groupCode);
const user_id = (await NTQQUserApi.getUinByUid(notify.user2.uid)) || '';
@@ -497,12 +555,12 @@ export class NapCatOnebot11 {
// log("message update", message.sendStatus, message.msgId, message.msgSeq)
if (message.recallTime != '0') { //todo: 这个判断方法不太好,应该使用灰色消息元素来判断?
// 撤回消息上报
const oriMessage = await dbUtil.getMsgByLongId(message.msgId);
if (!oriMessage) {
const oriMessageId = await MessageUnique.getShortIdByMsgId(message.msgId);
if (!oriMessageId) {
continue;
}
if (message.chatType == ChatType.friend) {
const friendRecallEvent = new OB11FriendRecallNoticeEvent(parseInt(message!.senderUin), oriMessage!.id!);
const friendRecallEvent = new OB11FriendRecallNoticeEvent(parseInt(message!.senderUin), oriMessageId);
postOB11Event(friendRecallEvent);
} else if (message.chatType == ChatType.group) {
let operatorId = message.senderUin;
@@ -515,7 +573,7 @@ export class NapCatOnebot11 {
parseInt(message.peerUin),
parseInt(message.senderUin),
parseInt(operatorId),
oriMessage.id!
oriMessageId
);
postOB11Event(groupRecallEvent);
}

View File

@@ -11,7 +11,6 @@ import { normalize, sendMsg } from '../action/msg/SendMsg';
import { OB11FriendRequestEvent } from '../event/request/OB11FriendRequest';
import { OB11GroupRequestEvent } from '../event/request/OB11GroupRequest';
import { isNull } from '@/common/utils/helper';
import { dbUtil } from '@/common/utils/db';
import { getGroup, selfInfo } from '@/core/data';
import { NTQQFriendApi, NTQQGroupApi, NTQQUserApi } from '@/core/apis';
import createSendElements from '../action/msg/SendMsg/create-send-elements';
@@ -125,7 +124,6 @@ export function postOB11Event(msg: QuickActionEvent, reportSelf = false, postWs
}
async function handleMsg(msg: OB11Message, quickAction: QuickAction) {
msg = msg as OB11Message;
const rawMessage = await dbUtil.getMsgByShortId(msg.message_id);
const reply = quickAction.reply;
const peer: Peer = {
chatType: ChatType.friend,

View File

@@ -1,4 +1,5 @@
import { WebSocket } from 'ws';
import http from 'http';
import { actionMap } from '../../action';
import { OB11Response } from '../../action/OB11Response';
import { postWsEvent, registerWsEventSender, unregisterWsEventSender } from '../postOB11Event';
@@ -18,7 +19,7 @@ const heartbeatRunning = false;
class OB11WebsocketServer extends WebsocketServerBase {
public start(port: number, host: string) {
public start(port: number | http.Server, host: string = '') {
this.token = ob11Config.token;
super.start(port, host);
}

View File

@@ -21,6 +21,10 @@ const __dirname = dirname(__filename);
*/
export async function InitWebUi() {
const config = await WebUiConfig.GetWebUIConfig();
if (config.port == 0) {
log('[NapCat] [WebUi] Current WebUi is not run.');
return;
}
app.use(express.json());
// 初始服务
app.all('/', (_req, res) => {

View File

@@ -29,7 +29,7 @@ async function onSettingWindowCreated(view: Element) {
SettingItem(
'<span id="napcat-update-title">Napcat</span>',
undefined,
SettingButton('V1.6.7', 'napcat-update-button', 'secondary')
SettingButton('V1.6.8', 'napcat-update-button', 'secondary')
),
]),
SettingList([

View File

@@ -167,7 +167,7 @@ async function onSettingWindowCreated(view) {
SettingItem(
'<span id="napcat-update-title">Napcat</span>',
void 0,
SettingButton("V1.6.7", "napcat-update-button", "secondary")
SettingButton("V1.6.8", "napcat-update-button", "secondary")
)
]),
SettingList([

94
test/MessageUnique.ts Normal file
View File

@@ -0,0 +1,94 @@
describe('MessageUniqueWrapper', () => {
let messageUniqueWrapper: MessageUniqueWrapper;
beforeEach(() => {
messageUniqueWrapper = new MessageUniqueWrapper();
});
test('createMsg should return a unique shortId for a new message', () => {
const peer = new Peer();
peer.chatType = 1;
peer.peerUid = '123';
const msgId = 'msgId123';
const key = `${msgId}|${peer.chatType}|${peer.peerUid}`;
const hash = crypto.createHash('sha1').update(key);
const expectedShortId = parseInt(hash.digest('hex').slice(0, 8), 16);
const shortId = messageUniqueWrapper.createMsg(peer, msgId);
expect(shortId).toBeDefined();
expect(shortId).toBe(expectedShortId);
});
test('createMsg should return undefined if the same message is added again', () => {
const peer = new Peer();
peer.chatType = 1;
peer.peerUid = '123';
const msgId = 'msgId123';
const shortId = messageUniqueWrapper.createMsg(peer, msgId);
const secondShortId = messageUniqueWrapper.createMsg(peer, msgId);
expect(shortId).toBeDefined();
expect(secondShortId).toBeUndefined();
});
test('getMsgIdAndPeerByShortId should return the message and peer for a given shortId', () => {
const peer = new Peer();
peer.chatType = 1;
peer.peerUid = '123';
const msgId = 'msgId123';
messageUniqueWrapper.createMsg(peer, msgId);
const shortId = messageUniqueWrapper.getShortIdByMsgId(msgId);
const result = messageUniqueWrapper.getMsgIdAndPeerByShortId(shortId);
expect(result).toBeDefined();
expect(result.MsgId).toBe(msgId);
expect(result.Peer.chatType).toBe(peer.chatType);
expect(result.Peer.peerUid).toBe(peer.peerUid);
});
test('getMsgIdAndPeerByShortId should return undefined if the shortId does not exist', () => {
const peer = new Peer();
peer.chatType = 1;
peer.peerUid = '123';
const msgId = 'msgId123';
messageUniqueWrapper.createMsg(peer, msgId);
const invalidShortId = 12345678;
const result = messageUniqueWrapper.getMsgIdAndPeerByShortId(invalidShortId);
expect(result).toBeUndefined();
});
test('getShortIdByMsgId should return the shortId for a given message Id', () => {
const peer = new Peer();
peer.chatType = 1;
peer.peerUid = '123';
const msgId = 'msgId123';
messageUniqueWrapper.createMsg(peer, msgId);
const shortId = messageUniqueWrapper.getShortIdByMsgId(msgId);
expect(shortId).toBeDefined();
expect(typeof shortId).toBe('number');
});
test('getShortIdByMsgId should return undefined if the message Id does not exist', () => {
const peer = new Peer();
peer.chatType = 1;
peer.peerUid = '123';
const msgId = 'msgId123';
messageUniqueWrapper.createMsg(peer, msgId);
const invalidMsgId = 'invalidMsgId';
const shortId = messageUniqueWrapper.getShortIdByMsgId(invalidMsgId);
expect(shortId).toBeUndefined();
});
});

View File

@@ -1,4 +0,0 @@
let t = require('./NapCatNative.node');
console.log(t);
let r = t.ClearElectronAsNode();
console.log(r);

Binary file not shown.

View File

@@ -1,6 +0,0 @@
# Test For NapCatQQ
This is a test for NapCatQQ.
# 计划
1. 根据配置文件启动不同的测试 Event与Api
2. 标记特殊注意的测试

View File

@@ -1,4 +0,0 @@
def send_pic_local_msg(user, file):
pass
def send_pic_http_msg(user, pic_url):
pass

View File

@@ -1,6 +0,0 @@
import requests
import pyyaml
def __main__():
print("TEST")
__main__()

View File

@@ -1 +0,0 @@
# todo

View File

@@ -1 +0,0 @@
#发送消息

View File

@@ -1 +0,0 @@
pyyaml==5.4.1

View File

@@ -10,13 +10,11 @@ import fs from 'node:fs';
import babel from 'vite-plugin-babel';
import { version } from 'os';
// "@rollup/plugin-babel": "^6.0.4",
const external = ['silk-wasm', 'ws', 'express', 'uuid', 'fluent-ffmpeg', 'sqlite3', 'log4js',
'qrcode-terminal'];
const external = ['silk-wasm', 'ws', 'express', 'fluent-ffmpeg', 'log4js', 'qrcode-terminal'];
const nodeModules = [...builtinModules, builtinModules.map(m => `node:${m}`)].flat();
// let nodeModules = ["fs", "path", "events", "buffer", "url", "crypto", "fs/promise", "fsPromise", "os", "http", "net"]
// nodeModules = [...nodeModules, ...nodeModules.map(m => `node:${m}`)]
function genCpModule(module: string) {
return { src: `./node_modules/${module}`, dest: `dist/node_modules/${module}`, flatten: false };
}
@@ -28,9 +26,9 @@ if (process.env.NAPCAT_BUILDSYS == 'linux') {
} else if (process.env.NAPCAT_BUILDSYS == 'win32') {
if (process.env.NAPCAT_BUILDARCH == 'x64') {
}
startScripts = ['./script/napcat.ps1', './script/napcat.bat', './script/napcat-utf8.bat', './script/napcat-utf8.ps1', './script/napcat-log.ps1','./script/NapCat.164.bat','./script/napcat-9912.ps1','./script/napcat-9912-utf8.ps1','./script/napcat-9912.bat','./script/napcat-9912-utf8.bat'];
startScripts = ['./script/napcat.ps1', './script/napcat.bat', './script/napcat-utf8.bat', './script/napcat-utf8.ps1', './script/napcat-log.ps1', './script/NapCat.164.bat', './script/napcat-9912.ps1', './script/napcat-9912-utf8.ps1', './script/napcat-9912.bat', './script/napcat-9912-utf8.bat'];
} else {
startScripts = ['./script/napcat.sh', './script/napcat.ps1', './script/napcat.bat', './script/napcat-utf8.bat', './script/napcat-utf8.ps1', './script/napcat-log.ps1','./script/napcat-9912.ps1','./script/napcat-9912-utf8.ps1','./script/napcat-9912.bat','./script/napcat-9912-utf8.bat'];
startScripts = ['./script/napcat.sh', './script/napcat.ps1', './script/napcat.bat', './script/napcat-utf8.bat', './script/napcat-utf8.ps1', './script/napcat-log.ps1', './script/napcat-9912.ps1', './script/napcat-9912-utf8.ps1', './script/napcat-9912.bat', './script/napcat-9912-utf8.bat'];
}
const baseConfigPlugin: PluginOption[] = [
@@ -107,7 +105,7 @@ export default defineConfig(({ mode }): UserConfig => {
return {
...baseConfig(mode),
plugins: [
...baseConfigPlugin,
...baseConfigPlugin,
// {
// ...(obfuscator({
// options: {