Compare commits

...

51 Commits

Author SHA1 Message Date
手瓜一十雪
c54a58d6e4 release: v1.6.6 2024-07-16 15:55:25 +08:00
手瓜一十雪
34cb1ea3fd Merge pull request #126 from cnxysoft/main
修复戳一戳
2024-07-16 15:52:39 +08:00
Alen
f640b0ca91 修复戳一戳 2024-07-16 15:47:23 +08:00
手瓜一十雪
60e16da42e Merge pull request #121 from pohgxz/main
增加winQQ-9912一键启动脚本
2024-07-14 08:44:55 +08:00
Nepenthe
0cdceb95d6 增加winQQ-9912一键启动脚本 2024-07-13 16:09:14 +00:00
手瓜一十雪
70bd22d925 fix: typo 2024-07-13 20:27:18 +08:00
手瓜一十雪
82462dd647 docs: 规划 2024-07-13 20:21:48 +08:00
手瓜一十雪
c0466e943d build: test 2024-07-13 19:37:39 +08:00
手瓜一十雪
b187b4695d refactor: uin<->uid 2024-07-13 19:37:02 +08:00
手瓜一十雪
b1956d2a37 refactor: poke 2024-07-13 19:10:47 +08:00
手瓜一十雪
590b622e5f build: test 2024-07-13 18:58:52 +08:00
手瓜一十雪
3d8174396a feat: LinuxQQ版本25765 2024-07-13 18:58:29 +08:00
手瓜一十雪
b8dc6e9bd9 feat: 再次提升版本 25765 2024-07-13 18:56:42 +08:00
手瓜一十雪
8ee99109dc chore: 整理代码 2024-07-13 18:20:44 +08:00
手瓜一十雪
902041d4ee refactor: 新增启动脚本 2024-07-13 18:15:00 +08:00
手瓜一十雪
cc34aef47e style: code lint 2024-07-13 18:12:38 +08:00
手瓜一十雪
0afbbe7c7a refactor: 废弃部分代码 2024-07-13 18:10:41 +08:00
手瓜一十雪
8004553ba7 refactor: groupNotifies 2024-07-13 18:04:55 +08:00
手瓜一十雪
0023b2846a feat: 第二次大致整理 2024-07-13 17:23:05 +08:00
手瓜一十雪
34775c1816 fix: 整理常量 2024-07-12 18:08:45 +08:00
手瓜一十雪
e0759e704b feat:大部分消息元素 2024-07-12 18:01:48 +08:00
手瓜一十雪
0aa225ca78 fix: typo 2024-07-12 17:04:28 +08:00
手瓜一十雪
b43b4ee5c0 feat: test code 2024-07-12 16:59:08 +08:00
手瓜一十雪
0cdb8cecbf feat: 群精华 代码未测试 2024-07-12 11:02:10 +08:00
手瓜一十雪
fd6a306742 feat: 懒得写了 2024-07-12 10:54:01 +08:00
手瓜一十雪
7f3b3d2277 feat: 群精华 2024-07-12 10:46:57 +08:00
手瓜一十雪
8be5b977bf Merge pull request #117 from po-lan/main
对缓存进一步优化
2024-07-12 09:52:17 +08:00
po-lan
d7ddb15f9c 对缓存进一步优化
LRUCache 将所有被移除的缓存数据作为事件参数传递给事件处理程序。

在数据库操作部分,优化了读写流程,以确保每个群组至多执行三次数据库操作:

读取:先判断缓存中是否存在用户记录,若不存在则读取数据库。
创建:如果用户记录在数据库中不存在,则新增记录。
修改:如果用户记录在数据库中存在,则进行修改。
即使单个群组内有大量用户,每种操作也只会执行一次。
2024-07-12 00:46:03 +08:00
手瓜一十雪
9a6a1798d0 build: poke能用25493 2024-07-11 12:44:42 +08:00
手瓜一十雪
14196fd349 build: 移除调试代码 2024-07-11 12:31:00 +08:00
手瓜一十雪
941b89a523 feat: uin转换优化&poke支持重写 2024-07-11 12:28:11 +08:00
手瓜一十雪
a5f9e5f8c0 Merge pull request #113 from idranme/main
perf: audio
2024-07-11 09:49:56 +08:00
idranme
80c3356c8f perf: audio 2024-07-10 17:44:17 +00:00
手瓜一十雪
914136b750 refactor: 移除异常代码 2024-07-10 21:39:03 +08:00
手瓜一十雪
f9a60795f5 feat: uid转换优化 2024-07-10 21:33:31 +08:00
手瓜一十雪
19640927c7 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-07-10 21:11:43 +08:00
手瓜一十雪
22faac7e36 fix: friend uid 异常 2024-07-10 21:11:28 +08:00
手瓜一十雪
30d260ab32 Merge pull request #111 from idranme/main
fix: error catch
2024-07-10 11:55:53 +08:00
idranme
115120d066 Update file.ts 2024-07-10 11:35:55 +08:00
idranme
1327844736 fix: error catch 2024-07-10 03:25:25 +00:00
手瓜一十雪
29904f3cb7 feat: 164 way03启动脚本补充 2024-07-06 13:23:31 +08:00
手瓜一十雪
50395594b7 Merge pull request #106 from jetjinser/fix-editorconfig
fix: `.editorconfig` wrong pair `end_of_line`
2024-07-05 22:43:23 +08:00
Jinser Kafka
9360af88b3 fix: .editorconfig 2024-07-05 19:36:32 +08:00
手瓜一十雪
376370336c release: 1.6.5 2024-07-05 16:50:57 +08:00
手瓜一十雪
70df6e3302 Merge pull request #105 from po-lan/main
对缓存小优化
2024-07-05 16:49:30 +08:00
手瓜一十雪
0a1fc2dc12 feat: 1.6.5 2024-07-05 16:49:16 +08:00
手瓜一十雪
9857f6e437 feat: 优化载入流程 2024-07-05 16:47:08 +08:00
手瓜一十雪
56d6ebe916 refactor: 迁移到新库 2024-07-05 15:48:03 +08:00
po-lan
81134ea2d4 Update LRUCache.ts 2024-07-05 12:13:24 +08:00
po-lan
a9f3e7fc54 Update db.ts
通过读取缓存修复刚说话缺无法获取发言时间的问题
2024-07-05 12:12:40 +08:00
po-lan
eb84e2f8c9 Update LRUCache.ts
Add a get function to the cache
2024-07-05 12:09:59 +08:00
52 changed files with 786 additions and 321 deletions

View File

@@ -1,21 +1,21 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf|crlf
insert_final_newline = true
# Matches multiple files with brace expansion notation
# Set default charset
charset = utf-8
# 2 space indentation
[*.{cjs,mjs,js,jsx,ts,tsx,css,scss,sass,html,json}]
indent_style = space
indent_size = 2
# Unfortunately, EditorConfig doesn't support space configuration inside import braces directly.
# You'll need to rely on your linter/formatter like ESLint or Prettier for that.
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
# Matches multiple files with brace expansion notation
# Set default charset
charset = utf-8
# 2 space indentation
[*.{cjs,mjs,js,jsx,ts,tsx,css,scss,sass,html,json}]
indent_style = space
indent_size = 2
# Unfortunately, EditorConfig doesn't support space configuration inside import braces directly.
# You'll need to rely on your linter/formatter like ESLint or Prettier for that.

3
.gitignore vendored
View File

@@ -14,4 +14,5 @@ dist/
# Build
*.db
checkVersion.sh
checkVersion.sh
bun.lockb

View File

@@ -0,0 +1,17 @@
# v1.6.5
QQ Version: Windows 9.9.12-26000 / Linux 3.2.9-26000
## 使用前警告
1. 在最近版本由于QQ本体大幅变动为了保证NapCat可用性NapCat近期启动与安装方式将将大幅变动请关注文档和社群获取。
2. 在Core上完全执行开源请不要用于违法用途如此可能造成NapCat完全停止更新。
3. 针对原启动方式的围堵NapCat研发了多种方式除此其余理论与扩展的分析和思路将部分展示于Docs以便各位参与开发与维护NapCat。
## 其余·备注
启动方式: WayBoot.03 Electron Main进程为Node 直接注入代码 同理项目: LiteLoader
## 修复与优化
1. 修复了一些问题
## 新增与调整
没有哦
新增的 API 详细见[API文档](https://napneko.github.io/zh-CN/develop/extends_api)

View File

@@ -0,0 +1,18 @@
# v1.6.5
QQ Version: Windows 9.9.12-26000 / Linux 3.2.9-26000
## 使用前警告
1. 在最近版本由于QQ本体大幅变动为了保证NapCat可用性NapCat近期启动与安装方式将将大幅变动请关注文档和社群获取。
2. 在Core上完全执行开源请不要用于违法用途如此可能造成NapCat完全停止更新。
3. 针对原启动方式的围堵NapCat研发了多种方式除此其余理论与扩展的分析和思路将部分展示于Docs以便各位参与开发与维护NapCat。
## 其余·备注
启动方式: WayBoot.03 Electron Main进程为Node 直接注入代码 同理项目: LiteLoader
## 修复与优化
1. 优化了WrapperNative载入代码
2. 优化缓存
## 新增与调整
没有哦
新增的 API 详细见[API文档](https://napneko.github.io/zh-CN/develop/extends_api)

View File

@@ -1,2 +0,0 @@
# 开始
jadx 跳转于 `com.tencent.qqnt.kernel.*`

View File

@@ -1,42 +0,0 @@
# Android
```java
GroupMemberExtReq groupMemberExtReq = new GroupMemberExtReq();
groupMemberExtReq.sourceType = MemberExtSourceType.TITLETYPE.ordinal();
groupMemberExtReq.groupCode = longOrNull.longValue();
groupMemberExtReq.beginUin = "0";
groupMemberExtReq.dataTime = "0";
Long[] lArr = new Long[1];
AppInterface a2 = dVar.a();
lArr[0] = Long.valueOf(a2 != null ? a2.getLongAccountUin() : 0L);
arrayListOf = CollectionsKt__CollectionsKt.arrayListOf(lArr);
groupMemberExtReq.uinList = arrayListOf;
MemberExtInfoFilter memberExtInfoFilter = new MemberExtInfoFilter();
memberExtInfoFilter.memberLevelInfoUin = 1;
memberExtInfoFilter.memberLevelInfoPoint = 1;
memberExtInfoFilter.memberLevelInfoActiveDay = 1;
memberExtInfoFilter.memberLevelInfoLevel = 1;
memberExtInfoFilter.levelName = 1;
memberExtInfoFilter.dataTime = 1;
memberExtInfoFilter.sysShowFlag = 1;
memberExtInfoFilter.userShowFlag = 1;
memberExtInfoFilter.userShowFlagNew = 1;
memberExtInfoFilter.levelNameNew = 1;
Unit unit = Unit.INSTANCE;
groupMemberExtReq.memberExtFilter = memberExtInfoFilter;
troopLevelFrequencyControl.f(troopUin, new TroopListRepo$fetchTroopLevelInfo$2(b2, groupMemberExtReq, troopUin, new com.tencent.qqnt.troopmemberlist.report.c("fetchTroopLevelInfo")));
```
# Win
参数解析位于 sub_181456A10(24108) -> wrapper.node(24108)+1456A10
IGroupService.GetMemberExt(param: object);
param展开如下
```
groupCode string
beginUin string
dataTime string
uinList Array<string>
uinNum string
groupType string
richCardNameVer string
sourceType number
memberExtFilter object// 参数解析位于 sub_18145A6D0(24108) -> wrapper.node(24108)+145A6D0
```

View File

@@ -0,0 +1,16 @@
# 开发方向
方向一 NativeCall/Hook:
1. 崩溃检测机制的实现
2. Api_Caller 的Hook 可以拿到Event/Handler 进一步提升NC 即时的拦截与处理一些事件比如ReCall拦截
3. Node包装层 进一步分析拿到脱离自带Listener/Adapter可以拿到一些更加底层的数据变动 或许包括更多二进制数据
方向二 全新的无头启动 Way01
1. 基于Node启动原理借助导出符号获取函数地址 再次还原NodeMain
方向三 发包与收包
1. 参考 方向一/3 大概可以收包
2. 发包 (暂时没有计划)
方向四 版本控制
1. 根据不同版本进行逻辑控制
2. 某些参数的自动提取

View File

@@ -1,24 +0,0 @@
# 前排提示
由于Core未处于开源非组织人员无法参与Core开发此处为Core开发提示
# 准备工具
frida ida-pro jadx x64dbg ce 内部调试脚本
## ida-pro
1. 用于快速分析入参和返回类型
2. 通过静态QLog推测语义
3. 提取Listener与Service (常用)
## frida
1. 用于动态获取QLog推测语义
2. 捕捉Native函数 实际入参与数据 分析中间流程
## jadx
1. 通过其它平台实现 静态获取QLog推测语义
2. 提供部分未调用代码 参考
## x64dbg
1. 验证IDA的Hook点
## 内部脚本
1. 提取Listener与Service (不调用无类型 不推荐)
2. 获取NT调用流程

View File

@@ -2,7 +2,7 @@
"name": "napcat",
"private": true,
"type": "module",
"version": "1.6.4",
"version": "1.6.6",
"scripts": {
"watch:dev": "vite --mode development",
"watch:prod": "vite --mode production",
@@ -64,7 +64,7 @@
"json-schema-to-ts": "^3.1.0",
"log4js": "^6.9.1",
"qrcode-terminal": "^0.12.0",
"silk-wasm": "^3.3.4",
"silk-wasm": "^3.6.1",
"sqlite3": "^5.1.7",
"uuid": "^10.0.0",
"ws": "^8.16.0"

28
script/NapCat.164.bat Normal file
View File

@@ -0,0 +1,28 @@
@echo off
chcp 65001
:: 检查是否有管理员权限
net session >nul 2>&1
if %errorlevel% neq 0 (
echo 请求管理员权限...
powershell -Command "Start-Process '%~f0' -Verb runAs"
exit /b
)
:: 如果有管理员权限,继续执行
setlocal enabledelayedexpansion
:loop_read
for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do (
set "RetString=%%b"
goto :napcat_boot
)
:napcat_boot
for %%a in ("!RetString!") do (
set "pathWithoutUninstall=%%~dpa"
)
set "QQPath=!pathWithoutUninstall!QQ.exe"
echo !QQPath!
"!QQPath!" --enable-logging %*
pause

3
script/NapCat.Way01.bat Normal file
View File

@@ -0,0 +1,3 @@
REM 全新启动脚本 基于 Hook Native 预计版本1.6.0左右发布
@echo off
pause

View File

@@ -0,0 +1,18 @@
@echo off
setlocal enabledelayedexpansion
chcp 65001
:loop_read
for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do (
set "RetString=%%b"
goto :napcat_boot
)
:napcat_boot
for %%a in ("!RetString!") do (
set "pathWithoutUninstall=%%~dpa"
)
set "QQPath=!pathWithoutUninstall!"
cd /d !QQPath!
echo !QQPath!
QQ.exe --enable-logging %*

View File

@@ -0,0 +1,41 @@
function Get-QQpath {
try {
$key = Get-ItemProperty -Path "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ"
$uninstallString = $key.UninstallString
return [System.IO.Path]::GetDirectoryName($uninstallString) + "\"
}
catch {
throw "get QQ path error: $_"
}
}
function Select-QQPath {
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.Application]::EnableVisualStyles()
$dialogTitle = "Select QQ.exe"
$filePicker = New-Object System.Windows.Forms.OpenFileDialog
$filePicker.Title = $dialogTitle
$filePicker.Filter = "Executable Files (*.exe)|*.exe|All Files (*.*)|*.*"
$filePicker.FilterIndex = 1
$null = $filePicker.ShowDialog()
if (-not ($filePicker.FileName)) {
throw "User did not select an .exe file."
}
return $filePicker.FileName
}
$params = $args -join " "
Try {
$QQpath = Get-QQpath
}
Catch {
$QQpath = Select-QQPath
}
if (!(Test-Path $QQpath)) {
throw "provided QQ path is invalid: $QQpath"
}
Set-Location -Path $QQpath
Start-Process powershell -ArgumentList "-noexit", "-noprofile", "-command &{& chcp 65001;& ./QQ.exe --enable-logging $params}"

17
script/napcat-9912.bat Normal file
View File

@@ -0,0 +1,17 @@
@echo off
setlocal enabledelayedexpansion
:loop_read
for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do (
set "RetString=%%b"
goto :napcat_boot
)
:napcat_boot
for %%a in ("!RetString!") do (
set "pathWithoutUninstall=%%~dpa"
)
set QQPath=!pathWithoutUninstall!
cd /d !QQPath!
echo !QQPath!
QQ.exe --enable-logging %*

41
script/napcat-9912.ps1 Normal file
View File

@@ -0,0 +1,41 @@
function Get-QQpath {
try {
$key = Get-ItemProperty -Path "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ"
$uninstallString = $key.UninstallString
return [System.IO.Path]::GetDirectoryName($uninstallString) + "\"
}
catch {
throw "get QQ path error: $_"
}
}
function Select-QQPath {
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.Application]::EnableVisualStyles()
$dialogTitle = "Select QQ.exe"
$filePicker = New-Object System.Windows.Forms.OpenFileDialog
$filePicker.Title = $dialogTitle
$filePicker.Filter = "Executable Files (*.exe)|*.exe|All Files (*.*)|*.*"
$filePicker.FilterIndex = 1
$null = $filePicker.ShowDialog()
if (-not ($filePicker.FileName)) {
throw "User did not select an .exe file."
}
return $filePicker.FileName
}
$params = $args -join " "
Try {
$QQpath = Get-QQpath
}
Catch {
$QQpath = Select-QQPath
}
if (!(Test-Path $QQpath)) {
throw "provided QQ path is invalid: $QQpath"
}
Set-Location -Path $QQpath
Start-Process powershell -ArgumentList "-noexit", "-noprofile", "-command &{& ./QQ.exe --enable-logging $params}"

View File

@@ -1,7 +1,7 @@
import { sleep } from '@/common/utils/helper';
import { logError } from './log';
type AsyncQueueTask = (() => void) | (()=>Promise<void>);
// 2024.7.13 废弃
export class AsyncQueue {
private tasks: (AsyncQueueTask)[] = [];

View File

@@ -1,4 +1,3 @@
import { NodeIKernelMsgListener } from '@/core';
import { NodeIQQNTWrapperSession } from '@/core/wrapper';
import { randomUUID } from 'crypto';

View File

@@ -21,7 +21,8 @@ class cacheNode<T> {
}
}
type cache<T> = { [key: group_id]: { [key: user_id]: cacheNode<T> } };
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;
@@ -29,9 +30,9 @@ class LRU<T> {
private cache: cache<T>;
private head: cacheNode<T> | null = null;
private tail: cacheNode<T> | null = null;
private onFuncs: ((node: cacheNode<T>) => void)[] = [];
private onFuncs: ((node: removeObject<T>) => void)[] = [];
constructor(maxAge: number = 2e4, maxSize: number = 5e3) {
constructor(maxAge: number = 6e4 * 3, maxSize: number = 1e4) {
this.maxAge = maxAge;
this.maxSize = maxSize;
this.cache = Object.create(null);
@@ -53,46 +54,39 @@ class LRU<T> {
node.prev = node.next = null;
delete this.cache[node.groupId][node.userId];
this.removeNode(node);
this.onFuncs.forEach((func) => func(node));
this.onFuncs.forEach((func) => func({ [node.groupId]: [node] }));
this.currentSize--;
}
public on(func: (node: cacheNode<T>) => void) {
public on(func: (node: removeObject<T>) => void) {
this.onFuncs.push(func);
}
private removeExpired() {
const now = Date.now();
let current = this.tail;
const nodesToRemove: cacheNode<T>[] = [];
let removedCount = 0;
let totalNodeNum = 0;
const removeObject: cache<T, { userId: user_id, value: T }[]> = {};
// 收集需要删除的节点
while (current && now - current.timestamp > this.maxAge) {
nodesToRemove.push(current);
// 收集节点
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;
removedCount++;
if (removedCount >= 100) break;
}
// 更新链表指向
if (nodesToRemove.length > 0) {
const newTail = nodesToRemove[nodesToRemove.length - 1].prev;
if (newTail) {
newTail.next = null;
} else {
this.head = null;
}
this.tail = newTail;
}
nodesToRemove.forEach((node) => {
node.prev = node.next = null;
delete this.cache[node.groupId][node.userId];
totalNodeNum++;
this.currentSize--;
this.onFuncs.forEach((func) => func(node));
});
}
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>) {
@@ -140,6 +134,28 @@ class LRU<T> {
}
}
}
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

@@ -38,11 +38,11 @@ type QQVersionConfigInfo = {
}
let _qqVersionConfigInfo: QQVersionConfigInfo = {
'baseVersion': '9.9.11-24568',
'curVersion': '9.9.11-24568',
'baseVersion': '9.9.12-25765',
'curVersion': '9.9.12-25765',
'prevVersion': '',
'onErrorVersions': [],
'buildId': '24568'
'buildId': '25765'
};
if (fs.existsSync(configVersionInfoPath)) {
@@ -55,23 +55,23 @@ if (fs.existsSync(configVersionInfoPath)) {
}
export const qqVersionConfigInfo: QQVersionConfigInfo = _qqVersionConfigInfo;
//V1_WIN_NQ_9.9.11_24568_GW_B
//V1_WIN_NQ_9.9.12_25765_GW_B
export const qqPkgInfo: QQPkgInfo = JSON.parse(fs.readFileSync(pkgInfoPath).toString());
// platform_type: 3,
// app_type: 4,
// app_version: '9.9.9-23159',
// qua: 'V1_WIN_NQ_9.9.9_23159_GW_B',
// appid: '537213764',
// app_version: '9.9.12-25765',
// qua: 'V1_WIN_NQ_9.9.12_25765_GW_B',
// appid: '537234702',
// platVer: '10.0.26100',
// clientVer: '9.9.9-23159',
// clientVer: '9.9.9-25765',
// Linux
// app_version: '3.2.9-24568',
// qua: 'V1_LNX_NQ_3.2.9_24568_GW_B',
// app_version: '3.2.9-25765',
// qua: 'V1_LNX_NQ_3.2.10_25765_GW_B',
let _appid: string = '537226369'; // 默认为 Windows 平台的 appid
let _appid: string = '537234702'; // 默认为 Windows 平台的 appid
if (systemPlatform === 'linux') {
_appid = '537226441';
_appid = '537234773';
}
// todo: mac 平台的 appid
export const appid = _appid;
export const appid = _appid;

View File

@@ -1,5 +1,5 @@
import fs from 'fs';
import { encode, getDuration, getWavFileInfo, isWav } from 'silk-wasm';
import { encode, getDuration, getWavFileInfo, isWav, isSilk } from 'silk-wasm';
import fsPromise from 'fs/promises';
import { log, logError } from './log';
import path from 'node:path';
@@ -63,10 +63,11 @@ export async function encodeSilk(filePath: string) {
// }
try {
const file = await fsPromise.readFile(filePath);
const pttPath = path.join(TEMP_DIR, uuidv4());
if (getFileHeader(filePath) !== '02232153494c4b') {
if (!isSilk(file)) {
log(`语音文件${filePath}需要转换成silk`);
const _isWav = await isWavFile(filePath);
const _isWav = isWav(file);
const pcmPath = pttPath + '.pcm';
let sampleRate = 0;
const convert = () => {
@@ -96,7 +97,7 @@ export async function encodeSilk(filePath: string) {
if (!_isWav) {
input = await convert();
} else {
input = fs.readFileSync(filePath);
input = file;
const allowSampleRate = [8000, 12000, 16000, 24000, 32000, 44100, 48000];
const { fmt } = getWavFileInfo(input);
// log(`wav文件信息`, fmt)
@@ -113,7 +114,7 @@ export async function encodeSilk(filePath: string) {
duration: silk.duration / 1000
};
} else {
const silk = fs.readFileSync(filePath);
const silk = file;
let duration = 0;
try {
duration = getDuration(silk) / 1000;

View File

@@ -72,7 +72,7 @@ class DBUtil extends DBUtilBase {
private cache: { gid: number; uid: number }[] = [];
private maxSize: number;
constructor(maxSize: number = 5000) {
constructor(maxSize: number = 50000) {
this.maxSize = maxSize;
}
@@ -120,57 +120,83 @@ class DBUtil extends DBUtilBase {
});
this.LURCache.on(async (node) => {
const { value: time, groupId, userId } = node;
this.LURCache.on(async (nodeObject) => {
logDebug('插入发言时间', userId, groupId);
await this.createGroupInfoTimeTableIfNotExist(groupId);
Object.entries(nodeObject).forEach(async ([_groupId, datas]) => {
const userIds = datas.map(v => v.userId);
const groupId = Number(_groupId);
logDebug('插入发言时间', _groupId);
const method = await this.getDataSetMethod(groupId, userId);
logDebug('插入发言时间方法判断', userId, groupId, method);
await this.createGroupInfoTimeTableIfNotExist(groupId);
const sql =
method == 'update'
? `UPDATE "${groupId}" SET last_sent_time = ? WHERE user_id = ?`
: `INSERT INTO "${groupId}" (last_sent_time, user_id) VALUES (?, ?)`;
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)!);
this.db!.all(sql, [time, userId], (err) => {
if (err) {
return logError('插入/更新发言时间失败', userId, groupId);
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}`);
}
});
}
logDebug('插入/更新发言时间成功', userId, groupId);
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 getDataSetMethod(groupId: number, userId: number) {
// 缓存记录
if (this.LastSentCache.get(groupId, userId)) {
logDebug('缓存命中', userId, groupId);
return 'update';
async getNeedCreatList(groupId: number, userIds: number[]) {
// 获取缓存中没有的
const unhas = userIds.filter(userId => !this.LastSentCache.get(groupId, userId));
if (unhas.length == 0) {
logDebug('缓存全部命中');
return [];
}
// 数据库判断
return new Promise<'insert' | 'update'>((resolve, reject) => {
this.db!.all(
`SELECT * FROM "${groupId}" WHERE user_id = ?`,
[userId],
(err, rows) => {
if (err) {
logError('查询发言时间存在失败', userId, groupId, err);
return logError('插入发言时间失败', userId, groupId, err);
}
logDebug('缓存未全部命中');
if (rows.length === 0) {
logDebug('查询发言时间不存在', userId, groupId);
return resolve('insert');
}
const sql = `SELECT * FROM "${groupId}" WHERE user_id IN (${unhas.map(() => '?').join(',')})`;
logDebug('查询发言时间存在', userId, groupId);
resolve('update');
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) =>
@@ -408,10 +434,12 @@ class DBUtil extends DBUtilBase {
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([]);
return resolve(cache.map(e => ({ ...e, join_time: 0 })));
}
Object.assign(rows, cache);
logDebug('查询发言时间成功', groupId, rows);
resolve(rows);
});

View File

@@ -160,7 +160,12 @@ export async function httpDownload(options: string | HttpDownloadOptions): Promi
}
}
}
const fetchRes = await fetch(url, { headers });
const fetchRes = await fetch(url, { headers }).catch((err) => {
if (err.cause) {
throw err.cause;
}
throw err;
});
if (!fetchRes.ok) throw new Error(`下载文件失败: ${fetchRes.statusText}`);
const blob = await fetchRes.blob();

View File

@@ -7,7 +7,7 @@ export class RequestUtil {
static async HttpsGetCookies(url: string): Promise<{ [key: string]: string }> {
const client = url.startsWith('https') ? https : http;
return new Promise((resolve, reject) => {
let req = client.get(url, (res) => {
const req = client.get(url, (res) => {
let cookies: { [key: string]: string } = {};
const handleRedirect = (res: http.IncomingMessage) => {
//console.log(res.headers.location);
@@ -44,7 +44,7 @@ export class RequestUtil {
});
}
});
req.on('error', (error: any) => {
req.on('error', (error: any) => {
reject(error);
});
});

View File

@@ -21,10 +21,7 @@ import { ISizeCalculationResult } from 'image-size/dist/types/interface';
import { sessionConfig } from '@/core/sessionConfig';
import { randomUUID } from 'crypto';
import { rkeyManager } from '../utils/rkey';
import { AsyncQueue } from '@/common/utils/AsyncQueue';
// const rkeyExpireTime = 1000;
const getRKeyTaskQueue = new AsyncQueue();
const downloadMediaTasks: Map<string, (arg: OnRichMediaDownloadCompleteParams) => void> = new Map<string, (arg: OnRichMediaDownloadCompleteParams) => void>();

View File

@@ -1,4 +1,4 @@
import { GroupMember, GroupRequestOperateTypes, GroupMemberRole, GroupNotify, Group, MemberExtSourceType } from '../entities';
import { GroupMember, GroupRequestOperateTypes, GroupMemberRole, GroupNotify, Group, MemberExtSourceType, GroupNotifyTypes } from '../entities';
import { GeneralCallResult, NTQQUserApi, napCatCore } from '@/core';
import { NTEventDispatch } from '@/common/utils/EventTask';
import { logDebug } from '@/common/utils/log';
@@ -31,6 +31,30 @@ export class NTQQGroupApi {
static async DelGroupFileFolder(groupCode: string, folderId: string) {
return napCatCore.session.getRichMediaService().deleteGroupFolder(groupCode, folderId);
}
static async addGroupEssence(GroupCode: string, msgId: string) {
// 代码没测过
// 需要 ob11msgid->msgId + (peer) -> msgSeq + msgRandom
let MsgData = await napCatCore.session.getMsgService().getMsgsIncludeSelf({ chatType: 2, guildId: '', peerUid: GroupCode }, msgId, 1, false);
let param = {
groupCode: GroupCode,
msgRandom: parseInt(MsgData.msgList[0].msgRandom),
msgSeq: parseInt(MsgData.msgList[0].msgSeq)
};
// GetMsgByShoretID(ShoretID); -> MsgService.getMsgs(Peer,MsgId,1,false); -> 组出参数
return napCatCore.session.getGroupService().addGroupEssence(param);
}
static async removeGroupEssence(GroupCode: string, msgId: string) {
// 代码没测过
// 需要 ob11msgid->msgId + (peer) -> msgSeq + msgRandom
let MsgData = await napCatCore.session.getMsgService().getMsgsIncludeSelf({ chatType: 2, guildId: '', peerUid: GroupCode }, msgId, 1, false);
let param = {
groupCode: GroupCode,
msgRandom: parseInt(MsgData.msgList[0].msgRandom),
msgSeq: parseInt(MsgData.msgList[0].msgSeq)
};
// GetMsgByShoretID(ShoretID); -> MsgService.getMsgs(Peer,MsgId,1,false); -> 组出参数
return napCatCore.session.getGroupService().removeGroupEssence(param);
}
static async getSingleScreenNotifies(num: number) {
let [_retData, _doubt, _seq, notifies] = await NTEventDispatch.CallNormalEvent
<(arg1: boolean, arg2: string, arg3: number) => Promise<any>, (doubt: boolean, seq: string, notifies: GroupNotify[]) => void>
@@ -87,15 +111,20 @@ export class NTQQGroupApi {
const _Pskey = (await NTQQUserApi.getPSkey(['qun.qq.com'])).domainPskeyMap.get('qun.qq.com')!;
return napCatCore.session.getGroupService().uploadGroupBulletinPic(GroupCode, _Pskey, imageurl);
}
static async handleGroupRequest(notify: GroupNotify, operateType: GroupRequestOperateTypes, reason?: string) {
static async handleGroupRequest(flag: string, operateType: GroupRequestOperateTypes, reason?: string) {
let flagitem = flag.split('|');
let groupCode = flagitem[0];
let seq = flagitem[1];
let type = parseInt(flagitem[2]);
return napCatCore.session.getGroupService().operateSysNotify(
false,
{
'operateType': operateType, // 2 拒绝
'targetMsg': {
'seq': notify.seq, // 通知序列号
'type': notify.type,
'groupCode': notify.group.groupCode,
'seq': seq, // 通知序列号
'type': type,
'groupCode': groupCode,
'postscript': reason || ''
}
});

View File

@@ -1,7 +1,7 @@
import { ModifyProfileParams, SelfInfo, User, UserDetailInfoByUin } from '@/core/entities';
import { selfInfo } from '@/core/data';
import { friends, selfInfo } from '@/core/data';
import { CacheClassFuncAsync } from '@/common/utils/helper';
import { GeneralCallResult, napCatCore } from '@/core';
import { GeneralCallResult, napCatCore, NTQQFriendApi } from '@/core';
import { ProfileListener } from '@/core/listeners';
import { rejects } from 'assert';
import { randomUUID } from 'crypto';
@@ -170,7 +170,30 @@ export class NTQQUserApi {
5000,
[Uin]
);
return ret.uidInfo.get(Uin);
let uid = ret.uidInfo.get(Uin); //通过QQ默认方式转换
if (!uid) {
Array.from(friends.values()).forEach((t) => {
if (t.uin == Uin) {
//logDebug('getUidByUin', t.uid, t.uin, Uin);
uid = t.uid;
}
//console.log(t.uid, t.uin, Uin);
});
//uid = Array.from(friends.values()).find((t) => { t.uin == Uin })?.uid; // 从NC维护的QQ Buddy缓存 转换
}
// if (!uid) {
// uid = (await NTQQFriendApi.getFriends(false)).find((t) => { t.uin == Uin })?.uid; //从QQ Native 缓存转换 方法一
// }
// if (!uid) {
// uid = (await NTQQFriendApi.getFriends(true)).find((t) => { t.uin == Uin })?.uid; //从QQ Native 非缓存转换 方法二
// }
if (!uid) {
let unveifyUid = (await NTQQUserApi.getUserDetailInfoByUin(Uin)).info.uid;//从QQ Native 特殊转换 方法三
if (unveifyUid.indexOf("*") == -1) {
uid = unveifyUid;
}
}
return uid;
}
static async getUinByUid(Uid: string | undefined) {
if (!Uid) {
@@ -182,7 +205,26 @@ export class NTQQUserApi {
5000,
[Uid]
);
return ret.uinInfo.get(Uid);
let uin = ret.uinInfo.get(Uid);
if (!uin) {
//从Buddy缓存获取Uin
Array.from(friends.values()).forEach((t) => {
if (t.uid == Uid) {
uin = t.uin;
}
})
}
if (!uin) {
uin = (await NTQQUserApi.getUserDetailInfo(Uid)).uin; //从QQ Native 转换
}
// if (!uin) {
// uin = (await NTQQFriendApi.getFriends(false)).find((t) => { t.uid == Uid })?.uin; //从QQ Native 缓存转换
// }
// if (!uin) {
// uin = (await NTQQFriendApi.getFriends(true)).find((t) => { t.uid == Uid })?.uin; //从QQ Native 非缓存转换
// }
return uin;
}
static async getUserDetailInfoByUin(Uin: string) {
return NTEventDispatch.CallNoListenerEvent

View File

@@ -1,9 +1,9 @@
import {
type Friend,
type Group,
type GroupMember, GroupNotify,
type GroupMember,
type SelfInfo,
BuddyCategoryType
type BuddyCategoryType
} from './entities';
import { isNumeric } from '@/common/utils/helper';
import { NTQQGroupApi } from '@/core/apis';
@@ -30,7 +30,6 @@ export const groupMembers: Map<string, Map<string, GroupMember>> = new Map<strin
export const friends: Map<string, Friend> = new Map<string, Friend>();
export const rawFriends: Array<BuddyCategoryType> = []; // 带分组的好友列表
export const groupNotifies: Record<string, GroupNotify> = {}; // flag->GroupNotify
export async function getGroup(qq: string | number): Promise<Group | undefined> {
let group = groups.get(qq.toString());
if (!group) {

View File

@@ -8,6 +8,7 @@ import {
SendPicElement,
SendPttElement,
SendReplyElement,
sendShareLocationElement,
SendTextElement,
SendVideoElement
} from './index';
@@ -27,6 +28,16 @@ export const mFaceCache = new Map<string, string>(); // emojiId -> faceName
export class SendMsgElementConstructor {
static location(): sendShareLocationElement {
return {
elementType: ElementType.SHARELOCATION,
elementId: '',
shareLocationElement: {
text: "测试",
ext: ""
}
}
}
static text(content: string): SendTextElement {
return {
elementType: ElementType.TEXT,

View File

@@ -23,6 +23,7 @@ export interface GetFileListParam {
showOnlinedocFolder: number
}
export enum ElementType {
UNKNOWN = 0,
TEXT = 1,
PIC = 2,
FILE = 3,
@@ -30,11 +31,182 @@ export enum ElementType {
VIDEO = 5,
FACE = 6,
REPLY = 7,
WALLET = 9,
GreyTip = 8,//Poke别叫戳一搓了 官方名字拍一拍 戳一戳是另一个名字
ARK = 10,
MFACE = 11,
MARKDOWN = 14
LIVEGIFT = 12,
STRUCTLONGMSG = 13,
MARKDOWN = 14,
GIPHY = 15,
MULTIFORWARD = 16,
INLINEKEYBOARD = 17,
INTEXTGIFT = 18,
CALENDAR = 19,
YOLOGAMERESULT = 20,
AVRECORD = 21,
FEED = 22,
TOFURECORD = 23,
ACEBUBBLE = 24,
ACTIVITY = 25,
TOFU = 26,
FACEBUBBLE = 27,
SHARELOCATION = 28,
TASKTOPMSG = 29,
RECOMMENDEDMSG = 43,
ACTIONBAR = 44
}
export interface SendActionBarElement {
elementType: ElementType.ACTIONBAR;
elementId: string;
actionBarElement: {
rows: InlineKeyboardRow[];
botAppid: string;
}
}
export interface SendRecommendedMsgElement {
elementType: ElementType.RECOMMENDEDMSG;
elementId: string;
recommendedMsgElement: {
rows: InlineKeyboardRow[];
botAppid: string;
}
}
export interface InlineKeyboardButton {
id: string;
label: string;
visitedLabel: string;
unsupportTips: string;
data: string;
specifyRoleIds: string[];
specifyTinyids: string[];
style: number;
type: number;
clickLimit: number;
atBotShowChannelList: boolean;
permissionType: number;
}
export interface InlineKeyboardRow {
buttons: InlineKeyboardButton[];
}
export interface TofuElementContent {
color: string;
tittle: string;
}
export interface SendTaskTopMsgElement {
elementType: ElementType.TASKTOPMSG;
elementId: string;
taskTopMsgElement: {
msgTitle: string;
msgSummary: string;
iconUrl: string;
topMsgType: number;
}
}
export interface SendTofuRecordElement {
elementType: ElementType.TOFURECORD;
elementId: string;
tofuRecordElement: {
type: number;
busiid: string;
busiuuid: string;
descriptionContent: string;
contentlist: TofuElementContent[],
background: string;
icon: string;
uinlist: string[],
uidlist: string[],
busiExtra: string;
updateTime: string;
dependedmsgid: string;
msgtime: string;
onscreennotify: boolean;
}
}
export interface SendFaceBubbleElement {
elementType: ElementType.FACEBUBBLE;
elementId: string;
faceBubbleElement: {
faceCount: number;
faceSummary: string;
faceFlag: number;
content: string;
oldVersionStr: string;
faceType: number;
others: string;
yellowFaceInfo: {
index: number;
buf: string;
compatibleText: string;
text: string;
}
}
}
export interface SendavRecordElement {
elementType: ElementType.AVRECORD;
elementId: string;
avRecordElement: {
type: number;
time: string;
text: string;
mainType: number;
hasRead: boolean;
extraType: number;
}
}
export interface YoloUserInfo {
uid: string;
result: number;
rank: number;
bizId: string
}
export interface SendInlineKeyboardElement {
elementType: ElementType.INLINEKEYBOARD;
elementId: string;
inlineKeyboardElement: {
rows: number;
botAppid: string;
}
}
export interface SendYoloGameResultElement {
elementType: ElementType.YOLOGAMERESULT;
yoloGameResultElement: {
UserInfo: YoloUserInfo[];
}
}
export interface SendGiphyElement {
elementType: ElementType.GIPHY;
elementId: string;
giphyElement: {
id: string;
isClip: boolean;
width: number;
height: number;
}
}
export interface SendWalletElement {
elementType: ElementType.UNKNOWN;//不做 设置位置
elementId: string;
walletElement: {}
}
export interface SendCalendarElement {
elementType: ElementType.CALENDAR;
elementId: string;
calendarElement: {
summary: string;
msg: string;
expireTimeMs: string;
schemaType: number;
schema: string
}
}
export interface SendliveGiftElement {
elementType: ElementType.LIVEGIFT;
elementId: string;
liveGiftElement: {}
}
export interface SendTextElement {
elementType: ElementType.TEXT;
elementId: string;
@@ -118,6 +290,30 @@ export interface SendMarketFaceElement {
elementType: ElementType.MFACE;
marketFaceElement: MarketFaceElement;
}
export interface SendstructLongMsgElement {
elementType: ElementType.STRUCTLONGMSG;
elementId: string;
structLongMsgElement: {
xmlContent: string;
resId: string;
}
}
export interface SendactionBarElement {
elementType: ElementType.ACTIONBAR;
elementId: string;
actionBarElement: {
rows: number;
botAppid: string;
}
}
export interface sendShareLocationElement {
elementType: ElementType.SHARELOCATION;
elementId: string;
shareLocationElement: {
text: string;
ext: string;
}
}
export interface FileElement {
fileMd5?: string;
@@ -162,7 +358,7 @@ export interface SendMarkdownElement {
}
export type SendMessageElement = SendTextElement | SendPttElement |
SendPicElement | SendReplyElement | SendFaceElement | SendMarketFaceElement | SendFileElement | SendVideoElement | SendArkElement | SendMarkdownElement
SendPicElement | SendReplyElement | SendFaceElement | SendMarketFaceElement | SendFileElement | SendVideoElement | SendArkElement | SendMarkdownElement | sendShareLocationElement
export enum AtType {
notAt = 0,
@@ -289,6 +485,7 @@ export interface GrayTipElement {
templId: string;
};
jsonGrayTipElement: {
busiId?: number;
jsonStr: string;
};
}
@@ -490,6 +687,7 @@ export interface MultiForwardMsgElement {
}
export interface RawMessage {
msgRandom: string;
// int32, 自己维护的消息id
id?: number;

View File

@@ -149,5 +149,18 @@ export interface NodeIKernelGroupService {
modifyGroupExtInfo(groupCode: string, arg: unknown): void;
//需要提前判断是否存在 高版本新增
addGroupEssence(param: {
groupCode: string
msgRandom: number,
msgSeq: number
}): Promise<unknown>;
//需要提前判断是否存在 高版本新增
removeGroupEssence(param: {
groupCode: string
msgRandom: number,
msgSeq: number
}): Promise<unknown>;
isNull(): boolean;
}

View File

@@ -171,7 +171,7 @@ export interface NodeIQQNTWrapperSession {
getTicketService(): NodeIKernelTicketService;
getTipOffService(): NodeIKernelTipOffService;
getNodeMiscService(): NodeIKernelNodeMiscService;
getRichMediaService(): NodeIKernelRichMediaService;
@@ -286,11 +286,7 @@ let wrapperNodePath = path.resolve(path.dirname(process.execPath), './resources/
if (!fs.existsSync(wrapperNodePath)) {
wrapperNodePath = path.join(path.dirname(process.execPath), `resources/app/versions/${qqVersionConfigInfo.curVersion}/wrapper.node`);
}
let WrapperLoader = path.join(__dirname, "WrapperLoader.cjs");
//此处待优化
fs.writeFileSync(WrapperLoader, `
module.exports = require("${wrapperNodePath.replace(/\\/g, "\\\\")}");
exports = module.exports;
`)
const QQWrapper: WrapperNodeApi = (await import("file://" + WrapperLoader)).default;
const nativemodule: any = { exports: {} };
process.dlopen(nativemodule, wrapperNodePath);
const QQWrapper: WrapperNodeApi = nativemodule.exports;
export default QQWrapper;

View File

@@ -1,2 +0,0 @@
//统一到处入口
export * from './main';

View File

@@ -1,29 +0,0 @@
enum NapCatCorePlatform {
Node = 'Node',//命令行模式加载
LiteLoader = 'LiteLoader',//LL插件模式加载
}
class NewNapCatCore {
platform: NapCatCorePlatform; // 平台
constructor(platform: NapCatCorePlatform) {
this.platform = platform;
}
}
export class NapCatCoreManger {
static core: NewNapCatCore | undefined = undefined;
static defaultPlatform: NapCatCorePlatform = NapCatCorePlatform.Node;
static SetDefaultCore(platform: NapCatCorePlatform) {
if (this.core !== undefined) {
return;
}
this.defaultPlatform = platform;
}
static GetPlatform(): NapCatCorePlatform {
return NapCatCoreManger.defaultPlatform;
}
static GetInstance(): NewNapCatCore {
if (this.core === undefined) {
this.core = new NewNapCatCore(NapCatCoreManger.defaultPlatform);
}
return this.core;
}
}

View File

@@ -1 +0,0 @@
//拦截proxy到会话

View File

@@ -1 +0,0 @@
//初始化跟之前一样

View File

@@ -1 +0,0 @@
//proxy封装工具类

View File

@@ -2,7 +2,6 @@ import BaseAction from '../BaseAction';
import { GroupRequestOperateTypes } from '@/core/entities';
import { ActionName } from '../types';
import { NTQQGroupApi } from '@/core/apis/group';
import { groupNotifies } from '@/core/data';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
const SchemaData = {
@@ -23,11 +22,7 @@ export default class SetGroupAddRequest extends BaseAction<Payload, null> {
protected async _handle(payload: Payload): Promise<null> {
const flag = payload.flag.toString();
const approve = payload.approve?.toString() !== 'false';
const notify = groupNotifies[flag];
if (!notify) {
throw `${flag}对应的加群通知不存在`;
}
await NTQQGroupApi.handleGroupRequest(notify,
await NTQQGroupApi.handleGroupRequest(flag,
approve ? GroupRequestOperateTypes.approve : GroupRequestOperateTypes.reject,
payload.reason
);

View File

@@ -209,6 +209,10 @@ const _handlers: {
[OB11MessageDataType.xml]: () => undefined,
[OB11MessageDataType.poke]: () => undefined,
[OB11MessageDataType.Location]: async () => {
return SendMsgElementConstructor.location();
}
};
const handlers = <{

View File

@@ -102,6 +102,7 @@ async function createContext(payload: OB11PostSendMsg, contextMode: ContextMode)
if ((contextMode === ContextMode.Private || contextMode === ContextMode.Normal) && payload.user_id) {
const Uid = await NTQQUserApi.getUidByUin(payload.user_id.toString());
const isBuddy = await NTQQFriendApi.isBuddy(Uid!);
//console.log("[调试代码] UIN:", payload.user_id, " UID:", Uid, " IsBuddy:", isBuddy);
return {
peer: {
chatType: isBuddy ? ChatType.friend : ChatType.temp,
@@ -144,7 +145,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
}
protected async _handle(payload: OB11PostSendMsg): Promise<{ message_id: number }> {
let { peer, group } = await createContext(payload, this.contextMode);
const { peer, group } = await createContext(payload, this.contextMode);
const messages = normalize(
payload.message,

View File

@@ -43,6 +43,8 @@ import { deleteGroup, getGroupMember, groupMembers, selfInfo, tempGroupCodeMap }
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';
export class OB11Constructor {
@@ -162,7 +164,7 @@ export class OB11Constructor {
message_data['type'] = 'image';
// message_data["data"]["file"] = element.picElement.sourcePath
message_data['data']['file'] = element.picElement.fileName;
message_data['subType']= element.picElement.picSubType;
message_data['subType'] = element.picElement.picSubType;
// message_data["data"]["path"] = element.picElement.sourcePath
// let currentRKey = "CAQSKAB6JWENi5LMk0kc62l8Pm3Jn1dsLZHyRLAnNmHGoZ3y_gDZPqZt-64"
@@ -285,12 +287,36 @@ export class OB11Constructor {
resMsg.raw_message = resMsg.raw_message.trim();
return resMsg;
}
static async PrivateEvent(msg: RawMessage): Promise<OB11BaseNoticeEvent | undefined> {
if (msg.chatType !== ChatType.friend) {
return;
}
for (const element of msg.elements) {
if (element.grayTipElement) {
if (element.grayTipElement.subElementType == GrayTipElementSubType.MEMBER_NEW_TITLE) {
const json = JSON.parse(element.grayTipElement.jsonGrayTipElement.jsonStr);
if (element.grayTipElement.jsonGrayTipElement.busiId == 1061) {
//判断业务类型
//Poke事件
let pokedetail: any[] = json.items;
//筛选item带有uid的元素
pokedetail = pokedetail.filter(item => item.uid);
//console.log("[NapCat] 群拍一拍 群:", pokedetail, parseInt(msg.peerUid), " ", await NTQQUserApi.getUinByUid(pokedetail[0].uid), "拍了拍", await NTQQUserApi.getUinByUid(pokedetail[1].uid));
if (pokedetail.length == 2) {
return new OB11FriendPokeEvent(parseInt((await NTQQUserApi.getUinByUid(pokedetail[0].uid))!), parseInt((await NTQQUserApi.getUinByUid(pokedetail[1].uid))!));
}
}
//下面得改 上面也是错的grayTipElement.subElementType == GrayTipElementSubType.MEMBER_NEW_TITLE
}
}
}
}
static async GroupEvent(msg: RawMessage): Promise<OB11GroupNoticeEvent | undefined> {
//log("group msg", msg);
if (msg.chatType !== ChatType.group) {
return;
}
if (msg.senderUin) {
if (msg.senderUin && msg.senderUin !== '0') {
const member = await getGroupMember(msg.peerUid, msg.senderUin);
if (member && member.cardName !== msg.sendMemberName) {
const newCardName = msg.sendMemberName || '';
@@ -299,7 +325,6 @@ export class OB11Constructor {
return event;
}
}
// log("group msg", msg);
for (const element of msg.elements) {
const grayTipElement = element.grayTipElement;
const groupElement = grayTipElement?.groupElement;
@@ -369,11 +394,8 @@ export class OB11Constructor {
busid: element.fileElement.fileBizId || 0
});
}
if (grayTipElement) {
const xmlElement = grayTipElement.xmlElement;
if (xmlElement?.templId === '10382') {
if (grayTipElement.xmlElement?.templId === '10382') {
// 表情回应消息
// "content":
// "<gtip align=\"center\">
@@ -385,7 +407,7 @@ export class OB11Constructor {
const emojiLikeData = new fastXmlParser.XMLParser({
ignoreAttributes: false,
attributeNamePrefix: ''
}).parse(xmlElement.content);
}).parse(grayTipElement.xmlElement.content);
logDebug('收到表情回应我的消息', emojiLikeData);
try {
const senderUin = emojiLikeData.gtip.qq.jp;
@@ -422,6 +444,7 @@ export class OB11Constructor {
}
}
}
//代码歧义 GrayTipElementSubType.MEMBER_NEW_TITLE
else if (grayTipElement.subElementType == GrayTipElementSubType.MEMBER_NEW_TITLE) {
const json = JSON.parse(grayTipElement.jsonGrayTipElement.jsonStr);
/*
@@ -448,6 +471,18 @@ export class OB11Constructor {
}
* */
if (grayTipElement.jsonGrayTipElement.busiId == 1061) {
//判断业务类型
//Poke事件
let pokedetail: any[] = json.items;
//筛选item带有uid的元素
pokedetail = pokedetail.filter(item => item.uid);
//console.log("[NapCat] 群拍一拍 群:", pokedetail, parseInt(msg.peerUid), " ", await NTQQUserApi.getUinByUid(pokedetail[0].uid), "拍了拍", await NTQQUserApi.getUinByUid(pokedetail[1].uid));
if (pokedetail.length == 2) {
return new OB11GroupPokeEvent(parseInt(msg.peerUid), parseInt((await NTQQUserApi.getUinByUid(pokedetail[0].uid))!), parseInt((await NTQQUserApi.getUinByUid(pokedetail[1].uid))!));
}
}
//下面得改 上面也是错的grayTipElement.subElementType == GrayTipElementSubType.MEMBER_NEW_TITLE
const memberUin = json.items[1].param[0];
const title = json.items[3].txt;
logDebug('收到群成员新头衔消息', json);

View File

@@ -1,6 +1,6 @@
import { OB11BaseNoticeEvent } from './OB11BaseNoticeEvent';
export abstract class OB11GroupNoticeEvent extends OB11BaseNoticeEvent {
group_id: number;
user_id: number;
group_id: number = 0;
user_id: number = 0;
}

View File

@@ -2,29 +2,28 @@ import { OB11BaseNoticeEvent } from './OB11BaseNoticeEvent';
import { selfInfo } from '@/core/data';
import { OB11BaseEvent } from '../OB11BaseEvent';
class OB11PokeEvent extends OB11BaseNoticeEvent{
class OB11PokeEvent extends OB11BaseNoticeEvent {
notice_type = 'notify';
sub_type = 'poke';
target_id = parseInt(selfInfo.uin);
user_id: number;
target_id = 0;
user_id = 0;
}
export class OB11FriendPokeEvent extends OB11PokeEvent{
sender_id: number;
constructor(user_id: number) {
export class OB11FriendPokeEvent extends OB11PokeEvent {
constructor(user_id: number, target_id: number) {
super();
this.target_id = target_id;
this.user_id = user_id;
this.sender_id = user_id;
}
}
export class OB11GroupPokeEvent extends OB11PokeEvent{
export class OB11GroupPokeEvent extends OB11PokeEvent {
group_id: number;
constructor(group_id: number, user_id: number=0) {
constructor(group_id: number, user_id: number = 0, target_id: number = 0,) {
super();
this.group_id = group_id;
this.target_id = user_id;
this.target_id = target_id;
this.user_id = user_id;
}
}

View File

@@ -19,7 +19,7 @@ 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, groupNotifies, selfInfo, tempGroupCodeMap } from '@/core/data';
import { getGroup, getGroupMember, selfInfo, tempGroupCodeMap } from '@/core/data';
import { dbUtil } from '@/common/utils/db';
import { BuddyListener, GroupListener, NodeIKernelBuddyListener } from '@/core/listeners';
import { OB11FriendRequestEvent } from '@/onebot11/event/request/OB11FriendRequest';
@@ -37,8 +37,6 @@ 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 { MiniAppUtil } from '@/common/utils/Packet';
import { RequestUtil } from '@/common/utils/request';
//下面几个其实应该移进Core-Data 缓存实现 但是现在在这里方便
//
@@ -50,7 +48,7 @@ export interface LineDevice {
export let DeviceList = new Array<LineDevice>();
//peer->cached(boolen)
const PokeCache = new Map<string, boolean>();
// const PokeCache = new Map<string, boolean>();
export class NapCatOnebot11 {
private bootTime: number = Date.now() / 1000; // 秒
@@ -116,38 +114,39 @@ export class NapCatOnebot11 {
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(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);
}
// 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 = [];
@@ -332,12 +331,21 @@ export class NapCatOnebot11 {
postOB11Event(msg);
// log("post msg", msg)
}).catch(e => logError('constructMessage error: ', e));
OB11Constructor.GroupEvent(message).then(groupEvent => {
if (groupEvent) {
// log("post group event", groupEvent);
postOB11Event(groupEvent);
}
}).catch(e => logError('constructGroupEvent error: ', e));
OB11Constructor.PrivateEvent(message).then(privateEvent => {
if (privateEvent) {
// log("post private event", privateEvent);
postOB11Event(privateEvent);
}
});
}
}
async SetConfig(NewOb11: OB11Config) {
@@ -407,13 +415,8 @@ export class NapCatOnebot11 {
if (notifyTime < this.bootTime) {
continue;
}
const flag = notify.group.groupCode + '|' + notify.seq;
const existNotify = groupNotifies[flag];
if (existNotify) {
continue;
}
const flag = notify.group.groupCode + '|' + notify.seq + '|' + notify.type;
logDebug('收到群通知', notify);
groupNotifies[flag] = notify;
// let member2: GroupMember;
// if (notify.user2.uid) {
// member2 = await getGroupMember(notify.group.groupCode, null, notify.user2.uid);

View File

@@ -1,12 +1,11 @@
import { Response } from 'express';
import { OB11Response } from '../action/OB11Response';
import { HttpServerBase } from '@/common/server/http';
import { actionHandlers, actionMap } from '../action';
import { actionMap } from '../action';
import { ob11Config } from '@/onebot11/config';
import { selfInfo } from '@/core/data';
import { OB11HeartbeatEvent } from '@/onebot11/event/meta/OB11HeartbeatEvent';
import { postOB11Event } from '@/onebot11/server/postOB11Event';
import { napCatCore } from '@/core';
class OB11HTTPServer extends HttpServerBase {
name = 'OneBot V11 server';

View File

@@ -12,7 +12,7 @@ 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, groupNotifies, selfInfo } from '@/core/data';
import { getGroup, selfInfo } from '@/core/data';
import { NTQQFriendApi, NTQQGroupApi, NTQQUserApi } from '@/core/apis';
import createSendElements from '../action/msg/SendMsg/create-send-elements';
@@ -168,7 +168,7 @@ async function handleMsg(msg: OB11Message, quickAction: QuickAction) {
async function handleGroupRequest(request: OB11GroupRequestEvent, quickAction: QuickActionGroupRequest) {
if (!isNull(quickAction.approve)) {
NTQQGroupApi.handleGroupRequest(
groupNotifies[request.flag],
request.flag,
quickAction.approve ? GroupRequestOperateTypes.approve : GroupRequestOperateTypes.reject,
quickAction.reason,
).then().catch(logError);

View File

@@ -87,7 +87,7 @@ export class ReverseWebsocket {
'X-Self-ID': selfInfo.uin,
'Authorization': `Bearer ${token}`,
'x-client-role': 'Universal', // koishi-adapter-onebot 需要这个字段
"User-Agent": "OneBot/11",
'User-Agent': 'OneBot/11',
}
});
registerWsEventSender(this.websocket);

View File

@@ -59,7 +59,8 @@ export enum OB11MessageDataType {
poke = 'poke',
dice = 'dice',
RPS = 'rps',
miniapp = 'miniapp'//json类
miniapp = 'miniapp',//json类
Location = 'location'
}
export interface OB11MessageMFace {

View File

@@ -1 +1 @@
export const version = '1.6.4';
export const version = '1.6.6';

View File

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

View File

@@ -21,20 +21,16 @@ function genCpModule(module: string) {
return { src: `./node_modules/${module}`, dest: `dist/node_modules/${module}`, flatten: false };
}
let startScripts: string[] | undefined = undefined;
let MoeHooModule: any = [];
if (process.env.NAPCAT_BUILDSYS == 'linux') {
if (process.env.NAPCAT_BUILDARCH == 'x64') {
MoeHooModule = [{ src: './src/core.lib/MoeHoo-linux-x64.node', dest: 'dist' }];
}
startScripts = ['./script/napcat.sh'];
} else if (process.env.NAPCAT_BUILDSYS == 'win32') {
if (process.env.NAPCAT_BUILDARCH == 'x64') {
MoeHooModule = [{ src: './src/core.lib/MoeHoo-win32-x64.node', dest: 'dist' }];
}
startScripts = ['./script/napcat.ps1', './script/napcat.bat', './script/napcat-utf8.bat', './script/napcat-utf8.ps1', './script/napcat-log.ps1'];
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 {
MoeHooModule = [{ src: './src/core.lib/MoeHoo-win32-x64.node', dest: 'dist' }, { src: './src/core.lib/MoeHoo-linux-x64.node', dest: 'dist' }];
startScripts = ['./script/napcat.sh', './script/napcat.ps1', './script/napcat.bat', './script/napcat-utf8.bat', './script/napcat-utf8.ps1', './script/napcat-log.ps1'];
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[] = [