Compare commits

...

124 Commits

Author SHA1 Message Date
idranme
67d3dfb3cf Merge pull request #367 from LLOneBot/dev
3.30.5
2024-08-25 23:09:44 +08:00
idranme
afe8392a1e chore: v3.30.5 2024-08-25 23:07:33 +08:00
idranme
c1f5c5cd58 fix 2024-08-25 20:00:13 +08:00
idranme
85001a40da Merge pull request #366 from LLOneBot/dev
3.30.4
2024-08-23 17:05:03 +08:00
idranme
867a05c85a chore: v3.30.4 2024-08-23 17:03:58 +08:00
idranme
d8a63f6561 fix 2024-08-23 17:02:31 +08:00
idranme
e9fb9d1b30 Update publish.yml 2024-08-23 16:08:59 +08:00
idranme
b4fc987537 Merge pull request #365 from LLOneBot/dev
3.30.3
2024-08-23 13:40:59 +08:00
idranme
d0ccf53d88 chore: v3.30.3 2024-08-23 13:39:26 +08:00
idranme
d5ca94569d fix 2024-08-23 13:32:58 +08:00
idranme
bf72685501 Merge pull request #363 from LLOneBot/dev
3.30.2
2024-08-23 00:30:48 +08:00
idranme
c07467b670 chore: v3.30.2 2024-08-23 00:08:52 +08:00
idranme
ea164fb048 fix: friend list 2024-08-22 23:47:15 +08:00
idranme
0c0ad9a616 Merge pull request #362 from LLOneBot/dev
3.30.1
2024-08-22 20:41:32 +08:00
idranme
7bb4808e2d chore: v3.30.1 2024-08-22 20:18:16 +08:00
idranme
3f7592d06d opt 2024-08-22 20:17:28 +08:00
idranme
2f341fcf43 fix 2024-08-22 18:16:08 +08:00
idranme
9c59e5903e Merge pull request #360 from LLOneBot/dev
3.30.0
2024-08-22 12:41:06 +08:00
idranme
339ba409ee chore: v3.30.0 2024-08-22 12:37:43 +08:00
idranme
099da66661 fix: poke event 2024-08-22 12:32:09 +08:00
idranme
adcde6e49e fix 2024-08-22 06:37:28 +08:00
idranme
b3b8f9cd72 fix 2024-08-22 06:23:35 +08:00
idranme
8b57ebd7de fix: adaptation 27187 2024-08-22 05:45:02 +08:00
idranme
1afaeb0396 fix: adaptation 27187 2024-08-22 03:34:42 +08:00
idranme
235a986253 fix: adaptation 27187 2024-08-22 02:48:01 +08:00
idranme
b16bea9548 fix: adaptation 27187 2024-08-22 02:01:44 +08:00
idranme
7897034d13 opt 2024-08-22 00:42:12 +08:00
idranme
eabe891838 opt 2024-08-21 23:36:35 +08:00
idranme
75d3fc27f0 chore: remove unused methods 2024-08-21 22:51:00 +08:00
idranme
111bb4dd88 fix: adaptation 27187 2024-08-21 22:14:52 +08:00
idranme
f8bf60a3a0 Merge pull request #357 from cnxysoft/dev
fix: Linux上报
2024-08-21 17:50:42 +08:00
Alen
7c22eb3376 fix: Linux上报 2024-08-21 17:42:33 +08:00
Alen
7e1f7ac7f5 Merge remote-tracking branch 'upstream/dev' into dev 2024-08-21 10:56:00 +08:00
idranme
4ea02676f7 Merge pull request #354 from LLOneBot/dev
3.29.6
2024-08-21 00:29:41 +08:00
idranme
ddefb4c194 chore: v3.29.6 2024-08-21 00:27:47 +08:00
idranme
2792fa4776 fix 2024-08-21 00:14:15 +08:00
idranme
c37858e2f9 opt 2024-08-20 21:13:27 +08:00
idranme
59a11faa7f Merge pull request #352 from LLOneBot/dev
3.29.5
2024-08-19 17:40:30 +08:00
idranme
3b3795c946 chore: v3.29.5 2024-08-19 17:38:42 +08:00
idranme
ff18937828 fix 2024-08-19 17:29:58 +08:00
idranme
65d02d7f21 Merge pull request #351 from LLOneBot/main
merge
2024-08-19 12:59:10 +08:00
idranme
9cb8ba017e Merge pull request #350 from snsin09/nocache
ws修复必须no_cache参数
2024-08-19 12:55:27 +08:00
yota
1e579858b8 ws修复必须no_cache参数 2024-08-19 09:47:24 +08:00
idranme
db0c800851 Merge pull request #347 from LLOneBot/dev
3.29.4
2024-08-18 21:09:15 +08:00
idranme
e912911dd8 chore: v3.29.4 2024-08-18 21:04:30 +08:00
idranme
2245d0d3de fix 2024-08-18 20:58:26 +08:00
idranme
a56eac0251 Merge pull request #345 from LLOneBot/main
merge
2024-08-18 16:45:02 +08:00
linyuchen
8be0562c19 Merge pull request #344 from LLOneBot/linyuchen-patch-1
Fix: typo
2024-08-17 23:46:12 +08:00
linyuchen
f4c77f3e20 Fix: typo 2024-08-17 23:45:41 +08:00
linyuchen
508e6f2928 Merge pull request #342 from gfhdhytghd/patch-1
Update LICENSE
2024-08-17 16:20:50 +08:00
lin
9353cb0432 Update LICENSE
修改许可证以在法律层面上禁止宣传
2024-08-17 14:21:27 +08:00
idranme
816e07f47c Merge pull request #341 from LLOneBot/dev
3.29.3
2024-08-16 22:27:41 +08:00
idranme
46b1e8e67d chore: v3.29.3 2024-08-16 22:25:17 +08:00
idranme
8542594181 fix 2024-08-16 21:58:05 +08:00
idranme
0d7aa9bd2c fix 2024-08-16 21:28:43 +08:00
idranme
a47ee4c3e4 fix 2024-08-16 09:53:23 +08:00
idranme
0182803ae1 Merge pull request #339 from LLOneBot/dev
3.29.2
2024-08-15 11:14:35 +08:00
idranme
94c1aea6df chore: v3.29.2 2024-08-15 10:57:15 +08:00
idranme
d143dc043c fix 2024-08-15 10:31:51 +08:00
idranme
3f4b0b44cf feat: cache recalled message content 2024-08-14 23:04:15 +08:00
idranme
26fc0c68b2 Merge pull request #337 from LLOneBot/dev
3.29.1
2024-08-14 19:00:42 +08:00
idranme
c1d7aa7aed chore: v3.29.1 2024-08-14 18:59:27 +08:00
idranme
6aa44bdd79 fix: /get_image 2024-08-14 18:20:39 +08:00
idranme
77f3bfc5c5 Merge pull request #335 from LLOneBot/dev
3.29.0
2024-08-13 22:11:17 +08:00
idranme
2715552814 chore: v3.29.0 2024-08-13 22:08:36 +08:00
idranme
8ed0e6c1be fix 2024-08-13 21:59:13 +08:00
idranme
260a0be184 Merge branch 'dev' of https://github.com/LLOneBot/LLOneBot into dev 2024-08-13 19:31:10 +08:00
idranme
6582ffe964 fix: msg 2024-08-13 19:29:22 +08:00
linyuchen
f8e231b8b8 chore: v3.28.7
fix: CPU占用过高
fix: 好友列表变动hook失败
2024-08-13 19:09:13 +08:00
Alen
4efcf5b520 Merge remote-tracking branch 'upstream/dev' into dev 2024-08-12 19:56:48 +08:00
Alen
9ff6ff7cab Merge remote-tracking branch 'upstream/dev' into dev 2024-08-12 16:09:24 +08:00
idranme
a0f5cc0e36 Merge pull request #333 from LLOneBot/dev
Update README.md
2024-08-12 15:01:26 +08:00
idranme
277c2a9b67 Update README.md 2024-08-12 15:00:41 +08:00
idranme
874acdd7fe Merge pull request #331 from LLOneBot/dev
3.28.6
2024-08-12 00:03:25 +08:00
idranme
b2b996df9c chore: v3.28.6 2024-08-12 00:01:39 +08:00
idranme
4427774c2d fix: multiForwardMsg 2024-08-12 00:01:06 +08:00
idranme
41c04faa05 Merge pull request #330 from LLOneBot/dev
3.28.5
2024-08-11 20:01:29 +08:00
idranme
6ad4492f01 chore: v3.28.5 2024-08-11 20:00:47 +08:00
idranme
d52f16bc88 opt 2024-08-11 19:42:44 +08:00
idranme
2b0179acd1 opt 2024-08-11 18:10:27 +08:00
idranme
f540f324a1 Merge pull request #329 from LLOneBot/dev
3.28.4
2024-08-11 12:21:37 +08:00
idranme
128f40a51d chore: v3.28.4 2024-08-11 12:17:47 +08:00
idranme
c815e0ca6b sync 2024-08-11 12:16:53 +08:00
idranme
1da720e0a7 sync 2024-08-11 02:43:14 +08:00
idranme
1472c9c949 opt 2024-08-11 00:23:17 +08:00
idranme
4678253815 sync 2024-08-11 00:18:54 +08:00
idranme
e1176e18cd Merge pull request #328 from LLOneBot/dev
3.28.3
2024-08-10 23:19:09 +08:00
idranme
107f02f21f chore: 3.28.3 2024-08-10 23:17:38 +08:00
idranme
51f8db3a83 opt 2024-08-10 22:31:14 +08:00
idranme
25691a4124 sync 2024-08-10 22:09:35 +08:00
idranme
40f03e6401 sync 2024-08-10 21:34:28 +08:00
idranme
9f89094978 sync 2024-08-10 20:36:15 +08:00
idranme
04f837145c sync 2024-08-10 18:14:33 +08:00
idranme
6126920830 sync 2024-08-10 17:17:19 +08:00
idranme
5c219aa003 opt 2024-08-09 22:32:54 +08:00
Alen
594a421163 Merge remote-tracking branch 'upstream/dev' into dev 2024-08-09 22:15:54 +08:00
idranme
ce5cf82339 Merge pull request #325 from LLOneBot/dev
3.28.2
2024-08-09 18:10:50 +08:00
idranme
6931277e33 chore: v3.28.2 2024-08-09 18:07:50 +08:00
idranme
be1b9c21c1 feat: support for at message segment specifying name 2024-08-09 18:02:52 +08:00
idranme
b02cd3af00 Create .editorconfig 2024-08-09 16:46:08 +08:00
idranme
22dcbac16f Merge pull request #324 from LLOneBot/dev
fix ci
2024-08-09 16:06:44 +08:00
idranme
44faedd6c0 fix ci 2024-08-09 16:05:51 +08:00
idranme
fb3b673e63 Merge pull request #323 from LLOneBot/dev
fix ci
2024-08-09 15:53:42 +08:00
idranme
4e377f86d1 fix ci 2024-08-09 15:53:04 +08:00
idranme
e8bd98020b Merge pull request #322 from LLOneBot/dev
v3.28.1
2024-08-09 15:49:29 +08:00
idranme
c520034934 chore: v3.28.1 2024-08-09 15:47:57 +08:00
idranme
5d5fd403b8 fix: filtering at segments when sending private chat messages 2024-08-09 15:44:18 +08:00
idranme
1fc02229df sync 2024-08-09 15:40:08 +08:00
idranme
6c8d3db3a4 opt 2024-08-09 14:26:30 +08:00
idranme
c5b69561af sync 2024-08-09 14:20:59 +08:00
idranme
b5bffff941 fix 2024-08-07 23:17:13 +08:00
idranme
1a2cdc8c0e opt 2024-08-07 22:08:47 +08:00
idranme
50ab62f103 opt: config 2024-08-07 21:39:26 +08:00
Alen
b748d84e8a Merge branch 'dev' of https://github.com/cnxysoft/LLOneBot into dev 2024-08-07 15:06:19 +08:00
Alen
e8d83d2958 Merge remote-tracking branch 'upstream/dev' into dev 2024-08-07 15:06:11 +08:00
idranme
5005d83ce0 opt: audio encoding and decoding 2024-08-07 04:22:51 +08:00
idranme
d7e40e488c Update README.md
LLAPI 已删库
2024-08-06 22:31:39 +08:00
idranme
4958e22770 Update README.md 2024-08-06 22:28:49 +08:00
idranme
a5e3f94228 chore: deps 2024-08-06 22:26:21 +08:00
idranme
9e57b2c17e Update publish.yml 2024-08-06 14:51:17 +08:00
idranme
e1ff366e10 clean 2024-08-06 02:32:28 +08:00
Alen
cdb34ffe61 Merge remote-tracking branch 'upstream/dev' into dev 2024-08-05 22:15:48 +08:00
Alen
a45c56bd85 Merge remote-tracking branch 'upstream/dev' into dev 2024-08-05 10:09:12 +08:00
Alen
bb07ebd5d7 Merge branch 'main' into dev 2024-08-05 10:07:28 +08:00
104 changed files with 5684 additions and 3387 deletions

9
.editorconfig Normal file
View File

@@ -0,0 +1,9 @@
root = true
[*]
insert_final_newline = true
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true

View File

@@ -14,7 +14,7 @@ jobs:
- name: setup node - name: setup node
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 18 node-version: 20
- name: install dependenies - name: install dependenies
run: | run: |
@@ -27,7 +27,6 @@ jobs:
- name: zip - name: zip
run: | run: |
sudo apt install zip -y sudo apt install zip -y
cp manifest.json ./dist/manifest.json
cd ./dist/ cd ./dist/
zip -r ../LLOneBot.zip ./* zip -r ../LLOneBot.zip ./*

View File

@@ -1,14 +0,0 @@
# 3.24.0
## 修复
* 修复图片rkey导致链接失效的问题
* 修复/get_image, /get_file 无法获取图片的问题
* 修复上报他人管理员被取消通知
## 新增
* 新增表情回应发送和上报
* 新增商城表情发送,和上报 url
* 新增转发单条消息接口 `forward_friend_single_msg`, `forward_group_single_msg`
* 新增新增好友事件

View File

@@ -1,4 +1,4 @@
MIT License MIT Without Public Sicial Media Promotion License
Copyright (c) 2024 LLOneBot Copyright (c) 2024 LLOneBot
@@ -19,3 +19,7 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.
You may use this software in accordance with the above terms, but you are not
allowed to promote this project or your projects based on this project on any
public social media.

View File

@@ -1,11 +1,11 @@
# LLOneBot # LLOneBot
LiteLoaderQQNT 插件,实现 OneBot 11 协议,用 QQ 机器人开发 LiteLoaderQQNT 插件,实现 OneBot 11 协议,用 QQ 机器人开发
> [!CAUTION]\ > [!CAUTION]\
> **请不要在 QQ 官方群聊和任何影响力较大的简中互联网平台(包括但不限于: B站,微博,知乎,抖音等)发布和讨论*任何*与本插件存在相关性的信息** > **请不要在 QQ 官方群聊和任何影响力较大的简中互联网平台(包括但不限于: 哔哩哔哩,微博,知乎,抖音等)发布和讨论*任何*与本插件存在相关性的信息**
TG群<https://t.me/+nLZEnpne-pQ1OWFl> TG 群:<https://t.me/+nLZEnpne-pQ1OWFl>
## 安装方法 ## 安装方法
@@ -23,29 +23,15 @@ TG群<https://t.me/+nLZEnpne-pQ1OWFl>
<https://llonebot.github.io/zh-CN/develop/api> <https://llonebot.github.io/zh-CN/develop/api>
## TODO
- [x] 重构摆脱LLAPI目前调用LLAPI只能在renderer进程调用需重构成在main进程调用
- [x] 支持正、反向websocket感谢@disymayufei的PR
- [x] 转发消息记录
- [x] 好友点赞api
- [x] 群管理功能,禁言、踢人,改群名片等
- [x] 视频消息
- [x] 文件消息
- [x] 群禁言事件上报
- [x] 优化加群成功事件上报
- [x] 清理缓存api
- [ ] 框架对接文档
## Stargazers over time ## Stargazers over time
[![Stargazers over time](https://starchart.cc/LLOneBot/LLOneBot.svg?variant=adaptive)](https://starchart.cc/LLOneBot/LLOneBot) [![Stargazers over time](https://starchart.cc/LLOneBot/LLOneBot.svg?variant=adaptive)](https://starchart.cc/LLOneBot/LLOneBot)
## 鸣谢 ## 鸣谢
- [NapCatQQ](https://github.com/NapNeko/NapCatQQ)
- [LiteLoaderQQNT](https://liteloaderqqnt.github.io/guide/install.html) - [LiteLoaderQQNT](https://liteloaderqqnt.github.io/guide/install.html)
- [LLAPI](https://github.com/Night-stars-1/LiteLoaderQQNT-Plugin-LLAPI) - [chronocat](https://github.com/chrononeko/chronocat)
- [chronocat](https://github.com/chrononeko/chronocat/)
- [koishi-plugin-adapter-onebot](https://github.com/koishijs/koishi-plugin-adapter-onebot) - [koishi-plugin-adapter-onebot](https://github.com/koishijs/koishi-plugin-adapter-onebot)
- [silk-wasm](https://github.com/idranme/silk-wasm) - [silk-wasm](https://github.com/idranme/silk-wasm)

View File

@@ -5,14 +5,7 @@ import './scripts/gen-manifest'
const external = [ const external = [
'silk-wasm', 'silk-wasm',
'ws', 'ws',
'level', '@minatojs/sql.js',
'classic-level',
'abstract-level',
'level-supports',
'level-transcoder',
'module-error',
'catering',
'node-gyp-build',
] ]
function genCpModule(module: string) { function genCpModule(module: string) {

View File

@@ -3,8 +3,8 @@
"type": "extension", "type": "extension",
"name": "LLOneBot", "name": "LLOneBot",
"slug": "LLOneBot", "slug": "LLOneBot",
"description": "实现 OneBot 11 协议,用 QQ 机器人开发", "description": "实现 OneBot 11 协议,用 QQ 机器人开发",
"version": "3.28.0", "version": "3.30.5",
"icon": "./icon.webp", "icon": "./icon.webp",
"authors": [ "authors": [
{ {
@@ -13,7 +13,7 @@
} }
], ],
"repository": { "repository": {
"repo": "linyuchen/LiteLoaderQQNT-OneBotApi", "repo": "LLOneBot/LLOneBot",
"branch": "main", "branch": "main",
"release": { "release": {
"tag": "latest", "tag": "latest",

View File

@@ -2,7 +2,7 @@
"name": "llonebot", "name": "llonebot",
"version": "1.0.0", "version": "1.0.0",
"type": "module", "type": "module",
"description": "NTQQLiteLoaderOneBotApi", "description": "",
"main": "dist/main.js", "main": "dist/main.js",
"scripts": { "scripts": {
"build": "electron-vite build", "build": "electron-vite build",
@@ -16,27 +16,30 @@
"author": "", "author": "",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@minatojs/driver-sqlite": "^4.5.0",
"compressing": "^1.10.1", "compressing": "^1.10.1",
"cordis": "^3.18.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"express": "^4.18.2", "cosmokit": "^1.6.2",
"express": "^4.19.2",
"fast-xml-parser": "^4.4.1", "fast-xml-parser": "^4.4.1",
"file-type": "^19.0.0", "file-type": "^19.4.1",
"fluent-ffmpeg": "^2.1.2", "fluent-ffmpeg": "^2.1.3",
"level": "^8.0.1", "minato": "^3.5.1",
"silk-wasm": "^3.6.1", "silk-wasm": "^3.6.1",
"ws": "^8.18.0" "ws": "^8.18.0"
}, },
"devDependencies": { "devDependencies": {
"@types/cors": "^2.8.17", "@types/cors": "^2.8.17",
"@types/express": "^4.17.20", "@types/express": "^4.17.21",
"@types/fluent-ffmpeg": "^2.1.24", "@types/fluent-ffmpeg": "^2.1.25",
"@types/node": "^20.11.24", "@types/node": "^20.14.15",
"@types/ws": "^8.5.12", "@types/ws": "^8.5.12",
"electron": "^29.0.1", "electron": "^31.4.0",
"electron-vite": "^2.3.0", "electron-vite": "^2.3.0",
"typescript": "^5.5.4", "typescript": "^5.5.4",
"vite": "^5.3.5", "vite": "^5.4.2",
"vite-plugin-cp": "^4.0.8" "vite-plugin-cp": "^4.0.8"
}, },
"packageManager": "yarn@4.4.0" "packageManager": "yarn@4.4.1"
} }

View File

@@ -6,7 +6,7 @@ const manifest = {
type: 'extension', type: 'extension',
name: 'LLOneBot', name: 'LLOneBot',
slug: 'LLOneBot', slug: 'LLOneBot',
description: '实现 OneBot 11 协议,用 QQ 机器人开发', description: '实现 OneBot 11 协议,用 QQ 机器人开发',
version, version,
icon: './icon.webp', icon: './icon.webp',
authors: [ authors: [
@@ -16,7 +16,7 @@ const manifest = {
} }
], ],
repository: { repository: {
repo: 'linyuchen/LiteLoaderQQNT-OneBotApi', repo: 'LLOneBot/LLOneBot',
branch: 'main', branch: 'main',
release: { release: {
tag: 'latest', tag: 'latest',

View File

@@ -1,17 +0,0 @@
import { Level } from 'level'
const db = new Level(process.env['level_db_path'] as string, { valueEncoding: 'json' })
async function getGroupNotify() {
let keys = await db.keys().all()
let result: string[] = []
for (const key of keys) {
// console.log(key)
if (key.startsWith('group_notify_')) {
result.push(key)
}
}
return result
}
getGroupNotify().then(console.log)

View File

@@ -2,13 +2,9 @@ import fs from 'node:fs'
import { Config, OB11Config } from './types' import { Config, OB11Config } from './types'
import { mergeNewProperties } from './utils/helper' import { mergeNewProperties } from './utils/helper'
import path from 'node:path' import path from 'node:path'
import { selfInfo } from './data' import { getSelfUin } from './data'
import { DATA_DIR } from './utils' import { DATA_DIR } from './utils'
export const HOOK_LOG = false
export const ALLOW_SEND_TEMP_MSG = false
export class ConfigUtil { export class ConfigUtil {
private readonly configPath: string private readonly configPath: string
private config: Config | null = null private config: Config | null = null
@@ -52,6 +48,7 @@ export class ConfigUtil {
autoDeleteFile: false, autoDeleteFile: false,
autoDeleteFileSecond: 60, autoDeleteFileSecond: 60,
musicSignUrl: '', musicSignUrl: '',
msgCacheExpire: 120
} }
if (!fs.existsSync(this.configPath)) { if (!fs.existsSync(this.configPath)) {
@@ -97,6 +94,6 @@ export class ConfigUtil {
} }
export function getConfigUtil() { export function getConfigUtil() {
const configFilePath = path.join(DATA_DIR, `config_${selfInfo.uin}.json`) const configFilePath = path.join(DATA_DIR, `config_${getSelfUin()}.json`)
return new ConfigUtil(configFilePath) return new ConfigUtil(configFilePath)
} }

View File

@@ -1,124 +1,107 @@
import { import {
CategoryFriend,
type Friend,
type FriendRequest,
type Group,
type GroupMember, type GroupMember,
type SelfInfo, type SelfInfo,
User,
} from '../ntqqapi/types' } from '../ntqqapi/types'
import { type FileCache, type LLOneBotError } from './types' import { type LLOneBotError } from './types'
import { NTQQGroupApi } from '../ntqqapi/api/group' import { NTQQGroupApi } from '../ntqqapi/api/group'
import { log } from './utils/log'
import { isNumeric } from './utils/helper' import { isNumeric } from './utils/helper'
import { NTQQFriendApi } from '../ntqqapi/api' import { NTQQUserApi } from '../ntqqapi/api'
import { WebApiGroupMember } from '@/ntqqapi/api/webapi' import { RawMessage } from '../ntqqapi/types'
import { getConfigUtil } from './config'
export const selfInfo: SelfInfo = { export const llonebotError: LLOneBotError = {
ffmpegError: '',
httpServerError: '',
wsServerError: '',
otherError: 'LLOneBot 未能正常启动,请检查日志查看错误',
}
// 群号 -> 群成员map(uid=>GroupMember)
export const groupMembers: Map<string, Map<string, GroupMember>> = new Map<string, Map<string, GroupMember>>()
export async function getGroupMember(groupCode: string | number, memberUinOrUid: string | number) {
const groupCodeStr = groupCode.toString()
const memberUinOrUidStr = memberUinOrUid.toString()
let members = groupMembers.get(groupCodeStr)
if (!members) {
try {
members = await NTQQGroupApi.getGroupMembers(groupCodeStr)
// 更新群成员列表
groupMembers.set(groupCodeStr, members)
}
catch (e) {
return null
}
}
const getMember = () => {
let member: GroupMember | undefined = undefined
if (isNumeric(memberUinOrUidStr)) {
member = Array.from(members!.values()).find(member => member.uin === memberUinOrUidStr)
} else {
member = members!.get(memberUinOrUidStr)
}
return member
}
let member = getMember()
if (!member) {
members = await NTQQGroupApi.getGroupMembers(groupCodeStr)
groupMembers.set(groupCodeStr, members)
member = getMember()
}
return member
}
const selfInfo: SelfInfo = {
uid: '', uid: '',
uin: '', uin: '',
nick: '', nick: '',
online: true, online: true,
} }
export const WebGroupData = {
GroupData: new Map<string, Array<WebApiGroupMember>>(),
GroupTime: new Map<string, number>(),
}
export let groups: Group[] = []
export let friends: Friend[] = []
export let friendRequests: Map<number, FriendRequest> = new Map<number, FriendRequest>()
export const llonebotError: LLOneBotError = {
ffmpegError: '',
httpServerError: '',
wsServerError: '',
otherError: 'LLOnebot未能正常启动请检查日志查看错误',
}
export async function getFriend(uinOrUid: string): Promise<Friend | undefined> { export async function getSelfNick(force = false): Promise<string> {
let filterKey = isNumeric(uinOrUid.toString()) ? 'uin' : 'uid' if ((!selfInfo.nick || force) && selfInfo.uid) {
let filterValue = uinOrUid const userInfo = await NTQQUserApi.getUserDetailInfo(selfInfo.uid)
let friend = friends.find((friend) => friend[filterKey] === filterValue.toString()) if (userInfo) {
if (!friend) { selfInfo.nick = userInfo.nick
try { return userInfo.nick
const _friends = await NTQQFriendApi.getFriends(true)
friend = _friends.find((friend) => friend[filterKey] === filterValue.toString())
if (friend) {
friends.push(friend)
}
} catch (e: any) {
log('刷新好友列表失败', e.stack.toString())
} }
} }
return friend
return selfInfo.nick
} }
export async function getGroup(qq: string): Promise<Group | undefined> { export function getSelfInfo() {
let group = groups.find((group) => group.groupCode === qq.toString()) return selfInfo
if (!group) { }
try {
const _groups = await NTQQGroupApi.getGroups(true) export function setSelfInfo(data: Partial<SelfInfo>) {
group = _groups.find((group) => group.groupCode === qq.toString()) Object.assign(selfInfo, data)
if (group) { }
groups.push(group)
} export function getSelfUid() {
} catch (e) { return selfInfo['uid']
} }
export function getSelfUin() {
return selfInfo['uin']
}
const messages: Map<string, RawMessage> = new Map()
/** 缓存近期消息内容 */
export async function addMsgCache(msg: RawMessage) {
const expire = getConfigUtil().getConfig().msgCacheExpire! * 1000
if (expire === 0) {
return
} }
return group const id = msg.msgId
messages.set(id, msg)
setTimeout(() => {
messages.delete(id)
}, expire)
} }
export function deleteGroup(groupCode: string) { /** 获取近期消息内容 */
const groupIndex = groups.findIndex((group) => group.groupCode === groupCode.toString()) export function getMsgCache(msgId: string) {
// log(groups, groupCode, groupIndex); return messages.get(msgId)
if (groupIndex !== -1) {
log('删除群', groupCode)
groups.splice(groupIndex, 1)
}
} }
export async function getGroupMember(groupQQ: string | number, memberUinOrUid: string | number) {
groupQQ = groupQQ.toString()
memberUinOrUid = memberUinOrUid.toString()
const group = await getGroup(groupQQ)
if (group) {
const filterKey = isNumeric(memberUinOrUid) ? 'uin' : 'uid'
const filterValue = memberUinOrUid
let filterFunc: (member: GroupMember) => boolean = (member) => member[filterKey] === filterValue
let member = group.members?.find(filterFunc)
if (!member) {
try {
const _members = await NTQQGroupApi.getGroupMembers(groupQQ)
if (_members.length > 0) {
group.members = _members
}
} catch (e) {
// log("刷新群成员列表失败", e.stack.toString())
}
member = group.members?.find(filterFunc)
}
return member
}
return null
}
export async function refreshGroupMembers(groupQQ: string) {
const group = groups.find((group) => group.groupCode === groupQQ)
if (group) {
group.members = await NTQQGroupApi.getGroupMembers(groupQQ)
}
}
export const uidMaps: Record<string, string> = {} // 一串加密的字符串(uid) -> qq号
export function getUidByUin(uin: string) {
for (const uid in uidMaps) {
if (uidMaps[uid] === uin) {
return uid
}
}
}
export let tempGroupCodeMap: Record<string, string> = {} // peerUid => 群号
export let rawFriends: CategoryFriend[] = []

View File

@@ -1,275 +0,0 @@
import { Level } from 'level'
import { type GroupNotify, RawMessage } from '../ntqqapi/types'
import { DATA_DIR } from './utils'
import { selfInfo } from './data'
import { FileCache } from './types'
import { log } from './utils/log'
type ReceiveTempUinMap = Record<string, string>
class DBUtil {
public readonly DB_KEY_PREFIX_MSG_ID = 'msg_id_'
public readonly DB_KEY_PREFIX_MSG_SHORT_ID = 'msg_short_id_'
public readonly DB_KEY_PREFIX_MSG_SEQ_ID = 'msg_seq_id_'
public readonly DB_KEY_PREFIX_FILE = 'file_'
public readonly DB_KEY_PREFIX_GROUP_NOTIFY = 'group_notify_'
private readonly DB_KEY_RECEIVED_TEMP_UIN_MAP = 'received_temp_uin_map'
public db: Level | undefined
public cache: Record<string, RawMessage | string | FileCache | GroupNotify | ReceiveTempUinMap> = {} // <msg_id_ | msg_short_id_ | msg_seq_id_><id>: RawMessage
private currentShortId: number | undefined
/*
* 数据库结构
* msg_id_101231230999: {} // 长id: RawMessage
* msg_short_id_1: 101231230999 // 短id: 长id
* msg_seq_id_1: 101231230999 // 序列id: 长id
* file_7827DBAFJFW2323.png: {} // 文件名: FileCache
* */
constructor() {
let initCount = 0
new Promise((resolve, reject) => {
const initDB = () => {
initCount++
// if (initCount > 50) {
// return reject("init db fail")
// }
try {
if (!selfInfo.uin) {
setTimeout(initDB, 300)
return
}
const DB_PATH = DATA_DIR + `/msg_${selfInfo.uin}`
this.db = new Level(DB_PATH, { valueEncoding: 'json' })
console.log('llonebot init db success')
resolve(null)
} catch (e: any) {
console.log('init db fail', e.stack.toString())
setTimeout(initDB, 300)
}
}
setTimeout(initDB)
}).then()
const expiredMilliSecond = 1000 * 60 * 60
setInterval(() => {
// this.cache = {}
// 清理时间较久的缓存
const now = Date.now()
for (let key in this.cache) {
let message: RawMessage = this.cache[key] as RawMessage
if (message?.msgTime) {
if (now - parseInt(message.msgTime) * 1000 > expiredMilliSecond) {
delete this.cache[key]
// log("clear cache", key, message.msgTime);
}
}
}
}, expiredMilliSecond)
}
public async getReceivedTempUinMap(): Promise<ReceiveTempUinMap> {
try {
this.cache[this.DB_KEY_RECEIVED_TEMP_UIN_MAP] = JSON.parse(await this.db?.get(this.DB_KEY_RECEIVED_TEMP_UIN_MAP)!)
} catch (e) { }
return (this.cache[this.DB_KEY_RECEIVED_TEMP_UIN_MAP] || {}) as ReceiveTempUinMap
}
public setReceivedTempUinMap(data: ReceiveTempUinMap) {
this.cache[this.DB_KEY_RECEIVED_TEMP_UIN_MAP] = data
this.db?.put(this.DB_KEY_RECEIVED_TEMP_UIN_MAP, JSON.stringify(data)).then()
}
private addCache(msg: RawMessage) {
const longIdKey = this.DB_KEY_PREFIX_MSG_ID + msg.msgId
const shortIdKey = this.DB_KEY_PREFIX_MSG_SHORT_ID + msg.msgShortId
const seqIdKey = this.DB_KEY_PREFIX_MSG_SEQ_ID + msg.msgSeq
this.cache[longIdKey] = this.cache[shortIdKey] = msg
}
public clearCache() {
this.cache = {}
}
async getMsgByShortId(shortMsgId: number): Promise<RawMessage | undefined> {
const shortMsgIdKey = this.DB_KEY_PREFIX_MSG_SHORT_ID + shortMsgId
if (this.cache[shortMsgIdKey]) {
// log("getMsgByShortId cache", shortMsgIdKey, this.cache[shortMsgIdKey])
return this.cache[shortMsgIdKey] as RawMessage
}
try {
const longId = await this.db?.get(shortMsgIdKey)
const msg = await this.getMsgByLongId(longId!)
this.addCache(msg!)
return msg
} catch (e: any) {
log('getMsgByShortId db error', e.stack.toString())
}
}
async getMsgByLongId(longId: string): Promise<RawMessage | undefined> {
const longIdKey = this.DB_KEY_PREFIX_MSG_ID + longId
if (this.cache[longIdKey]) {
return this.cache[longIdKey] as RawMessage
}
try {
const data = await this.db?.get(longIdKey)
const msg = JSON.parse(data!)
this.addCache(msg)
return msg
} catch (e) {
// log("getMsgByLongId db error", e.stack.toString())
}
}
async getMsgBySeqId(seqId: string): Promise<RawMessage | undefined> {
const seqIdKey = this.DB_KEY_PREFIX_MSG_SEQ_ID + seqId
if (this.cache[seqIdKey]) {
return this.cache[seqIdKey] as RawMessage
}
try {
const longId = await this.db?.get(seqIdKey)
const msg = await this.getMsgByLongId(longId!)
this.addCache(msg!)
return msg
} catch (e: any) {
log('getMsgBySeqId db error', e.stack.toString())
}
}
async addMsg(msg: RawMessage) {
// 有则更新,无则添加
// log("addMsg", msg.msgId, msg.msgSeq, msg.msgShortId);
const longIdKey = this.DB_KEY_PREFIX_MSG_ID + msg.msgId
let existMsg: RawMessage | undefined = this.cache[longIdKey] as RawMessage
if (!existMsg) {
try {
existMsg = await this.getMsgByLongId(msg.msgId)
} catch (e) {
// log("addMsg getMsgByLongId error", e.stack.toString())
}
}
if (existMsg) {
// log("消息已存在", existMsg.msgSeq, existMsg.msgShortId, existMsg.msgId)
this.updateMsg(msg).then()
return existMsg.msgShortId
}
const shortMsgId = await this.genMsgShortId()
const shortIdKey = this.DB_KEY_PREFIX_MSG_SHORT_ID + shortMsgId
const seqIdKey = this.DB_KEY_PREFIX_MSG_SEQ_ID + msg.msgSeq
msg.msgShortId = shortMsgId
this.addCache(msg)
// log("新增消息记录", msg.msgId)
this.db?.put(shortIdKey, msg.msgId).then().catch()
this.db?.put(longIdKey, JSON.stringify(msg)).then().catch()
try {
await this.db?.get(seqIdKey)
} catch (e) {
// log("新的seqId", seqIdKey)
this.db?.put(seqIdKey, msg.msgId).then().catch()
}
if (!this.cache[seqIdKey]) {
this.cache[seqIdKey] = msg
}
return shortMsgId
// log(`消息入库 ${seqIdKey}: ${msg.msgId}, ${shortMsgId}: ${msg.msgId}`);
}
async updateMsg(msg: RawMessage) {
const longIdKey = this.DB_KEY_PREFIX_MSG_ID + msg.msgId
let existMsg: RawMessage | undefined = this.cache[longIdKey] as RawMessage
if (!existMsg) {
try {
existMsg = await this.getMsgByLongId(msg.msgId)
} catch (e) {
existMsg = msg
}
}
Object.assign(existMsg!, msg)
this.db?.put(longIdKey, JSON.stringify(existMsg)).then().catch()
const shortIdKey = this.DB_KEY_PREFIX_MSG_SHORT_ID + existMsg?.msgShortId
const seqIdKey = this.DB_KEY_PREFIX_MSG_SEQ_ID + msg.msgSeq
if (!this.cache[seqIdKey]) {
this.cache[seqIdKey] = existMsg!
}
this.db?.put(shortIdKey, msg.msgId).then().catch()
try {
await this.db?.get(seqIdKey)
} catch (e) {
this.db?.put(seqIdKey, msg.msgId).then().catch()
// log("更新seqId error", e.stack, seqIdKey);
}
// log("更新消息", existMsg.msgSeq, existMsg.msgShortId, existMsg.msgId);
}
private async genMsgShortId(): Promise<number> {
const key = 'msg_current_short_id'
if (this.currentShortId === undefined) {
try {
const id = await this.db?.get(key)
this.currentShortId = parseInt(id!)
} catch (e) {
this.currentShortId = -2147483640
}
}
this.currentShortId++
this.db?.put(key, this.currentShortId.toString()).then().catch()
return this.currentShortId
}
async addFileCache(fileNameOrUuid: string, data: FileCache) {
const key = this.DB_KEY_PREFIX_FILE + fileNameOrUuid
if (this.cache[key]) {
return
}
let cacheDBData = { ...data }
delete cacheDBData['downloadFunc']
this.cache[fileNameOrUuid] = data
try {
await this.db?.put(key, JSON.stringify(cacheDBData))
} catch (e: any) {
log('addFileCache db error', e.stack.toString())
}
}
async getFileCache(fileNameOrUuid: string): Promise<FileCache | undefined> {
const key = this.DB_KEY_PREFIX_FILE + fileNameOrUuid
if (this.cache[key]) {
return this.cache[key] as FileCache
}
try {
const data = await this.db?.get(key)
return JSON.parse(data!)
} catch (e) {
// log("getFileCache db error", e.stack.toString())
}
}
async addGroupNotify(notify: GroupNotify) {
const key = this.DB_KEY_PREFIX_GROUP_NOTIFY + notify.seq
let existNotify = this.cache[key] as GroupNotify
if (existNotify) {
return
}
this.cache[key] = notify
this.db?.put(key, JSON.stringify(notify)).then().catch()
}
async getGroupNotify(seq: string): Promise<GroupNotify | undefined> {
const key = this.DB_KEY_PREFIX_GROUP_NOTIFY + seq
if (this.cache[key]) {
return this.cache[key] as GroupNotify
}
try {
const data = await this.db?.get(key)
return JSON.parse(data!)
} catch (e) {
// log("getGroupNotify db error", e.stack.toString())
}
}
}
export const dbUtil = new DBUtil()

View File

@@ -58,7 +58,7 @@ export abstract class HttpServerBase {
start(port: number) { start(port: number) {
try { try {
this.expressAPP.get('/', (req: Request, res: Response) => { this.expressAPP.get('/', (req: Request, res: Response) => {
res.send(`${this.name}已启动`) res.send(`${this.name} 已启动`)
}) })
this.listen(port) this.listen(port)
llonebotError.httpServerError = '' llonebotError.httpServerError = ''
@@ -100,7 +100,7 @@ export abstract class HttpServerBase {
} else if (req.query) { } else if (req.query) {
payload = { ...req.query, ...req.body } payload = { ...req.query, ...req.body }
} }
log('收到http请求', url, payload) log('收到 HTTP 请求', url, payload)
try { try {
res.send(await handler(res, payload)) res.send(await handler(res, payload))
} catch (e: any) { } catch (e: any) {

View File

@@ -1,93 +0,0 @@
import { WebSocket, WebSocketServer } from 'ws'
import urlParse from 'url'
import { IncomingMessage } from 'node:http'
import { log } from '../utils/log'
import { getConfigUtil } from '../config'
import { llonebotError } from '../data'
class WebsocketClientBase {
private wsClient: WebSocket | undefined
constructor() { }
send(msg: string) {
if (this.wsClient && this.wsClient.readyState == WebSocket.OPEN) {
this.wsClient.send(msg)
}
}
onMessage(msg: string) { }
}
export class WebsocketServerBase {
private ws: WebSocketServer | null = null
constructor() {
console.log(`llonebot websocket service started`)
}
start(port: number) {
try {
this.ws = new WebSocketServer({ port, maxPayload: 1024 * 1024 * 1024 })
llonebotError.wsServerError = ''
} catch (e: any) {
llonebotError.wsServerError = '正向ws服务启动失败, ' + e.toString()
}
this.ws?.on('connection', (wsClient, req) => {
const url = req.url?.split('?').shift()
this.authorize(wsClient, req)
this.onConnect(wsClient, url!, req)
wsClient.on('message', async (msg) => {
this.onMessage(wsClient, url!, msg.toString())
})
})
}
stop() {
llonebotError.wsServerError = ''
this.ws?.close((err) => {
log('ws server close failed!', err)
})
this.ws = null
}
restart(port: number) {
this.stop()
this.start(port)
}
authorize(wsClient: WebSocket, req) {
let token = getConfigUtil().getConfig().token
const url = req.url.split('?').shift()
log('ws connect', url)
let clientToken: string = ''
const authHeader = req.headers['authorization']
if (authHeader) {
clientToken = authHeader.split('Bearer ').pop()
log('receive ws header token', clientToken)
} else {
const parsedUrl = urlParse.parse(req.url, true)
const urlToken = parsedUrl.query.access_token
if (urlToken) {
if (Array.isArray(urlToken)) {
clientToken = urlToken[0]
} else {
clientToken = urlToken
}
log('receive ws url token', clientToken)
}
}
if (token && clientToken != token) {
this.authorizeFailed(wsClient)
return wsClient.close()
}
}
authorizeFailed(wsClient: WebSocket) { }
onConnect(wsClient: WebSocket, url: string, req: IncomingMessage) { }
onMessage(wsClient: WebSocket, url: string, msg: string) { }
sendHeart() { }
}

View File

@@ -12,10 +12,12 @@ export interface OB11Config {
enableHttpHeart?: boolean enableHttpHeart?: boolean
enableQOAutoQuote: boolean // 快速操作回复自动引用原消息 enableQOAutoQuote: boolean // 快速操作回复自动引用原消息
} }
export interface CheckVersion { export interface CheckVersion {
result: boolean result: boolean
version: string version: string
} }
export interface Config { export interface Config {
enableLLOB: boolean enableLLOB: boolean
ob11: OB11Config ob11: OB11Config
@@ -30,6 +32,8 @@ export interface Config {
ffmpeg?: string // ffmpeg路径 ffmpeg?: string // ffmpeg路径
musicSignUrl?: string musicSignUrl?: string
ignoreBeforeLoginMsg?: boolean ignoreBeforeLoginMsg?: boolean
/** 单位为秒 */
msgCacheExpire?: number
} }
export interface LLOneBotError { export interface LLOneBotError {
@@ -41,11 +45,22 @@ export interface LLOneBotError {
export interface FileCache { export interface FileCache {
fileName: string fileName: string
filePath: string
fileSize: string fileSize: string
fileUuid?: string msgId: string
url?: string peerUid: string
msgId?: string chatType: number
elementId: string elementId: string
downloadFunc?: () => Promise<void> elementType: number
}
export interface FileCacheV2 {
fileName: string
fileSize: string
fileUuid: string
msgId: string
msgTime: number
peerUid: string
chatType: number
elementId: string
elementType: number
} }

View File

@@ -16,11 +16,13 @@ export interface ListenerIBase {
new(listener: any): ListenerClassBase new(listener: any): ListenerClassBase
} }
// forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/common/utils/EventTask.ts#L20
export class NTEventWrapper { export class NTEventWrapper {
private ListenerMap: { [key: string]: ListenerIBase } | undefined//ListenerName-Unique -> Listener构造函数 private ListenerMap: { [key: string]: ListenerIBase } | undefined//ListenerName-Unique -> Listener构造函数
private WrapperSession: NodeIQQNTWrapperSession | undefined//WrapperSession private WrapperSession: NodeIQQNTWrapperSession | undefined//WrapperSession
private ListenerManger: Map<string, ListenerClassBase> = new Map<string, ListenerClassBase>() //ListenerName-Unique -> Listener实例 private ListenerManger: Map<string, ListenerClassBase> = new Map<string, ListenerClassBase>() //ListenerName-Unique -> Listener实例
private EventTask = new Map<string, Map<string, Map<string, Internal_MapKey>>>()//tasks ListenerMainName -> ListenerSubName-> uuid -> {timeout,createtime,func} private EventTask = new Map<string, Map<string, Map<string, Internal_MapKey>>>()//tasks ListenerMainName -> ListenerSubName-> uuid -> {timeout,createtime,func}
public initialised = false
constructor() { constructor() {
} }
@@ -33,7 +35,7 @@ export class NTEventWrapper {
if (typeof target[prop] === 'undefined') { if (typeof target[prop] === 'undefined') {
// 如果方法不存在返回一个函数这个函数调用existentMethod // 如果方法不存在返回一个函数这个函数调用existentMethod
return (...args: any[]) => { return (...args: any[]) => {
current.DispatcherListener.apply(current, [ListenerMainName, prop, ...args]).then() current.dispatcherListener.apply(current, [ListenerMainName, prop, ...args]).then()
} }
} }
// 如果方法存在,正常返回 // 如果方法存在,正常返回
@@ -45,9 +47,10 @@ export class NTEventWrapper {
init({ ListenerMap, WrapperSession }: { ListenerMap: { [key: string]: typeof ListenerClassBase }, WrapperSession: NodeIQQNTWrapperSession }) { init({ ListenerMap, WrapperSession }: { ListenerMap: { [key: string]: typeof ListenerClassBase }, WrapperSession: NodeIQQNTWrapperSession }) {
this.ListenerMap = ListenerMap this.ListenerMap = ListenerMap
this.WrapperSession = WrapperSession this.WrapperSession = WrapperSession
this.initialised = true
} }
CreatEventFunction<T extends (...args: any) => any>(eventName: string): T | undefined { createEventFunction<T extends (...args: any) => any>(eventName: string): T | undefined {
const eventNameArr = eventName.split('/') const eventNameArr = eventName.split('/')
type eventType = { type eventType = {
[key: string]: () => { [key: string]: (...params: Parameters<T>) => Promise<ReturnType<T>> } [key: string]: () => { [key: string]: (...params: Parameters<T>) => Promise<ReturnType<T>> }
@@ -68,14 +71,14 @@ export class NTEventWrapper {
} }
} }
CreatListenerFunction<T>(listenerMainName: string, uniqueCode: string = ''): T { createListenerFunction<T>(listenerMainName: string, uniqueCode: string = ''): T {
const ListenerType = this.ListenerMap![listenerMainName] const ListenerType = this.ListenerMap![listenerMainName]
let Listener = this.ListenerManger.get(listenerMainName + uniqueCode) let Listener = this.ListenerManger.get(listenerMainName + uniqueCode)
if (!Listener && ListenerType) { if (!Listener && ListenerType) {
Listener = new ListenerType(this.createProxyDispatch(listenerMainName)) Listener = new ListenerType(this.createProxyDispatch(listenerMainName))
const ServiceSubName = listenerMainName.match(/^NodeIKernel(.*?)Listener$/)![1] const ServiceSubName = listenerMainName.match(/^NodeIKernel(.*?)Listener$/)![1]
const Service = 'NodeIKernel' + ServiceSubName + 'Service/addKernel' + ServiceSubName + 'Listener' const Service = 'NodeIKernel' + ServiceSubName + 'Service/addKernel' + ServiceSubName + 'Listener'
const addfunc = this.CreatEventFunction<(listener: T) => number>(Service) const addfunc = this.createEventFunction<(listener: T) => number>(Service)
addfunc!(Listener as T) addfunc!(Listener as T)
//console.log(addfunc!(Listener as T)) //console.log(addfunc!(Listener as T))
this.ListenerManger.set(listenerMainName + uniqueCode, Listener) this.ListenerManger.set(listenerMainName + uniqueCode, Listener)
@@ -84,7 +87,7 @@ export class NTEventWrapper {
} }
//统一回调清理事件 //统一回调清理事件
async DispatcherListener(ListenerMainName: string, ListenerSubName: string, ...args: any[]) { async dispatcherListener(ListenerMainName: string, ListenerSubName: string, ...args: any[]) {
//console.log("[EventDispatcher]",ListenerMainName, ListenerSubName, ...args) //console.log("[EventDispatcher]",ListenerMainName, ListenerSubName, ...args)
this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.forEach((task, uuid) => { this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.forEach((task, uuid) => {
//console.log(task.func, uuid, task.createtime, task.timeout) //console.log(task.func, uuid, task.createtime, task.timeout)
@@ -100,7 +103,7 @@ export class NTEventWrapper {
async CallNoListenerEvent<EventType extends (...args: any[]) => Promise<any> | any>(EventName = '', timeout: number = 3000, ...args: Parameters<EventType>) { async CallNoListenerEvent<EventType extends (...args: any[]) => Promise<any> | any>(EventName = '', timeout: number = 3000, ...args: Parameters<EventType>) {
return new Promise<Awaited<ReturnType<EventType>>>(async (resolve, reject) => { return new Promise<Awaited<ReturnType<EventType>>>(async (resolve, reject) => {
const EventFunc = this.CreatEventFunction<EventType>(EventName) const EventFunc = this.createEventFunction<EventType>(EventName)
let complete = false let complete = false
const Timeouter = setTimeout(() => { const Timeouter = setTimeout(() => {
if (!complete) { if (!complete) {
@@ -149,7 +152,7 @@ export class NTEventWrapper {
this.EventTask.get(ListenerMainName)?.set(ListenerSubName, new Map()) this.EventTask.get(ListenerMainName)?.set(ListenerSubName, new Map())
} }
this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.set(id, eventCallbak) this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.set(id, eventCallbak)
this.CreatListenerFunction(ListenerMainName) this.createListenerFunction(ListenerMainName)
}) })
} }
@@ -195,8 +198,8 @@ export class NTEventWrapper {
this.EventTask.get(ListenerMainName)?.set(ListenerSubName, new Map()) this.EventTask.get(ListenerMainName)?.set(ListenerSubName, new Map())
} }
this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.set(id, eventCallbak) this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.set(id, eventCallbak)
this.CreatListenerFunction(ListenerMainName) this.createListenerFunction(ListenerMainName)
const EventFunc = this.CreatEventFunction<EventType>(EventName) const EventFunc = this.createEventFunction<EventType>(EventName)
retEvent = await EventFunc!(...(args as any[])) retEvent = await EventFunc!(...(args as any[]))
}) })
} }

View File

@@ -0,0 +1,163 @@
import { Peer } from '@/ntqqapi/types'
import { createHash } from 'node:crypto'
import { LimitedHashTable } from './table'
import { DATA_DIR } from './index'
import Database, { Tables } from 'minato'
import SQLite from '@minatojs/driver-sqlite'
import fsPromise from 'node:fs/promises'
import fs from 'node:fs'
import path from 'node:path'
import { FileCacheV2 } from '../types'
interface SQLiteTables extends Tables {
message: {
shortId: number
msgId: string
chatType: number
peerUid: string
}
file_v2: FileCacheV2
}
interface MsgIdAndPeerByShortId {
MsgId: string
Peer: Peer
}
// forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/common/utils/MessageUnique.ts#L84
class MessageUniqueWrapper {
private msgDataMap: LimitedHashTable<string, number>
private msgIdMap: LimitedHashTable<string, number>
private db: Database<SQLiteTables> | undefined
constructor(maxMap: number = 1000) {
this.msgIdMap = new LimitedHashTable<string, number>(maxMap)
this.msgDataMap = new LimitedHashTable<string, number>(maxMap)
}
async init(uin: string) {
const dbDir = path.join(DATA_DIR, 'database')
if (!fs.existsSync(dbDir)) {
await fsPromise.mkdir(dbDir)
}
const database = new Database<SQLiteTables>()
await database.connect(SQLite, {
path: path.join(dbDir, `${uin}.db`)
})
database.extend('message', {
shortId: 'integer(10)',
chatType: 'unsigned',
msgId: 'string(24)',
peerUid: 'string(24)'
}, {
primary: 'shortId'
})
database.extend('file_v2', {
fileName: 'string',
fileSize: 'string',
fileUuid: 'string(128)',
msgId: 'string(24)',
msgTime: 'unsigned(10)',
peerUid: 'string(24)',
chatType: 'unsigned',
elementId: 'string(24)',
elementType: 'unsigned',
}, {
primary: 'fileUuid',
indexes: ['fileName']
})
this.db = database
}
async getRecentMsgIds(Peer: Peer, size: number): Promise<string[]> {
const heads = this.msgIdMap.getHeads(size)
if (!heads) {
return []
}
const data: (MsgIdAndPeerByShortId | undefined)[] = []
for (const t of heads) {
data.push(await MessageUnique.getMsgIdAndPeerByShortId(t.value))
}
const ret = data.filter((t) => t?.Peer.chatType === Peer.chatType && t?.Peer.peerUid === Peer.peerUid)
return ret.map((t) => t?.MsgId).filter((t) => t !== undefined)
}
createMsg(peer: Peer, msgId: string): number | undefined {
const key = `${msgId}|${peer.chatType}|${peer.peerUid}`
const hash = createHash('md5').update(key).digest()
//设置第一个bit为0 保证shortId为正数
hash[0] &= 0x7f
const shortId = hash.readInt32BE(0)
//减少性能损耗
// const isExist = this.msgIdMap.getKey(shortId)
// if (isExist && isExist === msgId) {
// return shortId
// }
this.msgIdMap.set(msgId, shortId)
this.msgDataMap.set(key, shortId)
this.db?.upsert('message', [{
msgId,
shortId,
chatType: peer.chatType,
peerUid: peer.peerUid
}], 'shortId').then()
return shortId
}
async getMsgIdAndPeerByShortId(shortId: number): Promise<MsgIdAndPeerByShortId | 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 }
}
const items = await this.db?.get('message', { shortId })
if (items?.length) {
const { msgId, chatType, peerUid } = items[0]
return {
MsgId: msgId,
Peer: {
chatType,
peerUid,
guildId: '',
}
}
}
return undefined
}
getShortIdByMsgId(msgId: string): number | undefined {
return this.msgIdMap.getValue(msgId)
}
async getPeerByMsgId(msgId: string) {
const shortId = this.msgIdMap.getValue(msgId)
if (!shortId) return undefined
return await this.getMsgIdAndPeerByShortId(shortId)
}
resize(maxSize: number): void {
this.msgIdMap.resize(maxSize)
this.msgDataMap.resize(maxSize)
}
addFileCache(data: FileCacheV2) {
return this.db?.upsert('file_v2', [data], 'fileUuid')
}
getFileCacheByName(fileName: string) {
return this.db?.get('file_v2', { fileName }, {
sort: { msgTime: 'desc' }
})
}
getFileCacheById(fileUuid: string) {
return this.db?.get('file_v2', { fileUuid })
}
}
export const MessageUnique: MessageUniqueWrapper = new MessageUniqueWrapper()

View File

@@ -1,5 +1,4 @@
import path from 'node:path' import path from 'node:path'
import fs from 'node:fs'
import os from 'node:os' import os from 'node:os'
import { systemPlatform } from './system' import { systemPlatform } from './system'
@@ -33,37 +32,11 @@ if (typeof configVersionInfoPath !== 'string') {
export { configVersionInfoPath } export { configVersionInfoPath }
type QQPkgInfo = { type QQPkgInfo = {
version: string; version: string
buildVersion: string; buildVersion: string
platform: string; platform: string
eleArch: string; eleArch: string
} }
type QQVersionConfigInfo = {
baseVersion: string;
curVersion: string;
prevVersion: string;
onErrorVersions: Array<any>;
buildId: string;
}
let _qqVersionConfigInfo: QQVersionConfigInfo = {
'baseVersion': '9.9.9-23361',
'curVersion': '9.9.9-23361',
'prevVersion': '',
'onErrorVersions': [],
'buildId': '23361',
}
if (fs.existsSync(configVersionInfoPath)) {
try {
const _ = JSON.parse(fs.readFileSync(configVersionInfoPath).toString())
_qqVersionConfigInfo = Object.assign(_qqVersionConfigInfo, _)
} catch (e) {
console.error('Load QQ version config info failed, Use default version', e)
}
}
export const qqVersionConfigInfo: QQVersionConfigInfo = _qqVersionConfigInfo
export const qqPkgInfo: QQPkgInfo = require(pkgInfoPath) export const qqPkgInfo: QQPkgInfo = require(pkgInfoPath)
// platform_type: 3, // platform_type: 3,
@@ -74,10 +47,6 @@ export const qqPkgInfo: QQPkgInfo = require(pkgInfoPath)
// platVer: '10.0.26100', // platVer: '10.0.26100',
// clientVer: '9.9.9-23159', // clientVer: '9.9.9-23159',
let _appid: string = '537213803' // 默认为 Windows 平台的 appid export function getBuildVersion(): number {
if (systemPlatform === 'linux') { return +qqPkgInfo.buildVersion
_appid = '537213827'
} }
// todo: mac 平台的 appid
export const appid = _appid
export const isQQ998: boolean = qqPkgInfo.buildVersion >= '22106'

View File

@@ -1,124 +1,93 @@
import fs from 'fs'
import fsPromise from 'fs/promises'
import { decode, encode, getDuration, getWavFileInfo, isWav, isSilk } from 'silk-wasm'
import { log } from './log'
import path from 'node:path' import path from 'node:path'
import ffmpeg from 'fluent-ffmpeg'
import fsPromise from 'node:fs/promises'
import { decode, encode, getDuration, getWavFileInfo, isWav, isSilk, EncodeResult } from 'silk-wasm'
import { log } from './log'
import { TEMP_DIR } from './index' import { TEMP_DIR } from './index'
import { getConfigUtil } from '../config' import { getConfigUtil } from '../config'
import { spawn } from 'node:child_process'
import { randomUUID } from 'node:crypto' import { randomUUID } from 'node:crypto'
import { Readable } from 'node:stream'
interface FFmpegOptions {
input?: string[]
output?: string[]
}
type Input = string | Readable
function convert(input: Input, options: FFmpegOptions): Promise<Buffer>
function convert(input: Input, options: FFmpegOptions, outputPath: string): Promise<string>
function convert(input: Input, options: FFmpegOptions, outputPath?: string): Promise<Buffer> | Promise<string> {
return new Promise<any>((resolve, reject) => {
const chunks: Buffer[] = []
let command = ffmpeg(input)
.on('error', err => {
log(`FFmpeg处理转换出错: `, err.message)
reject(err)
})
.on('end', () => {
if (!outputPath) {
resolve(Buffer.concat(chunks))
} else {
resolve(outputPath)
}
})
if (options.input) {
command = command.inputOptions(options.input)
}
if (options.output) {
command = command.outputOptions(options.output)
}
const ffmpegPath = getConfigUtil().getConfig().ffmpeg
if (ffmpegPath) {
command = command.setFfmpegPath(ffmpegPath)
}
if (!outputPath) {
const stream = command.pipe()
stream.on('data', chunk => {
chunks.push(chunk)
})
} else {
command.save(outputPath)
}
})
}
export async function encodeSilk(filePath: string) { export async function encodeSilk(filePath: string) {
function getFileHeader(filePath: string) {
// 定义要读取的字节数
const bytesToRead = 7
try {
const buffer = fs.readFileSync(filePath, {
encoding: null,
flag: 'r',
})
const fileHeader = buffer.toString('hex', 0, bytesToRead)
return fileHeader
} catch (err) {
console.error('读取文件错误:', err)
return
}
}
async function isWavFile(filePath: string) {
return isWav(fs.readFileSync(filePath))
}
async function guessDuration(pttPath: string) {
const pttFileInfo = await fsPromise.stat(pttPath)
let duration = pttFileInfo.size / 1024 / 3 // 3kb/s
duration = Math.floor(duration)
duration = Math.max(1, duration)
log(`通过文件大小估算语音的时长:`, duration)
return duration
}
// function verifyDuration(oriDuration: number, guessDuration: number) {
// // 单位都是秒
// if (oriDuration - guessDuration > 10) {
// return guessDuration
// }
// oriDuration = Math.max(1, oriDuration)
// return oriDuration
// }
// async function getAudioSampleRate(filePath: string) {
// try {
// const mm = await import('music-metadata');
// const metadata = await mm.parseFile(filePath);
// log(`${filePath}采样率`, metadata.format.sampleRate);
// return metadata.format.sampleRate;
// } catch (error) {
// log(`${filePath}采样率获取失败`, error.stack);
// // console.error(error);
// }
// }
try { try {
const file = await fsPromise.readFile(filePath) const file = await fsPromise.readFile(filePath)
const pttPath = path.join(TEMP_DIR, randomUUID())
if (!isSilk(file)) { if (!isSilk(file)) {
log(`语音文件${filePath}需要转换成silk`) log(`语音文件${filePath}需要转换成silk`)
const _isWav = isWav(file) let result: EncodeResult
const pcmPath = pttPath + '.pcm' const allowSampleRate = [8000, 12000, 16000, 24000, 32000, 44100, 48000]
let sampleRate = 0 if (isWav(file) && allowSampleRate.includes(getWavFileInfo(file).fmt.sampleRate)) {
const convert = () => { result = await encode(file, 0)
return new Promise<Buffer>((resolve, reject) => {
const ffmpegPath = getConfigUtil().getConfig().ffmpeg || process.env.FFMPEG_PATH || 'ffmpeg'
const cp = spawn(ffmpegPath, ['-y', '-i', filePath, '-ar', '24000', '-ac', '1', '-f', 's16le', pcmPath])
cp.on('error', (err) => {
log(`FFmpeg处理转换出错: `, err.message)
return reject(err)
})
cp.on('exit', (code, signal) => {
const EXIT_CODES = [0, 255]
if (code == null || EXIT_CODES.includes(code)) {
sampleRate = 24000
const data = fs.readFileSync(pcmPath)
fs.unlink(pcmPath, (err) => {
})
return resolve(data)
}
log(`FFmpeg exit: code=${code ?? 'unknown'} sig=${signal ?? 'unknown'}`)
reject(Error(`FFmpeg处理转换失败`))
})
})
}
let input: Buffer
if (!_isWav) {
input = await convert()
} else { } else {
input = file const input = await convert(filePath, {
const allowSampleRate = [8000, 12000, 16000, 24000, 32000, 44100, 48000] output: [
const { fmt } = getWavFileInfo(input) '-ar 24000',
// log(`wav文件信息`, fmt) '-ac 1',
if (!allowSampleRate.includes(fmt.sampleRate)) { '-f s16le'
input = await convert() ]
} })
result = await encode(input, 24000)
} }
const silk = await encode(input, sampleRate) const pttPath = path.join(TEMP_DIR, randomUUID())
fs.writeFileSync(pttPath, silk.data) await fsPromise.writeFile(pttPath, result.data)
log(`语音文件${filePath}转换成功!`, pttPath, `时长:`, silk.duration) log(`语音文件${filePath}转换成功!`, pttPath, `时长:`, result.duration)
return { return {
converted: true, converted: true,
path: pttPath, path: pttPath,
duration: silk.duration / 1000, duration: result.duration / 1000,
} }
} else { } else {
const silk = file const silk = file
let duration = 0 let duration = 1
try { try {
duration = getDuration(silk) / 1000 duration = getDuration(silk) / 1000
} catch (e: any) { } catch (e: any) {
log('获取语音文件时长失败, 使用文件大小推测时长', filePath, e.stack) log('获取语音文件时长失败, 默认为1秒', filePath, e.stack)
duration = await guessDuration(filePath)
} }
return { return {
converted: false, converted: false,
path: filePath, path: filePath,
@@ -131,40 +100,20 @@ export async function encodeSilk(filePath: string) {
} }
} }
export async function decodeSilk(inputFilePath: string, outFormat: 'mp3' | 'amr' | 'wma' | 'm4a' | 'spx' | 'ogg' | 'wav' | 'flac' = 'mp3') { type OutFormat = 'mp3' | 'amr' | 'wma' | 'm4a' | 'spx' | 'ogg' | 'wav' | 'flac'
const silkArrayBuffer = await fsPromise.readFile(inputFilePath)
const data = (await decode(silkArrayBuffer, 24000)).data export async function decodeSilk(inputFilePath: string, outFormat: OutFormat = 'mp3') {
const fileName = path.join(TEMP_DIR, path.basename(inputFilePath)) const silk = await fsPromise.readFile(inputFilePath)
const outPCMPath = fileName + '.pcm' const { data } = await decode(silk, 24000)
const outFilePath = fileName + '.' + outFormat const tmpPath = path.join(TEMP_DIR, path.basename(inputFilePath))
await fsPromise.writeFile(outPCMPath, data) const outFilePath = tmpPath + `.${outFormat}`
const convert = () => { const pcmFilePath = tmpPath + '.pcm'
return new Promise<string>((resolve, reject) => { await fsPromise.writeFile(pcmFilePath, data)
const ffmpegPath = getConfigUtil().getConfig().ffmpeg || process.env.FFMPEG_PATH || 'ffmpeg' return convert(pcmFilePath, {
const cp = spawn(ffmpegPath, [ input: [
'-y', '-f s16le',
'-f', 's16le', // PCM format '-ar 24000',
'-ar', '24000', // Sample rate '-ac 1'
'-ac', '1', // Number of audio channels ]
'-i', outPCMPath, }, outFilePath)
outFilePath,
])
cp.on('error', (err) => {
log(`FFmpeg处理转换出错: `, err.message)
return reject(err)
})
cp.on('exit', (code, signal) => {
const EXIT_CODES = [0, 255]
if (code == null || EXIT_CODES.includes(code)) {
fs.unlink(outPCMPath, (err) => {
})
return resolve(outFilePath)
}
const exitErr = `FFmpeg exit: code=${code ?? 'unknown'} sig=${signal ?? 'unknown'}`
log(exitErr)
reject(Error(`FFmpeg处理转换失败,${exitErr}`))
})
})
}
return convert()
} }

View File

@@ -1,10 +1,9 @@
import fs from 'node:fs' import fs from 'node:fs'
import fsPromise from 'node:fs/promises' import fsPromise from 'node:fs/promises'
import path from 'node:path' import path from 'node:path'
import { log, TEMP_DIR } from './index' import { TEMP_DIR } from './index'
import { dbUtil } from '../db'
import * as fileType from 'file-type'
import { randomUUID, createHash } from 'node:crypto' import { randomUUID, createHash } from 'node:crypto'
import { fileURLToPath } from 'node:url'
export function isGIF(path: string) { export function isGIF(path: string) {
const buffer = Buffer.alloc(4) const buffer = Buffer.alloc(4)
@@ -25,7 +24,7 @@ export function checkFileReceived(path: string, timeout: number = 3000): Promise
} else if (Date.now() - startTime > timeout) { } else if (Date.now() - startTime > timeout) {
reject(new Error(`文件不存在: ${path}`)) reject(new Error(`文件不存在: ${path}`))
} else { } else {
setTimeout(check, 100) setTimeout(check, 200)
} }
} }
@@ -33,31 +32,6 @@ export function checkFileReceived(path: string, timeout: number = 3000): Promise
}) })
} }
export async function file2base64(path: string) {
let result = {
err: '',
data: '',
}
try {
// 读取文件内容
// if (!fs.existsSync(path)){
// path = path.replace("\\Ori\\", "\\Thumb\\");
// }
try {
await checkFileReceived(path, 5000)
} catch (e: any) {
result.err = e.toString()
return result
}
const data = await fsPromise.readFile(path)
// 转换为Base64编码
result.data = data.toString('base64')
} catch (err: any) {
result.err = err.toString()
}
return result
}
export function calculateFileMD5(filePath: string): Promise<string> { export function calculateFileMD5(filePath: string): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// 创建一个流式读取器 // 创建一个流式读取器
@@ -110,119 +84,118 @@ export async function httpDownload(options: string | HttpDownloadOptions): Promi
return Buffer.from(await fetchRes.arrayBuffer()) return Buffer.from(await fetchRes.arrayBuffer())
} }
export enum FileUriType {
Unknown = 0,
FileURL = 1,
RemoteURL = 2,
OneBotBase64 = 3,
DataURL = 4,
Path = 5
}
export function checkUriType(uri: string): { type: FileUriType } {
if (uri.startsWith('base64://')) {
return { type: FileUriType.OneBotBase64 }
}
if (uri.startsWith('data:')) {
return { type: FileUriType.DataURL }
}
if (uri.startsWith('http://') || uri.startsWith('https://')) {
return { type: FileUriType.RemoteURL }
}
if (uri.startsWith('file://')) {
return { type: FileUriType.FileURL }
}
try {
if (fs.existsSync(uri)) return { type: FileUriType.Path }
} catch { }
return { type: FileUriType.Unknown }
}
interface FetchFileRes {
data: Buffer
url: string
}
async function fetchFile(url: string): Promise<FetchFileRes> {
const headers: Record<string, string> = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36',
'Host': new URL(url).hostname
}
const raw = await fetch(url, { headers }).catch((err) => {
if (err.cause) {
throw err.cause
}
throw err
})
if (!raw.ok) throw new Error(`statusText: ${raw.statusText}`)
return {
data: Buffer.from(await raw.arrayBuffer()),
url: raw.url
}
}
type Uri2LocalRes = { type Uri2LocalRes = {
success: boolean success: boolean
errMsg: string errMsg: string
fileName: string fileName: string
ext: string
path: string path: string
isLocal: boolean isLocal: boolean
} }
export async function uri2local(uri: string, fileName: string | null = null): Promise<Uri2LocalRes> { export async function uri2local(uri: string, filename?: string): Promise<Uri2LocalRes> {
let res = { const { type } = checkUriType(uri)
success: false,
errMsg: '', if (type === FileUriType.FileURL) {
fileName: '', const filePath = fileURLToPath(uri)
ext: '', const fileName = path.basename(filePath)
path: '', return { success: true, errMsg: '', fileName, path: filePath, isLocal: true }
isLocal: false,
}
if (!fileName) {
fileName = randomUUID()
}
let filePath = path.join(TEMP_DIR, fileName)
let url: URL | null = null
try {
url = new URL(uri)
} catch (e: any) {
res.errMsg = `uri ${uri} 解析失败,` + e.toString() + ` 可能${uri}不存在`
return res
} }
// log("uri protocol", url.protocol, uri); if (type === FileUriType.Path) {
if (url.protocol == 'base64:') { const fileName = path.basename(uri)
// base64转成文件 return { success: true, errMsg: '', fileName, path: uri, isLocal: true }
let base64Data = uri.split('base64://')[1] }
try {
const buffer = Buffer.from(base64Data, 'base64')
await fsPromise.writeFile(filePath, buffer)
} catch (e: any) {
res.errMsg = `base64文件下载失败,` + e.toString()
return res
}
} else if (url.protocol == 'http:' || url.protocol == 'https:') {
// 下载文件
let buffer: Buffer | null = null
try {
buffer = await httpDownload(uri)
} catch (e: any) {
res.errMsg = `${url}下载失败,` + e.toString()
return res
}
try {
const pathInfo = path.parse(decodeURIComponent(url.pathname))
if (pathInfo.name) {
fileName = pathInfo.name
if (pathInfo.ext) {
fileName += pathInfo.ext
// res.ext = pathInfo.ext
}
}
fileName = fileName.replace(/[/\\:*?"<>|]/g, '_')
res.fileName = fileName
filePath = path.join(TEMP_DIR, randomUUID() + fileName)
await fsPromise.writeFile(filePath, buffer)
} catch (e: any) {
res.errMsg = `${url}下载失败,` + e.toString()
return res
}
} else {
let pathname: string
if (url.protocol === 'file:') {
// await fs.copyFile(url.pathname, filePath);
pathname = decodeURIComponent(url.pathname)
if (process.platform === 'win32') {
filePath = pathname.slice(1)
} else {
filePath = pathname
}
} else {
const cache = await dbUtil.getFileCache(uri)
if (cache) {
filePath = cache.filePath
} else {
filePath = uri
}
}
res.isLocal = true if (type === FileUriType.RemoteURL) {
}
// else{
// res.errMsg = `不支持的file协议,` + url.protocol
// return res
// }
// if (isGIF(filePath) && !res.isLocal) {
// await fs.rename(filePath, filePath + ".gif");
// filePath += ".gif";
// }
if (!res.isLocal && !res.ext) {
try { try {
const ext = (await fileType.fileTypeFromFile(filePath))?.ext const res = await fetchFile(uri)
if (ext) { const match = res.url.match(/.+\/([^/?]*)(?=\?)?/)
log('获取文件类型', ext, filePath) if (match?.[1]) {
await fsPromise.rename(filePath, filePath + `.${ext}`) filename ??= match[1].replace(/[/\\:*?"<>|]/g, '_')
filePath += `.${ext}` } else {
res.fileName += `.${ext}` filename ??= randomUUID()
res.ext = ext
} }
} catch (e) { const filePath = path.join(TEMP_DIR, filename)
// log("获取文件类型失败", filePath,e.stack) await fsPromise.writeFile(filePath, res.data)
return { success: true, errMsg: '', fileName: filename, path: filePath, isLocal: false }
} catch (e: any) {
const errMsg = `${uri}下载失败,` + e.toString()
return { success: false, errMsg, fileName: '', path: '', isLocal: false }
} }
} }
res.success = true
res.path = filePath if (type === FileUriType.OneBotBase64) {
return res filename ??= randomUUID()
const filePath = path.join(TEMP_DIR, filename)
const base64 = uri.replace(/^base64:\/\//, '')
await fsPromise.writeFile(filePath, base64, 'base64')
return { success: true, errMsg: '', fileName: filename, path: filePath, isLocal: false }
}
if (type === FileUriType.DataURL) {
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
const capture = /^data:([\w/.+-]+);base64,(.*)$/.exec(uri)
if (capture) {
filename ??= randomUUID()
const [, _type, base64] = capture
const filePath = path.join(TEMP_DIR, filename)
await fsPromise.writeFile(filePath, base64, 'base64')
return { success: true, errMsg: '', fileName: filename, path: filePath, isLocal: false }
}
}
return { success: false, errMsg: '未知文件类型', fileName: '', path: '', isLocal: false }
} }
export async function copyFolder(sourcePath: string, destPath: string) { export async function copyFolder(sourcePath: string, destPath: string) {

View File

@@ -41,7 +41,7 @@ export function mergeNewProperties(newObj: any, oldObj: any) {
}) })
} }
export function isNull(value: any): value is null | undefined | void { export function isNull(value: unknown) {
return value === undefined || value === null return value === undefined || value === null
} }
@@ -74,24 +74,96 @@ export function wrapText(str: string, maxLength: number): string {
* @returns 处理后缓存或调用原方法的结果 * @returns 处理后缓存或调用原方法的结果
*/ */
export function cacheFunc(ttl: number, customKey: string = '') { export function cacheFunc(ttl: number, customKey: string = '') {
const cache = new Map<string, { expiry: number; value: any }>(); const cache = new Map<string, { expiry: number; value: any }>()
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor): PropertyDescriptor { return function (target: any, propertyKey: string, descriptor: PropertyDescriptor): PropertyDescriptor {
const originalMethod = descriptor.value; const originalMethod = descriptor.value
const className = target.constructor.name; // 获取类名 const className = target.constructor.name // 获取类名
const methodName = propertyKey; // 获取方法名 const methodName = propertyKey // 获取方法名
descriptor.value = async function (...args: any[]) { descriptor.value = async function (...args: any[]) {
const cacheKey = `${customKey}${className}.${methodName}:${JSON.stringify(args)}`; const cacheKey = `${customKey}${className}.${methodName}:${JSON.stringify(args)}`
const cached = cache.get(cacheKey); const cached = cache.get(cacheKey)
if (cached && cached.expiry > Date.now()) { if (cached && cached.expiry > Date.now()) {
return cached.value; return cached.value
} else { } else {
const result = await originalMethod.apply(this, args); const result = await originalMethod.apply(this, args)
cache.set(cacheKey, { value: result, expiry: Date.now() + ttl }); cache.set(cacheKey, { value: result, expiry: Date.now() + ttl })
return result; return result
} }
}; }
return descriptor; return descriptor
}; }
}
export function CacheClassFuncAsync(ttl = 3600 * 1000, customKey = '') {
function logExecutionTime(target: any, methodName: string, descriptor: PropertyDescriptor) {
const cache = new Map<string, { expiry: number; value: any }>()
const originalMethod = descriptor.value
descriptor.value = async function (...args: any[]) {
const key = `${customKey}${String(methodName)}.(${args.map(arg => JSON.stringify(arg)).join(', ')})`
cache.forEach((value, key) => {
if (value.expiry < Date.now()) {
cache.delete(key)
}
})
const cachedValue = cache.get(key)
if (cachedValue && cachedValue.expiry > Date.now()) {
return cachedValue.value
}
const result = await originalMethod.apply(this, args)
cache.set(key, { expiry: Date.now() + ttl, value: result })
return result
}
}
return logExecutionTime
}
export function CacheClassFuncAsyncExtend(ttl: number = 3600 * 1000, customKey: string = '', checker: any = (...data: any[]) => { return true }) {
function logExecutionTime(target: any, methodName: string, descriptor: PropertyDescriptor) {
const cache = new Map<string, { expiry: number; value: any }>()
const originalMethod = descriptor.value
descriptor.value = async function (...args: any[]) {
const key = `${customKey}${String(methodName)}.(${args.map(arg => JSON.stringify(arg)).join(', ')})`
cache.forEach((value, key) => {
if (value.expiry < Date.now()) {
cache.delete(key)
}
})
const cachedValue = cache.get(key)
if (cachedValue && cachedValue.expiry > Date.now()) {
return cachedValue.value
}
const result = await originalMethod.apply(this, args)
if (!checker(...args, result)) {
return result //丢弃缓存
}
cache.set(key, { expiry: Date.now() + ttl, value: result })
return result
}
}
return logExecutionTime
}
// forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/common/utils/helper.ts#L14
export class UUIDConverter {
static encode(highStr: string, lowStr: string): string {
const high = BigInt(highStr)
const low = BigInt(lowStr)
const highHex = high.toString(16).padStart(16, '0')
const lowHex = low.toString(16).padStart(16, '0')
const combinedHex = highHex + lowHex
const uuid = `${combinedHex.substring(0, 8)}-${combinedHex.substring(8, 12)}-${combinedHex.substring(
12,
16,
)}-${combinedHex.substring(16, 20)}-${combinedHex.substring(20)}`
return uuid
}
static decode(uuid: string): { high: string; low: string } {
const hex = uuid.replace(/-/g, '')
const high = BigInt('0x' + hex.substring(0, 16))
const low = BigInt('0x' + hex.substring(16))
return { high: high.toString(), low: low.toString() }
}
} }

View File

@@ -1,5 +1,4 @@
import path from 'node:path' import path from 'node:path'
import fs from 'fs'
export * from './file' export * from './file'
export * from './helper' export * from './helper'
@@ -7,13 +6,9 @@ export * from './log'
export * from './qqlevel' export * from './qqlevel'
export * from './QQBasicInfo' export * from './QQBasicInfo'
export * from './upgrade' export * from './upgrade'
export const DATA_DIR = global.LiteLoader.plugins['LLOneBot'].path.data export const DATA_DIR: string = global.LiteLoader.plugins['LLOneBot'].path.data
export const TEMP_DIR = path.join(DATA_DIR, 'temp') export const TEMP_DIR: string = path.join(DATA_DIR, 'temp')
export const PLUGIN_DIR = global.LiteLoader.plugins['LLOneBot'].path.plugin export const PLUGIN_DIR: string = global.LiteLoader.plugins['LLOneBot'].path.plugin
if (!fs.existsSync(TEMP_DIR)) {
fs.mkdirSync(TEMP_DIR, { recursive: true })
}
export { getVideoInfo } from './video' export { getVideoInfo } from './video'
export { checkFfmpeg } from './video' export { checkFfmpeg } from './video'
export { encodeSilk } from './audio' export { encodeSilk } from './audio'
export { isQQ998 } from './QQBasicInfo'

View File

@@ -1,4 +1,4 @@
import { selfInfo } from '../data' import { getSelfInfo } from '../data'
import fs from 'fs' import fs from 'fs'
import path from 'node:path' import path from 'node:path'
import { DATA_DIR, truncateString } from './index' import { DATA_DIR, truncateString } from './index'
@@ -15,7 +15,7 @@ export function log(...msg: any[]) {
if (!getConfigUtil().getConfig().log) { if (!getConfigUtil().getConfig().log) {
return //console.log(...msg); return //console.log(...msg);
} }
const selfInfo = getSelfInfo()
const userInfo = selfInfo.uin ? `${selfInfo.nick}(${selfInfo.uin})` : '' const userInfo = selfInfo.uin ? `${selfInfo.nick}(${selfInfo.uin})` : ''
let logMsg = '' let logMsg = ''
for (let msgItem of msg) { for (let msgItem of msg) {
@@ -31,5 +31,5 @@ export function log(...msg: any[]) {
logMsg = `${currentDateTime} ${userInfo}: ${logMsg}\n\n` logMsg = `${currentDateTime} ${userInfo}: ${logMsg}\n\n`
// sendLog(...msg); // sendLog(...msg);
// console.log(msg) // console.log(msg)
fs.appendFile(path.join(logDir, logFileName), logMsg, (err: any) => {}) fs.appendFile(path.join(logDir, logFileName), logMsg, () => {})
} }

View File

@@ -16,7 +16,7 @@ export class RequestUtil {
const redirectUrl = new URL(res.headers.location, url); const redirectUrl = new URL(res.headers.location, url);
RequestUtil.HttpsGetCookies(redirectUrl.href).then((redirectCookies) => { RequestUtil.HttpsGetCookies(redirectUrl.href).then((redirectCookies) => {
// 合并重定向过程中的cookies // 合并重定向过程中的cookies
log('redirectCookies', redirectCookies) //log('redirectCookies', redirectCookies)
cookies = { ...cookies, ...redirectCookies }; cookies = { ...cookies, ...redirectCookies };
resolve(cookies); resolve(cookies);
}); });
@@ -33,7 +33,7 @@ export class RequestUtil {
}); });
if (res.headers['set-cookie']) { if (res.headers['set-cookie']) {
// console.log(res.headers['set-cookie']); // console.log(res.headers['set-cookie']);
log('set-cookie', url, res.headers['set-cookie']); //log('set-cookie', url, res.headers['set-cookie']);
res.headers['set-cookie'].forEach((cookie) => { res.headers['set-cookie'].forEach((cookie) => {
const parts = cookie.split(';')[0].split('='); const parts = cookie.split(';')[0].split('=');
const key = parts[0]; const key = parts[0];

72
src/common/utils/table.ts Normal file
View File

@@ -0,0 +1,72 @@
// forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/common/utils/MessageUnique.ts#L5
export class LimitedHashTable<K, V> {
private keyToValue: Map<K, V> = new Map()
private valueToKey: Map<V, K> = new Map()
private maxSize: number
constructor(maxSize: number) {
this.maxSize = maxSize
}
resize(count: number) {
this.maxSize = count
}
set(key: K, value: V): void {
this.keyToValue.set(key, value)
this.valueToKey.set(value, key)
while (this.keyToValue.size !== this.valueToKey.size) {
console.log('keyToValue.size !== valueToKey.size Error Atom')
this.keyToValue.clear()
this.valueToKey.clear()
}
while (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)
}
}
getValue(key: K): V | undefined {
return this.keyToValue.get(key)
}
getKey(value: V): K | undefined {
return this.valueToKey.get(value)
}
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)
this.valueToKey.delete(value)
}
}
getKeyList(): K[] {
return Array.from(this.keyToValue.keys())
}
//获取最近刚写入的几个值
getHeads(size: number): { key: K; value: V }[] | undefined {
const keyList = this.getKeyList()
if (keyList.length === 0) {
return undefined
}
const result: { key: K; value: V }[] = []
const listSize = Math.min(size, keyList.length)
for (let i = 0; i < listSize; i++) {
const key = keyList[listSize - i]
result.push({ key, value: this.keyToValue.get(key)! })
}
return result
}
}

View File

@@ -1,7 +1,8 @@
// 运行在 Electron 主进程 下的插件入口 // 运行在 Electron 主进程 下的插件入口
import { BrowserWindow, dialog, ipcMain } from 'electron' import { BrowserWindow, dialog, ipcMain } from 'electron'
import * as fs from 'node:fs' import path from 'node:path'
import fs from 'node:fs'
import { Config } from '../common/types' import { Config } from '../common/types'
import { import {
CHANNEL_CHECK_VERSION, CHANNEL_CHECK_VERSION,
@@ -13,53 +14,46 @@ import {
CHANNEL_UPDATE, CHANNEL_UPDATE,
} from '../common/channels' } from '../common/channels'
import { ob11WebsocketServer } from '../onebot11/server/ws/WebsocketServer' import { ob11WebsocketServer } from '../onebot11/server/ws/WebsocketServer'
import { DATA_DIR, qqPkgInfo } from '../common/utils' import { DATA_DIR, getBuildVersion, TEMP_DIR } from '../common/utils'
import { import {
friendRequests,
getFriend,
getGroup,
getGroupMember,
groups,
llonebotError, llonebotError,
refreshGroupMembers, setSelfInfo,
selfInfo, getSelfInfo,
uidMaps, getSelfUid,
getSelfUin,
addMsgCache
} from '../common/data' } from '../common/data'
import { hookNTQQApiCall, hookNTQQApiReceive, ReceiveCmdS, registerReceiveHook, startHook } from '../ntqqapi/hook' import { hookNTQQApiCall, hookNTQQApiReceive, ReceiveCmdS, registerReceiveHook, startHook } from '../ntqqapi/hook'
import { OB11Constructor } from '../onebot11/constructor' import { OB11Constructor } from '../onebot11/constructor'
import { import {
ChatType,
FriendRequestNotify, FriendRequestNotify,
GroupMemberRole, GroupNotify,
GroupNotifies,
GroupNotifyTypes, GroupNotifyTypes,
RawMessage, RawMessage,
BuddyReqType,
} from '../ntqqapi/types' } from '../ntqqapi/types'
import { httpHeart, ob11HTTPServer } from '../onebot11/server/http' import { httpHeart, ob11HTTPServer } from '../onebot11/server/http'
import { postOb11Event } from '../onebot11/server/post-ob11-event' import { postOb11Event } from '../onebot11/server/post-ob11-event'
import { ob11ReverseWebsockets } from '../onebot11/server/ws/ReverseWebsocket' import { ob11ReverseWebsockets } from '../onebot11/server/ws/ReverseWebsocket'
import { OB11GroupRequestEvent } from '../onebot11/event/request/OB11GroupRequest' import { OB11GroupRequestEvent } from '../onebot11/event/request/OB11GroupRequest'
import { OB11FriendRequestEvent } from '../onebot11/event/request/OB11FriendRequest' import { OB11FriendRequestEvent } from '../onebot11/event/request/OB11FriendRequest'
import * as path from 'node:path' import { MessageUnique } from '../common/utils/MessageUnique'
import { dbUtil } from '../common/db'
import { setConfig } from './setConfig' import { setConfig } from './setConfig'
import { NTQQUserApi } from '../ntqqapi/api/user' import { NTQQUserApi, NTQQGroupApi } from '../ntqqapi/api'
import { NTQQGroupApi } from '../ntqqapi/api/group'
import { checkNewVersion, upgradeLLOneBot } from '../common/utils/upgrade' import { checkNewVersion, upgradeLLOneBot } from '../common/utils/upgrade'
import { log } from '../common/utils/log' import { log } from '../common/utils/log'
import { getConfigUtil } from '../common/config' import { getConfigUtil } from '../common/config'
import { checkFfmpeg } from '../common/utils/video' import { checkFfmpeg } from '../common/utils/video'
import { GroupDecreaseSubType, OB11GroupDecreaseEvent } from '../onebot11/event/notice/OB11GroupDecreaseEvent' import { GroupDecreaseSubType, OB11GroupDecreaseEvent } from '../onebot11/event/notice/OB11GroupDecreaseEvent'
import '../ntqqapi/wrapper' import '../ntqqapi/wrapper'
import { sentMessages } from '@/ntqqapi/api'
import { NTEventDispatch } from '../common/utils/EventTask' import { NTEventDispatch } from '../common/utils/EventTask'
import { wrapperApi, wrapperConstructor } from '../ntqqapi/wrapper' import { wrapperConstructor, getSession } from '../ntqqapi/wrapper'
import { Peer } from '../ntqqapi/types'
let mainWindow: BrowserWindow | null = null let mainWindow: BrowserWindow | null = null
// 加载插件时触发 // 加载插件时触发
function onLoad() { function onLoad() {
log('llonebot main onLoad')
ipcMain.handle(CHANNEL_CHECK_VERSION, async (event, arg) => { ipcMain.handle(CHANNEL_CHECK_VERSION, async (event, arg) => {
return checkNewVersion() return checkNewVersion()
}) })
@@ -101,7 +95,7 @@ function onLoad() {
} }
ipcMain.handle(CHANNEL_ERROR, async (event, arg) => { ipcMain.handle(CHANNEL_ERROR, async (event, arg) => {
const ffmpegOk = await checkFfmpeg(getConfigUtil().getConfig().ffmpeg) const ffmpegOk = await checkFfmpeg(getConfigUtil().getConfig().ffmpeg)
llonebotError.ffmpegError = ffmpegOk ? '' : '没有找到ffmpeg,音频只能发送wav和silk,视频尺寸可能异常' llonebotError.ffmpegError = ffmpegOk ? '' : '没有找到 FFmpeg, 音频只能发送 WAV 和 SILK, 视频尺寸可能异常'
let { httpServerError, wsServerError, otherError, ffmpegError } = llonebotError let { httpServerError, wsServerError, otherError, ffmpegError } = llonebotError
let error = `${otherError}\n${httpServerError}\n${wsServerError}\n${ffmpegError}` let error = `${otherError}\n${httpServerError}\n${wsServerError}\n${ffmpegError}`
error = error.replace('\n\n', '\n') error = error.replace('\n\n', '\n')
@@ -155,21 +149,23 @@ function onLoad() {
const { debug, reportSelfMessage } = getConfigUtil().getConfig() const { debug, reportSelfMessage } = getConfigUtil().getConfig()
for (let message of msgList) { for (let message of msgList) {
// 过滤启动之前的消息 // 过滤启动之前的消息
// log('收到新消息', message);
if (parseInt(message.msgTime) < startTime / 1000) { if (parseInt(message.msgTime) < startTime / 1000) {
continue continue
} }
// log("收到新消息", message.msgId, message.msgSeq) // log("收到新消息", message.msgId, message.msgSeq)
// if (message.senderUin !== selfInfo.uin){ const peer: Peer = {
message.msgShortId = await dbUtil.addMsg(message) chatType: message.chatType,
// } peerUid: message.peerUid
}
message.msgShortId = MessageUnique.createMsg(peer, message.msgId)
addMsgCache(message)
OB11Constructor.message(message) OB11Constructor.message(message)
.then((msg) => { .then((msg) => {
if (!debug && msg.message.length === 0) { if (!debug && msg.message.length === 0) {
return return
} }
const isSelfMsg = msg.user_id.toString() == selfInfo.uin const isSelfMsg = msg.user_id.toString() === getSelfUin()
if (isSelfMsg && !reportSelfMessage) { if (isSelfMsg && !reportSelfMessage) {
return return
} }
@@ -187,19 +183,12 @@ function onLoad() {
} }
}) })
OB11Constructor.PrivateEvent(message).then((privateEvent) => { OB11Constructor.PrivateEvent(message).then((privateEvent) => {
log(message) //log(message)
if (privateEvent) { if (privateEvent) {
// log("post private event", privateEvent); // log("post private event", privateEvent);
postOb11Event(privateEvent) postOb11Event(privateEvent)
} }
}) })
// OB11Constructor.FriendAddEvent(message).then((friendAddEvent) => {
// log(message)
// if (friendAddEvent) {
// // log("post friend add event", friendAddEvent);
// postOb11Event(friendAddEvent)
// }
// })
} }
} }
@@ -217,33 +206,22 @@ function onLoad() {
const recallMsgIds: string[] = [] // 避免重复上报 const recallMsgIds: string[] = [] // 避免重复上报
registerReceiveHook<{ msgList: Array<RawMessage> }>([ReceiveCmdS.UPDATE_MSG], async (payload) => { registerReceiveHook<{ msgList: Array<RawMessage> }>([ReceiveCmdS.UPDATE_MSG], async (payload) => {
for (const message of payload.msgList) { for (const message of payload.msgList) {
const sentMessage = sentMessages[message.msgId]
if (sentMessage) {
Object.assign(sentMessage, message)
}
log('message update', message.msgId, message)
if (message.recallTime != '0') { if (message.recallTime != '0') {
if (recallMsgIds.includes(message.msgId)) { if (recallMsgIds.includes(message.msgId)) {
continue continue
} }
recallMsgIds.push(message.msgId) recallMsgIds.push(message.msgId)
const oriMessage = await dbUtil.getMsgByLongId(message.msgId) const oriMessageId = MessageUnique.getShortIdByMsgId(message.msgId)
if (!oriMessage) { if (!oriMessageId) {
continue continue
} }
oriMessage.recallTime = message.recallTime OB11Constructor.RecallEvent(message, oriMessageId).then((recallEvent) => {
dbUtil.updateMsg(oriMessage).then()
message.msgShortId = oriMessage.msgShortId
OB11Constructor.RecallEvent(message).then((recallEvent) => {
if (recallEvent) { if (recallEvent) {
log('post recall event', recallEvent) //log('post recall event', recallEvent)
postOb11Event(recallEvent) postOb11Event(recallEvent)
} }
}) })
// 不让入库覆盖原来消息,不然就获取不到撤回的消息内容了
continue
} }
dbUtil.updateMsg(message).then()
} }
}) })
registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, async (payload) => { registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, async (payload) => {
@@ -258,6 +236,7 @@ function onLoad() {
log('report self message error: ', e.stack.toString()) log('report self message error: ', e.stack.toString())
} }
}) })
const processedGroupNotify: string[] = []
registerReceiveHook<{ registerReceiveHook<{
doubt: boolean doubt: boolean
oldestUnreadSeq: string oldestUnreadSeq: string
@@ -265,131 +244,87 @@ function onLoad() {
}>(ReceiveCmdS.UNREAD_GROUP_NOTIFY, async (payload) => { }>(ReceiveCmdS.UNREAD_GROUP_NOTIFY, async (payload) => {
if (payload.unreadCount) { if (payload.unreadCount) {
// log("开始获取群通知详情") // log("开始获取群通知详情")
let notify: GroupNotifies let notifies: GroupNotify[]
try { try {
notify = await NTQQGroupApi.getGroupNotifies() notifies = (await NTQQGroupApi.getSingleScreenNotifies(14)).slice(0, payload.unreadCount)
} catch (e) { } catch (e) {
// log("获取群通知详情失败", e); // log("获取群通知详情失败", e);
return return
} }
const notifies = notify.notifies.slice(0, payload.unreadCount)
// log("获取群通知详情完成", notifies, payload);
for (const notify of notifies) { for (const notify of notifies) {
try { try {
notify.time = Date.now() notify.time = Date.now()
// const notifyTime = parseInt(notify.seq) / 1000 const notifyTime = parseInt(notify.seq) / 1000
// log(`加群通知时间${notifyTime}`, `LLOneBot启动时间${startTime}`); const flag = notify.group.groupCode + '|' + notify.seq + '|' + notify.type
// if (notifyTime < startTime) { if (notifyTime < startTime || processedGroupNotify.includes(flag)) {
// continue;
// }
let existNotify = await dbUtil.getGroupNotify(notify.seq)
if (existNotify) {
continue continue
} }
log('收到群通知', notify) processedGroupNotify.push(flag)
await dbUtil.addGroupNotify(notify)
// let member2: GroupMember;
// if (notify.user2.uid) {
// member2 = await getGroupMember(notify.group.groupCode, null, notify.user2.uid);
// }
// 原本的群管变更通知事件处理
// if (
// [GroupNotifyTypes.ADMIN_SET, GroupNotifyTypes.ADMIN_UNSET, GroupNotifyTypes.ADMIN_UNSET_OTHER].includes(
// notify.type,
// )
// ) {
// const member1 = await getGroupMember(notify.group.groupCode, notify.user1.uid)
// log('有管理员变动通知')
// refreshGroupMembers(notify.group.groupCode).then()
// let groupAdminNoticeEvent = new OB11GroupAdminNoticeEvent()
// groupAdminNoticeEvent.group_id = parseInt(notify.group.groupCode)
// log('开始获取变动的管理员')
// if (member1) {
// log('变动管理员获取成功')
// groupAdminNoticeEvent.user_id = parseInt(member1.uin)
// groupAdminNoticeEvent.sub_type = [
// GroupNotifyTypes.ADMIN_UNSET,
// GroupNotifyTypes.ADMIN_UNSET_OTHER,
// ].includes(notify.type)
// ? 'unset'
// : 'set'
// // member1.role = notify.type == GroupNotifyTypes.ADMIN_SET ? GroupMemberRole.admin : GroupMemberRole.normal;
// postOb11Event(groupAdminNoticeEvent, true)
// }
// else {
// log('获取群通知的成员信息失败', notify, getGroup(notify.group.groupCode))
// }
// }
// else
if (notify.type == GroupNotifyTypes.MEMBER_EXIT || notify.type == GroupNotifyTypes.KICK_MEMBER) { if (notify.type == GroupNotifyTypes.MEMBER_EXIT || notify.type == GroupNotifyTypes.KICK_MEMBER) {
log('有成员退出通知', notify) log('有成员退出通知', notify)
try { const member1Uin = (await NTQQUserApi.getUinByUid(notify.user1.uid))!
const member1 = await NTQQUserApi.getUserDetailInfo(notify.user1.uid) let operatorId = member1Uin
let operatorId = member1.uin let subType: GroupDecreaseSubType = 'leave'
let subType: GroupDecreaseSubType = 'leave' if (notify.user2.uid) {
if (notify.user2.uid) { // 是被踢的
// 是被踢的 const member2Uin = await NTQQUserApi.getUinByUid(notify.user2.uid)
const member2 = await getGroupMember(notify.group.groupCode, notify.user2.uid) if (member2Uin) {
operatorId = member2?.uin! operatorId = member2Uin
subType = 'kick'
} }
let groupDecreaseEvent = new OB11GroupDecreaseEvent( subType = 'kick'
parseInt(notify.group.groupCode),
parseInt(member1.uin),
parseInt(operatorId),
subType,
)
postOb11Event(groupDecreaseEvent, true)
} catch (e: any) {
log('获取群通知的成员信息失败', notify, e.stack.toString())
} }
const groupDecreaseEvent = new OB11GroupDecreaseEvent(
parseInt(notify.group.groupCode),
parseInt(member1Uin),
parseInt(operatorId),
subType,
)
postOb11Event(groupDecreaseEvent, true)
} }
else if ([GroupNotifyTypes.JOIN_REQUEST, GroupNotifyTypes.JOIN_REQUEST_BY_INVITED].includes(notify.type)) { else if ([GroupNotifyTypes.JOIN_REQUEST, GroupNotifyTypes.JOIN_REQUEST_BY_INVITED].includes(notify.type)) {
log('有加群请求') log('有加群请求')
let requestQQ = uidMaps[notify.user1.uid] let requestQQ = ''
if (!requestQQ) { try {
try { // uid-->uin
requestQQ = (await NTQQUserApi.getUinByUid(notify.user1.uid))
if (isNaN(parseInt(requestQQ))) {
requestQQ = (await NTQQUserApi.getUserDetailInfo(notify.user1.uid)).uin requestQQ = (await NTQQUserApi.getUserDetailInfo(notify.user1.uid)).uin
} catch (e) {
log('获取加群人QQ号失败', e)
} }
} catch (e) {
log('获取加群人QQ号失败 Uid:', notify.user1.uid, e)
} }
let invitorId: number let invitorId: string
if (notify.type == GroupNotifyTypes.JOIN_REQUEST_BY_INVITED) { if (notify.type == GroupNotifyTypes.JOIN_REQUEST_BY_INVITED) {
// groupRequestEvent.sub_type = 'invite' // groupRequestEvent.sub_type = 'invite'
let invitorQQ = uidMaps[notify.user2.uid] try {
if (!invitorQQ) { // uid-->uin
try { invitorId = (await NTQQUserApi.getUinByUid(notify.user2.uid))
let invitor = (await NTQQUserApi.getUserDetailInfo(notify.user2.uid)) if (isNaN(parseInt(invitorId))) {
invitorId = parseInt(invitor.uin) invitorId = (await NTQQUserApi.getUserDetailInfo(notify.user2.uid)).uin
} catch (e) {
invitorId = 0
log('获取邀请人QQ号失败', e)
} }
} catch (e) {
invitorId = ''
log('获取邀请人QQ号失败 Uid:', notify.user2.uid, e)
} }
} }
const groupRequestEvent = new OB11GroupRequestEvent( const groupRequestEvent = new OB11GroupRequestEvent(
parseInt(notify.group.groupCode), parseInt(notify.group.groupCode),
parseInt(requestQQ) || 0, parseInt(requestQQ) || 0,
notify.seq, flag,
notify.postscript, notify.postscript,
invitorId!, invitorId! === undefined ? undefined : +invitorId,
'add' 'add'
) )
postOb11Event(groupRequestEvent) postOb11Event(groupRequestEvent)
} }
else if (notify.type == GroupNotifyTypes.INVITE_ME) { else if (notify.type == GroupNotifyTypes.INVITE_ME) {
log('收到邀请我加群通知') log('收到邀请我加群通知')
let userId = uidMaps[notify.user2.uid] const userId = (await NTQQUserApi.getUinByUid(notify.user2.uid)) || ''
if (!userId) {
userId = (await NTQQUserApi.getUserDetailInfo(notify.user2.uid))?.uin
}
const groupInviteEvent = new OB11GroupRequestEvent( const groupInviteEvent = new OB11GroupRequestEvent(
parseInt(notify.group.groupCode), parseInt(notify.group.groupCode),
parseInt(userId), parseInt(userId),
notify.seq, flag,
undefined, undefined,
undefined, undefined,
'invite' 'invite'
@@ -408,67 +343,57 @@ function onLoad() {
registerReceiveHook<FriendRequestNotify>(ReceiveCmdS.FRIEND_REQUEST, async (payload) => { registerReceiveHook<FriendRequestNotify>(ReceiveCmdS.FRIEND_REQUEST, async (payload) => {
for (const req of payload.data.buddyReqs) { for (const req of payload.data.buddyReqs) {
const flag = req.friendUid + req.reqTime if (!!req.isInitiator || (req.isDecide && req.reqType !== BuddyReqType.KMEINITIATORWAITPEERCONFIRM)) {
if (req.isUnread && parseInt(req.reqTime) > startTime / 1000) { continue
friendRequests[flag] = req
log('有新的好友请求', req)
let userId: number
try {
const requester = await NTQQUserApi.getUserDetailInfo(req.friendUid)
userId = parseInt(requester.uin)
} catch (e) {
log('获取加好友者QQ号失败', e)
}
const friendRequestEvent = new OB11FriendRequestEvent(userId!, req.extWords, flag)
postOb11Event(friendRequestEvent)
} }
if (+req.reqTime < startTime / 1000) {
continue
}
let userId = 0
try {
const requesterUin = await NTQQUserApi.getUinByUid(req.friendUid)
userId = parseInt(requesterUin)
} catch (e) {
log('获取加好友者QQ号失败', e)
}
const flag = req.friendUid + '|' + req.reqTime
const comment = req.extWords
const friendRequestEvent = new OB11FriendRequestEvent(
userId,
comment,
flag
)
postOb11Event(friendRequestEvent)
} }
}) })
} }
let startTime = 0 // 毫秒 let startTime = 0 // 毫秒
async function start() { async function start(uid: string, uin: string) {
log('llonebot pid', process.pid) log('process pid', process.pid)
const config = getConfigUtil().getConfig() const config = getConfigUtil().getConfig()
if (!config.enableLLOB) { if (!config.enableLLOB) {
llonebotError.otherError = 'LLOneBot 未启动'
log('LLOneBot 开关设置为关闭不启动LLOneBot') log('LLOneBot 开关设置为关闭不启动LLOneBot')
return return
} }
if (!fs.existsSync(TEMP_DIR)) {
fs.mkdirSync(TEMP_DIR, { recursive: true })
}
llonebotError.otherError = '' llonebotError.otherError = ''
startTime = Date.now() startTime = Date.now()
dbUtil.getReceivedTempUinMap().then((m) => { const WrapperSession = getSession()
for (const [key, value] of Object.entries(m)) { if (WrapperSession) {
uidMaps[value] = key NTEventDispatch.init({ ListenerMap: wrapperConstructor, WrapperSession })
}
})
NTEventDispatch.init({ ListenerMap: wrapperConstructor, WrapperSession: wrapperApi.NodeIQQNTWrapperSession! })
try {
log('start get groups')
const _groups = await NTQQGroupApi.getGroups()
log('_groups', _groups)
await Promise.all(
_groups.map(async (group) => {
try {
const members = await NTQQGroupApi.getGroupMembers(group.groupCode)
group.members = members
groups.push(group)
} catch (e) {
log('获取群成员失败', e)
}
})
)
}
catch (e) {
log('获取群列表失败', e)
}
finally {
log('start activate group member info')
NTQQGroupApi.activateMemberInfoChange().then().catch(log)
NTQQGroupApi.activateMemberListChange().then().catch(log)
startReceiveHook().then()
} }
MessageUnique.init(uin)
//log('start activate group member info')
// 下面两个会导致CPU占用过高QQ卡死
// NTQQGroupApi.activateMemberInfoChange().then().catch(log)
// NTQQGroupApi.activateMemberListChange().then().catch(log)
startReceiveHook().then()
if (config.ob11.enableHttp) { if (config.ob11.enableHttp) {
ob11HTTPServer.start(config.ob11.httpPort) ob11HTTPServer.start(config.ob11.httpPort)
@@ -486,54 +411,27 @@ function onLoad() {
log('LLOneBot start') log('LLOneBot start')
} }
let getSelfNickCount = 0 const buildVersion = getBuildVersion()
const init = async () => {
try {
log('start get self info')
const _ = await NTQQUserApi.getSelfInfo()
log('get self info api result:', _)
Object.assign(selfInfo, _)
selfInfo.nick = selfInfo.uin
} catch (e) {
log('retry get self info', e)
}
if (!selfInfo.uin) {
selfInfo.uin = globalThis.authData?.uin
selfInfo.uid = globalThis.authData?.uid
selfInfo.nick = selfInfo.uin
}
log('self info', selfInfo, globalThis.authData)
if (selfInfo.uin) {
async function getUserNick() {
try {
getSelfNickCount++
const userInfo = await NTQQUserApi.getUserDetailInfo(selfInfo.uid)
log('self info', userInfo)
if (userInfo) {
selfInfo.nick = userInfo.nick
return
}
} catch (e: any) {
log('get self nickname failed', e.stack)
}
if (getSelfNickCount < 10) {
return setTimeout(getUserNick, 1000)
}
}
getUserNick().then() const intervalId = setInterval(() => {
start().then() const current = getSelfInfo()
if (!current.uin) {
setSelfInfo({
uin: globalThis.authData?.uin,
uid: globalThis.authData?.uid,
nick: current.uin,
})
} }
else { if (current.uin && (buildVersion >= 27187 || getSession())) {
setTimeout(init, 1000) clearInterval(intervalId)
start(current.uid, current.uin)
} }
} }, 600)
setTimeout(init, 1000)
} }
// 创建窗口时触发 // 创建窗口时触发
function onBrowserWindowCreated(window: BrowserWindow) { function onBrowserWindowCreated(window: BrowserWindow) {
if (selfInfo.uid) { if (window.id !== 2) {
return return
} }
mainWindow = window mainWindow = window

View File

@@ -1,4 +1,5 @@
import { callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod } from '../ntcall' import { invoke, NTClass, NTMethod } from '../ntcall'
import { GeneralCallResult } from '../services'
import { import {
CacheFileList, CacheFileList,
CacheFileListItem, CacheFileListItem,
@@ -9,102 +10,89 @@ import {
ChatType, ChatType,
ElementType, ElementType,
IMAGE_HTTP_HOST, IMAGE_HTTP_HOST,
IMAGE_HTTP_HOST_NT, PicElement, IMAGE_HTTP_HOST_NT,
PicElement,
} from '../types' } from '../types'
import path from 'node:path' import path from 'node:path'
import fs from 'node:fs' import fs from 'node:fs'
import { ReceiveCmdS } from '../hook' import { ReceiveCmdS } from '../hook'
import { log } from '@/common/utils' import { log, TEMP_DIR } from '@/common/utils'
import { rkeyManager } from '@/ntqqapi/api/rkey' import { rkeyManager } from '@/ntqqapi/helper/rkey'
import { wrapperApi } from '@/ntqqapi/wrapper' import { getSession } from '@/ntqqapi/wrapper'
import { Peer } from '@/ntqqapi/types/msg' import { Peer } from '@/ntqqapi/types/msg'
import { calculateFileMD5 } from '@/common/utils/file'
import { fileTypeFromFile } from 'file-type'
import fsPromise from 'node:fs/promises'
import { NTEventDispatch } from '@/common/utils/EventTask'
import { OnRichMediaDownloadCompleteParams } from '@/ntqqapi/listeners'
import { Time } from 'cosmokit'
export class NTQQFileApi { export class NTQQFileApi {
static async getVideoUrl(peer: Peer, msgId: string, elementId: string): Promise<string> { /** 27187 TODO */
const session = wrapperApi.NodeIQQNTWrapperSession static async getVideoUrl(peer: Peer, msgId: string, elementId: string) {
const session = getSession()
return (await session?.getRichMediaService().getVideoPlayUrlV2(peer, return (await session?.getRichMediaService().getVideoPlayUrlV2(peer,
msgId, msgId,
elementId, elementId,
0, 0,
{ downSourceType: 1, triggerType: 1 })).urlResult?.domainUrl[0]?.url; { downSourceType: 1, triggerType: 1 }))?.urlResult.domainUrl[0].url
} }
static async getFileType(filePath: string) { static async getFileType(filePath: string) {
return await callNTQQApi<{ ext: string }>({ return fileTypeFromFile(filePath)
className: NTQQApiClass.FS_API,
methodName: NTQQApiMethod.FILE_TYPE,
args: [filePath],
})
}
static async getFileMd5(filePath: string) {
return await callNTQQApi<string>({
className: NTQQApiClass.FS_API,
methodName: NTQQApiMethod.FILE_MD5,
args: [filePath],
})
}
static async copyFile(filePath: string, destPath: string) {
return await callNTQQApi<string>({
className: NTQQApiClass.FS_API,
methodName: NTQQApiMethod.FILE_COPY,
args: [
{
fromPath: filePath,
toPath: destPath,
},
],
})
}
static async getFileSize(filePath: string) {
return await callNTQQApi<number>({
className: NTQQApiClass.FS_API,
methodName: NTQQApiMethod.FILE_SIZE,
args: [filePath],
})
} }
// 上传文件到QQ的文件夹 // 上传文件到QQ的文件夹
static async uploadFile(filePath: string, elementType: ElementType = ElementType.PIC, elementSubType: number = 0) { static async uploadFile(filePath: string, elementType: ElementType = ElementType.PIC, elementSubType = 0) {
const md5 = await NTQQFileApi.getFileMd5(filePath) const fileMd5 = await calculateFileMD5(filePath)
let ext = (await NTQQFileApi.getFileType(filePath))?.ext let ext = (await NTQQFileApi.getFileType(filePath))?.ext || ''
if (ext) { if (ext) {
ext = '.' + ext ext = '.' + ext
} else {
ext = ''
} }
let fileName = `${path.basename(filePath)}` let fileName = `${path.basename(filePath)}`
if (fileName.indexOf('.') === -1) { if (fileName.indexOf('.') === -1) {
fileName += ext fileName += ext
} }
const mediaPath = await callNTQQApi<string>({ const session = getSession()
methodName: NTQQApiMethod.MEDIA_FILE_PATH, let mediaPath: string
args: [ if (session) {
{ mediaPath = session?.getMsgService().getRichMediaFilePathForGuild({
path_info: { md5HexStr: fileMd5,
md5HexStr: md5, fileName: fileName,
fileName: fileName, elementType: elementType,
elementType: elementType, elementSubType,
elementSubType, thumbSize: 0,
thumbSize: 0, needCreate: true,
needCreate: true, downloadType: 1,
downloadType: 1, file_uuid: ''
file_uuid: '', })
} else {
mediaPath = await invoke<string>({
methodName: NTMethod.MEDIA_FILE_PATH,
args: [
{
path_info: {
md5HexStr: fileMd5,
fileName: fileName,
elementType: elementType,
elementSubType,
thumbSize: 0,
needCreate: true,
downloadType: 1,
file_uuid: '',
},
}, },
}, ],
], })
}) }
log('media path', mediaPath) await fsPromise.copyFile(filePath, mediaPath)
await NTQQFileApi.copyFile(filePath, mediaPath) const fileSize = (await fsPromise.stat(filePath)).size
const fileSize = await NTQQFileApi.getFileSize(filePath)
return { return {
md5, md5: fileMd5,
fileName, fileName,
path: mediaPath, path: mediaPath,
fileSize, fileSize,
ext, ext
} }
} }
@@ -115,19 +103,48 @@ export class NTQQFileApi {
elementId: string, elementId: string,
thumbPath: string, thumbPath: string,
sourcePath: string, sourcePath: string,
force: boolean = false, timeout = 1000 * 60 * 2,
force = false
) { ) {
// 用于下载收到的消息中的图片等 // 用于下载收到的消息中的图片等
if (sourcePath && fs.existsSync(sourcePath)) { if (sourcePath && fs.existsSync(sourcePath)) {
if (force) { if (force) {
fs.unlinkSync(sourcePath) try {
await fsPromise.unlink(sourcePath)
} catch { }
} else { } else {
return sourcePath return sourcePath
} }
} }
const apiParams = [ let filePath: string
{ if (NTEventDispatch.initialised) {
getReq: { const 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) {
return true
}
return false
},
{
fileModelId: '0', fileModelId: '0',
downloadSourceType: 0, downloadSourceType: 0,
triggerType: 1, triggerType: 1,
@@ -137,48 +154,72 @@ export class NTQQFileApi {
elementId: elementId, elementId: elementId,
thumbSize: 0, thumbSize: 0,
downloadType: 1, downloadType: 1,
filePath: thumbPath, filePath: thumbPath
}, }
}, )
null, filePath = data[1].filePath
] } else {
// log("需要下载media", sourcePath); const data = await invoke<{ notifyInfo: OnRichMediaDownloadCompleteParams }>({
await callNTQQApi({ methodName: NTMethod.DOWNLOAD_MEDIA,
methodName: NTQQApiMethod.DOWNLOAD_MEDIA, args: [
args: apiParams, {
cbCmd: ReceiveCmdS.MEDIA_DOWNLOAD_COMPLETE, getReq: {
cmdCB: (payload: { notifyInfo: { filePath: string; msgId: string } }) => { fileModelId: '0',
log('media 下载完成判断', payload.notifyInfo.msgId, msgId) downloadSourceType: 0,
return payload.notifyInfo.msgId == msgId triggerType: 1,
}, msgId: msgId,
}) chatType: chatType,
return sourcePath peerUid: peerUid,
elementId: elementId,
thumbSize: 0,
downloadType: 1,
filePath: thumbPath,
},
},
null,
],
cbCmd: ReceiveCmdS.MEDIA_DOWNLOAD_COMPLETE,
cmdCB: payload => payload.notifyInfo.msgId === msgId,
timeout
})
filePath = data.notifyInfo.filePath
}
if (filePath.startsWith('\\')) {
const downloadPath = TEMP_DIR
filePath = path.join(downloadPath, filePath)
// 下载路径是下载文件夹的相对路径
}
return filePath
} }
static async getImageSize(filePath: string) { static async getImageSize(filePath: string) {
return await callNTQQApi<{ width: number; height: number }>({ return await invoke<{ width: number; height: number }>({
className: NTQQApiClass.FS_API, className: NTClass.FS_API,
methodName: NTQQApiMethod.IMAGE_SIZE, methodName: NTMethod.IMAGE_SIZE,
args: [filePath], args: [filePath],
}) })
} }
static async getImageUrl(picElement: PicElement, chatType: ChatType) { static async getImageUrl(element: PicElement) {
const isPrivateImage = chatType !== ChatType.group if (!element) {
const url = picElement.originImageUrl // 没有域名 return ''
const md5HexStr = picElement.md5HexStr }
const fileMd5 = picElement.md5HexStr const url: string = element.originImageUrl! // 没有域名
const fileUuid = picElement.fileUuid const md5HexStr = element.md5HexStr
const fileMd5 = element.md5HexStr
if (url) { if (url) {
if (url.startsWith('/download')) { const UrlParse = new URL(IMAGE_HTTP_HOST + url) //临时解析拼接
// console.log('rkey', rkey); const imageAppid = UrlParse.searchParams.get('appid')
if (url.includes('&rkey=')) { const isNewPic = imageAppid && ['1406', '1407'].includes(imageAppid)
if (isNewPic) {
let UrlRkey = UrlParse.searchParams.get('rkey')
if (UrlRkey) {
return IMAGE_HTTP_HOST_NT + url return IMAGE_HTTP_HOST_NT + url
} }
const rkeyData = await rkeyManager.getRkey()
const rkeyData = await rkeyManager.getRkey(); UrlRkey = imageAppid === '1406' ? rkeyData.private_rkey : rkeyData.group_rkey
const existsRKey = isPrivateImage ? rkeyData.private_rkey : rkeyData.group_rkey; return IMAGE_HTTP_HOST_NT + url + `${UrlRkey}`
return IMAGE_HTTP_HOST_NT + url + `${existsRKey}`
} else { } else {
// 老的图片url不需要rkey // 老的图片url不需要rkey
return IMAGE_HTTP_HOST + url return IMAGE_HTTP_HOST + url
@@ -187,15 +228,15 @@ export class NTQQFileApi {
// 没有url需要自己拼接 // 没有url需要自己拼接
return `${IMAGE_HTTP_HOST}/gchatpic_new/0/0-0-${(fileMd5 || md5HexStr)!.toUpperCase()}/0` return `${IMAGE_HTTP_HOST}/gchatpic_new/0/0-0-${(fileMd5 || md5HexStr)!.toUpperCase()}/0`
} }
log('图片url获取失败', picElement) log('图片url获取失败', element)
return '' return ''
} }
} }
export class NTQQFileCacheApi { export class NTQQFileCacheApi {
static async setCacheSilentScan(isSilent: boolean = true) { static async setCacheSilentScan(isSilent: boolean = true) {
return await callNTQQApi<GeneralCallResult>({ return await invoke<GeneralCallResult>({
methodName: NTQQApiMethod.CACHE_SET_SILENCE, methodName: NTMethod.CACHE_SET_SILENCE,
args: [ args: [
{ {
isSilent, isSilent,
@@ -206,21 +247,21 @@ export class NTQQFileCacheApi {
} }
static getCacheSessionPathList() { static getCacheSessionPathList() {
return callNTQQApi< return invoke<
{ {
key: string key: string
value: string value: string
}[] }[]
>({ >({
className: NTQQApiClass.OS_API, className: NTClass.OS_API,
methodName: NTQQApiMethod.CACHE_PATH_SESSION, methodName: NTMethod.CACHE_PATH_SESSION,
}) })
} }
static clearCache(cacheKeys: Array<string> = ['tmp', 'hotUpdate']) { static clearCache(cacheKeys: Array<string> = ['tmp', 'hotUpdate']) {
return callNTQQApi<any>({ return invoke<any>({
// TODO: 目前还不知道真正的返回值是什么 // TODO: 目前还不知道真正的返回值是什么
methodName: NTQQApiMethod.CACHE_CLEAR, methodName: NTMethod.CACHE_CLEAR,
args: [ args: [
{ {
keys: cacheKeys, keys: cacheKeys,
@@ -231,8 +272,8 @@ export class NTQQFileCacheApi {
} }
static addCacheScannedPaths(pathMap: object = {}) { static addCacheScannedPaths(pathMap: object = {}) {
return callNTQQApi<GeneralCallResult>({ return invoke<GeneralCallResult>({
methodName: NTQQApiMethod.CACHE_ADD_SCANNED_PATH, methodName: NTMethod.CACHE_ADD_SCANNED_PATH,
args: [ args: [
{ {
pathMap: { ...pathMap }, pathMap: { ...pathMap },
@@ -243,35 +284,35 @@ export class NTQQFileCacheApi {
} }
static scanCache() { static scanCache() {
callNTQQApi<GeneralCallResult>({ invoke<GeneralCallResult>({
methodName: ReceiveCmdS.CACHE_SCAN_FINISH, methodName: ReceiveCmdS.CACHE_SCAN_FINISH,
classNameIsRegister: true, classNameIsRegister: true,
}).then() }).then()
return callNTQQApi<CacheScanResult>({ return invoke<CacheScanResult>({
methodName: NTQQApiMethod.CACHE_SCAN, methodName: NTMethod.CACHE_SCAN,
args: [null, null], args: [null, null],
timeoutSecond: 300, timeout: 300 * Time.second,
}) })
} }
static getHotUpdateCachePath() { static getHotUpdateCachePath() {
return callNTQQApi<string>({ return invoke<string>({
className: NTQQApiClass.HOTUPDATE_API, className: NTClass.HOTUPDATE_API,
methodName: NTQQApiMethod.CACHE_PATH_HOT_UPDATE, methodName: NTMethod.CACHE_PATH_HOT_UPDATE,
}) })
} }
static getDesktopTmpPath() { static getDesktopTmpPath() {
return callNTQQApi<string>({ return invoke<string>({
className: NTQQApiClass.BUSINESS_API, className: NTClass.BUSINESS_API,
methodName: NTQQApiMethod.CACHE_PATH_DESKTOP_TEMP, methodName: NTMethod.CACHE_PATH_DESKTOP_TEMP,
}) })
} }
static getChatCacheList(type: ChatType, pageSize: number = 1000, pageIndex: number = 0) { static getChatCacheList(type: ChatType, pageSize: number = 1000, pageIndex: number = 0) {
return new Promise<ChatCacheList>((res, rej) => { return new Promise<ChatCacheList>((res, rej) => {
callNTQQApi<ChatCacheList>({ invoke<ChatCacheList>({
methodName: NTQQApiMethod.CACHE_CHAT_GET, methodName: NTMethod.CACHE_CHAT_GET,
args: [ args: [
{ {
chatType: type, chatType: type,
@@ -290,8 +331,8 @@ export class NTQQFileCacheApi {
static getFileCacheInfo(fileType: CacheFileType, pageSize: number = 1000, lastRecord?: CacheFileListItem) { static getFileCacheInfo(fileType: CacheFileType, pageSize: number = 1000, lastRecord?: CacheFileListItem) {
const _lastRecord = lastRecord ? lastRecord : { fileType: fileType } const _lastRecord = lastRecord ? lastRecord : { fileType: fileType }
return callNTQQApi<CacheFileList>({ return invoke<CacheFileList>({
methodName: NTQQApiMethod.CACHE_FILE_GET, methodName: NTMethod.CACHE_FILE_GET,
args: [ args: [
{ {
fileType: fileType, fileType: fileType,
@@ -306,8 +347,8 @@ export class NTQQFileCacheApi {
} }
static async clearChatCache(chats: ChatCacheListItemBasic[] = [], fileKeys: string[] = []) { static async clearChatCache(chats: ChatCacheListItemBasic[] = [], fileKeys: string[] = []) {
return await callNTQQApi<GeneralCallResult>({ return await invoke<GeneralCallResult>({
methodName: NTQQApiMethod.CACHE_CHAT_CLEAR, methodName: NTMethod.CACHE_CHAT_CLEAR,
args: [ args: [
{ {
chats, chats,

View File

@@ -1,14 +1,16 @@
import { Friend, FriendRequest, FriendV2 } from '../types' import { Friend, FriendV2, SimpleInfo, CategoryFriend } from '../types'
import { ReceiveCmdS } from '../hook' import { ReceiveCmdS } from '../hook'
import { callNTQQApi, GeneralCallResult, NTQQApiMethod } from '../ntcall' import { invoke, NTMethod, NTClass } from '../ntcall'
import { friendRequests } from '../../common/data' import { getSession } from '@/ntqqapi/wrapper'
import { wrapperApi } from '@/ntqqapi/wrapper'
import { BuddyListReqType, NodeIKernelProfileService } from '../services' import { BuddyListReqType, NodeIKernelProfileService } from '../services'
import { NTEventDispatch } from '../../common/utils/EventTask' import { NTEventDispatch } from '@/common/utils/EventTask'
import { LimitedHashTable } from '@/common/utils/table'
import { pick } from 'cosmokit'
export class NTQQFriendApi { export class NTQQFriendApi {
/** 大于或等于 26702 应使用 getBuddyV2 */
static async getFriends(forced = false) { static async getFriends(forced = false) {
const data = await callNTQQApi<{ const data = await invoke<{
data: { data: {
categoryId: number categoryId: number
categroyName: string categroyName: string
@@ -16,66 +18,170 @@ export class NTQQFriendApi {
buddyList: Friend[] buddyList: Friend[]
}[] }[]
}>({ }>({
methodName: NTQQApiMethod.FRIENDS, className: NTClass.NODE_STORE_API,
args: [{ force_update: forced }, undefined], methodName: 'getBuddyList',
cbCmd: ReceiveCmdS.FRIENDS, cbCmd: ReceiveCmdS.FRIENDS,
afterFirstCmd: false, afterFirstCmd: false,
}) })
// log('获取好友列表', data) const _friends: Friend[] = []
let _friends: Friend[] = [] for (const item of data.data) {
for (const fData of data.data) { _friends.push(...item.buddyList)
_friends.push(...fData.buddyList)
} }
return _friends return _friends
} }
static async likeFriend(uid: string, count = 1) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.LIKE_FRIEND,
args: [
{
doLikeUserInfo: {
friendUid: uid,
sourceId: 71,
doLikeCount: count,
doLikeTollCount: 0,
},
},
null,
],
})
}
static async handleFriendRequest(flag: string, accept: boolean) { static async handleFriendRequest(flag: string, accept: boolean) {
const request: FriendRequest = friendRequests[flag] const data = flag.split('|')
if (!request) { if (data.length < 2) {
throw `flat: ${flag}, 对应的好友请求不存在` return
} }
const result = await callNTQQApi<GeneralCallResult>({ const friendUid = data[0]
methodName: NTQQApiMethod.HANDLE_FRIEND_REQUEST, const reqTime = data[1]
args: [ const session = getSession()
{ if (session) {
approvalInfo: { return session.getBuddyService().approvalFriendRequest({
friendUid: request.friendUid, friendUid,
reqTime: request.reqTime, reqTime,
accept, accept
})
} else {
return await invoke({
methodName: NTMethod.HANDLE_FRIEND_REQUEST,
args: [
{
approvalInfo: {
friendUid,
reqTime,
accept,
},
}, },
}, ],
], })
}) }
delete friendRequests[flag]
return result
} }
static async getBuddyV2(refresh = false): Promise<FriendV2[]> { static async getBuddyV2(refresh = false): Promise<FriendV2[]> {
const uids: string[] = [] const session = getSession()
const session = wrapperApi.NodeIQQNTWrapperSession if (session) {
const buddyService = session?.getBuddyService() const uids: string[] = []
const buddyListV2 = refresh ? await buddyService?.getBuddyListV2('0', BuddyListReqType.KNOMAL) : await buddyService?.getBuddyListV2('0', BuddyListReqType.KNOMAL) const buddyService = session.getBuddyService()
uids.push(...buddyListV2?.data.flatMap(item => item.buddyUids)!) const buddyListV2 = await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL)
const data = await NTEventDispatch.CallNoListenerEvent<NodeIKernelProfileService['getCoreAndBaseInfo']>( uids.push(...buddyListV2.data.flatMap(item => item.buddyUids))
'NodeIKernelProfileService/getCoreAndBaseInfo', 5000, 'nodeStore', uids const data = await NTEventDispatch.CallNoListenerEvent<NodeIKernelProfileService['getCoreAndBaseInfo']>(
) 'NodeIKernelProfileService/getCoreAndBaseInfo', 5000, 'nodeStore', uids
return Array.from(data.values()) )
return Array.from(data.values())
} else {
const data = await invoke<{
buddyCategory: CategoryFriend[]
userSimpleInfos: Record<string, SimpleInfo>
}>({
className: NTClass.NODE_STORE_API,
methodName: 'getBuddyList',
args: [refresh],
cbCmd: ReceiveCmdS.FRIENDS,
afterFirstCmd: false,
})
const categoryUids: Map<number, string[]> = new Map()
for (const item of data.buddyCategory) {
categoryUids.set(item.categoryId, item.buddyUids)
}
return Object.values(data.userSimpleInfos).filter(v => v.baseInfo && categoryUids.get(v.baseInfo.categoryId)?.includes(v.uid!))
}
}
static async getBuddyIdMap(refresh = false): Promise<LimitedHashTable<string, string>> {
const retMap: LimitedHashTable<string, string> = new LimitedHashTable<string, string>(5000)
const session = getSession()
if (session) {
const uids: string[] = []
const buddyService = session?.getBuddyService()
const buddyListV2 = await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL)
uids.push(...buddyListV2.data.flatMap(item => item.buddyUids))
const data = await NTEventDispatch.CallNoListenerEvent<NodeIKernelProfileService['getCoreAndBaseInfo']>(
'NodeIKernelProfileService/getCoreAndBaseInfo', 5000, 'nodeStore', uids
)
data.forEach((value, key) => {
retMap.set(value.uin!, value.uid!)
})
} else {
const data = await invoke<{
buddyCategory: CategoryFriend[]
userSimpleInfos: Record<string, SimpleInfo>
}>({
className: NTClass.NODE_STORE_API,
methodName: 'getBuddyList',
args: [refresh],
cbCmd: ReceiveCmdS.FRIENDS,
afterFirstCmd: false,
})
for (const item of Object.values(data.userSimpleInfos)) {
retMap.set(item.uin!, item.uid!)
}
}
return retMap
}
static async getBuddyV2ExWithCate(refresh = false) {
const session = getSession()
if (session) {
const uids: string[] = []
const categoryMap: Map<string, any> = new Map()
const buddyService = session.getBuddyService()
const buddyListV2 = (await buddyService?.getBuddyListV2('0', BuddyListReqType.KNOMAL))?.data
uids.push(
...buddyListV2?.flatMap(item => {
item.buddyUids.forEach(uid => {
categoryMap.set(uid, { categoryId: item.categoryId, categroyName: item.categroyName })
})
return item.buddyUids
})!)
const data = await NTEventDispatch.CallNoListenerEvent<NodeIKernelProfileService['getCoreAndBaseInfo']>(
'NodeIKernelProfileService/getCoreAndBaseInfo', 5000, 'nodeStore', uids
)
return Array.from(data).map(([key, value]) => {
const category = categoryMap.get(key)
return category ? { ...value, categoryId: category.categoryId, categroyName: category.categroyName } : value
})
} else {
const data = await invoke<{
buddyCategory: CategoryFriend[]
userSimpleInfos: Record<string, SimpleInfo>
}>({
className: NTClass.NODE_STORE_API,
methodName: 'getBuddyList',
args: [refresh],
cbCmd: ReceiveCmdS.FRIENDS,
afterFirstCmd: false,
})
const category: Map<number, Pick<CategoryFriend, 'buddyUids' | 'categroyName'>> = new Map()
for (const item of data.buddyCategory) {
category.set(item.categoryId, pick(item, ['buddyUids', 'categroyName']))
}
return Object.values(data.userSimpleInfos)
.filter(v => v.baseInfo && category.get(v.baseInfo.categoryId)?.buddyUids.includes(v.uid!))
.map(value => {
return {
...value,
categoryId: value.baseInfo.categoryId,
categroyName: category.get(value.baseInfo.categoryId)?.categroyName
}
})
}
}
static async isBuddy(uid: string): Promise<boolean> {
const session = getSession()
if (session) {
return session.getBuddyService().isBuddy(uid)
} else {
return await invoke<boolean>({
methodName: 'nodeIKernelBuddyService/isBuddy',
args: [
{ uid },
null,
],
})
}
} }
} }

View File

@@ -1,132 +1,80 @@
import { ReceiveCmdS } from '../hook' import { ReceiveCmdS } from '../hook'
import { Group, GroupMember, GroupMemberRole, GroupNotifies, GroupNotify, GroupRequestOperateTypes } from '../types' import { Group, GroupMember, GroupMemberRole, GroupNotifies, GroupRequestOperateTypes, GroupNotify } from '../types'
import { callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod } from '../ntcall' import { invoke, NTClass, NTMethod } from '../ntcall'
import { deleteGroup, uidMaps } from '../../common/data' import { GeneralCallResult } from '../services'
import { dbUtil } from '../../common/db'
import { log } from '../../common/utils/log'
import { NTQQWindowApi, NTQQWindows } from './window' import { NTQQWindowApi, NTQQWindows } from './window'
import { wrapperApi } from '../wrapper' import { getSession } from '../wrapper'
import { NTEventDispatch } from '@/common/utils/EventTask'
import { NodeIKernelGroupListener } from '../listeners'
import { NodeIKernelGroupService } from '../services'
export class NTQQGroupApi { export class NTQQGroupApi {
static async activateMemberListChange() { static async getGroups(forced = false): Promise<Group[]> {
return await callNTQQApi<GeneralCallResult>({ if (NTEventDispatch.initialised) {
methodName: NTQQApiMethod.ACTIVATE_MEMBER_LIST_CHANGE, type ListenerType = NodeIKernelGroupListener['onGroupListUpdate']
classNameIsRegister: true, const [, , groupList] = await NTEventDispatch.CallNormalEvent
args: [], <(force: boolean) => Promise<any>, ListenerType>
}) (
} 'NodeIKernelGroupService/getGroupList',
'NodeIKernelGroupListener/onGroupListUpdate',
static async activateMemberInfoChange() { 1,
return await callNTQQApi<GeneralCallResult>({ 5000,
methodName: NTQQApiMethod.ACTIVATE_MEMBER_INFO_CHANGE, () => true,
classNameIsRegister: true, forced
args: [], )
}) return groupList
} } else {
const result = await invoke<{
static async getGroupAllInfo(groupCode: string, source: number = 4) { updateType: number
return await callNTQQApi<GeneralCallResult & Group>({ groupList: Group[]
methodName: NTQQApiMethod.GET_GROUP_ALL_INFO,
args: [
{
groupCode,
source
},
null,
],
})
}
static async getGroups(forced = false) {
// let cbCmd = ReceiveCmdS.GROUPS
// if (process.platform != 'win32') {
// cbCmd = ReceiveCmdS.GROUPS_STORE
// }
const result = await callNTQQApi<{
updateType: number
groupList: Group[]
}>({
methodName: NTQQApiMethod.GROUPS,
args: [{ force_update: forced }, undefined],
cbCmd: [ReceiveCmdS.GROUPS, ReceiveCmdS.GROUPS_STORE],
afterFirstCmd: false,
})
log('get groups result', result)
return result.groupList
}
static async getGroupMembers(groupQQ: string, num = 3000): Promise<GroupMember[]> {
const sceneId = await callNTQQApi({
methodName: NTQQApiMethod.GROUP_MEMBER_SCENE,
args: [
{
groupCode: groupQQ,
scene: 'groupMemberList_MainWindow',
},
],
})
// log("get group member sceneId", sceneId)
try {
const result = await callNTQQApi<{
result: { infos: any }
}>({ }>({
methodName: NTQQApiMethod.GROUP_MEMBERS, className: NTClass.NODE_STORE_API,
methodName: 'getGroupList',
cbCmd: ReceiveCmdS.GROUPS_STORE,
afterFirstCmd: false,
})
return result.groupList
}
}
static async getGroupMembers(groupQQ: string, num = 3000): Promise<Map<string, GroupMember>> {
const session = getSession()
let result: Awaited<ReturnType<NodeIKernelGroupService['getNextMemberList']>>
if (session) {
const groupService = session.getGroupService()
const sceneId = groupService.createMemberListScene(groupQQ, 'groupMemberList_MainWindow')
result = await groupService.getNextMemberList(sceneId, undefined, num)
} else {
const sceneId = await invoke<string>({
methodName: NTMethod.GROUP_MEMBER_SCENE,
args: [ args: [
{ {
sceneId: sceneId, groupCode: groupQQ,
num: num, scene: 'groupMemberList_MainWindow',
},
],
})
result = await invoke<
ReturnType<NodeIKernelGroupService['getNextMemberList']>
>({
methodName: NTMethod.GROUP_MEMBERS,
args: [
{
sceneId,
num,
}, },
null, null,
], ],
}) })
// log("members info", typeof result.result.infos, Object.keys(result.result.infos))
const values = result.result.infos.values()
const members: GroupMember[] = Array.from(values)
for (const member of members) {
uidMaps[member.uid] = member.uin
}
// log(uidMaps)
// log("members info", values)
log(`get group ${groupQQ} members success`)
return members
} catch (e) {
log(`get group ${groupQQ} members failed`, e)
return []
} }
} if (result.errCode !== 0) {
throw ('获取群成员列表出错,' + result.errMsg)
static async getGroupMembersInfo(groupCode: string, uids: string[], forceUpdate: boolean = false) { }
return await callNTQQApi<GeneralCallResult>({ return result.result.infos
methodName: NTQQApiMethod.GROUP_MEMBERS_INFO,
args: [
{
forceUpdate,
groupCode,
uids
},
null,
],
})
}
static async getGroupNotifies() {
// 获取管理员变更
// 加群通知,退出通知,需要管理员权限
callNTQQApi<GeneralCallResult>({
methodName: ReceiveCmdS.GROUP_NOTIFY,
classNameIsRegister: true,
}).then()
return await callNTQQApi<GroupNotifies>({
methodName: NTQQApiMethod.GET_GROUP_NOTICE,
cbCmd: ReceiveCmdS.GROUP_NOTIFY,
afterFirstCmd: false,
args: [{ doubt: false, startSeq: '', number: 14 }, null],
})
} }
static async getGroupIgnoreNotifies() { static async getGroupIgnoreNotifies() {
await NTQQGroupApi.getGroupNotifies() await NTQQGroupApi.getSingleScreenNotifies(14)
return await NTQQWindowApi.openWindow<GeneralCallResult & GroupNotifies>( return await NTQQWindowApi.openWindow<GeneralCallResult & GroupNotifies>(
NTQQWindows.GroupNotifyFilterWindow, NTQQWindows.GroupNotifyFilterWindow,
[], [],
@@ -134,134 +82,211 @@ export class NTQQGroupApi {
) )
} }
static async handleGroupRequest(seq: string, operateType: GroupRequestOperateTypes, reason?: string) { static async getSingleScreenNotifies(num: number) {
const notify = await dbUtil.getGroupNotify(seq) if (NTEventDispatch.initialised) {
if (!notify) { const [_retData, _doubt, _seq, notifies] = await NTEventDispatch.CallNormalEvent
throw `${seq}对应的加群通知不存在` <(arg1: boolean, arg2: string, arg3: number) => Promise<any>, (doubt: boolean, seq: string, notifies: GroupNotify[]) => void>
(
'NodeIKernelGroupService/getSingleScreenNotifies',
'NodeIKernelGroupListener/onGroupSingleScreenNotifies',
1,
5000,
() => true,
false,
'',
num,
)
return notifies
} else {
invoke({
methodName: ReceiveCmdS.GROUP_NOTIFY,
classNameIsRegister: true,
})
return (await invoke<GroupNotifies>({
methodName: NTMethod.GET_GROUP_NOTICE,
cbCmd: ReceiveCmdS.GROUP_NOTIFY,
afterFirstCmd: false,
args: [{ doubt: false, startSeq: '', number: num }, null],
})).notifies
} }
// delete groupNotifies[seq] }
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.HANDLE_GROUP_REQUEST, /** 27187 TODO */
args: [ static async delGroupFile(groupCode: string, files: string[]) {
const session = getSession()
return session?.getRichMediaService().deleteGroupFile(groupCode, [102], files)
}
static async handleGroupRequest(flag: string, operateType: GroupRequestOperateTypes, reason?: string) {
const flagitem = flag.split('|')
const groupCode = flagitem[0]
const seq = flagitem[1]
const type = parseInt(flagitem[2])
const session = getSession()
if (session) {
return session.getGroupService().operateSysNotify(
false,
{ {
doubt: false, 'operateType': operateType, // 2 拒绝
operateMsg: { 'targetMsg': {
operateType: operateType, // 2 拒绝 'seq': seq, // 通知序列号
targetMsg: { 'type': type,
seq: seq, // 通知序列号 'groupCode': groupCode,
type: notify.type, 'postscript': reason || ' ' // 仅传空值可能导致处理失败,故默认给个空格
groupCode: notify.group.groupCode, }
postscript: reason, })
} else {
return await invoke({
methodName: NTMethod.HANDLE_GROUP_REQUEST,
args: [
{
doubt: false,
operateMsg: {
operateType,
targetMsg: {
seq,
type,
groupCode,
postscript: reason || ' ' // 仅传空值可能导致处理失败,故默认给个空格
},
}, },
}, },
}, null,
null, ],
], })
}) }
} }
static async quitGroup(groupQQ: string) { static async quitGroup(groupQQ: string) {
const result = await callNTQQApi<GeneralCallResult>({ const session = getSession()
methodName: NTQQApiMethod.QUIT_GROUP, if (session) {
args: [{ groupCode: groupQQ }, null], return session.getGroupService().quitGroup(groupQQ)
}) } else {
if (result.result === 0) { return await invoke({
deleteGroup(groupQQ) methodName: NTMethod.QUIT_GROUP,
args: [{ groupCode: groupQQ }, null],
})
} }
return result
} }
static async kickMember( static async kickMember(
groupQQ: string, groupQQ: string,
kickUids: string[], kickUids: string[],
refuseForever: boolean = false, refuseForever = false,
kickReason: string = '', kickReason = '',
) { ) {
return await callNTQQApi<GeneralCallResult>({ const session = getSession()
methodName: NTQQApiMethod.KICK_MEMBER, if (session) {
args: [ return session.getGroupService().kickMember(groupQQ, kickUids, refuseForever, kickReason)
{ } else {
groupCode: groupQQ, return await invoke({
kickUids, methodName: NTMethod.KICK_MEMBER,
refuseForever, args: [
kickReason, {
}, groupCode: groupQQ,
], kickUids,
}) refuseForever,
kickReason,
},
],
})
}
} }
static async banMember(groupQQ: string, memList: Array<{ uid: string, timeStamp: number }>) { static async banMember(groupQQ: string, memList: Array<{ uid: string, timeStamp: number }>) {
// timeStamp为秒数, 0为解除禁言 // timeStamp为秒数, 0为解除禁言
return await callNTQQApi<GeneralCallResult>({ const session = getSession()
methodName: NTQQApiMethod.MUTE_MEMBER, if (session) {
args: [ return session.getGroupService().setMemberShutUp(groupQQ, memList)
{ } else {
groupCode: groupQQ, return await invoke({
memList, methodName: NTMethod.MUTE_MEMBER,
}, args: [
], {
}) groupCode: groupQQ,
memList,
},
],
})
}
} }
static async banGroup(groupQQ: string, shutUp: boolean) { static async banGroup(groupQQ: string, shutUp: boolean) {
return await callNTQQApi<GeneralCallResult>({ const session = getSession()
methodName: NTQQApiMethod.MUTE_GROUP, if (session) {
args: [ return session.getGroupService().setGroupShutUp(groupQQ, shutUp)
{ } else {
groupCode: groupQQ, return await invoke({
shutUp, methodName: NTMethod.MUTE_GROUP,
}, args: [
null, {
], groupCode: groupQQ,
}) shutUp,
},
null,
],
})
}
} }
static async setMemberCard(groupQQ: string, memberUid: string, cardName: string) { static async setMemberCard(groupQQ: string, memberUid: string, cardName: string) {
NTQQGroupApi.activateMemberListChange().then().catch(log) const session = getSession()
const res = await callNTQQApi<GeneralCallResult>({ if (session) {
methodName: NTQQApiMethod.SET_MEMBER_CARD, return session.getGroupService().modifyMemberCardName(groupQQ, memberUid, cardName)
args: [ } else {
{ return await invoke({
groupCode: groupQQ, methodName: NTMethod.SET_MEMBER_CARD,
uid: memberUid, args: [
cardName, {
}, groupCode: groupQQ,
null, uid: memberUid,
], cardName,
}) },
NTQQGroupApi.getGroupMembersInfo(groupQQ, [memberUid], true).then().catch(log) null,
return res ],
})
}
} }
static async setMemberRole(groupQQ: string, memberUid: string, role: GroupMemberRole) { static async setMemberRole(groupQQ: string, memberUid: string, role: GroupMemberRole) {
return await callNTQQApi<GeneralCallResult>({ const session = getSession()
methodName: NTQQApiMethod.SET_MEMBER_ROLE, if (session) {
args: [ return session.getGroupService().modifyMemberRole(groupQQ, memberUid, role)
{ } else {
groupCode: groupQQ, return await invoke({
uid: memberUid, methodName: NTMethod.SET_MEMBER_ROLE,
role, args: [
}, {
null, groupCode: groupQQ,
], uid: memberUid,
}) role,
},
null,
],
})
}
} }
static async setGroupName(groupQQ: string, groupName: string) { static async setGroupName(groupQQ: string, groupName: string) {
return await callNTQQApi<GeneralCallResult>({ const session = getSession()
methodName: NTQQApiMethod.SET_GROUP_NAME, if (session) {
args: [ return session.getGroupService().modifyGroupName(groupQQ, groupName, false)
{ } else {
groupCode: groupQQ, return await invoke({
groupName, methodName: NTMethod.SET_GROUP_NAME,
}, args: [
null, {
], groupCode: groupQQ,
}) groupName,
},
null,
],
})
}
} }
static async getGroupAtAllRemainCount(groupCode: string) { static async getGroupAtAllRemainCount(groupCode: string) {
return await callNTQQApi< return await invoke<
GeneralCallResult & { GeneralCallResult & {
atInfo: { atInfo: {
canAtAll: boolean canAtAll: boolean
@@ -272,7 +297,7 @@ export class NTQQGroupApi {
} }
} }
>({ >({
methodName: NTQQApiMethod.GROUP_AT_ALL_REMAIN_COUNT, methodName: NTMethod.GROUP_AT_ALL_REMAIN_COUNT,
args: [ args: [
{ {
groupCode, groupCode,
@@ -282,46 +307,31 @@ export class NTQQGroupApi {
}) })
} }
// 头衔不可用 /** 27187 TODO */
static async setGroupTitle(groupQQ: string, uid: string, title: string) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_GROUP_TITLE,
args: [
{
groupCode: groupQQ,
uid,
title,
},
null,
],
})
}
static publishGroupBulletin(groupQQ: string, title: string, content: string) { }
static async removeGroupEssence(GroupCode: string, msgId: string) { static async removeGroupEssence(GroupCode: string, msgId: string) {
const session = wrapperApi.NodeIQQNTWrapperSession const session = getSession()
// 代码没测过 // 代码没测过
// 需要 ob11msgid->msgId + (peer) -> msgSeq + msgRandom // 需要 ob11msgid->msgId + (peer) -> msgSeq + msgRandom
let MsgData = await session?.getMsgService().getMsgsIncludeSelf({ chatType: 2, guildId: '', peerUid: GroupCode }, msgId, 1, false) let MsgData = await session?.getMsgService().getMsgsIncludeSelf({ chatType: 2, guildId: '', peerUid: GroupCode }, msgId, 1, false)
let param = { let param = {
groupCode: GroupCode, groupCode: GroupCode,
msgRandom: parseInt(MsgData.msgList[0].msgRandom), msgRandom: parseInt(MsgData?.msgList[0].msgRandom!),
msgSeq: parseInt(MsgData.msgList[0].msgSeq) msgSeq: parseInt(MsgData?.msgList[0].msgSeq!)
} }
// GetMsgByShoretID(ShoretID) -> MsgService.getMsgs(Peer,MsgId,1,false) -> 组出参数 // GetMsgByShoretID(ShoretID) -> MsgService.getMsgs(Peer,MsgId,1,false) -> 组出参数
return session?.getGroupService().removeGroupEssence(param) return session?.getGroupService().removeGroupEssence(param)
} }
/** 27187 TODO */
static async addGroupEssence(GroupCode: string, msgId: string) { static async addGroupEssence(GroupCode: string, msgId: string) {
const session = wrapperApi.NodeIQQNTWrapperSession const session = getSession()
// 代码没测过 // 代码没测过
// 需要 ob11msgid->msgId + (peer) -> msgSeq + msgRandom // 需要 ob11msgid->msgId + (peer) -> msgSeq + msgRandom
let MsgData = await session?.getMsgService().getMsgsIncludeSelf({ chatType: 2, guildId: '', peerUid: GroupCode }, msgId, 1, false) let MsgData = await session?.getMsgService().getMsgsIncludeSelf({ chatType: 2, guildId: '', peerUid: GroupCode }, msgId, 1, false)
let param = { let param = {
groupCode: GroupCode, groupCode: GroupCode,
msgRandom: parseInt(MsgData.msgList[0].msgRandom), msgRandom: parseInt(MsgData?.msgList[0].msgRandom!),
msgSeq: parseInt(MsgData.msgList[0].msgSeq) msgSeq: parseInt(MsgData?.msgList[0].msgSeq!)
} }
// GetMsgByShoretID(ShoretID) -> MsgService.getMsgs(Peer,MsgId,1,false) -> 组出参数 // GetMsgByShoretID(ShoretID) -> MsgService.getMsgs(Peer,MsgId,1,false) -> 组出参数
return session?.getGroupService().addGroupEssence(param) return session?.getGroupService().addGroupEssence(param)

View File

@@ -1,295 +1,372 @@
import { callNTQQApi, GeneralCallResult, NTQQApiMethod } from '../ntcall' import { invoke, NTMethod } from '../ntcall'
import { ChatType, RawMessage, SendMessageElement, Peer } from '../types' import { GeneralCallResult, TmpChatInfoApi } from '../services'
import { dbUtil } from '../../common/db' import { RawMessage, SendMessageElement, Peer, ChatType2 } from '../types'
import { selfInfo } from '../../common/data' import { getSelfNick, getSelfUid } from '../../common/data'
import { ReceiveCmdS, registerReceiveHook } from '../hook' import { getSession } from '@/ntqqapi/wrapper'
import { log } from '../../common/utils/log' import { NTEventDispatch } from '@/common/utils/EventTask'
import { sleep } from '../../common/utils/helper'
import { isQQ998 } from '../../common/utils'
import { wrapperApi } from '@/ntqqapi/wrapper'
export let sendMessagePool: Record<string, ((sendSuccessMsg: RawMessage) => void) | null> = {} // peerUid: callbackFunc function generateMsgId() {
const timestamp = Math.floor(Date.now() / 1000)
export let sentMessages: Record<string, RawMessage> = {} // msgId: RawMessage const random = Math.floor(Math.random() * Math.pow(2, 32))
const buffer = Buffer.alloc(8)
async function sendWaiter(peer: Peer, waitComplete = true, timeout: number = 10000) { buffer.writeUInt32BE(timestamp, 0)
// 等待上一个相同的peer发送完 buffer.writeUInt32BE(random, 4)
const peerUid = peer.peerUid const msgId = BigInt('0x' + buffer.toString('hex')).toString()
let checkLastSendUsingTime = 0 return msgId
const waitLastSend = async () => {
if (checkLastSendUsingTime > timeout) {
throw '发送超时'
}
let lastSending = sendMessagePool[peer.peerUid]
if (lastSending) {
// log("有正在发送的消息,等待中...")
await sleep(500)
checkLastSendUsingTime += 500
return await waitLastSend()
}
else {
return
}
}
await waitLastSend()
let sentMessage: RawMessage | null = null
sendMessagePool[peerUid] = async (rawMessage: RawMessage) => {
delete sendMessagePool[peerUid]
sentMessage = rawMessage
sentMessages[rawMessage.msgId] = rawMessage
}
let checkSendCompleteUsingTime = 0
const checkSendComplete = async (): Promise<RawMessage> => {
if (sentMessage) {
if (waitComplete) {
if (sentMessage.sendStatus == 2) {
delete sentMessages[sentMessage.msgId]
return sentMessage
}
}
else {
delete sentMessages[sentMessage.msgId]
return sentMessage
}
// log(`给${peerUid}发送消息成功`)
}
checkSendCompleteUsingTime += 500
if (checkSendCompleteUsingTime > timeout) {
throw '发送超时'
}
await sleep(500)
return await checkSendComplete()
}
return checkSendComplete()
} }
export class NTQQMsgApi { export class NTQQMsgApi {
static enterOrExitAIO(peer: Peer, enter: boolean) { static async getTempChatInfo(chatType: ChatType2, peerUid: string) {
return callNTQQApi<GeneralCallResult>({ const session = getSession()
methodName: NTQQApiMethod.ENTER_OR_EXIT_AIO, if (session) {
args: [ return session.getMsgService().getTempChatInfo(chatType, peerUid)
{ } else {
"info_list": [ return await invoke<TmpChatInfoApi>({
{ methodName: 'nodeIKernelMsgService/getTempChatInfo',
peer, args: [
"option": enter ? 1 : 2 {
} chatType,
] peerUid,
}, },
{ null,
"send": true ],
}, })
], }
})
} }
static async setEmojiLike(peer: Peer, msgSeq: string, emojiId: string, set: boolean = true) { 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//sysface_res/apng/ 下可以看到所有QQ表情预览
// nt_qq\global\nt_data\Emoji\emoji-resource\face_config.json 里面有所有表情的id, 自带表情id是QSid, 标准emoji表情id是QCid // nt_qq\global\nt_data\Emoji\emoji-resource\face_config.json 里面有所有表情的id, 自带表情id是QSid, 标准emoji表情id是QCid
// 其实以官方文档为准是最好的https://bot.q.qq.com/wiki/develop/api-v2/openapi/emoji/model.html#EmojiType // 其实以官方文档为准是最好的https://bot.q.qq.com/wiki/develop/api-v2/openapi/emoji/model.html#EmojiType
emojiId = emojiId.toString() emojiId = emojiId.toString()
return await callNTQQApi<GeneralCallResult>({ const session = getSession()
methodName: NTQQApiMethod.EMOJI_LIKE, if (session) {
args: [ return session.getMsgService().setMsgEmojiLikes(peer, msgSeq, emojiId, emojiId.length > 3 ? '2' : '1', set)
{ } else {
peer, return await invoke({
msgSeq, methodName: NTMethod.EMOJI_LIKE,
emojiId, args: [
emojiType: emojiId.length > 3 ? '2' : '1', {
setEmoji: set, peer,
}, msgSeq,
null, emojiId,
], emojiType: emojiId.length > 3 ? '2' : '1',
}) setEmoji: set,
},
null,
],
})
}
} }
static async getMultiMsg(peer: Peer, rootMsgId: string, parentMsgId: string) { static async getMultiMsg(peer: Peer, rootMsgId: string, parentMsgId: string) {
return await callNTQQApi<GeneralCallResult & { msgList: RawMessage[] }>({ const session = getSession()
methodName: NTQQApiMethod.GET_MULTI_MSG, if (session) {
args: [ return session.getMsgService().getMultiMsg(peer, rootMsgId, parentMsgId)
{ } else {
peer, return await invoke<GeneralCallResult & { msgList: RawMessage[] }>({
rootMsgId, methodName: NTMethod.GET_MULTI_MSG,
parentMsgId, args: [
}, {
null, peer,
], rootMsgId,
}) parentMsgId,
} },
null,
static async getMsgBoxInfo(peer: Peer) { ],
return await callNTQQApi<GeneralCallResult>({ })
methodName: NTQQApiMethod.GET_MSG_BOX_INFO, }
args: [
{
contacts: [
peer
],
},
null,
],
})
} }
static async activateChat(peer: Peer) { static async activateChat(peer: Peer) {
// await this.fetchRecentContact(); return await invoke<GeneralCallResult>({
// await sleep(500); methodName: NTMethod.ACTIVE_CHAT_PREVIEW,
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.ACTIVE_CHAT_PREVIEW,
args: [{ peer, cnt: 20 }, null], args: [{ peer, cnt: 20 }, null],
}) })
} }
static async activateChatAndGetHistory(peer: Peer) { static async activateChatAndGetHistory(peer: Peer) {
// await this.fetchRecentContact(); return await invoke<GeneralCallResult>({
// await sleep(500); methodName: NTMethod.ACTIVE_CHAT_HISTORY,
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.ACTIVE_CHAT_HISTORY,
// 参数似乎不是这样 // 参数似乎不是这样
args: [{ peer, cnt: 20 }, null], args: [{ peer, cnt: 20 }, null],
}) })
} }
static async getMsgHistory(peer: Peer, msgId: string, count: number) { static async getMsgsByMsgId(peer: Peer | undefined, msgIds: string[] | undefined) {
// 消息时间从旧到新 if (!peer) throw new Error('peer is not allowed')
return await callNTQQApi<GeneralCallResult & { msgList: RawMessage[] }>({ if (!msgIds) throw new Error('msgIds is not allowed')
methodName: isQQ998 ? NTQQApiMethod.ACTIVE_CHAT_HISTORY : NTQQApiMethod.HISTORY_MSG, const session = getSession()
args: [ if (session) {
{ return session.getMsgService().getMsgsByMsgId(peer, msgIds)
peer, } else {
msgId, return await invoke<GeneralCallResult & {
cnt: count, msgList: RawMessage[]
queryOrder: true, }>({
}, methodName: 'nodeIKernelMsgService/getMsgsByMsgId',
null, args: [
], {
}) peer,
msgIds,
},
null,
],
})
}
} }
static async fetchRecentContact() { static async getMsgHistory(peer: Peer, msgId: string, count: number, isReverseOrder: boolean = false) {
await callNTQQApi({ const session = getSession()
methodName: NTQQApiMethod.RECENT_CONTACT, // 消息时间从旧到新
args: [ if (session) {
{ return session.getMsgService().getMsgsIncludeSelf(peer, msgId, count, isReverseOrder)
fetchParam: { } else {
anchorPointContact: { return await invoke<GeneralCallResult & { msgList: RawMessage[] }>({
contactId: '', methodName: NTMethod.HISTORY_MSG,
sortField: '', args: [
pos: 0, {
}, peer,
relativeMoveCount: 0, msgId,
listType: 2, // 1普通消息2群助手内的消息 cnt: count,
count: 200, queryOrder: isReverseOrder,
fetchOld: true,
}, },
}, null,
], ],
}) })
}
} }
static async recallMsg(peer: Peer, msgIds: string[]) { static async recallMsg(peer: Peer, msgIds: string[]) {
return await callNTQQApi({ const session = getSession()
methodName: NTQQApiMethod.RECALL_MSG, if (session) {
args: [ return session.getMsgService().recallMsg({
{ chatType: peer.chatType,
peer, peerUid: peer.peerUid
msgIds, }, msgIds)
}, } else {
null, return await invoke({
], methodName: NTMethod.RECALL_MSG,
}) args: [
{
peer,
msgIds,
},
null,
],
})
}
} }
static async sendMsg(peer: Peer, msgElements: SendMessageElement[], waitComplete = true, timeout = 10000) { static async sendMsg(peer: Peer, msgElements: SendMessageElement[], waitComplete = true, timeout = 10000) {
const waiter = sendWaiter(peer, waitComplete, timeout) const msgId = generateMsgId()
callNTQQApi({ peer.guildId = msgId
methodName: NTQQApiMethod.SEND_MSG, let msgList: RawMessage[]
args: [ if (NTEventDispatch.initialised) {
{ const data = await NTEventDispatch.CallNormalEvent<
msgId: '0', (msgId: string, peer: Peer, msgElements: SendMessageElement[], map: Map<any, any>) => Promise<unknown>,
peer, (msgList: RawMessage[]) => void
msgElements, >(
msgAttributeInfos: new Map(), 'NodeIKernelMsgService/sendMsg',
'NodeIKernelMsgListener/onMsgInfoListUpdate',
1,
timeout,
(msgRecords: RawMessage[]) => {
for (const msgRecord of msgRecords) {
if (msgRecord.guildId === msgId && msgRecord.sendStatus === 2) {
return true
}
}
return false
}, },
null, '0',
], peer,
}).then() msgElements,
return await waiter new Map()
)
msgList = data[1]
} else {
const data = await invoke<{ msgList: RawMessage[] }>({
methodName: 'nodeIKernelMsgService/sendMsg',
cbCmd: 'nodeIKernelMsgListener/onMsgInfoListUpdate',
afterFirstCmd: false,
cmdCB: payload => {
for (const msgRecord of payload.msgList) {
if (msgRecord.guildId === msgId && msgRecord.sendStatus === 2) {
return true
}
}
return false
},
args: [
{
msgId: '0',
peer,
msgElements,
msgAttributeInfos: new Map()
},
null
],
timeout
})
msgList = data.msgList
}
const retMsg = msgList.find(msgRecord => {
if (msgRecord.guildId === msgId) {
return true
}
})
return retMsg!
} }
static async forwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) { static async forwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) {
const waiter = sendWaiter(destPeer, true, 10000) const session = getSession()
callNTQQApi<GeneralCallResult>({ if (session) {
methodName: NTQQApiMethod.FORWARD_MSG, return session.getMsgService().forwardMsg(msgIds, srcPeer, [destPeer], [])
args: [ } else {
{ return await invoke<GeneralCallResult>({
msgIds: msgIds, methodName: NTMethod.FORWARD_MSG,
srcContact: srcPeer, args: [
dstContacts: [destPeer], {
commentElements: [], msgIds: msgIds,
msgAttributeInfos: new Map(), srcContact: srcPeer,
}, dstContacts: [destPeer],
null, commentElements: [],
], msgAttributeInfos: new Map(),
}).then().catch(log) },
return await waiter null,
],
})
}
} }
static async multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) { static async multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]): Promise<RawMessage> {
const msgInfos = msgIds.map((id) => { const senderShowName = await getSelfNick()
return { msgId: id, senderShowName: selfInfo.nick } const msgInfos = msgIds.map(id => {
return { msgId: id, senderShowName }
}) })
const apiArgs = [ const selfUid = getSelfUid()
{ let msgList: RawMessage[]
if (NTEventDispatch.initialised) {
const 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 == selfUid) {
return true
}
}
return false
},
msgInfos, msgInfos,
srcContact: srcPeer, srcPeer,
dstContact: destPeer, destPeer,
commentElements: [], [],
msgAttributeInfos: new Map(), new Map()
}, )
null, msgList = data[1]
] } else {
return await new Promise<RawMessage>((resolve, reject) => { const data = await invoke<{ msgList: RawMessage[] }>({
let complete = false methodName: 'nodeIKernelMsgService/multiForwardMsgWithComment',
setTimeout(() => { cbCmd: 'nodeIKernelMsgListener/onMsgInfoListUpdate',
if (!complete) { afterFirstCmd: false,
reject('转发消息超时') cmdCB: payload => {
} for (const msgRecord of payload.msgList) {
}, 5000) if (msgRecord.peerUid == destPeer.peerUid && msgRecord.senderUid == selfUid) {
registerReceiveHook(ReceiveCmdS.SELF_SEND_MSG, async (payload: { msgRecord: RawMessage }) => { return true
const msg = payload.msgRecord }
// 需要判断它是转发的消息,并且识别到是当前转发的这一条 }
const arkElement = msg.elements.find((ele) => ele.arkElement) return false
if (!arkElement) { },
// log("收到的不是转发消息") args: [
return {
} msgInfos,
const forwardData: any = JSON.parse(arkElement.arkElement.bytesData) srcContact: srcPeer,
if (forwardData.app != 'com.tencent.multimsg') { dstContact: destPeer,
return commentElements: [],
} msgAttributeInfos: new Map(),
if (msg.peerUid == destPeer.peerUid && msg.senderUid == selfInfo.uid) { },
complete = true null,
await dbUtil.addMsg(msg) ],
resolve(msg)
log('转发消息成功:', payload)
}
}) })
callNTQQApi<GeneralCallResult>({ msgList = data.msgList
methodName: NTQQApiMethod.MULTI_FORWARD_MSG, }
args: apiArgs, for (const msg of msgList) {
}).then((result) => { const arkElement = msg.elements.find(ele => ele.arkElement)
log('转发消息结果:', result, apiArgs) if (!arkElement) {
if (result.result !== 0) { continue
complete = true }
reject('转发消息失败,' + JSON.stringify(result)) const forwardData: any = JSON.parse(arkElement.arkElement.bytesData)
} if (forwardData.app != 'com.tencent.multimsg') {
}) continue
}) }
if (msg.peerUid == destPeer.peerUid && msg.senderUid == selfUid) {
return msg
}
}
throw new Error('转发消息超时')
} }
static async getMsgsBySeqAndCount(peer: Peer, seq: string, count: number, desc: boolean, z: boolean) { static async getMsgsBySeqAndCount(peer: Peer, seq: string, count: number, desc: boolean, z: boolean) {
const session = wrapperApi.NodeIQQNTWrapperSession const session = getSession()
return await session?.getMsgService().getMsgsBySeqAndCount(peer, seq, count, desc, z); if (session) {
return await session.getMsgService().getMsgsBySeqAndCount(peer, seq, count, desc, z)
} else {
return await invoke<GeneralCallResult & {
msgList: RawMessage[]
}>({
methodName: 'nodeIKernelMsgService/getMsgsBySeqAndCount',
args: [
{
peer,
cnt: count,
msgSeq: seq,
queryOrder: desc
},
null,
],
})
}
}
/** 27187 TODO */
static async getLastestMsgByUids(peer: Peer, count = 20, isReverseOrder = false) {
const session = getSession()
const ret = await session?.getMsgService().queryMsgsWithFilterEx('0', '0', '0', {
chatInfo: peer,
filterMsgType: [],
filterSendersUid: [],
filterMsgToTime: '0',
filterMsgFromTime: '0',
isReverseOrder: isReverseOrder, //此参数有点离谱 注意不是本次查询的排序 而是全部消历史信息的排序 默认false 从新消息拉取到旧消息
isIncludeCurrent: true,
pageLimit: count,
})
return ret
}
static async getSingleMsg(peer: Peer, seq: string) {
const session = getSession()
if (session) {
return await session.getMsgService().getSingleMsg(peer, seq)
} else {
return await invoke<GeneralCallResult & {
msgList: RawMessage[]
}>({
methodName: 'nodeIKernelMsgService/getSingleMsg',
args: [
{
peer,
msgSeq: seq,
},
null,
],
})
}
} }
} }

View File

@@ -1,185 +1,137 @@
import { callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod } from '../ntcall' import { invoke, NTMethod } from '../ntcall'
import { Group, SelfInfo, User } from '../types' import { GeneralCallResult } from '../services'
import { ReceiveCmdS } from '../hook' import { User, UserDetailInfoByUin, UserDetailInfoByUinV2, UserDetailInfoListenerArg } from '../types'
import { selfInfo, uidMaps } from '../../common/data' import { groupMembers, getSelfUin } from '@/common/data'
import { cacheFunc, isQQ998, log, sleep } from '../../common/utils' import { CacheClassFuncAsync, getBuildVersion } from '@/common/utils'
import { wrapperApi } from '@/ntqqapi/wrapper' import { getSession } from '@/ntqqapi/wrapper'
import { RequestUtil } from '@/common/utils/request' import { RequestUtil } from '@/common/utils/request'
import { NodeIKernelProfileService, UserDetailSource, ProfileBizType } from '../services' import { NodeIKernelProfileService, UserDetailSource, ProfileBizType, forceFetchClientKeyRetType } from '../services'
import { NodeIKernelProfileListener } from '../listeners' import { NodeIKernelProfileListener } from '../listeners'
import { NTEventDispatch } from '@/common/utils/EventTask' import { NTEventDispatch } from '@/common/utils/EventTask'
import { qqPkgInfo } from '@/common/utils/QQBasicInfo' import { NTQQFriendApi } from './friend'
import { Time } from 'cosmokit'
const userInfoCache: Record<string, User> = {} // uid: User
export interface ClientKeyData extends GeneralCallResult {
url: string
keyIndex: string
clientKey: string
expireTime: string
}
export class NTQQUserApi { export class NTQQUserApi {
static async setQQAvatar(filePath: string) { static async setQQAvatar(filePath: string) {
return await callNTQQApi<GeneralCallResult>({ return await invoke<GeneralCallResult>({
methodName: NTQQApiMethod.SET_QQ_AVATAR, methodName: NTMethod.SET_QQ_AVATAR,
args: [ args: [
{ {
path: filePath, path: filePath,
}, },
null, null,
], ],
timeoutSecond: 10, // 10秒不一定够 timeout: 10 * Time.second, // 10秒不一定够
}) })
} }
static async getSelfInfo() {
return await callNTQQApi<SelfInfo>({
className: NTQQApiClass.GLOBAL_DATA,
methodName: NTQQApiMethod.SELF_INFO,
timeoutSecond: 2,
})
}
static async getUserInfo(uid: string) {
const result = await callNTQQApi<{ profiles: Map<string, User> }>({
methodName: NTQQApiMethod.USER_INFO,
args: [{ force: true, uids: [uid] }, undefined],
cbCmd: ReceiveCmdS.USER_INFO,
})
return result.profiles.get(uid)
}
// 26702
static async fetchUserDetailInfo(uid: string) { static async fetchUserDetailInfo(uid: string) {
type EventService = NodeIKernelProfileService['fetchUserDetailInfo'] let info: UserDetailInfoListenerArg
type EventListener = NodeIKernelProfileListener['onUserDetailInfoChanged'] if (NTEventDispatch.initialised) {
const [_retData, profile] = await NTEventDispatch.CallNormalEvent type EventService = NodeIKernelProfileService['fetchUserDetailInfo']
<EventService, EventListener> type EventListener = NodeIKernelProfileListener['onUserDetailInfoChanged']
( const [_retData, profile] = await NTEventDispatch.CallNormalEvent
'NodeIKernelProfileService/fetchUserDetailInfo', <EventService, EventListener>
'NodeIKernelProfileListener/onUserDetailInfoChanged', (
1, 'NodeIKernelProfileService/fetchUserDetailInfo',
5000, 'NodeIKernelProfileListener/onUserDetailInfoChanged',
(profile) => { 1,
if (profile.uid === uid) { 5000,
return true; (profile) => profile.uid === uid,
} 'BuddyProfileStore',
return false; [uid],
}, UserDetailSource.KSERVER,
'BuddyProfileStore', [ProfileBizType.KALL]
[ )
uid info = profile
} else {
const result = await invoke<{ info: UserDetailInfoListenerArg }>({
methodName: 'nodeIKernelProfileService/fetchUserDetailInfo',
cbCmd: 'nodeIKernelProfileListener/onUserDetailInfoChanged',
afterFirstCmd: false,
cmdCB: payload => payload.info.uid === uid,
args: [
{
callFrom: 'BuddyProfileStore',
uid: [uid],
source: UserDetailSource.KSERVER,
bizList: [ProfileBizType.KALL]
},
null
], ],
UserDetailSource.KSERVER, })
[ info = result.info
ProfileBizType.KALL }
] const ret: User = {
) ...info.simpleInfo.coreInfo,
const RetUser: User = { ...info.simpleInfo.status,
...profile.simpleInfo.coreInfo, ...info.simpleInfo.vasInfo,
...profile.simpleInfo.status, ...info.commonExt,
...profile.simpleInfo.vasInfo, ...info.simpleInfo.baseInfo,
...profile.commonExt, qqLevel: info.commonExt?.qqLevel,
...profile.simpleInfo.baseInfo,
qqLevel: profile.commonExt.qqLevel,
pendantId: '' pendantId: ''
} }
return RetUser return ret
} }
static async getUserDetailInfo(uid: string, getLevel = false, withBizInfo = true) { static async getUserDetailInfo(uid: string, getLevel = false, withBizInfo = true) {
if (+qqPkgInfo.buildVersion >= 26702) { if (getBuildVersion() >= 26702) {
return this.fetchUserDetailInfo(uid) return NTQQUserApi.fetchUserDetailInfo(uid)
} }
// this.getUserInfo(uid) if (NTEventDispatch.initialised) {
let methodName = !isQQ998 ? NTQQApiMethod.USER_DETAIL_INFO : NTQQApiMethod.USER_DETAIL_INFO_WITH_BIZ_INFO type EventService = NodeIKernelProfileService['getUserDetailInfoWithBizInfo']
if (!withBizInfo) { type EventListener = NodeIKernelProfileListener['onProfileDetailInfoChanged']
methodName = NTQQApiMethod.USER_DETAIL_INFO const [_retData, profile] = await NTEventDispatch.CallNormalEvent
} <EventService, EventListener>
const fetchInfo = async () => { (
const result = await callNTQQApi<{ info: User }>({ 'NodeIKernelProfileService/getUserDetailInfoWithBizInfo',
methodName, 'NodeIKernelProfileListener/onProfileDetailInfoChanged',
cbCmd: ReceiveCmdS.USER_DETAIL_INFO, 2,
5000,
(profile) => profile.uid === uid,
uid,
[0]
)
return profile
} else {
const result = await invoke<{ info: User }>({
methodName: 'nodeIKernelProfileService/getUserDetailInfoWithBizInfo',
cbCmd: 'nodeIKernelProfileListener/onProfileDetailInfoChanged',
afterFirstCmd: false, afterFirstCmd: false,
cmdCB: (payload) => { cmdCB: (payload) => payload.info.uid === uid,
const success = payload.info.uid == uid
// log("get user detail info", success, uid, payload)
return success
},
args: [ args: [
{ {
uid, uid,
bizList: [0]
}, },
null, null,
], ],
}) })
const info = result.info return result.info
if (info?.uin) {
uidMaps[info.uid] = info.uin
}
return info
} }
// 首次请求两次才能拿到的等级信息
if (!userInfoCache[uid] && getLevel) {
await fetchInfo()
await sleep(1000)
}
const userInfo = await fetchInfo()
userInfoCache[uid] = userInfo
return userInfo
} }
// return 'p_uin=o0xxx; p_skey=orXDssiGF8axxxxxxxxxxxxxx_; skey='
static async getCookieWithoutSkey() {
return await callNTQQApi<string>({
className: NTQQApiClass.GROUP_HOME_WORK,
methodName: NTQQApiMethod.UPDATE_SKEY,
args: [
{
domain: 'qun.qq.com',
},
],
})
}
static async getQzoneCookies() {
const requestUrl = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + selfInfo.uin + '&clientkey=' + (await this.getClientKey()).clientKey + '&u1=https%3A%2F%2Fuser.qzone.qq.com%2F' + selfInfo.uin + '%2Finfocenter&keyindex=19%27'
let cookies: { [key: string]: string } = {}
try {
cookies = await RequestUtil.HttpsGetCookies(requestUrl)
} catch (e: any) {
log('获取QZone Cookies失败', e)
cookies = {}
}
return cookies
}
static async getSkey(): Promise<string> { static async getSkey(): Promise<string> {
const clientKeyData = await this.getClientKey() const clientKeyData = await NTQQUserApi.forceFetchClientKey()
if (clientKeyData.result !== 0) { if (clientKeyData?.result !== 0) {
throw new Error('获取clientKey失败') throw new Error('获取clientKey失败')
} }
const url = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + selfInfo.uin const url = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + getSelfUin()
+ '&clientkey=' + clientKeyData.clientKey + '&clientkey=' + clientKeyData.clientKey
+ '&u1=https%3A%2F%2Fh5.qzone.qq.com%2Fqqnt%2Fqzoneinpcqq%2Ffriend%3Frefresh%3D0%26clientuin%3D0%26darkMode%3D0&keyindex=' + clientKeyData.keyIndex + '&u1=https%3A%2F%2Fh5.qzone.qq.com%2Fqqnt%2Fqzoneinpcqq%2Ffriend%3Frefresh%3D0%26clientuin%3D0%26darkMode%3D0&keyindex=' + clientKeyData.keyIndex
return (await RequestUtil.HttpsGetCookies(url))?.skey return (await RequestUtil.HttpsGetCookies(url))?.skey
} }
@cacheFunc(60 * 30 * 1000) @CacheClassFuncAsync(1800 * 1000)
static async getCookies(domain: string) { static async getCookies(domain: string) {
if (domain.endsWith("qzone.qq.com")) { const clientKeyData = await NTQQUserApi.forceFetchClientKey()
let data = (await NTQQUserApi.getQzoneCookies()) if (clientKeyData?.result !== 0) {
const CookieValue = 'p_skey=' + data.p_skey + '; skey=' + data.skey + '; p_uin=o' + selfInfo.uin + '; uin=o' + selfInfo.uin throw new Error('获取clientKey失败')
return { bkn: NTQQUserApi.genBkn(data.p_skey), cookies: CookieValue }
} }
const skey = await this.getSkey() const uin = getSelfUin()
const pskey = (await this.getPSkey([domain])).get(domain) const requestUrl = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + uin + '&clientkey=' + clientKeyData.clientKey + '&u1=https%3A%2F%2F' + domain + '%2F' + uin + '%2Finfocenter&keyindex=19%27'
if (!pskey || !skey) { const cookies: { [key: string]: string; } = await RequestUtil.HttpsGetCookies(requestUrl)
throw new Error('获取Cookies失败') return cookies
}
const bkn = NTQQUserApi.genBkn(skey)
const cookies = `p_skey=${pskey}; skey=${skey}; p_uin=o${selfInfo.uin}; uin=o${selfInfo.uin}`
return { cookies, bkn }
} }
static genBkn(sKey: string) { static genBkn(sKey: string) {
@@ -194,17 +146,209 @@ export class NTQQUserApi {
return (hash & 0x7fffffff).toString() return (hash & 0x7fffffff).toString()
} }
static async getPSkey(domains: string[]): Promise<Map<string, string>> { static async like(uid: string, count = 1) {
const session = wrapperApi.NodeIQQNTWrapperSession const session = getSession()
const res = await session?.getTipOffService().getPskey(domains, true) if (session) {
if (res.result !== 0) { return session.getProfileLikeService().setBuddyProfileLike({
throw new Error(`获取Pskey失败: ${res.errMsg}`) friendUid: uid,
sourceId: 71,
doLikeCount: count,
doLikeTollCount: 0
})
} else {
return await invoke<GeneralCallResult & { succCounts: number }>({
methodName: 'nodeIKernelProfileLikeService/setBuddyProfileLike',
args: [
{
doLikeUserInfo: {
friendUid: uid,
sourceId: 71,
doLikeCount: count,
doLikeTollCount: 0
}
},
null,
],
})
} }
return res.domainPskeyMap
} }
static async getClientKey(): Promise<ClientKeyData> { static async getUidByUinV1(Uin: string) {
const session = wrapperApi.NodeIQQNTWrapperSession const session = getSession()
return await session?.getTicketService().forceFetchClientKey('') // 通用转换开始尝试
let uid = (await session?.getUixConvertService().getUid([Uin]))?.uidInfo.get(Uin)
//Uid 群友列表转
if (!uid) {
for (let groupMembersList of groupMembers.values()) {
for (let GroupMember of groupMembersList.values()) {
if (GroupMember.uin == Uin) {
uid = GroupMember.uid
}
}
}
}
if (!uid) {
let unveifyUid = (await NTQQUserApi.getUserDetailInfoByUin(Uin)).info.uid;//从QQ Native 特殊转换 方法三
if (unveifyUid.indexOf('*') == -1) {
uid = unveifyUid
}
}
return uid
}
static async getUidByUinV2(uin: string) {
const session = getSession()
if (session) {
let uid = (await session.getGroupService().getUidByUins([uin])).uids.get(uin)
if (uid) return uid
uid = (await session.getProfileService().getUidByUin('FriendsServiceImpl', [uin])).get(uin)
if (uid) return uid
uid = (await session.getUixConvertService().getUid([uin])).uidInfo.get(uin)
if (uid) return uid
} else {
let uid = (await invoke<{ uids: Map<string, string> }>({
methodName: 'nodeIKernelGroupService/getUidByUins',
args: [
{ uin: [uin] },
null,
],
})).uids.get(uin)
if (uid) return uid
uid = (await invoke<Map<string, string>>({
methodName: 'nodeIKernelProfileService/getUidByUin',
args: [
{
callFrom: 'FriendsServiceImpl',
uin: [uin],
},
null,
],
})).get(uin)
if (uid) return uid
uid = (await invoke<{ uidInfo: Map<string, string> }>({
methodName: 'nodeIKernelUixConvertService/getUid',
args: [
{ uin: [uin] },
null,
],
})).uidInfo.get(uin)
if (uid) return uid
}
const unveifyUid = (await NTQQUserApi.getUserDetailInfoByUinV2(uin)).detail.uid //从QQ Native 特殊转换
if (unveifyUid.indexOf('*') == -1) return unveifyUid
}
static async getUidByUin(Uin: string) {
if (getBuildVersion() >= 26702) {
return await NTQQUserApi.getUidByUinV2(Uin)
}
return await NTQQUserApi.getUidByUinV1(Uin)
}
static async getUserDetailInfoByUinV2(uin: string) {
if (NTEventDispatch.initialised) {
return await NTEventDispatch.CallNoListenerEvent
<(Uin: string) => Promise<UserDetailInfoByUinV2>>(
'NodeIKernelProfileService/getUserDetailInfoByUin',
5000,
uin
)
} else {
return await invoke<UserDetailInfoByUinV2>({
methodName: 'nodeIKernelProfileService/getUserDetailInfoByUin',
args: [
{ uin },
null,
],
})
}
}
static async getUserDetailInfoByUin(Uin: string) {
return NTEventDispatch.CallNoListenerEvent
<(Uin: string) => Promise<UserDetailInfoByUin>>(
'NodeIKernelProfileService/getUserDetailInfoByUin',
5000,
Uin
)
}
static async getUinByUidV1(Uid: string) {
const ret = await NTEventDispatch.CallNoListenerEvent
<(Uin: string[]) => Promise<{ uinInfo: Map<string, string> }>>(
'NodeIKernelUixConvertService/getUin',
5000,
[Uid]
)
let uin = ret.uinInfo.get(Uid)
if (!uin) {
uin = (await NTQQUserApi.getUserDetailInfo(Uid)).uin //从QQ Native 转换
}
return uin
}
static async getUinByUidV2(uid: string) {
const session = getSession()
if (session) {
let uin = (await session.getGroupService().getUinByUids([uid])).uins.get(uid)
if (uin) return uin
uin = (await session.getProfileService().getUinByUid('FriendsServiceImpl', [uid])).get(uid)
if (uin) return uin
uin = (await session.getUixConvertService().getUin([uid])).uinInfo.get(uid)
if (uin) return uin
return uin
} else {
let uin = (await invoke<{ uins: Map<string, string> }>({
methodName: 'nodeIKernelGroupService/getUinByUids',
args: [
{ uid: [uid] },
null,
],
})).uins.get(uid)
if (uin) return uin
uin = (await invoke<Map<string, string>>({
methodName: 'nodeIKernelProfileService/getUinByUid',
args: [
{
callFrom: 'FriendsServiceImpl',
uid: [uid],
},
null,
],
})).get(uid)
if (uin) return uin
uin = (await invoke<{ uinInfo: Map<string, string> }>({
methodName: 'nodeIKernelUixConvertService/getUin',
args: [
{ uid: [uid] },
null,
],
})).uinInfo.get(uid)
if (uin) return uin
}
let uin = (await NTQQFriendApi.getBuddyIdMap(true)).getKey(uid)
if (uin) return uin
uin = (await NTQQUserApi.getUserDetailInfo(uid)).uin //从QQ Native 转换
}
static async getUinByUid(Uid: string) {
if (getBuildVersion() >= 26702) {
return (await NTQQUserApi.getUinByUidV2(Uid))!
}
return await NTQQUserApi.getUinByUidV1(Uid)
}
static async forceFetchClientKey() {
const session = getSession()
if (session) {
return await session.getTicketService().forceFetchClientKey('')
} else {
return await invoke<forceFetchClientKeyRetType>({
methodName: 'nodeIKernelTicketService/forceFetchClientKey',
args: [{
domain: ''
}, null],
})
}
} }
} }

View File

@@ -1,4 +1,4 @@
import { WebGroupData, groups, selfInfo } from '@/common/data' import { getSelfUin } from '@/common/data'
import { log } from '@/common/utils/log' import { log } from '@/common/utils/log'
import { NTQQUserApi } from './user' import { NTQQUserApi } from './user'
import { RequestUtil } from '@/common/utils/request' import { RequestUtil } from '@/common/utils/request'
@@ -138,114 +138,44 @@ export class WebApi {
} }
static async getGroupMembers(GroupCode: string, cached: boolean = true): Promise<WebApiGroupMember[]> { static async getGroupMembers(GroupCode: string, cached: boolean = true): Promise<WebApiGroupMember[]> {
log('webapi 获取群成员', GroupCode); const memberData: Array<WebApiGroupMember> = new Array<WebApiGroupMember>()
let MemberData: Array<WebApiGroupMember> = new Array<WebApiGroupMember>(); const cookieObject = await NTQQUserApi.getCookies('qun.qq.com')
try { const cookieStr = Object.entries(cookieObject).map(([key, value]) => `${key}=${value}`).join('; ')
let cachedData = WebGroupData.GroupData.get(GroupCode); const retList: Promise<WebApiGroupMemberRet>[] = []
let cachedTime = WebGroupData.GroupTime.get(GroupCode); const params = new URLSearchParams({
st: '0',
if (!cachedTime || Date.now() - cachedTime > 1800 * 1000 || !cached) { end: '40',
const _Pskey = (await NTQQUserApi.getPSkey(['qun.qq.com']))['qun.qq.com']; sort: '1',
const _Skey = await NTQQUserApi.getSkey(); gc: GroupCode,
const CookieValue = 'p_skey=' + _Pskey + '; skey=' + _Skey + '; p_uin=o' + selfInfo.uin; bkn: WebApi.genBkn(cookieObject.skey)
if (!_Skey || !_Pskey) { })
return MemberData; const fastRet = await RequestUtil.HttpGetJson<WebApiGroupMemberRet>(`https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?${params}`, 'POST', '', { 'Cookie': cookieStr })
} if (!fastRet?.count || fastRet?.errcode !== 0 || !fastRet?.mems) {
const Bkn = WebApi.genBkn(_Skey); return []
const retList: Promise<WebApiGroupMemberRet>[] = []; } else {
const fastRet = await RequestUtil.HttpGetJson<WebApiGroupMemberRet>('https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?st=0&end=40&sort=1&gc=' + GroupCode + '&bkn=' + Bkn, 'POST', '', { 'Cookie': CookieValue }); for (const member of fastRet.mems) {
if (!fastRet?.count || fastRet?.errcode !== 0 || !fastRet?.mems) { memberData.push(member)
return [];
} else {
for (const key in fastRet.mems) {
MemberData.push(fastRet.mems[key]);
}
}
//初始化获取PageNum
const PageNum = Math.ceil(fastRet.count / 40);
//遍历批量请求
for (let i = 2; i <= PageNum; i++) {
const ret: Promise<WebApiGroupMemberRet> = RequestUtil.HttpGetJson<WebApiGroupMemberRet>('https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?st=' + (i - 1) * 40 + '&end=' + i * 40 + '&sort=1&gc=' + GroupCode + '&bkn=' + Bkn, 'POST', '', { 'Cookie': CookieValue });
retList.push(ret);
}
//批量等待
for (let i = 1; i <= PageNum; i++) {
const ret = await (retList[i]);
if (!ret?.count || ret?.errcode !== 0 || !ret?.mems) {
continue;
}
for (const key in ret.mems) {
MemberData.push(ret.mems[key]);
}
}
WebGroupData.GroupData.set(GroupCode, MemberData);
WebGroupData.GroupTime.set(GroupCode, Date.now());
} else {
MemberData = cachedData as Array<WebApiGroupMember>;
} }
} catch {
return MemberData;
} }
return MemberData; const pageNum = Math.ceil(fastRet.count / 40)
} //遍历批量请求
// public static async addGroupDigest(groupCode: string, msgSeq: string) { for (let i = 2; i <= pageNum; i++) {
// const url = `https://qun.qq.com/cgi-bin/group_digest/cancel_digest?random=665&X-CROSS-ORIGIN=fetch&group_code=${groupCode}&msg_seq=${msgSeq}&msg_random=444021292`; params.set('st', String((i - 1) * 40))
// const res = await this.request(url); params.set('end', String(i * 40))
// return await res.json(); const ret = RequestUtil.HttpGetJson<WebApiGroupMemberRet>(`https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?${params}`, 'POST', '', { 'Cookie': cookieStr })
// } retList.push(ret)
// public async getGroupDigest(groupCode: string) {
// const url = `https://qun.qq.com/cgi-bin/group_digest/digest_list?random=665&X-CROSS-ORIGIN=fetch&group_code=${groupCode}&page_start=0&page_limit=20`;
// const res = await this.request(url);
// return await res.json();
// }
static async setGroupNotice(GroupCode: string, Content: string = '') {
//https://web.qun.qq.com/cgi-bin/announce/add_qun_notice?bkn=${bkn}
//qid=${群号}&bkn=${bkn}&text=${内容}&pinned=0&type=1&settings={"is_show_edit_card":1,"tip_window_type":1,"confirm_required":1}
const _Pskey = (await NTQQUserApi.getPSkey(['qun.qq.com']))['qun.qq.com'];
const _Skey = await NTQQUserApi.getSkey();
const CookieValue = 'p_skey=' + _Pskey + '; skey=' + _Skey + '; p_uin=o' + selfInfo.uin;
let ret: any = undefined;
//console.log(CookieValue);
if (!_Skey || !_Pskey) {
//获取Cookies失败
return undefined;
} }
const Bkn = WebApi.genBkn(_Skey); //批量等待
const data = 'qid=' + GroupCode + '&bkn=' + Bkn + '&text=' + Content + '&pinned=0&type=1&settings={"is_show_edit_card":1,"tip_window_type":1,"confirm_required":1}'; for (let i = 1; i <= pageNum; i++) {
const url = 'https://web.qun.qq.com/cgi-bin/announce/add_qun_notice?bkn=' + Bkn; const ret = await (retList[i])
try { if (!ret?.count || ret?.errcode !== 0 || !ret?.mems) {
ret = await RequestUtil.HttpGetJson<any>(url, 'GET', '', { 'Cookie': CookieValue }); continue
return ret; }
} catch (e) { for (const member of ret.mems) {
return undefined; memberData.push(member)
}
return undefined;
}
static async getGrouptNotice(GroupCode: string): Promise<undefined | WebApiGroupNoticeRet> {
const _Pskey = (await NTQQUserApi.getPSkey(['qun.qq.com']))['qun.qq.com'];
const _Skey = await NTQQUserApi.getSkey();
const CookieValue = 'p_skey=' + _Pskey + '; skey=' + _Skey + '; p_uin=o' + selfInfo.uin;
let ret: WebApiGroupNoticeRet | undefined = undefined;
//console.log(CookieValue);
if (!_Skey || !_Pskey) {
//获取Cookies失败
return undefined;
}
const Bkn = WebApi.genBkn(_Skey);
const url = 'https://web.qun.qq.com/cgi-bin/announce/get_t_list?bkn=' + Bkn + '&qid=' + GroupCode + '&ft=23&ni=1&n=1&i=1&log_read=1&platform=1&s=-1&n=20';
try {
ret = await RequestUtil.HttpGetJson<WebApiGroupNoticeRet>(url, 'GET', '', { 'Cookie': CookieValue });
if (ret?.ec !== 0) {
return undefined;
} }
return ret;
} catch (e) {
return undefined;
} }
return undefined; return memberData
} }
static genBkn(sKey: string) { static genBkn(sKey: string) {
@@ -267,7 +197,7 @@ export class WebApi {
let res = ''; let res = '';
let resJson; let resJson;
try { try {
res = await RequestUtil.HttpGetText(url, 'GET', '', { 'Cookie': CookieValue }); res = await RequestUtil.HttpGetText(url, 'GET', '', { 'Cookie': cookieStr });
const match = res.match(/window\.__INITIAL_STATE__=(.*?);/); const match = res.match(/window\.__INITIAL_STATE__=(.*?);/);
if (match) { if (match) {
resJson = JSON.parse(match[1].trim()); resJson = JSON.parse(match[1].trim());
@@ -284,7 +214,8 @@ export class WebApi {
} }
let HonorInfo: any = { group_id: groupCode }; let HonorInfo: any = { group_id: groupCode };
const CookieValue = (await NTQQUserApi.getCookies('qun.qq.com')).cookies; const cookieObject = await NTQQUserApi.getCookies('qun.qq.com')
const cookieStr = Object.entries(cookieObject).map(([key, value]) => `${key}=${value}`).join('; ')
if (getType === WebHonorType.TALKACTIVE || getType === WebHonorType.ALL) { if (getType === WebHonorType.TALKACTIVE || getType === WebHonorType.ALL) {
try { try {

View File

@@ -1,4 +1,5 @@
import { callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod } from '../ntcall' import { invoke, NTClass, NTMethod } from '../ntcall'
import { GeneralCallResult } from '../services'
import { ReceiveCmd } from '../hook' import { ReceiveCmd } from '../hook'
import { BrowserWindow } from 'electron' import { BrowserWindow } from 'electron'
@@ -27,12 +28,12 @@ export class NTQQWindowApi {
static async openWindow<R = GeneralCallResult>( static async openWindow<R = GeneralCallResult>(
ntQQWindow: NTQQWindow, ntQQWindow: NTQQWindow,
args: any[], args: any[],
cbCmd: ReceiveCmd | null = null, cbCmd: ReceiveCmd | undefined,
autoCloseSeconds: number = 2, autoCloseSeconds: number = 2,
) { ) {
const result = await callNTQQApi<R>({ const result = await invoke<R>({
className: NTQQApiClass.WINDOW_API, className: NTClass.WINDOW_API,
methodName: NTQQApiMethod.OPEN_EXTRA_WINDOW, methodName: NTMethod.OPEN_EXTRA_WINDOW,
cbCmd, cbCmd,
afterFirstCmd: false, afterFirstCmd: false,
args: [ntQQWindow.windowName, ...args], args: [ntQQWindow.windowName, ...args],

View File

@@ -21,7 +21,7 @@ import { log } from '../common/utils/log'
import { defaultVideoThumb, getVideoInfo } from '../common/utils/video' import { defaultVideoThumb, getVideoInfo } from '../common/utils/video'
import { encodeSilk } from '../common/utils/audio' import { encodeSilk } from '../common/utils/audio'
import { isNull } from '../common/utils' import { isNull } from '../common/utils'
import faceConfig from './face_config.json'; import faceConfig from './helper/face_config.json'
export const mFaceCache = new Map<string, string>() // emojiId -> faceName export const mFaceCache = new Map<string, string>() // emojiId -> faceName
@@ -44,12 +44,12 @@ export class SendMsgElementConstructor {
} }
} }
static at(atUid: string, atNtUid: string, atType: AtType, atName: string): SendTextElement { static at(atUid: string, atNtUid: string, atType: AtType, display: string): SendTextElement {
return { return {
elementType: ElementType.TEXT, elementType: ElementType.TEXT,
elementId: '', elementId: '',
textElement: { textElement: {
content: `@${atName}`, content: display,
atType, atType,
atUid, atUid,
atTinyId: '', atTinyId: '',
@@ -77,7 +77,7 @@ export class SendMsgElementConstructor {
throw '文件异常大小为0' throw '文件异常大小为0'
} }
const maxMB = 30; const maxMB = 30;
if (fileSize > 1024 * 1024 * 30){ if (fileSize > 1024 * 1024 * 30) {
throw `图片过大,最大支持${maxMB}MB当前文件大小${fileSize}B` throw `图片过大,最大支持${maxMB}MB当前文件大小${fileSize}B`
} }
const imageSize = await NTQQFileApi.getImageSize(picPath) const imageSize = await NTQQFileApi.getImageSize(picPath)
@@ -104,21 +104,21 @@ export class SendMsgElementConstructor {
} }
} }
static async file(filePath: string, fileName: string = ''): Promise<SendFileElement> { static async file(filePath: string, fileName: string = '', folderId: string = ''): Promise<SendFileElement> {
const { md5, fileName: _fileName, path, fileSize } = await NTQQFileApi.uploadFile(filePath, ElementType.FILE) const { fileName: _fileName, path, fileSize } = await NTQQFileApi.uploadFile(filePath, ElementType.FILE)
if (fileSize === 0) { if (fileSize === 0) {
throw '文件异常大小为0' throw '文件异常,大小为 0'
} }
let element: SendFileElement = { const element: SendFileElement = {
elementType: ElementType.FILE, elementType: ElementType.FILE,
elementId: '', elementId: '',
fileElement: { fileElement: {
fileName: fileName || _fileName, fileName: fileName || _fileName,
filePath: path, folderId: folderId,
filePath: path!,
fileSize: fileSize.toString(), fileSize: fileSize.toString(),
}, },
} }
return element return element
} }
@@ -175,7 +175,6 @@ export class SendMsgElementConstructor {
setTimeout(useDefaultThumb, 5000) setTimeout(useDefaultThumb, 5000)
ffmpeg(filePath) ffmpeg(filePath)
.on('end', () => {})
.on('error', (err) => { .on('error', (err) => {
if (diyThumbPath) { if (diyThumbPath) {
fs.copyFile(diyThumbPath, thumbPath) fs.copyFile(diyThumbPath, thumbPath)
@@ -280,10 +279,10 @@ export class SendMsgElementConstructor {
faceId = parseInt(faceId.toString()) faceId = parseInt(faceId.toString())
// let faceType = parseInt(faceId.toString().substring(0, 1)); // let faceType = parseInt(faceId.toString().substring(0, 1));
let faceType = 1 let faceType = 1
if (faceId >= 222){ if (faceId >= 222) {
faceType = 2 faceType = 2
} }
if (face?.AniStickerType){ if (face?.AniStickerType) {
faceType = 3; faceType = 3;
} }
return { return {

View File

@@ -1,5 +1,3 @@
//远端rkey获取
import { log } from '@/common/utils' import { log } from '@/common/utils'
interface ServerRkeyData { interface ServerRkeyData {

View File

@@ -1,29 +1,29 @@
import type { BrowserWindow } from 'electron' import type { BrowserWindow } from 'electron'
import { NTQQApiClass, NTQQApiMethod } from './ntcall' import { NTClass, NTMethod } from './ntcall'
import { NTQQMsgApi, sendMessagePool } from './api/msg' import { NTQQMsgApi, NTQQFriendApi } from './api'
import { CategoryFriend, ChatType, Group, GroupMember, GroupMemberRole, RawMessage, User } from './types' import {
CategoryFriend,
ChatType,
GroupMember,
GroupMemberRole,
RawMessage,
SimpleInfo,
User
} from './types'
import { import {
deleteGroup,
friends,
getFriend,
getGroupMember, getGroupMember,
groups, rawFriends, setSelfInfo
selfInfo,
tempGroupCodeMap,
uidMaps,
} from '@/common/data' } from '@/common/data'
import { OB11GroupDecreaseEvent } from '../onebot11/event/notice/OB11GroupDecreaseEvent'
import { postOb11Event } from '../onebot11/server/post-ob11-event' import { postOb11Event } from '../onebot11/server/post-ob11-event'
import { getConfigUtil, HOOK_LOG } from '@/common/config' import { getConfigUtil } from '@/common/config'
import fs from 'fs' import fs from 'node:fs'
import { dbUtil } from '@/common/db'
import { NTQQGroupApi } from './api/group'
import { log } from '@/common/utils' import { log } from '@/common/utils'
import { randomUUID } from 'node:crypto'
import { MessageUnique } from '../common/utils/MessageUnique'
import { isNumeric, sleep } from '@/common/utils' import { isNumeric, sleep } from '@/common/utils'
import { OB11Constructor } from '../onebot11/constructor' import { OB11Constructor } from '../onebot11/constructor'
import { OB11GroupCardEvent } from '../onebot11/event/notice/OB11GroupCardEvent' import { OB11GroupCardEvent } from '../onebot11/event/notice/OB11GroupCardEvent'
import { OB11GroupAdminNoticeEvent } from '../onebot11/event/notice/OB11GroupAdminNoticeEvent' import { OB11GroupAdminNoticeEvent } from '../onebot11/event/notice/OB11GroupAdminNoticeEvent'
import { randomUUID } from 'node:crypto'
export let hookApiCallbacks: Record<string, (apiReturn: any) => void> = {} export let hookApiCallbacks: Record<string, (apiReturn: any) => void> = {}
@@ -48,83 +48,74 @@ export let ReceiveCmdS = {
CACHE_SCAN_FINISH: 'nodeIKernelStorageCleanListener/onFinishScan', CACHE_SCAN_FINISH: 'nodeIKernelStorageCleanListener/onFinishScan',
MEDIA_UPLOAD_COMPLETE: 'nodeIKernelMsgListener/onRichMediaUploadComplete', MEDIA_UPLOAD_COMPLETE: 'nodeIKernelMsgListener/onRichMediaUploadComplete',
SKEY_UPDATE: 'onSkeyUpdate', SKEY_UPDATE: 'onSkeyUpdate',
} } as const
export type ReceiveCmd = (typeof ReceiveCmdS)[keyof typeof ReceiveCmdS] export type ReceiveCmd = string
interface NTQQApiReturnData<PayloadType = unknown> extends Array<any> { interface NTQQApiReturnData<Payload = unknown> extends Array<any> {
0: { 0: {
type: 'request' type: 'request'
eventName: NTQQApiClass eventName: NTClass
callbackId?: string callbackId?: string
} }
1: { 1: {
cmdName: ReceiveCmd cmdName: ReceiveCmd
cmdType: 'event' cmdType: 'event'
payload: PayloadType payload: Payload
}[] }[]
} }
let receiveHooks: Array<{ const logHook = false
const receiveHooks: Array<{
method: ReceiveCmd[] method: ReceiveCmd[]
hookFunc: (payload: any) => void | Promise<void> hookFunc: (payload: any) => void | Promise<void>
id: string id: string
}> = [] }> = []
let callHooks: Array<{ const callHooks: Array<{
method: NTQQApiMethod[] method: NTMethod[]
hookFunc: (callParams: unknown[]) => void | Promise<void> hookFunc: (callParams: unknown[]) => void | Promise<void>
}> = [] }> = []
export function hookNTQQApiReceive(window: BrowserWindow) { export function hookNTQQApiReceive(window: BrowserWindow) {
const originalSend = window.webContents.send const originalSend = window.webContents.send
const patchSend = (channel: string, ...args: NTQQApiReturnData) => { const patchSend = (channel: string, ...args: NTQQApiReturnData) => {
// console.log("hookNTQQApiReceive", channel, args)
let isLogger = false
try { try {
isLogger = args[0]?.eventName?.startsWith('ns-LoggerApi') const isLogger = args[0]?.eventName?.startsWith('ns-LoggerApi')
} catch (e) { } if (logHook && !isLogger) {
if (!isLogger) { log(`received ntqq api message: ${channel}`, args)
try {
HOOK_LOG && log(`received ntqq api message: ${channel}`, args)
} catch (e) {
log('hook log error', e, args)
} }
} } catch { }
try { if (args?.[1] instanceof Array) {
if (args?.[1] instanceof Array) { for (const receiveData of args?.[1]) {
for (let receiveData of args?.[1]) { const ntQQApiMethodName = receiveData.cmdName
const ntQQApiMethodName = receiveData.cmdName // log(`received ntqq api message: ${channel} ${ntQQApiMethodName}`, JSON.stringify(receiveData))
// log(`received ntqq api message: ${channel} ${ntQQApiMethodName}`, JSON.stringify(receiveData)) for (const hook of receiveHooks) {
for (let hook of receiveHooks) { if (hook.method.includes(ntQQApiMethodName)) {
if (hook.method.includes(ntQQApiMethodName)) { new Promise((resolve, reject) => {
new Promise((resolve, reject) => { try {
try { hook.hookFunc(receiveData.payload)
let _ = hook.hookFunc(receiveData.payload) } catch (e: any) {
if (hook.hookFunc.constructor.name === 'AsyncFunction') { log('hook error', ntQQApiMethodName, e.stack.toString())
; (_ as Promise<void>).then() }
} resolve(undefined)
} catch (e) { }).then()
log('hook error', e, receiveData.payload)
}
}).then()
}
} }
} }
} }
if (args[0]?.callbackId) { }
// log("hookApiCallback", hookApiCallbacks, args) if (args[0]?.callbackId) {
const callbackId = args[0].callbackId // log("hookApiCallback", hookApiCallbacks, args)
if (hookApiCallbacks[callbackId]) { const callbackId = args[0].callbackId
// log("callback found") if (hookApiCallbacks[callbackId]) {
new Promise((resolve, reject) => { // log("callback found")
hookApiCallbacks[callbackId](args[1]) new Promise((resolve, reject) => {
}).then() hookApiCallbacks[callbackId](args[1])
delete hookApiCallbacks[callbackId] resolve(undefined)
} }).then()
delete hookApiCallbacks[callbackId]
} }
} catch (e: any) {
log('hookNTQQApiReceive error', e.stack.toString(), args)
} }
originalSend.call(window.webContents, channel, ...args) originalSend.call(window.webContents, channel, ...args)
} }
@@ -145,23 +136,21 @@ export function hookNTQQApiCall(window: BrowserWindow) {
} catch (e) { } } catch (e) { }
if (!isLogger) { if (!isLogger) {
try { try {
HOOK_LOG && log('call NTQQ api', thisArg, args) logHook && log('call NTQQ api', thisArg, args)
} catch (e) { } } catch (e) { }
try { try {
const _args: unknown[] = args[3][1] const _args: unknown[] = args[3][1]
const cmdName: NTQQApiMethod = _args[0] as NTQQApiMethod const cmdName: NTMethod = _args[0] as NTMethod
const callParams = _args.slice(1) const callParams = _args.slice(1)
callHooks.forEach((hook) => { callHooks.forEach((hook) => {
if (hook.method.includes(cmdName)) { if (hook.method.includes(cmdName)) {
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
try { try {
let _ = hook.hookFunc(callParams) hook.hookFunc(callParams)
if (hook.hookFunc.constructor.name === 'AsyncFunction') { } catch (e: any) {
(_ as Promise<void>).then()
}
} catch (e) {
log('hook call error', e, _args) log('hook call error', e, _args)
} }
resolve(undefined)
}).then() }).then()
} }
}) })
@@ -180,16 +169,16 @@ export function hookNTQQApiCall(window: BrowserWindow) {
const proxyIpcInvoke = new Proxy(ipc_invoke_proxy, { const proxyIpcInvoke = new Proxy(ipc_invoke_proxy, {
apply(target, thisArg, args) { apply(target, thisArg, args) {
// console.log(args); // console.log(args);
HOOK_LOG && log('call NTQQ invoke api', thisArg, args) //HOOK_LOG && log('call NTQQ invoke api', thisArg, args)
args[0]['_replyChannel']['sendReply'] = new Proxy(args[0]['_replyChannel']['sendReply'], { args[0]['_replyChannel']['sendReply'] = new Proxy(args[0]['_replyChannel']['sendReply'], {
apply(sendtarget, sendthisArg, sendargs) { apply(sendtarget, sendthisArg, sendargs) {
sendtarget.apply(sendthisArg, sendargs) sendtarget.apply(sendthisArg, sendargs)
}, },
}) })
let ret = target.apply(thisArg, args) let ret = target.apply(thisArg, args)
try { /*try {
HOOK_LOG && log('call NTQQ invoke api return', ret) HOOK_LOG && log('call NTQQ invoke api return', ret)
} catch (e) { } } catch (e) { }*/
return ret return ret
}, },
}) })
@@ -217,7 +206,7 @@ export function registerReceiveHook<PayloadType>(
} }
export function registerCallHook( export function registerCallHook(
method: NTQQApiMethod | NTQQApiMethod[], method: NTMethod | NTMethod[],
hookFunc: (callParams: unknown[]) => void | Promise<void>, hookFunc: (callParams: unknown[]) => void | Promise<void>,
): void { ): void {
if (!Array.isArray(method)) { if (!Array.isArray(method)) {
@@ -234,126 +223,7 @@ export function removeReceiveHook(id: string) {
receiveHooks.splice(index, 1) receiveHooks.splice(index, 1)
} }
let activatedGroups: string[] = []
async function updateGroups(_groups: Group[], needUpdate: boolean = true) {
for (let group of _groups) {
log('update group', group)
if (group.privilegeFlag === 0) {
deleteGroup(group.groupCode)
continue
}
log('update group', group)
// if (!activatedGroups.includes(group.groupCode)) {
NTQQMsgApi.activateChat({ peerUid: group.groupCode, chatType: ChatType.group })
.then((r) => {
// activatedGroups.push(group.groupCode);
// log(`激活群聊天窗口${group.groupName}(${group.groupCode})`, r)
// if (r.result !== 0) {
// setTimeout(() => NTQQMsgApi.activateGroupChat(group.groupCode).then(r => log(`再次激活群聊天窗口${group.groupName}(${group.groupCode})`, r)), 500);
// }else {
// }
})
.catch(log)
// }
let existGroup = groups.find((g) => g.groupCode == group.groupCode)
if (existGroup) {
Object.assign(existGroup, group)
} else {
groups.push(group)
existGroup = group
}
if (needUpdate) {
const members = await NTQQGroupApi.getGroupMembers(group.groupCode)
if (members) {
existGroup.members = members
}
}
}
}
async function processGroupEvent(payload: { groupList: Group[] }) {
try {
const newGroupList = payload.groupList
for (const group of newGroupList) {
let existGroup = groups.find((g) => g.groupCode == group.groupCode)
if (existGroup) {
if (existGroup.memberCount > group.memberCount) {
log(`群(${group.groupCode})成员数量减少${existGroup.memberCount} -> ${group.memberCount}`)
const oldMembers = existGroup.members
await sleep(200) // 如果请求QQ API的速度过快通常无法正确拉取到最新的群信息因此这里人为引入一个延时
const newMembers = await NTQQGroupApi.getGroupMembers(group.groupCode)
group.members = newMembers
const newMembersSet = new Set<string>() // 建立索引降低时间复杂度
for (const member of newMembers) {
newMembersSet.add(member.uin)
}
// 判断bot是否是管理员如果是管理员不需要从这里得知有人退群这里的退群无法得知是主动退群还是被踢
let bot = await getGroupMember(group.groupCode, selfInfo.uin)
if (bot?.role == GroupMemberRole.admin || bot?.role == GroupMemberRole.owner) {
continue
}
for (const member of oldMembers) {
if (!newMembersSet.has(member.uin) && member.uin != selfInfo.uin) {
postOb11Event(
new OB11GroupDecreaseEvent(
parseInt(group.groupCode),
parseInt(member.uin),
parseInt(member.uin),
'leave',
),
)
break
}
}
}
if (group.privilegeFlag === 0) {
deleteGroup(group.groupCode)
}
}
}
updateGroups(newGroupList, false).then()
} catch (e: any) {
updateGroups(payload.groupList).then()
log('更新群信息错误', e.stack.toString())
}
}
export async function startHook() { export async function startHook() {
// 群列表变动
registerReceiveHook<{ groupList: Group[]; updateType: number }>(ReceiveCmdS.GROUPS, (payload) => {
// updateType 3是群列表变动2是群成员变动
// log("群列表变动", payload.updateType, payload.groupList)
if (payload.updateType != 2) {
updateGroups(payload.groupList).then()
}
else {
if (process.platform == 'win32') {
processGroupEvent(payload).then()
}
}
})
registerReceiveHook<{ groupList: Group[]; updateType: number }>(ReceiveCmdS.GROUPS_STORE, (payload) => {
// updateType 3是群列表变动2是群成员变动
// log("群列表变动", payload.updateType, payload.groupList)
if (payload.updateType != 2) {
updateGroups(payload.groupList).then()
}
else {
if (process.platform != 'win32') {
processGroupEvent(payload).then()
}
}
})
registerReceiveHook<{ registerReceiveHook<{
groupCode: string groupCode: string
dataSource: number dataSource: number
@@ -402,41 +272,30 @@ export async function startHook() {
registerReceiveHook<{ registerReceiveHook<{
data: CategoryFriend[] data: CategoryFriend[]
}>(ReceiveCmdS.FRIENDS, (payload) => { }>(ReceiveCmdS.FRIENDS, (payload) => {
rawFriends.length = 0; // log("onBuddyListChange", payload)
rawFriends.push(...payload.data); // let friendListV2: {userSimpleInfos: Map<string, SimpleInfo>} = []
for (const fData of payload.data) { type V2data = { userSimpleInfos: Map<string, SimpleInfo> }
const _friends = fData.buddyList let friendList: User[] = [];
for (let friend of _friends) { if ((payload as any).userSimpleInfos) {
NTQQMsgApi.activateChat({ peerUid: friend.uid, chatType: ChatType.friend }).then() // friendListV2 = payload as any
let existFriend = friends.find((f) => f.uin == friend.uin) friendList = Object.values((payload as unknown as V2data).userSimpleInfos).map((v: SimpleInfo) => {
if (!existFriend) { return {
friends.push(friend) ...v.coreInfo,
}
else {
Object.assign(existFriend, friend)
} }
})
}
else {
for (const fData of payload.data) {
friendList.push(...fData.buddyList)
} }
} }
log('好友列表变动', friendList.length)
for (let friend of friendList) {
NTQQMsgApi.activateChat({ peerUid: friend.uid, chatType: ChatType.friend }).then()
}
}) })
registerReceiveHook<{ msgList: Array<RawMessage> }>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], (payload) => { registerReceiveHook<{ msgList: Array<RawMessage> }>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], (payload) => {
// 保存一下uid
for (const message of payload.msgList) {
const uid = message.senderUid
const uin = message.senderUin
if (uid && uin) {
if (message.chatType === ChatType.temp) {
dbUtil.getReceivedTempUinMap().then((receivedTempUinMap) => {
if (!receivedTempUinMap[uin]) {
receivedTempUinMap[uin] = uid
dbUtil.setReceivedTempUinMap(receivedTempUinMap)
}
})
}
uidMaps[uid] = uin
}
}
// 自动清理新消息文件 // 自动清理新消息文件
const { autoDeleteFile } = getConfigUtil().getConfig() const { autoDeleteFile } = getConfigUtil().getConfig()
if (!autoDeleteFile) { if (!autoDeleteFile) {
@@ -459,10 +318,6 @@ export async function startHook() {
if (msgElement.picElement) { if (msgElement.picElement) {
pathList.push(...Object.values(msgElement.picElement.thumbPath)) pathList.push(...Object.values(msgElement.picElement.thumbPath))
} }
const aioOpGrayTipElement = msgElement.grayTipElement?.aioOpGrayTipElement
if (aioOpGrayTipElement) {
tempGroupCodeMap[aioOpGrayTipElement.peerUid] = aioOpGrayTipElement.fromGrpCodeOfTmpChat
}
// log("需要清理的文件", pathList); // log("需要清理的文件", pathList);
for (const path of pathList) { for (const path of pathList) {
@@ -478,23 +333,18 @@ export async function startHook() {
}) })
registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, ({ msgRecord }) => { registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, ({ msgRecord }) => {
const message = msgRecord const { msgId, chatType, peerUid } = msgRecord
const peerUid = message.peerUid const peer = {
// log("收到自己发送成功的消息", Object.keys(sendMessagePool), message); chatType,
// log("收到自己发送成功的消息", message.msgId, message.msgSeq); peerUid
dbUtil.addMsg(message).then()
const sendCallback = sendMessagePool[peerUid]
if (sendCallback) {
try {
sendCallback(message)
} catch (e: any) {
log('receive self msg error', e.stack)
}
} }
MessageUnique.createMsg(peer, msgId)
}) })
registerReceiveHook<{ info: { status: number } }>(ReceiveCmdS.SELF_STATUS, (info) => { registerReceiveHook<{ info: { status: number } }>(ReceiveCmdS.SELF_STATUS, (info) => {
selfInfo.online = info.info.status !== 20 setSelfInfo({
online: info.info.status !== 20
})
}) })
let activatedPeerUids: string[] = [] let activatedPeerUids: string[] = []
@@ -532,18 +382,15 @@ export async function startHook() {
} }
}) })
registerCallHook(NTQQApiMethod.DELETE_ACTIVE_CHAT, async (payload) => { registerCallHook(NTMethod.DELETE_ACTIVE_CHAT, async (payload) => {
const peerUid = payload[0] as string const peerUid = payload[0] as string
log('激活的聊天窗口被删除,准备重新激活', peerUid) log('激活的聊天窗口被删除,准备重新激活', peerUid)
let chatType = ChatType.friend let chatType = ChatType.friend
if (isNumeric(peerUid)) { if (isNumeric(peerUid)) {
chatType = ChatType.group chatType = ChatType.group
} }
else { else if (!(await NTQQFriendApi.isBuddy(peerUid))) {
// 检查是否好友 chatType = ChatType.temp
if (!(await getFriend(peerUid))) {
chatType = ChatType.temp
}
} }
const peer = { peerUid, chatType } const peer = { peerUid, chatType }
await sleep(1000) await sleep(1000)

View File

@@ -0,0 +1,240 @@
import { Group, GroupListUpdateType, GroupMember, GroupNotify } from '@/ntqqapi/types'
interface IGroupListener {
onGroupListUpdate(updateType: GroupListUpdateType, groupList: Group[]): void
onGroupExtListUpdate(...args: unknown[]): void
onGroupSingleScreenNotifies(doubt: boolean, seq: string, notifies: GroupNotify[]): void
onGroupNotifiesUpdated(dboubt: boolean, notifies: GroupNotify[]): void
onGroupNotifiesUnreadCountUpdated(...args: unknown[]): void
onGroupDetailInfoChange(...args: unknown[]): void
onGroupAllInfoChange(...args: unknown[]): void
onGroupsMsgMaskResult(...args: unknown[]): void
onGroupConfMemberChange(...args: unknown[]): void
onGroupBulletinChange(...args: unknown[]): void
onGetGroupBulletinListResult(...args: unknown[]): void
onMemberListChange(arg: {
sceneId: string,
ids: string[],
infos: Map<string, GroupMember>,
finish: boolean,
hasRobot: boolean
}): void
onMemberInfoChange(groupCode: string, changeType: number, members: Map<string, GroupMember>): void
onSearchMemberChange(...args: unknown[]): void
onGroupBulletinRichMediaDownloadComplete(...args: unknown[]): void
onGroupBulletinRichMediaProgressUpdate(...args: unknown[]): void
onGroupStatisticInfoChange(...args: unknown[]): void
onJoinGroupNotify(...args: unknown[]): void
onShutUpMemberListChanged(...args: unknown[]): void
onGroupBulletinRemindNotify(...args: unknown[]): void
onGroupFirstBulletinNotify(...args: unknown[]): void
onJoinGroupNoVerifyFlag(...args: unknown[]): void
onGroupArkInviteStateResult(...args: unknown[]): void
// 发现于Win 9.9.9 23159
onGroupMemberLevelInfoChange(...args: unknown[]): void
}
export interface NodeIKernelGroupListener extends IGroupListener {
// eslint-disable-next-line @typescript-eslint/no-misused-new
new(listener: IGroupListener): NodeIKernelGroupListener
}
export class GroupListener implements IGroupListener {
// 发现于Win 9.9.9 23159
onGroupMemberLevelInfoChange(...args: unknown[]): void {
}
onGetGroupBulletinListResult(...args: unknown[]) {
}
onGroupAllInfoChange(...args: unknown[]) {
}
onGroupBulletinChange(...args: unknown[]) {
}
onGroupBulletinRemindNotify(...args: unknown[]) {
}
onGroupArkInviteStateResult(...args: unknown[]) {
}
onGroupBulletinRichMediaDownloadComplete(...args: unknown[]) {
}
onGroupConfMemberChange(...args: unknown[]) {
}
onGroupDetailInfoChange(...args: unknown[]) {
}
onGroupExtListUpdate(...args: unknown[]) {
}
onGroupFirstBulletinNotify(...args: unknown[]) {
}
onGroupListUpdate(updateType: GroupListUpdateType, groupList: Group[]) {
}
onGroupNotifiesUpdated(dboubt: boolean, notifies: GroupNotify[]) {
}
onGroupBulletinRichMediaProgressUpdate(...args: unknown[]) {
}
onGroupNotifiesUnreadCountUpdated(...args: unknown[]) {
}
onGroupSingleScreenNotifies(doubt: boolean, seq: string, notifies: GroupNotify[]) {
}
onGroupsMsgMaskResult(...args: unknown[]) {
}
onGroupStatisticInfoChange(...args: unknown[]) {
}
onJoinGroupNotify(...args: unknown[]) {
}
onJoinGroupNoVerifyFlag(...args: unknown[]) {
}
onMemberInfoChange(groupCode: string, changeType: number, members: Map<string, GroupMember>) {
}
onMemberListChange(arg: {
sceneId: string,
ids: string[],
infos: Map<string, GroupMember>, // uid -> GroupMember
finish: boolean,
hasRobot: boolean
}) {
}
onSearchMemberChange(...args: unknown[]) {
}
onShutUpMemberListChanged(...args: unknown[]) {
}
}
export class DebugGroupListener implements IGroupListener {
onGroupMemberLevelInfoChange(...args: unknown[]): void {
console.log('onGroupMemberLevelInfoChange:', ...args)
}
onGetGroupBulletinListResult(...args: unknown[]) {
console.log('onGetGroupBulletinListResult:', ...args)
}
onGroupAllInfoChange(...args: unknown[]) {
console.log('onGroupAllInfoChange:', ...args)
}
onGroupBulletinChange(...args: unknown[]) {
console.log('onGroupBulletinChange:', ...args)
}
onGroupBulletinRemindNotify(...args: unknown[]) {
console.log('onGroupBulletinRemindNotify:', ...args)
}
onGroupArkInviteStateResult(...args: unknown[]) {
console.log('onGroupArkInviteStateResult:', ...args)
}
onGroupBulletinRichMediaDownloadComplete(...args: unknown[]) {
console.log('onGroupBulletinRichMediaDownloadComplete:', ...args)
}
onGroupConfMemberChange(...args: unknown[]) {
console.log('onGroupConfMemberChange:', ...args)
}
onGroupDetailInfoChange(...args: unknown[]) {
console.log('onGroupDetailInfoChange:', ...args)
}
onGroupExtListUpdate(...args: unknown[]) {
console.log('onGroupExtListUpdate:', ...args)
}
onGroupFirstBulletinNotify(...args: unknown[]) {
console.log('onGroupFirstBulletinNotify:', ...args)
}
onGroupListUpdate(...args: unknown[]) {
console.log('onGroupListUpdate:', ...args)
}
onGroupNotifiesUpdated(...args: unknown[]) {
console.log('onGroupNotifiesUpdated:', ...args)
}
onGroupBulletinRichMediaProgressUpdate(...args: unknown[]) {
console.log('onGroupBulletinRichMediaProgressUpdate:', ...args)
}
onGroupNotifiesUnreadCountUpdated(...args: unknown[]) {
console.log('onGroupNotifiesUnreadCountUpdated:', ...args)
}
onGroupSingleScreenNotifies(doubt: boolean, seq: string, notifies: GroupNotify[]) {
console.log('onGroupSingleScreenNotifies:')
}
onGroupsMsgMaskResult(...args: unknown[]) {
console.log('onGroupsMsgMaskResult:', ...args)
}
onGroupStatisticInfoChange(...args: unknown[]) {
console.log('onGroupStatisticInfoChange:', ...args)
}
onJoinGroupNotify(...args: unknown[]) {
console.log('onJoinGroupNotify:', ...args)
}
onJoinGroupNoVerifyFlag(...args: unknown[]) {
console.log('onJoinGroupNoVerifyFlag:', ...args)
}
onMemberInfoChange(groupCode: string, changeType: number, members: Map<string, GroupMember>) {
console.log('onMemberInfoChange:', groupCode, changeType, members)
}
onMemberListChange(...args: unknown[]) {
console.log('onMemberListChange:', ...args)
}
onSearchMemberChange(...args: unknown[]) {
console.log('onSearchMemberChange:', ...args)
}
onShutUpMemberListChanged(...args: unknown[]) {
console.log('onShutUpMemberListChanged:', ...args)
}
}

View File

@@ -0,0 +1,514 @@
import { ChatType, RawMessage } from '@/ntqqapi/types'
export interface OnRichMediaDownloadCompleteParams {
fileModelId: string,
msgElementId: string,
msgId: string,
fileId: string,
fileProgress: string, // '0'
fileSpeed: string, // '0'
fileErrCode: string, // '0'
fileErrMsg: string,
fileDownType: number, // 暂时未知
thumbSize: number,
filePath: string,
totalSize: string,
trasferStatus: number,
step: number,
commonFileInfo: unknown | null,
fileSrvErrCode: string,
clientMsg: string,
businessId: number,
userTotalSpacePerDay: unknown | null,
userUsedSpacePerDay: unknown | null
}
export interface onGroupFileInfoUpdateParamType {
retCode: number
retMsg: string
clientWording: string
isEnd: boolean
item: Array<any>
allFileCount: string
nextIndex: string
reqId: string
}
// {
// sessionType: 1,
// chatType: 100,
// peerUid: 'u_PVQ3tl6K78xxxx',
// groupCode: '809079648',
// 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 IKernelMsgListener {
onAddSendMsg(msgRecord: RawMessage): void
onBroadcastHelperDownloadComplete(broadcastHelperTransNotifyInfo: unknown): void
onBroadcastHelperProgressUpdate(broadcastHelperTransNotifyInfo: unknown): void
onChannelFreqLimitInfoUpdate(contact: unknown, z: unknown, freqLimitInfo: unknown): void
onContactUnreadCntUpdate(hashMap: unknown): void
onCustomWithdrawConfigUpdate(customWithdrawConfig: unknown): void
onDraftUpdate(contact: unknown, arrayList: unknown, j2: unknown): void
onEmojiDownloadComplete(emojiNotifyInfo: unknown): void
onEmojiResourceUpdate(emojiResourceInfo: unknown): void
onFeedEventUpdate(firstViewDirectMsgNotifyInfo: unknown): void
onFileMsgCome(arrayList: unknown): void
onFirstViewDirectMsgUpdate(firstViewDirectMsgNotifyInfo: unknown): void
onFirstViewGroupGuildMapping(arrayList: unknown): void
onGrabPasswordRedBag(i2: unknown, str: unknown, i3: unknown, recvdOrder: unknown, msgRecord: unknown): void
onGroupFileInfoAdd(groupItem: unknown): void
onGroupFileInfoUpdate(groupFileListResult: onGroupFileInfoUpdateParamType): void
onGroupGuildUpdate(groupGuildNotifyInfo: unknown): void
onGroupTransferInfoAdd(groupItem: unknown): void
onGroupTransferInfoUpdate(groupFileListResult: unknown): void
onGuildInteractiveUpdate(guildInteractiveNotificationItem: unknown): void
onGuildMsgAbFlagChanged(guildMsgAbFlag: unknown): void
onGuildNotificationAbstractUpdate(guildNotificationAbstractInfo: unknown): void
onHitCsRelatedEmojiResult(downloadRelateEmojiResultInfo: unknown): void
onHitEmojiKeywordResult(hitRelatedEmojiWordsResult: unknown): void
onHitRelatedEmojiResult(relatedWordEmojiInfo: unknown): void
onImportOldDbProgressUpdate(importOldDbMsgNotifyInfo: unknown): void
onInputStatusPush(inputStatusInfo: unknown): void
onKickedOffLine(kickedInfo: unknown): void
onLineDev(arrayList: unknown): void
onLogLevelChanged(j2: unknown): void
onMsgAbstractUpdate(arrayList: unknown): void
onMsgBoxChanged(arrayList: unknown): void
onMsgDelete(contact: unknown, arrayList: unknown): void
onMsgEventListUpdate(hashMap: unknown): void
onMsgInfoListAdd(arrayList: unknown): void
onMsgInfoListUpdate(msgList: RawMessage[]): void
onMsgQRCodeStatusChanged(i2: unknown): void
onMsgRecall(i2: unknown, str: unknown, j2: unknown): void
onMsgSecurityNotify(msgRecord: unknown): void
onMsgSettingUpdate(msgSetting: unknown): void
onNtFirstViewMsgSyncEnd(): void
onNtMsgSyncEnd(): void
onNtMsgSyncStart(): void
onReadFeedEventUpdate(firstViewDirectMsgNotifyInfo: unknown): void
onRecvGroupGuildFlag(i2: unknown): void
onRecvMsg(...arrayList: unknown[]): void
onRecvMsgSvrRspTransInfo(j2: unknown, contact: unknown, i2: unknown, i3: unknown, str: unknown, bArr: unknown): void
onRecvOnlineFileMsg(arrayList: unknown): void
onRecvS2CMsg(arrayList: unknown): void
onRecvSysMsg(arrayList: unknown): void
onRecvUDCFlag(i2: unknown): void
onRichMediaDownloadComplete(fileTransNotifyInfo: OnRichMediaDownloadCompleteParams): void
onRichMediaProgerssUpdate(fileTransNotifyInfo: unknown): void
onRichMediaUploadComplete(fileTransNotifyInfo: 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
onSysMsgNotification(i2: unknown, j2: unknown, j3: unknown, arrayList: unknown): void
onTempChatInfoUpdate(tempChatInfo: TempOnRecvParams): void
onUnreadCntAfterFirstView(hashMap: unknown): void
onUnreadCntUpdate(hashMap: unknown): void
onUserChannelTabStatusChanged(z: unknown): void
onUserOnlineStatusChanged(z: unknown): void
onUserTabStatusChanged(arrayList: unknown): void
onlineStatusBigIconDownloadPush(i2: unknown, j2: unknown, str: unknown): void
onlineStatusSmallIconDownloadPush(i2: unknown, j2: unknown, str: unknown): void
// 第一次发现于Linux
onUserSecQualityChanged(...args: unknown[]): void
onMsgWithRichLinkInfoUpdate(...args: unknown[]): void
onRedTouchChanged(...args: unknown[]): void
// 第一次发现于Win 9.9.9 23159
onBroadcastHelperProgerssUpdate(...args: unknown[]): void
}
export interface NodeIKernelMsgListener extends IKernelMsgListener {
// eslint-disable-next-line @typescript-eslint/no-misused-new
new(listener: IKernelMsgListener): NodeIKernelMsgListener
}
export class MsgListener implements IKernelMsgListener {
onAddSendMsg(msgRecord: RawMessage) {
}
onBroadcastHelperDownloadComplete(broadcastHelperTransNotifyInfo: unknown) {
}
onBroadcastHelperProgressUpdate(broadcastHelperTransNotifyInfo: unknown) {
}
onChannelFreqLimitInfoUpdate(contact: unknown, z: unknown, freqLimitInfo: unknown) {
}
onContactUnreadCntUpdate(hashMap: unknown) {
}
onCustomWithdrawConfigUpdate(customWithdrawConfig: unknown) {
}
onDraftUpdate(contact: unknown, arrayList: unknown, j2: unknown) {
}
onEmojiDownloadComplete(emojiNotifyInfo: unknown) {
}
onEmojiResourceUpdate(emojiResourceInfo: unknown) {
}
onFeedEventUpdate(firstViewDirectMsgNotifyInfo: unknown) {
}
onFileMsgCome(arrayList: unknown) {
}
onFirstViewDirectMsgUpdate(firstViewDirectMsgNotifyInfo: unknown) {
}
onFirstViewGroupGuildMapping(arrayList: unknown) {
}
onGrabPasswordRedBag(i2: unknown, str: unknown, i3: unknown, recvdOrder: unknown, msgRecord: unknown) {
}
onGroupFileInfoAdd(groupItem: unknown) {
}
onGroupFileInfoUpdate(groupFileListResult: onGroupFileInfoUpdateParamType) {
}
onGroupGuildUpdate(groupGuildNotifyInfo: unknown) {
}
onGroupTransferInfoAdd(groupItem: unknown) {
}
onGroupTransferInfoUpdate(groupFileListResult: unknown) {
}
onGuildInteractiveUpdate(guildInteractiveNotificationItem: unknown) {
}
onGuildMsgAbFlagChanged(guildMsgAbFlag: unknown) {
}
onGuildNotificationAbstractUpdate(guildNotificationAbstractInfo: unknown) {
}
onHitCsRelatedEmojiResult(downloadRelateEmojiResultInfo: unknown) {
}
onHitEmojiKeywordResult(hitRelatedEmojiWordsResult: unknown) {
}
onHitRelatedEmojiResult(relatedWordEmojiInfo: unknown) {
}
onImportOldDbProgressUpdate(importOldDbMsgNotifyInfo: unknown) {
}
onInputStatusPush(inputStatusInfo: unknown) {
}
onKickedOffLine(kickedInfo: unknown) {
}
onLineDev(arrayList: unknown) {
}
onLogLevelChanged(j2: unknown) {
}
onMsgAbstractUpdate(arrayList: unknown) {
}
onMsgBoxChanged(arrayList: unknown) {
}
onMsgDelete(contact: unknown, arrayList: unknown) {
}
onMsgEventListUpdate(hashMap: unknown) {
}
onMsgInfoListAdd(arrayList: unknown) {
}
onMsgInfoListUpdate(msgList: RawMessage[]) {
}
onMsgQRCodeStatusChanged(i2: unknown) {
}
onMsgRecall(i2: unknown, str: unknown, j2: unknown) {
}
onMsgSecurityNotify(msgRecord: unknown) {
}
onMsgSettingUpdate(msgSetting: unknown) {
}
onNtFirstViewMsgSyncEnd() {
}
onNtMsgSyncEnd() {
}
onNtMsgSyncStart() {
}
onReadFeedEventUpdate(firstViewDirectMsgNotifyInfo: unknown) {
}
onRecvGroupGuildFlag(i2: unknown) {
}
onRecvMsg(arrayList: RawMessage[]) {
}
onRecvMsgSvrRspTransInfo(j2: unknown, contact: unknown, i2: unknown, i3: unknown, str: unknown, bArr: unknown) {
}
onRecvOnlineFileMsg(arrayList: unknown) {
}
onRecvS2CMsg(arrayList: unknown) {
}
onRecvSysMsg(arrayList: unknown) {
}
onRecvUDCFlag(i2: unknown) {
}
onRichMediaDownloadComplete(fileTransNotifyInfo: OnRichMediaDownloadCompleteParams) {
}
onRichMediaProgerssUpdate(fileTransNotifyInfo: unknown) {
}
onRichMediaUploadComplete(fileTransNotifyInfo: unknown) {
}
onSearchGroupFileInfoUpdate(searchGroupFileResult: unknown) {
}
onSendMsgError(j2: unknown, contact: unknown, i2: unknown, str: unknown) {
}
onSysMsgNotification(i2: unknown, j2: unknown, j3: unknown, arrayList: unknown) {
}
onTempChatInfoUpdate(tempChatInfo: TempOnRecvParams) {
}
onUnreadCntAfterFirstView(hashMap: unknown) {
}
onUnreadCntUpdate(hashMap: unknown) {
}
onUserChannelTabStatusChanged(z: unknown) {
}
onUserOnlineStatusChanged(z: unknown) {
}
onUserTabStatusChanged(arrayList: unknown) {
}
onlineStatusBigIconDownloadPush(i2: unknown, j2: unknown, str: unknown) {
}
onlineStatusSmallIconDownloadPush(i2: unknown, j2: unknown, str: unknown) {
}
// 第一次发现于Linux
onUserSecQualityChanged(...args: unknown[]) {
}
onMsgWithRichLinkInfoUpdate(...args: unknown[]) {
}
onRedTouchChanged(...args: unknown[]) {
}
// 第一次发现于Win 9.9.9-23159
onBroadcastHelperProgerssUpdate(...args: unknown[]) {
}
}

View File

@@ -1 +1,3 @@
export * from './NodeIKernelProfileListener' export * from './NodeIKernelProfileListener'
export * from './NodeIKernelGroupListener'
export * from './NodeIKernelMsgListener'

View File

@@ -1,19 +0,0 @@
import * as os from "os";
import path from "node:path";
import fs from "fs";
export function getModuleWithArchName(moduleName: string) {
const systemPlatform = os.platform()
const cpuArch = os.arch()
return `${moduleName}-${systemPlatform}-${cpuArch}.node`
}
export function cpModule(moduleName: string) {
const currentDir = path.resolve(__dirname);
const fileName = `./${getModuleWithArchName(moduleName)}`
try {
fs.copyFileSync(path.join(currentDir, fileName), path.join(currentDir, `${moduleName}.node`));
} catch (e) {
}
}

View File

@@ -1,58 +0,0 @@
import { log } from '../../../common/utils'
import { NTQQApi } from '../../ntcall'
import { cpModule } from '../cpmodule'
type PokeHandler = (id: string, isGroup: boolean) => void
type CrychicHandler = (event: string, id: string, isGroup: boolean) => void
let pokeRecords: Record<string, number> = {}
class Crychic {
private crychic: any = undefined
loadNode() {
if (!this.crychic) {
try {
cpModule('crychic')
this.crychic = require('./crychic.node')
this.crychic.init()
} catch (e) {
log('crychic加载失败', e)
}
}
}
registerPokeHandler(fn: PokeHandler) {
this.registerHandler((event, id, isGroup) => {
if (event === 'poke') {
let existTime = pokeRecords[id]
if (existTime) {
if (Date.now() - existTime < 1500) {
return
}
}
pokeRecords[id] = Date.now()
fn(id, isGroup)
}
})
}
registerHandler(fn: CrychicHandler) {
if (!this.crychic) return
this.crychic.setCryHandler(fn)
}
sendFriendPoke(friendUid: string) {
if (!this.crychic) return
this.crychic.sendFriendPoke(parseInt(friendUid))
NTQQApi.fetchUnitedCommendConfig().then()
}
sendGroupPoke(groupCode: string, memberUin: string) {
if (!this.crychic) return
this.crychic.sendGroupPoke(parseInt(memberUin), parseInt(groupCode))
NTQQApi.fetchUnitedCommendConfig().then()
}
}
export const crychic = new Crychic()

View File

@@ -1,33 +0,0 @@
import {cpModule} from "../cpmodule";
import { qqPkgInfo } from '@/common/utils/QQBasicInfo'
interface MoeHook {
GetRkey: () => string, // Return '&rkey=xxx'
HookRkey: (version: string) => string
}
class HookApi {
private readonly moeHook: MoeHook | null = null;
constructor() {
cpModule('MoeHoo');
try {
this.moeHook = require('./MoeHoo.node');
console.log("hook rkey qq version", this.moeHook!.HookRkey(qqPkgInfo.version));
console.log("hook rkey地址", this.moeHook!.HookRkey(qqPkgInfo.version));
} catch (e) {
console.log('加载 moehoo 失败', e);
}
}
getRKey(): string {
return this.moeHook?.GetRkey() || '';
}
isAvailable() {
return !!this.moeHook;
}
}
// export const hookApi = new HookApi();

View File

@@ -1,10 +1,10 @@
import { ipcMain } from 'electron' import { ipcMain } from 'electron'
import { hookApiCallbacks, ReceiveCmd, ReceiveCmdS, registerReceiveHook, removeReceiveHook } from './hook' import { hookApiCallbacks, registerReceiveHook, removeReceiveHook } from './hook'
import { log } from '../common/utils/log' import { log } from '../common/utils/log'
import { HOOK_LOG } from '../common/config'
import { randomUUID } from 'node:crypto' import { randomUUID } from 'node:crypto'
import { GeneralCallResult } from './services'
export enum NTQQApiClass { export enum NTClass {
NT_API = 'ns-ntApi', NT_API = 'ns-ntApi',
FS_API = 'ns-FsApi', FS_API = 'ns-FsApi',
OS_API = 'ns-OsApi', OS_API = 'ns-OsApi',
@@ -15,10 +15,10 @@ export enum NTQQApiClass {
SKEY_API = 'ns-SkeyApi', SKEY_API = 'ns-SkeyApi',
GROUP_HOME_WORK = 'ns-GroupHomeWork', GROUP_HOME_WORK = 'ns-GroupHomeWork',
GROUP_ESSENCE = 'ns-GroupEssence', GROUP_ESSENCE = 'ns-GroupEssence',
NODE_STORE_API = 'ns-NodeStoreApi'
} }
export enum NTQQApiMethod { export enum NTMethod {
TEST = 'NodeIKernelTipOffService/getPskey',
RECENT_CONTACT = 'nodeIKernelRecentContactService/fetchAndSubscribeABatchOfRecentContact', RECENT_CONTACT = 'nodeIKernelRecentContactService/fetchAndSubscribeABatchOfRecentContact',
ACTIVE_CHAT_PREVIEW = 'nodeIKernelMsgService/getAioFirstViewLatestMsgsAndAddActiveChat', // 激活聊天窗口,有时候必须这样才能收到消息, 并返回最新预览消息 ACTIVE_CHAT_PREVIEW = 'nodeIKernelMsgService/getAioFirstViewLatestMsgsAndAddActiveChat', // 激活聊天窗口,有时候必须这样才能收到消息, 并返回最新预览消息
ACTIVE_CHAT_HISTORY = 'nodeIKernelMsgService/getMsgsIncludeSelfAndAddActiveChat', // 激活聊天窗口,有时候必须这样才能收到消息, 并返回历史消息 ACTIVE_CHAT_HISTORY = 'nodeIKernelMsgService/getMsgsIncludeSelfAndAddActiveChat', // 激活聊天窗口,有时候必须这样才能收到消息, 并返回历史消息
@@ -57,7 +57,6 @@ export enum NTQQApiMethod {
HANDLE_GROUP_REQUEST = 'nodeIKernelGroupService/operateSysNotify', HANDLE_GROUP_REQUEST = 'nodeIKernelGroupService/operateSysNotify',
QUIT_GROUP = 'nodeIKernelGroupService/quitGroup', QUIT_GROUP = 'nodeIKernelGroupService/quitGroup',
GROUP_AT_ALL_REMAIN_COUNT = 'nodeIKernelGroupService/getGroupRemainAtTimes', GROUP_AT_ALL_REMAIN_COUNT = 'nodeIKernelGroupService/getGroupRemainAtTimes',
// READ_FRIEND_REQUEST = "nodeIKernelBuddyListener/onDoubtBuddyReqUnreadNumChange"
HANDLE_FRIEND_REQUEST = 'nodeIKernelBuddyService/approvalFriendRequest', HANDLE_FRIEND_REQUEST = 'nodeIKernelBuddyService/approvalFriendRequest',
KICK_MEMBER = 'nodeIKernelGroupService/kickMember', KICK_MEMBER = 'nodeIKernelGroupService/kickMember',
MUTE_MEMBER = 'nodeIKernelGroupService/setMemberShutUp', MUTE_MEMBER = 'nodeIKernelGroupService/setMemberShutUp',
@@ -87,59 +86,41 @@ export enum NTQQApiMethod {
OPEN_EXTRA_WINDOW = 'openExternalWindow', OPEN_EXTRA_WINDOW = 'openExternalWindow',
SET_QQ_AVATAR = 'nodeIKernelProfileService/setHeader', SET_QQ_AVATAR = 'nodeIKernelProfileService/setHeader',
GET_PSKEY = 'nodeIKernelTipOffService/getPskey',
UPDATE_SKEY = 'updatePskey',
FETCH_UNITED_COMMEND_CONFIG = 'nodeIKernelUnitedConfigService/fetchUnitedCommendConfig', // 发包需要调用的
} }
enum NTQQApiChannel { export enum NTChannel {
IPC_UP_2 = 'IPC_UP_2', IPC_UP_2 = 'IPC_UP_2',
IPC_UP_3 = 'IPC_UP_3', IPC_UP_3 = 'IPC_UP_3',
IPC_UP_1 = 'IPC_UP_1', IPC_UP_1 = 'IPC_UP_1',
} }
interface NTQQApiParams { interface InvokeParams<ReturnType> {
methodName: NTQQApiMethod | string methodName: string
className?: NTQQApiClass className?: NTClass
channel?: NTQQApiChannel channel?: NTChannel
classNameIsRegister?: boolean classNameIsRegister?: boolean
args?: unknown[] args?: unknown[]
cbCmd?: ReceiveCmd | ReceiveCmd[] | null cbCmd?: string | string[]
cmdCB?: (payload: any) => boolean cmdCB?: (payload: ReturnType) => boolean
afterFirstCmd?: boolean // 是否在methodName调用完之后再去hook cbCmd afterFirstCmd?: boolean // 是否在methodName调用完之后再去hook cbCmd
timeoutSecond?: number timeout?: number
} }
export function callNTQQApi<ReturnType>(params: NTQQApiParams) { export function invoke<ReturnType>(params: InvokeParams<ReturnType>) {
let { const className = params.className ?? NTClass.NT_API
className, const channel = params.channel ?? NTChannel.IPC_UP_2
methodName, const timeout = params.timeout ?? 5000
channel, const afterFirstCmd = params.afterFirstCmd ?? true
args,
cbCmd,
timeoutSecond: timeout,
classNameIsRegister,
cmdCB,
afterFirstCmd,
} = params
className = className ?? NTQQApiClass.NT_API
channel = channel ?? NTQQApiChannel.IPC_UP_2
args = args ?? []
timeout = timeout ?? 5
afterFirstCmd = afterFirstCmd ?? true
const uuid = randomUUID() const uuid = randomUUID()
HOOK_LOG && log('callNTQQApi', channel, className, methodName, args, uuid) let eventName = className + '-' + channel[channel.length - 1]
if (params.classNameIsRegister) {
eventName += '-register'
}
const apiArgs = [params.methodName, ...(params.args ?? [])]
//log('callNTQQApi', channel, eventName, apiArgs, uuid)
return new Promise((resolve: (data: ReturnType) => void, reject) => { return new Promise((resolve: (data: ReturnType) => void, reject) => {
// log("callNTQQApiPromise", channel, className, methodName, args, uuid)
const _timeout = timeout * 1000
let success = false let success = false
let eventName = className + '-' + channel[channel.length - 1] if (!params.cbCmd) {
if (classNameIsRegister) {
eventName += '-register'
}
const apiArgs = [methodName, ...args]
if (!cbCmd) {
// QQ后端会返回结果并且可以根据uuid识别 // QQ后端会返回结果并且可以根据uuid识别
hookApiCallbacks[uuid] = (r: ReturnType) => { hookApiCallbacks[uuid] = (r: ReturnType) => {
success = true success = true
@@ -149,10 +130,10 @@ export function callNTQQApi<ReturnType>(params: NTQQApiParams) {
else { else {
// 这里的callback比较特殊QQ后端先返回是否调用成功再返回一条结果数据 // 这里的callback比较特殊QQ后端先返回是否调用成功再返回一条结果数据
const secondCallback = () => { const secondCallback = () => {
const hookId = registerReceiveHook<ReturnType>(cbCmd, (payload) => { const hookId = registerReceiveHook<ReturnType>(params.cbCmd!, (payload) => {
// log(methodName, "second callback", cbCmd, payload, cmdCB); // log(methodName, "second callback", cbCmd, payload, cmdCB);
if (!!cmdCB) { if (!!params.cmdCB) {
if (cmdCB(payload)) { if (params.cmdCB(payload)) {
removeReceiveHook(hookId) removeReceiveHook(hookId)
success = true success = true
resolve(payload) resolve(payload)
@@ -167,23 +148,22 @@ export function callNTQQApi<ReturnType>(params: NTQQApiParams) {
} }
!afterFirstCmd && secondCallback() !afterFirstCmd && secondCallback()
hookApiCallbacks[uuid] = (result: GeneralCallResult) => { hookApiCallbacks[uuid] = (result: GeneralCallResult) => {
log(`${methodName} callback`, result) if (result?.result === 0 || result === undefined) {
if (result?.result == 0 || result === undefined) { log(`${params.methodName} callback`, result)
afterFirstCmd && secondCallback() afterFirstCmd && secondCallback()
} }
else { else {
success = true log('ntqq api call failed', result)
reject(`ntqq api call failed, ${result.errMsg}`) reject(`ntqq api call failed, ${result.errMsg}`)
} }
} }
} }
setTimeout(() => { setTimeout(() => {
// log("ntqq api timeout", success, channel, className, methodName)
if (!success) { if (!success) {
log(`ntqq api timeout ${channel}, ${eventName}, ${methodName}`, apiArgs) log(`ntqq api timeout ${channel}, ${eventName}, ${params.methodName}`, apiArgs)
reject(`ntqq api timeout ${channel}, ${eventName}, ${methodName}, ${apiArgs}`) reject(`ntqq api timeout ${channel}, ${eventName}, ${params.methodName}, ${apiArgs}`)
} }
}, _timeout) }, timeout)
ipcMain.emit( ipcMain.emit(
channel, channel,
@@ -198,29 +178,3 @@ export function callNTQQApi<ReturnType>(params: NTQQApiParams) {
) )
}) })
} }
export interface GeneralCallResult {
result: number // 0: success
errMsg: string
}
export class NTQQApi {
static async call(className: NTQQApiClass, cmdName: string, args: any[]) {
return await callNTQQApi<GeneralCallResult>({
className,
methodName: cmdName,
args: [...args],
})
}
static async fetchUnitedCommendConfig() {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.FETCH_UNITED_COMMEND_CONFIG,
args: [
{
groups: ['100243'],
},
],
})
}
}

View File

@@ -0,0 +1,249 @@
import { NodeIKernelGroupListener } from '@/ntqqapi/listeners'
import {
GroupExtParam,
GroupMember,
GroupMemberRole,
GroupNotifyTypes,
GroupRequestOperateTypes,
} from '@/ntqqapi/types'
import { GeneralCallResult } from './common'
//高版本的接口不应该随意使用 使用应该严格进行pr审核 同时部分ipc中未出现的接口不要过于依赖 应该做好数据兜底
export interface NodeIKernelGroupService {
getMemberCommonInfo(Req: {
groupCode: string,
startUin: string,
identifyFlag: string,
uinList: string[],
memberCommonFilter: {
memberUin: number,
uinFlag: number,
uinFlagExt: number,
uinMobileFlag: number,
shutUpTime: number,
privilege: number,
},
memberNum: number,
filterMethod: string,
onlineFlag: string,
realSpecialTitleFlag: number
}): Promise<unknown>
//26702
getGroupMemberLevelInfo(groupCode: string): Promise<unknown>
//26702
getGroupHonorList(groupCodes: Array<string>): unknown
getUinByUids(uins: string[]): Promise<{
errCode: number,
errMsg: string,
uins: Map<string, string>
}>
getUidByUins(uins: string[]): Promise<{
errCode: number,
errMsg: string,
uids: Map<string, string>
}>
//26702(其实更早 但是我不知道)
checkGroupMemberCache(arrayList: Array<string>): Promise<unknown>
//26702(其实更早 但是我不知道)
getGroupLatestEssenceList(groupCode: string): Promise<unknown>
//26702(其实更早 但是我不知道)
shareDigest(Req: {
appId: string,
appType: number,
msgStyle: number,
recvUin: string,
sendType: number,
clientInfo: {
platform: number
},
richMsg: {
usingArk: boolean,
title: string,
summary: string,
url: string,
pictureUrl: string,
brief: string
}
}): Promise<unknown>
//26702(其实更早 但是我不知道)
isEssenceMsg(Req: { groupCode: string, msgRandom: number, msgSeq: number }): Promise<unknown>
//26702(其实更早 但是我不知道)
queryCachedEssenceMsg(Req: { groupCode: string, msgRandom: number, msgSeq: number }): Promise<unknown>
//26702(其实更早 但是我不知道)
fetchGroupEssenceList(Req: { groupCode: string, pageStart: number, pageLimit: number }, Arg: unknown): Promise<unknown>
//26702
getAllMemberList(groupCode: string, forceFetch: boolean): Promise<{
errCode: number,
errMsg: string,
result: {
ids: Array<{
uid: string,
index: number//0
}>,
infos: {},
finish: true,
hasRobot: false
}
}>
setHeader(uid: string, path: string): unknown
addKernelGroupListener(listener: NodeIKernelGroupListener): number
removeKernelGroupListener(listenerId: unknown): void
createMemberListScene(groupCode: string, scene: string): string
destroyMemberListScene(SceneId: string): void
//About Arg (a) name: lastId 根据手Q来看为object {index:?(number),uid:string}
getNextMemberList(sceneId: string, a: undefined, num: number): Promise<{
errCode: number, errMsg: string,
result: { ids: string[], infos: Map<string, GroupMember>, finish: boolean, hasRobot: boolean }
}>
getPrevMemberList(): unknown
monitorMemberList(): unknown
searchMember(sceneId: string, keywords: string[]): unknown
getMemberInfo(group_id: string, uids: string[], forceFetch: boolean): Promise<GeneralCallResult>
//getMemberInfo [ '56729xxxx', [ 'u_4Nj08cwW5Hxxxxx' ], true ]
kickMember(groupCode: string, memberUids: string[], refuseForever: boolean, kickReason: string): Promise<void>
modifyMemberRole(groupCode: string, uid: string, role: GroupMemberRole): void
modifyMemberCardName(groupCode: string, uid: string, cardName: string): void
getTransferableMemberInfo(groupCode: string): unknown//获取整个群的
transferGroup(uid: string): void
getGroupList(force: boolean): Promise<GeneralCallResult>
getGroupExtList(force: boolean): Promise<GeneralCallResult>
getGroupDetailInfo(groupCode: string): unknown
getMemberExtInfo(param: GroupExtParam): Promise<unknown>//req
getGroupAllInfo(): unknown
getDiscussExistInfo(): unknown
getGroupConfMember(): unknown
getGroupMsgMask(): unknown
getGroupPortrait(): void
modifyGroupName(groupCode: string, groupName: string, arg: false): void
modifyGroupRemark(groupCode: string, remark: string): void
modifyGroupDetailInfo(groupCode: string, arg: unknown): void
setGroupMsgMask(groupCode: string, arg: unknown): void
changeGroupShieldSettingTemp(groupCode: string, arg: unknown): void
inviteToGroup(arg: unknown): void
inviteMembersToGroup(args: unknown[]): void
inviteMembersToGroupWithMsg(args: unknown): void
createGroup(arg: unknown): void
createGroupWithMembers(arg: unknown): void
quitGroup(groupCode: string): void
destroyGroup(groupCode: string): void
//获取单屏群通知列表
getSingleScreenNotifies(force: boolean, start_seq: string, num: number): Promise<GeneralCallResult>
clearGroupNotifies(groupCode: string): void
getGroupNotifiesUnreadCount(unknown: Boolean): Promise<GeneralCallResult>
clearGroupNotifiesUnreadCount(groupCode: string): void
operateSysNotify(
doubt: boolean,
operateMsg: {
operateType: GroupRequestOperateTypes, // 2 拒绝
targetMsg: {
seq: string, // 通知序列号
type: GroupNotifyTypes,
groupCode: string,
postscript: string
}
}): Promise<void>
setTop(groupCode: string, isTop: boolean): void
getGroupBulletin(groupCode: string): unknown
deleteGroupBulletin(groupCode: string, seq: string): void
publishGroupBulletin(groupCode: string, pskey: string, data: any): Promise<GeneralCallResult>
publishInstructionForNewcomers(groupCode: string, arg: unknown): void
uploadGroupBulletinPic(groupCode: string, pskey: string, imagePath: string): Promise<GeneralCallResult & {
errCode: number
picInfo?: {
id: string,
width: number,
height: number
}
}>
downloadGroupBulletinRichMedia(groupCode: string): unknown
getGroupBulletinList(groupCode: string): unknown
getGroupStatisticInfo(groupCode: string): unknown
getGroupRemainAtTimes(groupCode: string): number
getJoinGroupNoVerifyFlag(groupCode: string): unknown
getGroupArkInviteState(groupCode: string): unknown
reqToJoinGroup(groupCode: string, arg: unknown): void
setGroupShutUp(groupCode: string, shutUp: boolean): void
getGroupShutUpMemberList(groupCode: string): unknown[]
setMemberShutUp(groupCode: string, memberTimes: { uid: string, timeStamp: number }[]): Promise<void>
getGroupRecommendContactArkJson(groupCode: string): unknown
getJoinGroupLink(groupCode: string): unknown
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

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

View File

@@ -0,0 +1,744 @@
import { ElementType, MessageElement, Peer, RawMessage, SendMessageElement } from '@/ntqqapi/types'
import { NodeIKernelMsgListener } from '@/ntqqapi/listeners/NodeIKernelMsgListener'
import { GeneralCallResult } from './common'
export interface QueryMsgsParams {
chatInfo: Peer,
filterMsgType: [],
filterSendersUid: string[],
filterMsgFromTime: string,
filterMsgToTime: string,
pageLimit: number,
isReverseOrder: boolean,
isIncludeCurrent: boolean
}
export interface TmpChatInfoApi {
errMsg: string
result: number
tmpChatInfo?: TmpChatInfo
}
export interface TmpChatInfo {
chatType: number
fromNick: string
groupCode: string
peerUid: string
sessionType: number
sig: string
}
export interface NodeIKernelMsgService {
generateMsgUniqueId(chatType: number, time: string): string
addKernelMsgListener(nodeIKernelMsgListener: NodeIKernelMsgListener): number
sendMsg(msgId: string, peer: Peer, msgElements: SendMessageElement[], map: Map<any, any>): Promise<GeneralCallResult>
recallMsg(peer: Peer, msgIds: string[]): Promise<GeneralCallResult>
addKernelMsgImportToolListener(arg: Object): unknown
removeKernelMsgListener(args: unknown): unknown
addKernelTempChatSigListener(...args: unknown[]): unknown
removeKernelTempChatSigListener(...args: unknown[]): unknown
setAutoReplyTextList(AutoReplyText: Array<unknown>, i2: number): unknown
getAutoReplyTextList(...args: unknown[]): unknown
getOnLineDev(): void
kickOffLine(DevInfo: Object): unknown
setStatus(args: { status: number, extStatus: number, batteryStatus: number }): Promise<GeneralCallResult>
fetchStatusMgrInfo(): unknown
fetchStatusUnitedConfigInfo(): unknown
getOnlineStatusSmallIconBasePath(): unknown
getOnlineStatusSmallIconFileNameByUrl(Url: string): unknown
downloadOnlineStatusSmallIconByUrl(arg0: number, arg1: string): unknown
getOnlineStatusBigIconBasePath(): unknown
downloadOnlineStatusBigIconByUrl(arg0: number, arg1: string): unknown
getOnlineStatusCommonPath(arg: string): unknown
getOnlineStatusCommonFileNameByUrl(Url: string): unknown
downloadOnlineStatusCommonByUrl(arg0: string, arg1: string): unknown
// this.tokenType = i2
// this.apnsToken = bArr
// this.voipToken = bArr2
// this.profileId = str
setToken(arg: Object): unknown
switchForeGround(): unknown
switchBackGround(arg: Object): unknown
//hex
setTokenForMqq(token: string): unknown
switchForeGroundForMqq(...args: unknown[]): unknown
switchBackGroundForMqq(...args: unknown[]): unknown
getMsgSetting(...args: unknown[]): unknown
setMsgSetting(...args: unknown[]): unknown
addSendMsg(...args: unknown[]): unknown
cancelSendMsg(...args: unknown[]): unknown
switchToOfflineSendMsg(peer: Peer, MsgId: string): unknown
reqToOfflineSendMsg(...args: unknown[]): unknown
refuseReceiveOnlineFileMsg(peer: Peer, MsgId: string): unknown
resendMsg(...args: unknown[]): unknown
recallMsg(...args: unknown[]): unknown
reeditRecallMsg(...args: unknown[]): unknown
//调用请检查除开commentElements其余参数不能为null
forwardMsg(msgIds: string[], srcContact: Peer, dstContacts: Peer[], commentElements: MessageElement[]): Promise<GeneralCallResult>
forwardMsgWithComment(...args: unknown[]): unknown
forwardSubMsgWithComment(...args: unknown[]): unknown
forwardRichMsgInVist(...args: unknown[]): unknown
forwardFile(...args: unknown[]): unknown
//Array<Msg>, Peer from, Peer to
multiForwardMsg(...args: unknown[]): unknown
multiForwardMsgWithComment(...args: unknown[]): unknown
deleteRecallMsg(...args: unknown[]): unknown
deleteRecallMsgForLocal(...args: unknown[]): unknown
addLocalGrayTipMsg(...args: unknown[]): unknown
addLocalJsonGrayTipMsg(...args: unknown[]): unknown
addLocalJsonGrayTipMsgExt(...args: unknown[]): unknown
IsLocalJsonTipValid(...args: unknown[]): unknown
addLocalAVRecordMsg(...args: unknown[]): unknown
addLocalTofuRecordMsg(...args: unknown[]): unknown
addLocalRecordMsg(Peer: Peer, msgId: string, ele: MessageElement, attr: Array<any> | number, front: boolean): Promise<unknown>
deleteMsg(Peer: Peer, msgIds: Array<string>): Promise<any>
updateElementExtBufForUI(...args: unknown[]): unknown
updateMsgRecordExtPbBufForUI(...args: unknown[]): unknown
startMsgSync(...args: unknown[]): unknown
startGuildMsgSync(...args: unknown[]): unknown
isGuildChannelSync(...args: unknown[]): unknown
getMsgUniqueId(UniqueId: string): string
isMsgMatched(...args: unknown[]): unknown
getOnlineFileMsgs(...args: unknown[]): unknown
getAllOnlineFileMsgs(...args: unknown[]): unknown
getLatestDbMsgs(peer: Peer, cnt: number): Promise<unknown>
getLastMessageList(peer: Peer[]): Promise<unknown>
getAioFirstViewLatestMsgs(peer: Peer, num: number): unknown
//deprecated 从9.9.15-26702版本开始该接口已经废弃请使用getMsgsEx
getMsgs(peer: Peer, msgId: string, count: unknown, queryOrder: boolean): Promise<unknown>
getMsgsIncludeSelf(peer: Peer, msgId: string, count: number, queryOrder: boolean): Promise<GeneralCallResult & {
msgList: RawMessage[]
}>
// this.$peer = contact
// this.$msgTime = j2
// this.$clientSeq = j3
// this.$cnt = i2
getMsgsWithMsgTimeAndClientSeqForC2C(...args: unknown[]): Promise<GeneralCallResult & { msgList: RawMessage[] }>
getMsgsWithStatus(params: {
peer: Peer
msgId: string
msgTime: unknown
cnt: unknown
queryOrder: boolean
isIncludeSelf: boolean
appid: unknown
}): Promise<GeneralCallResult & { msgList: RawMessage[] }>
getMsgsBySeqRange(peer: Peer, startSeq: string, endSeq: string): Promise<GeneralCallResult & { msgList: RawMessage[] }>
getMsgsBySeqAndCount(peer: Peer, seq: string, count: number, desc: boolean, unknownArg: boolean): Promise<GeneralCallResult & { msgList: RawMessage[] }>
getMsgsByMsgId(peer: Peer, ids: string[]): Promise<GeneralCallResult & { msgList: RawMessage[] }>
getRecallMsgsByMsgId(peer: Peer, MsgId: string[]): Promise<unknown>
getMsgsBySeqList(peer: Peer, seqList: string[]): Promise<GeneralCallResult & { msgList: RawMessage[] }>
getSingleMsg(Peer: Peer, msgSeq: string): Promise<GeneralCallResult & { msgList: RawMessage[] }>
getSourceOfReplyMsg(peer: Peer, MsgId: string, SourceSeq: string): unknown
getSourceOfReplyMsgV2(peer: Peer, RootMsgId: string, ReplyMsgId: string): unknown
getMsgByClientSeqAndTime(peer: Peer, clientSeq: string, time: string): unknown
getSourceOfReplyMsgByClientSeqAndTime(peer: Peer, clientSeq: string, time: string): unknown
//cnt clientSeq?并不是吧
getMsgsByTypeFilter(peer: Peer, msgId: string, cnt: unknown, queryOrder: boolean, typeFilter: { type: number, subtype: Array<number> }): 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: QueryMsgsParams): Promise<unknown>
// this.chatType = i2
// this.peerUid = str
// this.chatInfo = new ChatInfo()
// this.filterMsgType = new ArrayList<>()
// this.filterSendersUid = new ArrayList<>()
// this.chatInfo = chatInfo
// this.filterMsgType = arrayList
// this.filterSendersUid = arrayList2
// this.filterMsgFromTime = j2
// this.filterMsgToTime = j3
// this.pageLimit = i2
// this.isReverseOrder = z
// this.isIncludeCurrent = z2
//queryMsgsWithFilterEx(0L, 0L, 0L, new QueryMsgsParams(new ChatInfo(2, str), new ArrayList(), new ArrayList(), 0L, 0L, 250, false, true))
queryMsgsWithFilterEx(msgId: string, msgTime: string, megSeq: string, param: QueryMsgsParams): Promise<GeneralCallResult & {
msgList: RawMessage[]
}>
//queryMsgsWithFilterEx(this.$msgId, this.$msgTime, this.$msgSeq, this.$param)
queryFileMsgsDesktop(...args: unknown[]): unknown
setMsgRichInfoFlag(...args: unknown[]): unknown
queryPicOrVideoMsgs(msgId: string, msgTime: string, megSeq: string, param: QueryMsgsParams): Promise<unknown>
queryPicOrVideoMsgsDesktop(...args: unknown[]): unknown
queryEmoticonMsgs(msgId: string, msgTime: string, msgSeq: string, Params: QueryMsgsParams): Promise<unknown>
queryTroopEmoticonMsgs(msgId: string, msgTime: string, msgSeq: string, Params: QueryMsgsParams): Promise<unknown>
queryMsgsAndAbstractsWithFilter(msgId: string, msgTime: string, megSeq: string, param: QueryMsgsParams): unknown
setFocusOnGuild(...args: unknown[]): unknown
setFocusSession(...args: unknown[]): unknown
enableFilterUnreadInfoNotify(...args: unknown[]): unknown
enableFilterMsgAbstractNotify(...args: unknown[]): unknown
onScenesChangeForSilenceMode(...args: unknown[]): unknown
getContactUnreadCnt(...args: unknown[]): unknown
getUnreadCntInfo(...args: unknown[]): unknown
getGuildUnreadCntInfo(...args: unknown[]): unknown
getGuildUnreadCntTabInfo(...args: unknown[]): unknown
getAllGuildUnreadCntInfo(...args: unknown[]): unknown
getAllJoinGuildCnt(...args: unknown[]): unknown
getAllDirectSessionUnreadCntInfo(...args: unknown[]): unknown
getCategoryUnreadCntInfo(...args: unknown[]): unknown
getGuildFeedsUnreadCntInfo(...args: unknown[]): unknown
setUnVisibleChannelCntInfo(...args: unknown[]): unknown
setUnVisibleChannelTypeCntInfo(...args: unknown[]): unknown
setVisibleGuildCntInfo(...args: unknown[]): unknown
setMsgRead(peer: Peer): Promise<GeneralCallResult>
setAllC2CAndGroupMsgRead(): Promise<unknown>
setGuildMsgRead(...args: unknown[]): unknown
setAllGuildMsgRead(...args: unknown[]): unknown
setMsgReadAndReport(...args: unknown[]): unknown
setSpecificMsgReadAndReport(...args: unknown[]): unknown
setLocalMsgRead(...args: unknown[]): unknown
setGroupGuildMsgRead(...args: unknown[]): unknown
getGuildGroupTransData(...args: unknown[]): unknown
setGroupGuildBubbleRead(...args: unknown[]): unknown
getGuildGroupBubble(...args: unknown[]): unknown
fetchGroupGuildUnread(...args: unknown[]): unknown
setGroupGuildFlag(...args: unknown[]): unknown
setGuildUDCFlag(...args: unknown[]): unknown
setGuildTabUserFlag(...args: unknown[]): unknown
setBuildMode(flag: number/*0 1 3*/): unknown
setConfigurationServiceData(...args: unknown[]): unknown
setMarkUnreadFlag(...args: unknown[]): unknown
getChannelEventFlow(...args: unknown[]): unknown
getMsgEventFlow(...args: unknown[]): unknown
getRichMediaFilePathForMobileQQSend(...args: unknown[]): unknown
getRichMediaFilePathForGuild(arg: {
md5HexStr: string,
fileName: string,
elementType: ElementType,
elementSubType: number,
thumbSize: 0,
needCreate: true,
downloadType: 1,
file_uuid: ''
}): string
assembleMobileQQRichMediaFilePath(...args: unknown[]): unknown
getFileThumbSavePathForSend(...args: unknown[]): unknown
getFileThumbSavePath(...args: unknown[]): unknown
//猜测居多
translatePtt2Text(MsgId: string, Peer: {}, MsgElement: {}): unknown
setPttPlayedState(...args: unknown[]): unknown
// NodeIQQNTWrapperSession fetchFavEmojiList [
// "",
// 48,
// true,
// true
// ]
fetchFavEmojiList(str: string, num: number, uk1: boolean, uk2: boolean): Promise<GeneralCallResult & {
emojiInfoList: Array<{
uin: string,
emoId: number,
emoPath: string,
isExist: boolean,
resId: string,
url: string,
md5: string,
emoOriginalPath: string,
thumbPath: string,
RomaingType: string,
isAPNG: false,
isMarkFace: false,
eId: string,
epId: string,
ocrWord: string,
modifyWord: string,
exposeNum: number,
clickNum: number,
desc: string
}>
}>
addFavEmoji(...args: unknown[]): unknown
fetchMarketEmoticonList(...args: unknown[]): unknown
fetchMarketEmoticonShowImage(...args: unknown[]): unknown
fetchMarketEmoticonAioImage(...args: unknown[]): unknown
fetchMarketEmotionJsonFile(...args: unknown[]): unknown
getMarketEmoticonPath(...args: unknown[]): unknown
getMarketEmoticonPathBySync(...args: unknown[]): unknown
fetchMarketEmoticonFaceImages(...args: unknown[]): unknown
fetchMarketEmoticonAuthDetail(...args: unknown[]): unknown
getFavMarketEmoticonInfo(...args: unknown[]): unknown
addRecentUsedFace(...args: unknown[]): unknown
getRecentUsedFaceList(...args: unknown[]): unknown
getMarketEmoticonEncryptKeys(...args: unknown[]): unknown
downloadEmojiPic(...args: unknown[]): unknown
deleteFavEmoji(...args: unknown[]): unknown
modifyFavEmojiDesc(...args: unknown[]): unknown
queryFavEmojiByDesc(...args: unknown[]): unknown
getHotPicInfoListSearchString(...args: unknown[]): unknown
getHotPicSearchResult(...args: unknown[]): unknown
getHotPicHotWords(...args: unknown[]): unknown
getHotPicJumpInfo(...args: unknown[]): unknown
getEmojiResourcePath(...args: unknown[]): unknown
JoinDragonGroupEmoji(JoinDragonGroupEmojiReq: any/*joinDragonGroupEmojiReq*/): unknown
getMsgAbstracts(...args: unknown[]): unknown
getMsgAbstract(...args: unknown[]): unknown
getMsgAbstractList(...args: unknown[]): unknown
getMsgAbstractListBySeqRange(...args: unknown[]): unknown
refreshMsgAbstracts(...args: unknown[]): unknown
refreshMsgAbstractsByGuildIds(...args: unknown[]): unknown
getRichMediaElement(...args: unknown[]): unknown
cancelGetRichMediaElement(...args: unknown[]): unknown
refuseGetRichMediaElement(...args: unknown[]): unknown
switchToOfflineGetRichMediaElement(...args: unknown[]): unknown
downloadRichMedia(...args: unknown[]): unknown
getFirstUnreadMsgSeq(args: {
peerUid: string
guildId: string
}): unknown
getFirstUnreadCommonMsg(...args: unknown[]): unknown
getFirstUnreadAtmeMsg(...args: unknown[]): unknown
getFirstUnreadAtallMsg(...args: unknown[]): unknown
getNavigateInfo(...args: unknown[]): unknown
getChannelFreqLimitInfo(...args: unknown[]): unknown
getRecentUseEmojiList(...args: unknown[]): unknown
getRecentEmojiList(...args: unknown[]): unknown
setMsgEmojiLikes(...args: unknown[]): unknown
getMsgEmojiLikesList(peer: Peer, msgSeq: string, emojiId: string, emojiType: string, cookie: string, bForward: boolean, number: number): Promise<{
result: number,
errMsg: string,
emojiLikesList:
Array<{
tinyId: string,
nickName: string,
headUrl: string
}>,
cookie: string,
isLastPage: boolean,
isFirstPage: boolean
}>
setMsgEmojiLikesForRole(...args: unknown[]): unknown
clickInlineKeyboardButton(...args: unknown[]): unknown
setCurOnScreenMsg(...args: unknown[]): unknown
setCurOnScreenMsgForMsgEvent(...args: unknown[]): unknown
getMiscData(key: string): unknown
setMiscData(key: string, value: string): unknown
getBookmarkData(...args: unknown[]): unknown
setBookmarkData(...args: unknown[]): unknown
sendShowInputStatusReq(ChatType: number, EventType: number, toUid: string): Promise<unknown>
queryCalendar(...args: unknown[]): unknown
queryFirstMsgSeq(peer: Peer, ...args: unknown[]): unknown
queryRoamCalendar(...args: unknown[]): unknown
queryFirstRoamMsg(...args: unknown[]): unknown
fetchLongMsg(peer: Peer, msgId: string): unknown
fetchLongMsgWithCb(...args: unknown[]): unknown
setIsStopKernelFetchLongMsg(...args: unknown[]): unknown
insertGameResultAsMsgToDb(...args: unknown[]): unknown
getMultiMsg(...args: unknown[]): Promise<GeneralCallResult & {
msgList: RawMessage[]
}>
setDraft(...args: unknown[]): unknown
getDraft(...args: unknown[]): unknown
deleteDraft(...args: unknown[]): unknown
getRecentHiddenSesionList(...args: unknown[]): unknown
setRecentHiddenSession(...args: unknown[]): unknown
delRecentHiddenSession(...args: unknown[]): unknown
getCurHiddenSession(...args: unknown[]): unknown
setCurHiddenSession(...args: unknown[]): unknown
setReplyDraft(...args: unknown[]): unknown
getReplyDraft(...args: unknown[]): unknown
deleteReplyDraft(...args: unknown[]): unknown
getFirstUnreadAtMsg(peer: Peer): unknown
clearMsgRecords(...args: unknown[]): unknown//设置已读后调用我觉得比较好 清理记录 现在别了
IsExistOldDb(...args: unknown[]): unknown
canImportOldDbMsg(...args: unknown[]): unknown
setPowerStatus(z: boolean): unknown
canProcessDataMigration(...args: unknown[]): unknown
importOldDbMsg(...args: unknown[]): unknown
stopImportOldDbMsgAndroid(...args: unknown[]): unknown
isMqqDataImportFinished(...args: unknown[]): unknown
getMqqDataImportTableNames(...args: unknown[]): unknown
getCurChatImportStatusByUin(...args: unknown[]): unknown
getDataImportUserLevel(): unknown
getMsgQRCode(...args: unknown[]): unknown
getGuestMsgAbstracts(...args: unknown[]): unknown
getGuestMsgByRange(...args: unknown[]): unknown
getGuestMsgAbstractByRange(...args: unknown[]): unknown
registerSysMsgNotification(...args: unknown[]): unknown
unregisterSysMsgNotification(...args: unknown[]): unknown
enterOrExitAio(...args: unknown[]): unknown
// this.peerUid = ""
// this.peerNickname = ""
// this.fromGroupCode = ""
// this.sig = new byte[0]
// this.selfUid = ""
// this.selfPhone = ""
// this.chatType = i2
// this.peerUid = str
// this.peerNickname = str2
// this.fromGroupCode = str3
// this.sig = bArr
// this.selfUid = str4
// this.selfPhone = str5
// this.gameSession = tempChatGameSession
prepareTempChat(args: unknown): unknown//主动临时消息 不做
sendSsoCmdReqByContend(cmd: string, param: string): Promise<unknown>
//chattype,uid->Promise<any>
getTempChatInfo(ChatType: number, Uid: string): Promise<TmpChatInfoApi>
setContactLocalTop(...args: unknown[]): unknown
switchAnonymousChat(...args: unknown[]): unknown
renameAnonyChatNick(...args: unknown[]): unknown
getAnonymousInfo(...args: unknown[]): unknown
updateAnonymousInfo(...args: unknown[]): unknown
sendSummonMsg(peer: Peer, MsgElement: unknown, MsgAttributeInfo: unknown): Promise<unknown>//频道的东西
outputGuildUnreadInfo(...args: unknown[]): unknown
checkMsgWithUrl(...args: unknown[]): unknown
checkTabListStatus(...args: unknown[]): unknown
getABatchOfContactMsgBoxInfo(...args: unknown[]): unknown
insertMsgToMsgBox(peer: Peer, msgId: string, arg: 2006): unknown
isHitEmojiKeyword(...args: unknown[]): unknown
getKeyWordRelatedEmoji(...args: unknown[]): unknown
recordEmoji(...args: unknown[]): unknown
fetchGetHitEmotionsByWord(args: Object): Promise<unknown>//表情推荐?
deleteAllRoamMsgs(...args: unknown[]): unknown//漫游消息?
packRedBag(...args: unknown[]): unknown
grabRedBag(...args: unknown[]): unknown
pullDetail(...args: unknown[]): unknown
selectPasswordRedBag(...args: unknown[]): unknown
pullRedBagPasswordList(...args: unknown[]): unknown
requestTianshuAdv(...args: unknown[]): unknown
tianshuReport(...args: unknown[]): unknown
tianshuMultiReport(...args: unknown[]): unknown
GetMsgSubType(a0: number, a1: number): unknown
setIKernelPublicAccountAdapter(...args: unknown[]): unknown
//tempChatGameSession有关
createUidFromTinyId(fromTinyId: string, toTinyId: string): unknown
dataMigrationGetDataAvaiableContactList(...args: unknown[]): unknown
dataMigrationGetMsgList(...args: unknown[]): unknown
dataMigrationStopOperation(...args: unknown[]): unknown
//新的希望
dataMigrationImportMsgPbRecord(DataMigrationMsgInfo: Array<{
extensionData: string//"Hex"
extraData: string //""
chatType: number
chatUin: string
msgType: number
msgTime: string
msgSeq: string
msgRandom: string
}>, DataMigrationResourceInfo: {
extraData: string
filePath: string
fileSize: string
msgRandom: string
msgSeq: string
msgSubType: number
msgType: number
}): unknown
dataMigrationGetResourceLocalDestinyPath(...args: unknown[]): unknown
dataMigrationSetIOSPathPrefix(...args: unknown[]): unknown
getServiceAssistantSwitch(...args: unknown[]): unknown
setServiceAssistantSwitch(...args: unknown[]): unknown
setSubscribeFolderUsingSmallRedPoint(...args: unknown[]): unknown
clearGuildNoticeRedPoint(...args: unknown[]): unknown
clearFeedNoticeRedPoint(...args: unknown[]): unknown
clearFeedSquareRead(...args: unknown[]): unknown
IsC2CStyleChatType(...args: unknown[]): unknown
IsTempChatType(uin: number): unknown//猜的
getGuildInteractiveNotification(...args: unknown[]): unknown
getGuildNotificationAbstract(...args: unknown[]): unknown
setFocusOnBase(...args: unknown[]): unknown
queryArkInfo(...args: unknown[]): unknown
queryUserSecQuality(...args: unknown[]): unknown
getGuildMsgAbFlag(...args: unknown[]): unknown
getGroupMsgStorageTime(): unknown//这是嘛啊
}

View File

@@ -0,0 +1,22 @@
import { BuddyProfileLikeReq } from '../types'
import { GeneralCallResult } from './common'
export interface NodeIKernelProfileLikeService {
addKernelProfileLikeListener(listener: NodeIKernelProfileLikeService): void
removeKernelProfileLikeListener(listener: unknown): void
setBuddyProfileLike(...args: unknown[]): { result: number, errMsg: string, succCounts: number }
getBuddyProfileLike(req: BuddyProfileLikeReq): Promise<GeneralCallResult & {
'info': {
'userLikeInfos': Array<any>,
'friendMaxVotes': number,
'start': number
}
}>
getProfileLikeScidResourceInfo(...args: unknown[]): void
isNull(): boolean
}

View File

@@ -0,0 +1,270 @@
import { GetFileListParam, MessageElement, Peer } from '../types'
import { GeneralCallResult } from './common'
export enum UrlFileDownloadType {
KUNKNOWN,
KURLFILEDOWNLOADPRIVILEGEICON,
KURLFILEDOWNLOADPHOTOWALL,
KURLFILEDOWNLOADQZONE,
KURLFILEDOWNLOADCOMMON,
KURLFILEDOWNLOADINSTALLAPP
}
export enum RMBizTypeEnum {
KUNKNOWN,
KC2CFILE,
KGROUPFILE,
KC2CPIC,
KGROUPPIC,
KDISCPIC,
KC2CVIDEO,
KGROUPVIDEO,
KC2CPTT,
KGROUPPTT,
KFEEDCOMMENTPIC,
KGUILDFILE,
KGUILDPIC,
KGUILDPTT,
KGUILDVIDEO
}
export interface CommonFileInfo {
bizType: number
chatType: number
elemId: string
favId: string
fileModelId: string
fileName: string
fileSize: string
md5: string
md510m: string
msgId: string
msgTime: string
parent: string
peerUid: string
picThumbPath: Array<string>
sha: string
sha3: string
subId: string
uuid: string
}
export interface NodeIKernelRichMediaService {
//getVideoPlayUrl(peer, msgId, elemId, videoCodecFormat, VideoRequestWay.KHAND, cb)
// public enum VideoCodecFormatType {
// KCODECFORMATH264,
// KCODECFORMATH265,
// KCODECFORMATH266,
// KCODECFORMATAV1
// }
// public enum VideoRequestWay {
// KUNKNOW,
// KHAND,
// KAUTO
// }
getVideoPlayUrl(peer: Peer, msgId: string, elemId: string, videoCodecFormat: number, VideoRequestWay: number): Promise<unknown>
//exParams (RMReqExParams)
// this.downSourceType = i2
// this.triggerType = i3
//peer, msgId, elemId, videoCodecFormat, exParams
// 1 0 频道在用
// 1 1
// 0 2
// public static final int KCOMMONREDENVELOPEMSGTYPEINMSGBOX = 1007
// public static final int KDOWNSOURCETYPEAIOINNER = 1
// public static final int KDOWNSOURCETYPEBIGSCREEN = 2
// public static final int KDOWNSOURCETYPEHISTORY = 3
// public static final int KDOWNSOURCETYPEUNKNOWN = 0
// public static final int KTRIGGERTYPEAUTO = 1
// public static final int KTRIGGERTYPEMANUAL = 0
getVideoPlayUrlV2(peer: Peer, msgId: string, elemId: string, videoCodecFormat: number, exParams: { downSourceType: number, triggerType: number }): Promise<GeneralCallResult & {
urlResult: {
v4IpUrl: [],
v6IpUrl: [],
domainUrl: Array<{
url: string,
isHttps: boolean,
httpsDomain: string
}>,
videoCodecFormat: number
}
}>
getRichMediaFileDir(elementType: number, downType: number, isTemp: boolean): unknown
// this.senderUid = ""
// this.peerUid = ""
// this.guildId = ""
// this.elem = new MsgElement()
// this.downloadType = i2
// this.thumbSize = i3
// this.msgId = j2
// this.msgRandom = j3
// this.msgSeq = j4
// this.msgTime = j5
// this.chatType = i4
// this.senderUid = str
// this.peerUid = str2
// this.guildId = str3
// this.elem = msgElement
// this.useHttps = num
getVideoPlayUrlInVisit(arg: {
downloadType: number,
thumbSize: number,
msgId: string,
msgRandom: string,
msgSeq: string,
msgTime: string,
chatType: number,
senderUid: string,
peerUid: string,
guildId: string,
ele: MessageElement,
useHttps: boolean
}): Promise<unknown>
//arg双端number
isFileExpired(arg: number): unknown
deleteGroupFolder(GroupCode: string, FolderId: string): Promise<GeneralCallResult & { groupFileCommonResult: { retCode: number, retMsg: string, clientWording: string } }>
//参数与getVideoPlayUrlInVisit一样
downloadRichMediaInVisit(arg: {
downloadType: number,
thumbSize: number,
msgId: string,
msgRandom: string,
msgSeq: string,
msgTime: string,
chatType: number,
senderUid: string,
peerUid: string,
guildId: string,
ele: MessageElement,
useHttps: boolean
}): unknown
//arg3为“”
downloadFileForModelId(peer: Peer, ModelId: string[], arg3: string): unknown
//第三个参数 Array<Type>
// this.fileId = ""
// this.fileName = ""
// this.fileId = str
// this.fileName = str2
// this.fileSize = j2
// this.fileModelId = j3
downloadFileForFileUuid(peer: Peer, uuid: string, arg3: {
fileId: string,
fileName: string,
fileSize: string,
fileModelId: string
}[]): Promise<unknown>
downloadFileByUrlList(fileDownloadTyp: UrlFileDownloadType, urlList: Array<string>): unknown
downloadFileForFileInfo(fileInfo: CommonFileInfo[], savePath: string): unknown
createGroupFolder(GroupCode: string, FolderName: string): Promise<GeneralCallResult & { resultWithGroupItem: { result: any, groupItem: Array<any> } }>
downloadFile(commonFile: CommonFileInfo, arg2: unknown, arg3: unknown, savePath: string): unknown
createGroupFolder(arg1: unknown, arg2: unknown): unknown
downloadGroupFolder(arg1: unknown, arg2: unknown, arg3: unknown): unknown
renameGroupFolder(arg1: unknown, arg2: unknown, arg3: unknown): unknown
deleteGroupFolder(arg1: unknown, arg2: unknown): unknown
deleteTransferInfo(arg1: unknown, arg2: unknown): unknown
cancelTransferTask(arg1: unknown, arg2: unknown, arg3: unknown): unknown
cancelUrlDownload(arg: unknown): unknown
updateOnlineVideoElemStatus(arg: unknown): unknown
getGroupSpace(arg: unknown): unknown
getGroupFileList(groupCode: string, params: GetFileListParam): Promise<GeneralCallResult & {
groupSpaceResult: {
retCode: number
retMsg: string
clientWording: string
totalSpace: number
usedSpace: number
allUpload: boolean
}
}>
getGroupFileInfo(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 & {
transGroupFileResult: {
result: any
successFileIdList: Array<any>
failFileIdList: Array<any>
}
}>
translateEnWordToZn(words: string[]): Promise<GeneralCallResult & { words: string[] }>
getScreenOCR(path: string): Promise<unknown>
batchGetGroupFileCount(Gids: Array<string>): Promise<GeneralCallResult & { groupCodes: Array<string>, groupFileCounts: Array<number> }>
queryPicDownloadSize(arg: unknown): unknown
searchGroupFile(arg1: unknown, arg2: unknown): unknown
searchMoreGroupFile(arg: unknown): unknown
cancelSearcheGroupFile(arg1: unknown, arg2: unknown, arg3: unknown): unknown
onlyDownloadFile(peer: Peer, arg2: unknown, arg3: Array<{
fileId: string,
fileName: string,
fileSize: string,
fileModelId: string
}
>): unknown
onlyUploadFile(arg1: unknown, arg2: unknown): unknown
isExtraLargePic(arg1: unknown, arg2: unknown, arg3: unknown): unknown
uploadRMFileWithoutMsg(arg: {
bizType: RMBizTypeEnum,
filePath: string,
peerUid: string,
transferId: string
useNTV2: string
}): Promise<unknown>
isNull(): boolean
}

View File

@@ -0,0 +1,128 @@
import { ChatType } from '../types'
export interface NodeIKernelSearchService {
addKernelSearchListener(...args: any[]): unknown// needs 1 arguments
removeKernelSearchListener(...args: any[]): unknown// needs 1 arguments
searchStranger(...args: any[]): unknown// needs 3 arguments
searchGroup(...args: any[]): unknown// needs 1 arguments
searchLocalInfo(keywords: string, unknown: number/*4*/): unknown
cancelSearchLocalInfo(...args: any[]): unknown// needs 3 arguments
searchBuddyChatInfo(...args: any[]): unknown// needs 2 arguments
searchMoreBuddyChatInfo(...args: any[]): unknown// needs 1 arguments
cancelSearchBuddyChatInfo(...args: any[]): unknown// needs 3 arguments
searchContact(...args: any[]): unknown// needs 2 arguments
searchMoreContact(...args: any[]): unknown// needs 1 arguments
cancelSearchContact(...args: any[]): unknown// needs 3 arguments
searchGroupChatInfo(...args: any[]): unknown// needs 3 arguments
resetSearchGroupChatInfoSortType(...args: any[]): unknown// needs 3 arguments
resetSearchGroupChatInfoFilterMembers(...args: any[]): unknown// needs 3 arguments
searchMoreGroupChatInfo(...args: any[]): unknown// needs 1 arguments
cancelSearchGroupChatInfo(...args: any[]): unknown// needs 3 arguments
searchChatsWithKeywords(...args: any[]): unknown// needs 3 arguments
searchMoreChatsWithKeywords(...args: any[]): unknown// needs 1 arguments
cancelSearchChatsWithKeywords(...args: any[]): unknown// needs 3 arguments
searchChatMsgs(...args: any[]): unknown// needs 2 arguments
searchMoreChatMsgs(...args: any[]): unknown// needs 1 arguments
cancelSearchChatMsgs(...args: any[]): unknown// needs 3 arguments
searchMsgWithKeywords(...args: any[]): unknown// needs 2 arguments
searchMoreMsgWithKeywords(...args: any[]): unknown// needs 1 arguments
cancelSearchMsgWithKeywords(...args: any[]): unknown// needs 3 arguments
searchFileWithKeywords(keywords: string[], source: number): Promise<string>// needs 2 arguments
searchMoreFileWithKeywords(...args: any[]): unknown// needs 1 arguments
cancelSearchFileWithKeywords(...args: any[]): unknown// needs 3 arguments
searchAtMeChats(...args: any[]): unknown// needs 3 arguments
searchMoreAtMeChats(...args: any[]): unknown// needs 1 arguments
cancelSearchAtMeChats(...args: any[]): unknown// needs 3 arguments
searchChatAtMeMsgs(...args: any[]): unknown// needs 1 arguments
searchMoreChatAtMeMsgs(...args: any[]): unknown// needs 1 arguments
cancelSearchChatAtMeMsgs(...args: any[]): unknown// needs 3 arguments
addSearchHistory(param: {
type: number,//4
contactList: [],
id: number,//-1
groupInfos: [],
msgs: [],
fileInfos: [
{
chatType: ChatType,
buddyChatInfo: Array<{ category_name: string, peerUid: string, peerUin: string, remark: string }>,
discussChatInfo: [],
groupChatInfo: Array<
{
groupCode: string,
isConf: boolean,
hasModifyConfGroupFace: boolean,
hasModifyConfGroupName: boolean,
groupName: string,
remark: string
}>,
dataLineChatInfo: [],
tmpChatInfo: [],
msgId: string,
msgSeq: string,
msgTime: string,
senderUid: string,
senderNick: string,
senderRemark: string,
senderCard: string,
elemId: string,
elemType: string,//3
fileSize: string,
filePath: string,
fileName: string,
hits: Array<
{
start: 12,
end: 14
}
>
}
]
}): Promise<{
result: number,
errMsg: string,
id?: number
}>
removeSearchHistory(...args: any[]): unknown// needs 1 arguments
searchCache(...args: any[]): unknown// needs 3 arguments
clearSearchCache(...args: any[]): unknown// needs 1 arguments
}

View File

@@ -0,0 +1,11 @@
import { forceFetchClientKeyRetType } from './common'
export interface NodeIKernelTicketService {
addKernelTicketListener(listener: unknown): void
removeKernelTicketListener(listenerId: unknown): void
forceFetchClientKey(arg: string): Promise<forceFetchClientKeyRetType>
isNull(): boolean
}

View File

@@ -0,0 +1,19 @@
import { GeneralCallResult } from './common'
export interface NodeIKernelTipOffService {
addKernelTipOffListener(listener: unknown): void
removeKernelTipOffListener(listenerId: unknown): void
tipOffSendJsData(args: unknown[]): Promise<unknown> //2
getPskey(domainList: string[], nocache: boolean): Promise<GeneralCallResult & { domainPskeyMap: Map<string, string> }> //2
tipOffSendJsData(args: unknown[]): Promise<unknown> //2
tipOffMsgs(args: unknown[]): Promise<unknown> //1
encodeUinAesInfo(args: unknown[]): Promise<unknown> //2
isNull(): boolean
}

View File

@@ -0,0 +1,5 @@
export interface NodeIKernelUixConvertService {
getUin(uid: string[]): Promise<{ uinInfo: Map<string, string> }>
getUid(uin: string[]): Promise<{ uidInfo: Map<string, string> }>
}

View File

@@ -1,2 +1,12 @@
export * from './common'
export * from './NodeIKernelBuddyService' export * from './NodeIKernelBuddyService'
export * from './NodeIKernelProfileService' export * from './NodeIKernelProfileService'
export * from './NodeIKernelGroupService'
export * from './NodeIKernelProfileLikeService'
export * from './NodeIKernelMsgService'
export * from './NodeIKernelMSFService'
export * from './NodeIKernelUixConvertService'
export * from './NodeIKernelRichMediaService'
export * from './NodeIKernelTicketService'
export * from './NodeIKernelTipOffService'
export * from './NodeIKernelSearchService'

View File

@@ -1,5 +1,12 @@
import { QQLevel, Sex } from './user' import { QQLevel, Sex } from './user'
export enum GroupListUpdateType {
REFRESHALL,
GETALL,
MODIFIED,
REMOVE
}
export interface Group { export interface Group {
groupCode: string groupCode: string
maxMember: number maxMember: number
@@ -38,7 +45,7 @@ export enum GroupMemberRole {
} }
export interface GroupMember { export interface GroupMember {
memberSpecialTitle: string memberSpecialTitle?: string
avatarPath: string avatarPath: string
cardName: string cardName: string
cardType: number cardType: number
@@ -53,4 +60,7 @@ export interface GroupMember {
isRobot: boolean isRobot: boolean
sex?: Sex sex?: Sex
qqLevel?: QQLevel qqLevel?: QQLevel
isChangeRole: boolean
joinTime: string
lastSpeakTime: string
} }

View File

@@ -1,6 +1,15 @@
import { GroupMemberRole } from './group' import { GroupMemberRole } from './group'
export interface GetFileListParam {
sortType: number
fileCount: number
startIndex: number
sortOrder: number
showOnlinedocFolder: number
}
export enum ElementType { export enum ElementType {
UNKNOWN = 0,
TEXT = 1, TEXT = 1,
PIC = 2, PIC = 2,
FILE = 3, FILE = 3,
@@ -8,20 +17,36 @@ export enum ElementType {
VIDEO = 5, VIDEO = 5,
FACE = 6, FACE = 6,
REPLY = 7, REPLY = 7,
WALLET = 9,
GreyTip = 8, //Poke别叫戳一搓了 官方名字拍一拍 戳一戳是另一个名字
ARK = 10, ARK = 10,
MFACE = 11, MFACE = 11,
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 SendTextElement { export interface SendTextElement {
elementType: ElementType.TEXT elementType: ElementType.TEXT
elementId: '' elementId: ''
textElement: { textElement: TextElement
content: string
atType: number
atUid: string
atTinyId: string
atNtUid: string
}
} }
export interface SendPttElement { export interface SendPttElement {
@@ -77,12 +102,7 @@ export interface SendPicElement {
export interface SendReplyElement { export interface SendReplyElement {
elementType: ElementType.REPLY elementType: ElementType.REPLY
elementId: '' elementId: ''
replyElement: { replyElement: ReplyElement
replayMsgSeq: string
replayMsgId: string
senderUin: string
senderUinStr: string
}
} }
export interface SendFaceElement { export interface SendFaceElement {
@@ -96,19 +116,35 @@ export interface SendMarketFaceElement {
marketFaceElement: MarketFaceElement marketFaceElement: MarketFaceElement
} }
export interface TextElement {
content: string
atType: number
atUid: string
atTinyId: string
atNtUid: string
}
export interface ReplyElement {
replayMsgSeq: string
replayMsgId: string
senderUin: string
senderUinStr: string
}
export interface FileElement { export interface FileElement {
fileMd5?: '' fileMd5?: string
fileName: string fileName: string
filePath: string filePath: string
fileSize: string fileSize: string
picHeight?: number picHeight?: number
picWidth?: number picWidth?: number
picThumbPath?: {} folderId?: string
file10MMd5?: '' picThumbPath?: Map<number, string>
fileSha?: '' file10MMd5?: string
fileSha3?: '' fileSha?: string
fileUuid?: '' fileSha3?: string
fileSubId?: '' fileUuid?: string
fileSubId?: string
thumbFileSize?: number thumbFileSize?: number
fileBizId?: number fileBizId?: number
} }
@@ -154,6 +190,50 @@ export enum ChatType {
temp = 100, temp = 100,
} }
// 来自Android分析
export enum ChatType2 {
KCHATTYPEADELIE = 42,
KCHATTYPEBUDDYNOTIFY = 5,
KCHATTYPEC2C = 1,
KCHATTYPECIRCLE = 113,
KCHATTYPEDATALINE = 8,
KCHATTYPEDATALINEMQQ = 134,
KCHATTYPEDISC = 3,
KCHATTYPEFAV = 41,
KCHATTYPEGAMEMESSAGE = 105,
KCHATTYPEGAMEMESSAGEFOLDER = 116,
KCHATTYPEGROUP = 2,
KCHATTYPEGROUPBLESS = 133,
KCHATTYPEGROUPGUILD = 9,
KCHATTYPEGROUPHELPER = 7,
KCHATTYPEGROUPNOTIFY = 6,
KCHATTYPEGUILD = 4,
KCHATTYPEGUILDMETA = 16,
KCHATTYPEMATCHFRIEND = 104,
KCHATTYPEMATCHFRIENDFOLDER = 109,
KCHATTYPENEARBY = 106,
KCHATTYPENEARBYASSISTANT = 107,
KCHATTYPENEARBYFOLDER = 110,
KCHATTYPENEARBYHELLOFOLDER = 112,
KCHATTYPENEARBYINTERACT = 108,
KCHATTYPEQQNOTIFY = 132,
KCHATTYPERELATEACCOUNT = 131,
KCHATTYPESERVICEASSISTANT = 118,
KCHATTYPESERVICEASSISTANTSUB = 201,
KCHATTYPESQUAREPUBLIC = 115,
KCHATTYPESUBSCRIBEFOLDER = 30,
KCHATTYPETEMPADDRESSBOOK = 111,
KCHATTYPETEMPBUSSINESSCRM = 102,
KCHATTYPETEMPC2CFROMGROUP = 100,
KCHATTYPETEMPC2CFROMUNKNOWN = 99,
KCHATTYPETEMPFRIENDVERIFY = 101,
KCHATTYPETEMPNEARBYPRO = 119,
KCHATTYPETEMPPUBLICACCOUNT = 103,
KCHATTYPETEMPWPA = 117,
KCHATTYPEUNKNOWN = 0,
KCHATTYPEWEIYUN = 40,
}
export interface PttElement { export interface PttElement {
canConvert2Text: boolean canConvert2Text: boolean
duration: number // 秒数 duration: number // 秒数
@@ -377,19 +457,23 @@ export interface RawMessage {
msgShortId?: number // 自己维护的消息id msgShortId?: number // 自己维护的消息id
msgTime: string // 时间戳,秒 msgTime: string // 时间戳,秒
msgSeq: string msgSeq: string
msgRandom: string
senderUid: string senderUid: string
senderUin?: string // 发送者QQ号 senderUin?: string // 发送者QQ号
peerUid: string // 群号 或者 QQ uid peerUid: string // 群号 或者 QQ uid
peerUin: string // 群号 或者 发送者QQ号 peerUin: string // 群号 或者 发送者QQ号
guildId: string
sendNickName: string sendNickName: string
sendMemberName?: string // 发送者群名片 sendMemberName?: string // 发送者群名片
chatType: ChatType chatType: ChatType
sendStatus?: number // 消息状态别人发的2是已撤回自己发的2是已发送 sendStatus?: number // 消息状态别人发的2是已撤回自己发的2是已发送
recallTime: string // 撤回时间, "0"是没有撤回 recallTime: string // 撤回时间, "0"是没有撤回
records: RawMessage[]
elements: { elements: {
elementId: string elementId: string
elementType: ElementType elementType: ElementType
replyElement: { replyElement: {
sourceMsgIdInRecords: string
senderUid: string // 原消息发送者QQ号 senderUid: string // 原消息发送者QQ号
sourceMsgIsIncPic: boolean // 原消息是否有图片 sourceMsgIsIncPic: boolean // 原消息是否有图片
sourceMsgText: string sourceMsgText: string
@@ -420,3 +504,37 @@ export interface Peer {
peerUid: string // 如果是群聊uid为群号私聊uid就是加密的字符串 peerUid: string // 如果是群聊uid为群号私聊uid就是加密的字符串
guildId?: string guildId?: string
} }
export interface MessageElement {
elementType: ElementType
elementId: string
extBufForUI: string //"0x"
textElement?: TextElement
faceElement?: FaceElement
marketFaceElement?: MarkdownElement
replyElement?: ReplyElement
picElement?: PicElement
pttElement?: PttElement
videoElement?: VideoElement
grayTipElement?: GrayTipElement
arkElement?: ArkElement
fileElement?: FileElement
liveGiftElement?: null
markdownElement?: MarkdownElement
structLongMsgElement?: any
multiForwardMsgElement?: MultiForwardMsgElement
giphyElement?: any
walletElement?: null
inlineKeyboardElement?: InlineKeyboardElement
textGiftElement?: null //????
calendarElement?: any
yoloGameResultElement?: any
avRecordElement?: any
structMsgElement?: null
faceBubbleElement?: any
shareLocationElement?: any
tofuRecordElement?: any
taskTopMsgElement?: any
recommendedMsgElement?: any
actionBarElement?: any
}

View File

@@ -48,8 +48,28 @@ export enum GroupRequestOperateTypes {
reject = 2, reject = 2,
} }
export enum BuddyReqType {
KMEINITIATOR,
KPEERINITIATOR,
KMEAGREED,
KMEAGREEDANDADDED,
KPEERAGREED,
KPEERAGREEDANDADDED,
KPEERREFUSED,
KMEREFUSED,
KMEIGNORED,
KMEAGREEANYONE,
KMESETQUESTION,
KMEAGREEANDADDFAILED,
KMSGINFO,
KMEINITIATORWAITPEERCONFIRM
}
export interface FriendRequest { export interface FriendRequest {
isInitiator?: boolean
isDecide: boolean
friendUid: string friendUid: string
reqType: BuddyReqType
reqTime: string // 时间戳,秒 reqTime: string // 时间戳,秒
extWords: string // 申请人填写的验证消息 extWords: string // 申请人填写的验证消息
isUnread: boolean isUnread: boolean
@@ -64,3 +84,41 @@ export interface FriendRequestNotify {
buddyReqs: FriendRequest[] buddyReqs: FriendRequest[]
} }
} }
export enum MemberExtSourceType {
DEFAULTTYPE = 0,
TITLETYPE = 1,
NEWGROUPTYPE = 2,
}
export interface GroupExtParam {
groupCode: string
seq: string
beginUin: string
dataTime: string
uinList: Array<string>
uinNum: string
groupType: string
richCardNameVer: string
sourceType: MemberExtSourceType
memberExtFilter: {
memberLevelInfoUin: number
memberLevelInfoPoint: number
memberLevelInfoActiveDay: number
memberLevelInfoLevel: number
memberLevelInfoName: number
levelName: number
dataTime: number
userShowFlag: number
sysShowFlag: number
timeToUpdate: number
nickName: number
specialTitle: number
levelNameNew: number
userShowFlagNew: number
msgNeedField: number
cmdUinFlagExt3Grocery: number
memberIcon: number
memberInfoSeq: number
}
}

View File

@@ -78,9 +78,12 @@ export interface Friend extends User {
export interface CategoryFriend { export interface CategoryFriend {
categoryId: number categoryId: number
categorySortId: number
categroyName: string categroyName: string
categroyMbCount: number categroyMbCount: number
buddyList: User[] onlineCount: number
buddyList: User[] // V1
buddyUids: string[]
} }
export interface CoreInfo { export interface CoreInfo {
@@ -259,3 +262,85 @@ export interface UserDetailInfoListenerArg {
commonExt: CommonExt commonExt: CommonExt
photoWall: PhotoWall photoWall: PhotoWall
} }
export interface BuddyProfileLikeReq {
friendUids: string[]
basic: number
vote: number
favorite: number
userProfile: number
type: number
start: number
limit: number
}
export interface UserDetailInfoByUinV2 {
result: number
errMsg: string
detail: {
uid: string
uin: string
simpleInfo: SimpleInfo
commonExt: CommonExt
photoWall: null
}
}
export interface UserDetailInfoByUin {
result: number
errMsg: string
info: {
uid: string //这个没办法用
qid: string
uin: string
nick: string
remark: string
longNick: string
avatarUrl: string
birthday_year: number
birthday_month: number
birthday_day: number
sex: number //0
topTime: string
constellation: number
shengXiao: number
kBloodType: number
homeTown: string
makeFriendCareer: number
pos: string
eMail: string
phoneNum: string
college: string
country: string
province: string
city: string
postCode: string
address: string
isBlock: boolean
isSpecialCareOpen: boolean
isSpecialCareZone: boolean
ringId: string
regTime: number
interest: string
termType: number
labels: any[]
qqLevel: { crownNum: number, sunNum: number, moonNum: number, starNum: number }
isHideQQLevel: number
privilegeIcon: { jumpUrl: string, openIconList: any[], closeIconList: any[] }
isHidePrivilegeIcon: number
photoWall: { picList: any[] }
vipFlag: boolean
yearVipFlag: boolean
svipFlag: boolean
vipLevel: number
status: number
qidianMasterFlag: number
qidianCrewFlag: number
qidianCrewFlag2: number
extStatus: number
recommendImgFlag: number
disableEmojiShortCuts: number
pendantId: string
vipNameColorId: string
}
}

View File

@@ -1,10 +1,32 @@
import { NodeIKernelBuddyService } from './services/NodeIKernelBuddyService' import {
NodeIKernelBuddyService,
NodeIKernelGroupService,
NodeIKernelProfileService,
NodeIKernelProfileLikeService,
NodeIKernelMSFService,
NodeIKernelMsgService,
NodeIKernelUixConvertService,
NodeIKernelRichMediaService,
NodeIKernelTicketService,
NodeIKernelTipOffService,
NodeIKernelSearchService
} from './services'
import os from 'node:os' import os from 'node:os'
const Process = require('node:process') const Process = require('node:process')
export interface NodeIQQNTWrapperSession { export interface NodeIQQNTWrapperSession {
[key: string]: any [key: string]: any
getBuddyService(): NodeIKernelBuddyService getBuddyService(): NodeIKernelBuddyService
getGroupService(): NodeIKernelGroupService
getProfileService(): NodeIKernelProfileService
getProfileLikeService(): NodeIKernelProfileLikeService
getMsgService(): NodeIKernelMsgService
getMSFService(): NodeIKernelMSFService
getUixConvertService(): NodeIKernelUixConvertService
getRichMediaService(): NodeIKernelRichMediaService
getTicketService(): NodeIKernelTicketService
getTipOffService(): NodeIKernelTipOffService
getSearchService(): NodeIKernelSearchService
} }
export interface WrapperApi { export interface WrapperApi {
@@ -28,7 +50,7 @@ export interface WrapperConstructor {
NodeIKernelProfileListener?: any NodeIKernelProfileListener?: any
} }
export const wrapperApi: WrapperApi = {} const wrapperApi: WrapperApi = {}
export const wrapperConstructor: WrapperConstructor = {} export const wrapperConstructor: WrapperConstructor = {}
@@ -66,3 +88,7 @@ Process.dlopen = function (module, filename, flags = os.constants.dlopen.RTLD_LA
} }
return dlopenRet return dlopenRet
} }
export function getSession() {
return wrapperApi['NodeIQQNTWrapperSession']
}

View File

@@ -10,7 +10,7 @@ export class OB11Response {
data: data, data: data,
message: message, message: message,
wording: message, wording: message,
echo: null, echo: undefined,
} }
} }

View File

@@ -1,12 +1,11 @@
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import fs from 'fs/promises' import fsPromise from 'node:fs/promises'
import { dbUtil } from '@/common/db'
import { getConfigUtil } from '@/common/config' import { getConfigUtil } from '@/common/config'
import { checkFileReceived, log, sleep, uri2local } from '@/common/utils' import { NTQQFileApi, NTQQGroupApi, NTQQUserApi, NTQQFriendApi, NTQQMsgApi } from '@/ntqqapi/api'
import { NTQQFileApi } from '@/ntqqapi/api'
import { ActionName } from '../types' import { ActionName } from '../types'
import { FileElement, RawMessage, VideoElement } from '@/ntqqapi/types' import { UUIDConverter } from '@/common/utils/helper'
import { FileCache } from '@/common/types' import { Peer, ChatType, ElementType } from '@/ntqqapi/types'
import { MessageUnique } from '@/common/utils/MessageUnique'
export interface GetFilePayload { export interface GetFilePayload {
file: string // 文件名或者fileUuid file: string // 文件名或者fileUuid
@@ -21,79 +20,60 @@ export interface GetFileResponse {
} }
export abstract class GetFileBase extends BaseAction<GetFilePayload, GetFileResponse> { export abstract class GetFileBase extends BaseAction<GetFilePayload, GetFileResponse> {
private getElement(msg: RawMessage, elementId: string): VideoElement | FileElement { // forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/onebot11/action/file/GetFile.ts#L44
let element = msg.elements.find((e) => e.elementId === elementId)
if (!element) {
throw new Error('element not found')
}
return element.fileElement
}
private async download(cache: FileCache, file: string) {
log('需要调用 NTQQ 下载文件api')
if (cache.msgId) {
let msg = await dbUtil.getMsgByLongId(cache.msgId)
if (msg) {
log('找到了文件 msg', msg)
let element = this.getElement(msg, cache.elementId)
log('找到了文件 element', element)
// 构建下载函数
await NTQQFileApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid, cache.elementId, '', '', true)
// 等待文件下载完成
msg = await dbUtil.getMsgByLongId(cache.msgId)
log('下载完成后的msg', msg)
cache.filePath = this.getElement(msg!, cache.elementId).filePath
await checkFileReceived(cache.filePath, 10 * 1000)
dbUtil.addFileCache(file, cache).then()
}
}
}
protected async _handle(payload: GetFilePayload): Promise<GetFileResponse> { protected async _handle(payload: GetFilePayload): Promise<GetFileResponse> {
let cache = await dbUtil.getFileCache(payload.file) const { enableLocalFile2Url } = getConfigUtil().getConfig()
if (!cache) {
throw new Error('file not found') let fileCache = await MessageUnique.getFileCacheById(String(payload.file))
if (!fileCache?.length) {
fileCache = await MessageUnique.getFileCacheByName(String(payload.file))
} }
const { autoDeleteFile, enableLocalFile2Url, autoDeleteFileSecond } = getConfigUtil().getConfig()
if (cache.downloadFunc) { if (fileCache?.length) {
await cache.downloadFunc() const downloadPath = await NTQQFileApi.downloadMedia(
} fileCache[0].msgId,
try { fileCache[0].chatType,
await fs.access(cache.filePath, fs.constants.F_OK) fileCache[0].peerUid,
} catch (e) { fileCache[0].elementId,
// log("file not found", e) '',
if (cache.url) { ''
const downloadResult = await uri2local(cache.url) )
if (downloadResult.success) { const res: GetFileResponse = {
cache.filePath = downloadResult.path file: downloadPath,
dbUtil.addFileCache(payload.file, cache).then() url: downloadPath,
} else { file_size: fileCache[0].fileSize,
await this.download(cache, payload.file) file_name: fileCache[0].fileName,
}
} else {
// 没有url的可能是私聊文件或者群文件需要自己下载
await this.download(cache, payload.file)
} }
} const peer: Peer = {
let res: GetFileResponse = { chatType: fileCache[0].chatType,
file: cache.filePath, peerUid: fileCache[0].peerUid,
url: cache.url, guildId: ''
file_size: cache.fileSize, }
file_name: cache.fileName, if (fileCache[0].elementType === ElementType.PIC) {
} const msgList = await NTQQMsgApi.getMsgsByMsgId(peer, [fileCache[0].msgId])
if (enableLocalFile2Url) { if (msgList.msgList.length === 0) {
if (!cache.url) { throw new Error('msg not found')
}
const msg = msgList.msgList[0]
const findEle = msg.elements.find(e => e.elementId === fileCache[0].elementId)
if (!findEle) {
throw new Error('element not found')
}
res.url = await NTQQFileApi.getImageUrl(findEle.picElement)
} else if (fileCache[0].elementType === ElementType.VIDEO) {
res.url = await NTQQFileApi.getVideoUrl(peer, fileCache[0].msgId, fileCache[0].elementId)
}
if (enableLocalFile2Url && downloadPath && (res.file === res.url || res.url === undefined)) {
try { try {
res.base64 = await fs.readFile(cache.filePath, 'base64') res.base64 = await fsPromise.readFile(downloadPath, 'base64')
} catch (e) { } catch (e) {
throw new Error('文件下载失败. ' + e) throw new Error('文件下载失败. ' + e)
} }
} }
//不手动删除?文件持久化了
return res
} }
// if (autoDeleteFile) { throw new Error('file not found')
// setTimeout(() => {
// fs.unlink(cache.filePath)
// }, autoDeleteFileSecond * 1000)
// }
return res
} }
} }

View File

@@ -3,4 +3,11 @@ import { ActionName } from '../types'
export default class GetImage extends GetFileBase { export default class GetImage extends GetFileBase {
actionName = ActionName.GetImage actionName = ActionName.GetImage
protected async _handle(payload: { file: string }) {
if (!payload.file) {
throw new Error('参数 file 不能为空')
}
return super._handle(payload)
}
} }

View File

@@ -1,24 +1,27 @@
import BaseAction from '../BaseAction'; import BaseAction from '../BaseAction';
import { ActionName } from '../types'; import { ActionName } from '../types';
import { NTQQGroupApi } from '../../../ntqqapi/api/group' import { NTQQGroupApi } from '@/ntqqapi/api/group'
import { dbUtil } from '@/common/db'; import { MessageUnique } from '@/common/utils/MessageUnique'
interface Payload { interface Payload {
message_id: number | string; message_id: number | string
} }
export default class GoCQHTTPDelEssenceMsg extends BaseAction<Payload, any> { export default class GoCQHTTPDelEssenceMsg extends BaseAction<Payload, any> {
actionName = ActionName.GoCQHTTP_DelEssenceMsg; actionName = ActionName.GoCQHTTP_DelEssenceMsg;
protected async _handle(payload: Payload): Promise<any> { protected async _handle(payload: Payload): Promise<any> {
const msg = await dbUtil.getMsgByShortId(parseInt(payload.message_id.toString())); if (!payload.message_id) {
throw Error('message_id不能为空')
}
const msg = await MessageUnique.getMsgIdAndPeerByShortId(+payload.message_id)
if (!msg) { if (!msg) {
throw new Error('msg not found'); throw new Error('msg not found')
} }
return await NTQQGroupApi.removeGroupEssence( return await NTQQGroupApi.removeGroupEssence(
msg.peerUid, msg.Peer.peerUid,
msg.msgId msg.MsgId,
); )
} }
} }

View File

@@ -0,0 +1,17 @@
import BaseAction from '../BaseAction'
import { ActionName } from '../types'
import { NTQQGroupApi } from '@/ntqqapi/api'
interface Payload {
group_id: string | number
file_id: string
busid?: 102
}
export class GoCQHTTPDelGroupFile extends BaseAction<Payload, void> {
actionName = ActionName.GoCQHTTP_DelGroupFile
async _handle(payload: Payload) {
await NTQQGroupApi.delGroupFile(payload.group_id.toString(), [payload.file_id])
}
}

View File

@@ -1,8 +1,9 @@
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import fs from 'fs' import fs from 'fs'
import { join as joinPath } from 'node:path' import fsPromise from 'fs/promises'
import { calculateFileMD5, httpDownload, TEMP_DIR } from '../../../common/utils' import path from 'node:path'
import { calculateFileMD5, httpDownload, TEMP_DIR } from '@/common/utils'
import { randomUUID } from 'node:crypto' import { randomUUID } from 'node:crypto'
interface Payload { interface Payload {
@@ -22,15 +23,15 @@ export default class GoCQHTTPDownloadFile extends BaseAction<Payload, FileRespon
protected async _handle(payload: Payload): Promise<FileResponse> { protected async _handle(payload: Payload): Promise<FileResponse> {
const isRandomName = !payload.name const isRandomName = !payload.name
let name = payload.name || randomUUID() const name = payload.name ? path.basename(payload.name) : randomUUID()
const filePath = joinPath(TEMP_DIR, name) const filePath = path.join(TEMP_DIR, name)
if (payload.base64) { if (payload.base64) {
fs.writeFileSync(filePath, payload.base64, 'base64') await fsPromise.writeFile(filePath, payload.base64, 'base64')
} else if (payload.url) { } else if (payload.url) {
const headers = this.getHeaders(payload.headers) const headers = this.getHeaders(payload.headers)
let buffer = await httpDownload({ url: payload.url, headers: headers }) const buffer = await httpDownload({ url: payload.url, headers: headers })
fs.writeFileSync(filePath, Buffer.from(buffer), 'binary') await fsPromise.writeFile(filePath, buffer)
} else { } else {
throw new Error('不存在任何文件, 无法下载') throw new Error('不存在任何文件, 无法下载')
} }
@@ -38,8 +39,8 @@ export default class GoCQHTTPDownloadFile extends BaseAction<Payload, FileRespon
if (isRandomName) { if (isRandomName) {
// 默认实现要名称未填写时文件名为文件 md5 // 默认实现要名称未填写时文件名为文件 md5
const md5 = await calculateFileMD5(filePath) const md5 = await calculateFileMD5(filePath)
const newPath = joinPath(TEMP_DIR, md5) const newPath = path.join(TEMP_DIR, md5)
fs.renameSync(filePath, newPath) await fsPromise.rename(filePath, newPath)
return { file: newPath } return { file: newPath }
} }
return { file: filePath } return { file: filePath }

View File

@@ -1,9 +1,9 @@
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { OB11ForwardMessage, OB11Message, OB11MessageData } from '../../types' import { OB11ForwardMessage, OB11Message, OB11MessageData } from '../../types'
import { NTQQMsgApi } from '@/ntqqapi/api' import { NTQQMsgApi } from '@/ntqqapi/api'
import { dbUtil } from '../../../common/db'
import { OB11Constructor } from '../../constructor' import { OB11Constructor } from '../../constructor'
import { ActionName } from '../types' import { ActionName } from '../types'
import { MessageUnique } from '@/common/utils/MessageUnique'
interface Payload { interface Payload {
message_id: string // long msg idgocq message_id: string // long msg idgocq
@@ -14,30 +14,30 @@ interface Response {
messages: (OB11Message & { content: OB11MessageData })[] messages: (OB11Message & { content: OB11MessageData })[]
} }
export class GoCQHTTGetForwardMsgAction extends BaseAction<Payload, any> { export class GoCQHTTGetForwardMsgAction extends BaseAction<Payload, Response> {
actionName = ActionName.GoCQHTTP_GetForwardMsg actionName = ActionName.GoCQHTTP_GetForwardMsg
protected async _handle(payload: Payload): Promise<any> { protected async _handle(payload: Payload): Promise<any> {
const message_id = payload.id || payload.message_id const msgId = payload.id || payload.message_id
if (!message_id) { if (!msgId) {
throw Error('message_id不能为空') throw Error('message_id不能为空')
} }
const rootMsg = await dbUtil.getMsgByLongId(message_id) const rootMsgId = MessageUnique.getShortIdByMsgId(msgId)
const rootMsg = await MessageUnique.getMsgIdAndPeerByShortId(rootMsgId || +msgId)
if (!rootMsg) { if (!rootMsg) {
throw Error('msg not found') throw Error('msg not found')
} }
let data = await NTQQMsgApi.getMultiMsg( const data = await NTQQMsgApi.getMultiMsg(rootMsg.Peer, rootMsg.MsgId, rootMsg.MsgId)
{ chatType: rootMsg.chatType, peerUid: rootMsg.peerUid }, if (data?.result !== 0) {
rootMsg.msgId, throw Error('找不到相关的聊天记录' + data?.errMsg)
rootMsg.msgId,
)
if (data.result !== 0) {
throw Error('找不到相关的聊天记录' + data.errMsg)
} }
let msgList = data.msgList const msgList = data.msgList
let messages = await Promise.all( const messages = await Promise.all(
msgList.map(async (msg) => { msgList.map(async (msg) => {
let resMsg = await OB11Constructor.message(msg) const resMsg = await OB11Constructor.message(msg)
resMsg.message_id = (await dbUtil.addMsg(msg))! resMsg.message_id = MessageUnique.createMsg({
chatType: msg.chatType,
peerUid: msg.peerUid,
}, msg.msgId)!
return resMsg return resMsg
}), }),
) )

View File

@@ -1,16 +1,17 @@
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { OB11Message, OB11User } from '../../types' import { OB11Message } from '../../types'
import { groups } from '../../../common/data'
import { ActionName } from '../types' import { ActionName } from '../types'
import { ChatType } from '../../../ntqqapi/types' import { ChatType } from '@/ntqqapi/types'
import { dbUtil } from '../../../common/db' import { NTQQMsgApi } from '@/ntqqapi/api/msg'
import { NTQQMsgApi } from '../../../ntqqapi/api/msg'
import { OB11Constructor } from '../../constructor' import { OB11Constructor } from '../../constructor'
import { RawMessage } from '@/ntqqapi/types'
import { MessageUnique } from '@/common/utils/MessageUnique'
interface Payload { interface Payload {
group_id: number group_id: number | string
message_seq: number message_seq?: number
count: number count?: number
reverseOrder?: boolean
} }
interface Response { interface Response {
@@ -21,23 +22,24 @@ export default class GoCQHTTPGetGroupMsgHistory extends BaseAction<Payload, Resp
actionName = ActionName.GoCQHTTP_GetGroupMsgHistory actionName = ActionName.GoCQHTTP_GetGroupMsgHistory
protected async _handle(payload: Payload): Promise<Response> { protected async _handle(payload: Payload): Promise<Response> {
const group = groups.find((group) => group.groupCode === payload.group_id.toString()) const count = payload.count || 20
if (!group) { const isReverseOrder = payload.reverseOrder || true
throw `${payload.group_id}不存在` const peer = { chatType: ChatType.group, peerUid: payload.group_id.toString() }
let msgList: RawMessage[] | undefined
// 包含 message_seq 0
if (!payload.message_seq) {
msgList = (await NTQQMsgApi.getLastestMsgByUids(peer, count))?.msgList
} else {
const startMsgId = (await MessageUnique.getMsgIdAndPeerByShortId(payload.message_seq))?.MsgId
if (!startMsgId) throw `消息${payload.message_seq}不存在`
msgList = (await NTQQMsgApi.getMsgHistory(peer, startMsgId, count)).msgList
} }
const startMsgId = (await dbUtil.getMsgByShortId(payload.message_seq))?.msgId || '0' if (!msgList?.length) throw '未找到消息'
// log("startMsgId", startMsgId) if (isReverseOrder) msgList.reverse()
let msgList = (
await NTQQMsgApi.getMsgHistory(
{ chatType: ChatType.group, peerUid: group.groupCode },
startMsgId,
parseInt(payload.count?.toString()) || 20,
)
).msgList
await Promise.all( await Promise.all(
msgList.map(async (msg) => { msgList.map(async msg => {
msg.msgShortId = await dbUtil.addMsg(msg) msg.msgShortId = MessageUnique.createMsg({ chatType: msg.chatType, peerUid: msg.peerUid }, msg.msgId)
}), })
) )
const ob11MsgList = await Promise.all(msgList.map((msg) => OB11Constructor.message(msg))) const ob11MsgList = await Promise.all(msgList.map((msg) => OB11Constructor.message(msg)))
return { messages: ob11MsgList } return { messages: ob11MsgList }

View File

@@ -1,19 +1,59 @@
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { OB11User } from '../../types' import { OB11User } from '../../types'
import { getUidByUin, uidMaps } from '../../../common/data'
import { OB11Constructor } from '../../constructor' import { OB11Constructor } from '../../constructor'
import { ActionName } from '../types' import { ActionName } from '../types'
import { NTQQUserApi } from '../../../ntqqapi/api/user' import { NTQQUserApi } from '../../../ntqqapi/api/user'
import { getBuildVersion } from '@/common/utils/QQBasicInfo'
import { OB11UserSex } from '../../types'
import { calcQQLevel } from '@/common/utils/qqlevel'
export default class GoCQHTTPGetStrangerInfo extends BaseAction<{ user_id: number }, OB11User> { interface Payload {
user_id: number | string
}
export default class GoCQHTTPGetStrangerInfo extends BaseAction<Payload, OB11User> {
actionName = ActionName.GoCQHTTP_GetStrangerInfo actionName = ActionName.GoCQHTTP_GetStrangerInfo
protected async _handle(payload: { user_id: number }): Promise<OB11User> { protected async _handle(payload: Payload): Promise<OB11User> {
const user_id = payload.user_id.toString() if (!(getBuildVersion() >= 26702)) {
const uid = getUidByUin(user_id) const user_id = payload.user_id.toString()
if (!uid) { const extendData = await NTQQUserApi.getUserDetailInfoByUin(user_id)
throw new Error('查无此人') const uid = (await NTQQUserApi.getUidByUin(user_id))!
if (!uid || uid.indexOf('*') != -1) {
const ret = {
...extendData,
user_id: parseInt(extendData.info.uin) || 0,
nickname: extendData.info.nick,
sex: OB11UserSex.unknown,
age: (extendData.info.birthday_year == 0) ? 0 : new Date().getFullYear() - extendData.info.birthday_year,
qid: extendData.info.qid,
level: extendData.info.qqLevel && calcQQLevel(extendData.info.qqLevel) || 0,
login_days: 0,
uid: ''
}
return ret
}
const data = { ...extendData, ...(await NTQQUserApi.getUserDetailInfo(uid)) }
return OB11Constructor.stranger(data)
} else {
const user_id = payload.user_id.toString()
const extendData = await NTQQUserApi.getUserDetailInfoByUinV2(user_id)
const uid = (await NTQQUserApi.getUidByUin(user_id))!
if (!uid || uid.indexOf('*') != -1) {
const ret = {
...extendData,
user_id: parseInt(extendData.detail.uin) || 0,
nickname: extendData.detail.simpleInfo.coreInfo.nick,
sex: OB11UserSex.unknown,
age: 0,
level: extendData.detail.commonExt.qqLevel && calcQQLevel(extendData.detail.commonExt.qqLevel) || 0,
login_days: 0,
uid: ''
}
return ret
}
const data = { ...extendData, ...(await NTQQUserApi.getUserDetailInfo(uid)) }
return OB11Constructor.stranger(data)
} }
return OB11Constructor.stranger(await NTQQUserApi.getUserDetailInfo(uid, true))
} }
} }

View File

@@ -1,23 +1,26 @@
import BaseAction from '../BaseAction'; import BaseAction from '../BaseAction'
import { ActionName } from '../types'; import { ActionName } from '../types'
import { NTQQGroupApi } from '../../../ntqqapi/api/group' import { NTQQGroupApi } from '@/ntqqapi/api/group'
import { dbUtil } from '@/common/db'; import { MessageUnique } from '@/common/utils/MessageUnique'
interface Payload { interface Payload {
message_id: number | string; message_id: number | string
} }
export default class GoCQHTTPSetEssenceMsg extends BaseAction<Payload, any> { export default class GoCQHTTPSetEssenceMsg extends BaseAction<Payload, any> {
actionName = ActionName.GoCQHTTP_SetEssenceMsg; actionName = ActionName.GoCQHTTP_SetEssenceMsg;
protected async _handle(payload: Payload): Promise<any> { protected async _handle(payload: Payload): Promise<any> {
const msg = await dbUtil.getMsgByShortId(parseInt(payload.message_id.toString())); if (!payload.message_id) {
throw Error('message_id不能为空')
}
const msg = await MessageUnique.getMsgIdAndPeerByShortId(+payload.message_id)
if (!msg) { if (!msg) {
throw new Error('msg not found'); throw new Error('msg not found')
} }
return await NTQQGroupApi.addGroupEssence( return await NTQQGroupApi.addGroupEssence(
msg.peerUid, msg.Peer.peerUid,
msg.msgId msg.MsgId
); )
} }
} }

View File

@@ -1,50 +1,70 @@
import fs from 'node:fs'
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { getGroup, getUidByUin } from '@/common/data'
import { ActionName } from '../types' import { ActionName } from '../types'
import { SendMsgElementConstructor } from '@/ntqqapi/constructor' import { SendMsgElementConstructor } from '@/ntqqapi/constructor'
import { ChatType, SendFileElement } from '@/ntqqapi/types' import { ChatType, SendFileElement } from '@/ntqqapi/types'
import fs from 'fs'
import { NTQQMsgApi } from '@/ntqqapi/api/msg'
import { uri2local } from '@/common/utils' import { uri2local } from '@/common/utils'
import { Peer } from '@/ntqqapi/types' import { Peer } from '@/ntqqapi/types'
import { sendMsg } from '../msg/SendMsg'
import { NTQQUserApi, NTQQFriendApi } from '@/ntqqapi/api'
interface Payload { interface Payload {
user_id: number user_id: number | string
group_id?: number group_id?: number | string
file: string file: string
name: string name: string
folder: string folder?: string
folder_id?: string
} }
class GoCQHTTPUploadFileBase extends BaseAction<Payload, null> { export class GoCQHTTPUploadGroupFile extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_UploadGroupFile actionName = ActionName.GoCQHTTP_UploadGroupFile
getPeer(payload: Payload): Peer {
if (payload.user_id) {
return { chatType: ChatType.friend, peerUid: getUidByUin(payload.user_id.toString())! }
}
return { chatType: ChatType.group, peerUid: payload.group_id?.toString()! }
}
protected async _handle(payload: Payload): Promise<null> { protected async _handle(payload: Payload): Promise<null> {
let file = payload.file let file = payload.file
if (fs.existsSync(file)) { if (fs.existsSync(file)) {
file = `file://${file}` file = `file://${file}`
} }
const downloadResult = await uri2local(file) const downloadResult = await uri2local(file)
if (downloadResult.errMsg) { if (!downloadResult.success) {
throw new Error(downloadResult.errMsg) throw new Error(downloadResult.errMsg)
} }
let sendFileEle: SendFileElement = await SendMsgElementConstructor.file(downloadResult.path, payload.name) const sendFileEle = await SendMsgElementConstructor.file(downloadResult.path, payload.name, payload.folder_id)
await NTQQMsgApi.sendMsg(this.getPeer(payload), [sendFileEle]) await sendMsg({
chatType: ChatType.group,
peerUid: payload.group_id?.toString()!,
}, [sendFileEle], [], true)
return null return null
} }
} }
export class GoCQHTTPUploadGroupFile extends GoCQHTTPUploadFileBase { export class GoCQHTTPUploadPrivateFile extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_UploadGroupFile
}
export class GoCQHTTPUploadPrivateFile extends GoCQHTTPUploadFileBase {
actionName = ActionName.GoCQHTTP_UploadPrivateFile actionName = ActionName.GoCQHTTP_UploadPrivateFile
async getPeer(payload: Payload): Promise<Peer> {
if (payload.user_id) {
const peerUid = await NTQQUserApi.getUidByUin(payload.user_id.toString())
if (!peerUid) {
throw `私聊${payload.user_id}不存在`
}
const isBuddy = await NTQQFriendApi.isBuddy(peerUid)
return { chatType: isBuddy ? ChatType.friend : ChatType.temp, peerUid }
}
throw '缺少参数 user_id'
}
protected async _handle(payload: Payload): Promise<null> {
const peer = await this.getPeer(payload)
let file = payload.file
if (fs.existsSync(file)) {
file = `file://${file}`
}
const downloadResult = await uri2local(file)
if (!downloadResult.success) {
throw new Error(downloadResult.errMsg)
}
const sendFileEle: SendFileElement = await SendMsgElementConstructor.file(downloadResult.path, payload.name)
await sendMsg(peer, [sendFileEle], [], true)
return null
}
} }

View File

@@ -1,18 +1,18 @@
import { OB11Group } from '../../types' import { OB11Group } from '../../types'
import { getGroup } from '../../../common/data'
import { OB11Constructor } from '../../constructor' import { OB11Constructor } from '../../constructor'
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { NTQQGroupApi } from '@/ntqqapi/api'
interface PayloadType { interface Payload {
group_id: number group_id: number | string
} }
class GetGroupInfo extends BaseAction<PayloadType, OB11Group> { class GetGroupInfo extends BaseAction<Payload, OB11Group> {
actionName = ActionName.GetGroupInfo actionName = ActionName.GetGroupInfo
protected async _handle(payload: PayloadType) { protected async _handle(payload: Payload) {
const group = await getGroup(payload.group_id.toString()) const group = (await NTQQGroupApi.getGroups()).find(e => e.groupCode == payload.group_id.toString())
if (group) { if (group) {
return OB11Constructor.group(group) return OB11Constructor.group(group)
} else { } else {

View File

@@ -1,10 +1,8 @@
import { OB11Group } from '../../types' import { OB11Group } from '../../types'
import { OB11Constructor } from '../../constructor' import { OB11Constructor } from '../../constructor'
import { groups } from '../../../common/data'
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { NTQQGroupApi } from '../../../ntqqapi/api' import { NTQQGroupApi } from '../../../ntqqapi/api'
import { log } from '../../../common/utils'
interface Payload { interface Payload {
no_cache: boolean | string no_cache: boolean | string
@@ -14,14 +12,8 @@ class GetGroupList extends BaseAction<Payload, OB11Group[]> {
actionName = ActionName.GetGroupList actionName = ActionName.GetGroupList
protected async _handle(payload: Payload) { protected async _handle(payload: Payload) {
if (groups.length === 0 || payload?.no_cache === true || payload?.no_cache === 'true') { const groupList = await NTQQGroupApi.getGroups(payload?.no_cache === true || payload?.no_cache === 'true')
try { return OB11Constructor.groups(groupList)
const groups = await NTQQGroupApi.getGroups(true)
log('强制刷新群列表, 数量:', groups.length)
return OB11Constructor.groups(groups)
} catch (e) {}
}
return OB11Constructor.groups(groups)
} }
} }

View File

@@ -1,30 +1,44 @@
import { OB11GroupMember } from '../../types' import { OB11GroupMember } from '../../types'
import { getGroupMember } from '../../../common/data' import { getGroupMember, getSelfUid } from '@/common/data'
import { OB11Constructor } from '../../constructor' import { OB11Constructor } from '../../constructor'
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { NTQQUserApi } from '../../../ntqqapi/api/user' import { NTQQUserApi, WebApi } from '@/ntqqapi/api'
import { log } from '../../../common/utils/log' import { isNull } from '@/common/utils/helper'
import { isNull } from '../../../common/utils/helper'
export interface PayloadType { interface Payload {
group_id: number group_id: number | string
user_id: number user_id: number | string
} }
class GetGroupMemberInfo extends BaseAction<PayloadType, OB11GroupMember> { class GetGroupMemberInfo extends BaseAction<Payload, OB11GroupMember> {
actionName = ActionName.GetGroupMemberInfo actionName = ActionName.GetGroupMemberInfo
protected async _handle(payload: PayloadType) { protected async _handle(payload: Payload) {
const member = await getGroupMember(payload.group_id.toString(), payload.user_id.toString()) const member = await getGroupMember(payload.group_id.toString(), payload.user_id.toString())
if (member) { if (member) {
if (isNull(member.sex)) { if (isNull(member.sex)) {
log('获取群成员详细信息') //log('获取群成员详细信息')
let info = await NTQQUserApi.getUserDetailInfo(member.uid, true) const info = await NTQQUserApi.getUserDetailInfo(member.uid, true)
log('群成员详细信息结果', info) //log('群成员详细信息结果', info)
Object.assign(member, info) Object.assign(member, info)
} }
return OB11Constructor.groupMember(payload.group_id.toString(), member) const ret = OB11Constructor.groupMember(payload.group_id.toString(), member)
const self = await getGroupMember(payload.group_id.toString(), getSelfUid())
if (self?.role === 3 || self?.role === 4) {
const webGroupMembers = await WebApi.getGroupMembers(payload.group_id.toString())
const target = webGroupMembers.find(e => e?.uin && e.uin === ret.user_id)
if (target) {
ret.join_time = target.join_time
ret.last_sent_time = target.last_speak_time
ret.qage = target.qage
ret.level = target.lv.level.toString()
}
}
const date = Math.round(Date.now() / 1000)
ret.last_sent_time ||= Number(member.lastSpeakTime || date)
ret.join_time ||= Number(member.joinTime || date)
return ret
} else { } else {
throw `群成员${payload.user_id}不存在` throw `群成员${payload.user_id}不存在`
} }

View File

@@ -1,30 +1,58 @@
import { OB11GroupMember } from '../../types' import { OB11GroupMember } from '../../types'
import { getGroup } from '../../../common/data'
import { OB11Constructor } from '../../constructor' import { OB11Constructor } from '../../constructor'
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { NTQQGroupApi } from '../../../ntqqapi/api/group' import { NTQQGroupApi, WebApi } from '@/ntqqapi/api'
import { log } from '../../../common/utils' import { getSelfUid } from '@/common/data'
export interface PayloadType { interface Payload {
group_id: number group_id: number | string
no_cache: boolean | string no_cache: boolean | string
} }
class GetGroupMemberList extends BaseAction<PayloadType, OB11GroupMember[]> { class GetGroupMemberList extends BaseAction<Payload, OB11GroupMember[]> {
actionName = ActionName.GetGroupMemberList actionName = ActionName.GetGroupMemberList
protected async _handle(payload: PayloadType) { protected async _handle(payload: Payload) {
const group = await getGroup(payload.group_id.toString()) const groupMembers = await NTQQGroupApi.getGroupMembers(payload.group_id.toString())
if (group) { const groupMembersArr = Array.from(groupMembers.values())
if (!group.members?.length || payload.no_cache === true || payload.no_cache === 'true') {
group.members = await NTQQGroupApi.getGroupMembers(payload.group_id.toString()) let _groupMembers = groupMembersArr.map(item => {
log('强制刷新群成员列表, 数量: ', group.members.length) return OB11Constructor.groupMember(payload.group_id.toString(), item)
} })
return OB11Constructor.groupMembers(group)
} else { const MemberMap: Map<number, OB11GroupMember> = new Map<number, OB11GroupMember>()
throw `${payload.group_id}不存在` const 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])
} }
const selfRole = groupMembers.get(getSelfUid())?.role
const isPrivilege = selfRole === 3 || selfRole === 4
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) {
continue
}
const MemberData = MemberMap.get(webGroupMembers[i]?.uin)
if (MemberData) {
MemberData.join_time = webGroupMembers[i]?.join_time
MemberData.last_sent_time = webGroupMembers[i]?.last_speak_time
MemberData.qage = webGroupMembers[i]?.qage
MemberData.level = webGroupMembers[i]?.lv.level.toString()
MemberMap.set(webGroupMembers[i]?.uin, MemberData)
}
}
}
_groupMembers = Array.from(MemberMap.values())
return _groupMembers
} }
} }

View File

@@ -5,22 +5,19 @@ import { NTQQGroupApi } from '../../../ntqqapi/api/group'
interface Payload { interface Payload {
flag: string flag: string
// sub_type: "add" | "invite", approve?: boolean | string
// type: "add" | "invite" reason?: string
approve: boolean
reason: string
} }
export default class SetGroupAddRequest extends BaseAction<Payload, null> { export default class SetGroupAddRequest extends BaseAction<Payload, null> {
actionName = ActionName.SetGroupAddRequest actionName = ActionName.SetGroupAddRequest
protected async _handle(payload: Payload): Promise<null> { protected async _handle(payload: Payload): Promise<null> {
const seq = payload.flag.toString() const flag = payload.flag.toString()
const approve = payload.approve.toString() === 'true' const approve = payload.approve?.toString() !== 'false'
await NTQQGroupApi.handleGroupRequest( await NTQQGroupApi.handleGroupRequest(flag,
seq,
approve ? GroupRequestOperateTypes.approve : GroupRequestOperateTypes.reject, approve ? GroupRequestOperateTypes.approve : GroupRequestOperateTypes.reject,
payload.reason, payload.reason || ''
) )
return null return null
} }

View File

@@ -1,6 +1,6 @@
import GetMsg from './msg/GetMsg' import GetMsg from './msg/GetMsg'
import GetLoginInfo from './system/GetLoginInfo' import GetLoginInfo from './system/GetLoginInfo'
import { GetFriendList, GetFriendWithCategory} from './user/GetFriendList' import { GetFriendList, GetFriendWithCategory } from './user/GetFriendList'
import GetGroupList from './group/GetGroupList' import GetGroupList from './group/GetGroupList'
import GetGroupInfo from './group/GetGroupInfo' import GetGroupInfo from './group/GetGroupInfo'
import GetGroupMemberList from './group/GetGroupMemberList' import GetGroupMemberList from './group/GetGroupMemberList'
@@ -53,6 +53,7 @@ import { GoCQHTTHandleQuickOperation } from './go-cqhttp/QuickOperation'
import GoCQHTTPSetEssenceMsg from './go-cqhttp/SetEssenceMsg' import GoCQHTTPSetEssenceMsg from './go-cqhttp/SetEssenceMsg'
import GoCQHTTPDelEssenceMsg from './go-cqhttp/DelEssenceMsg' import GoCQHTTPDelEssenceMsg from './go-cqhttp/DelEssenceMsg'
import GetEvent from './llonebot/GetEvent' import GetEvent from './llonebot/GetEvent'
import { GoCQHTTPDelGroupFile } from './go-cqhttp/DelGroupFile'
export const actionHandlers = [ export const actionHandlers = [
@@ -113,7 +114,8 @@ export const actionHandlers = [
new GoCQHTTGetForwardMsgAction(), new GoCQHTTGetForwardMsgAction(),
new GoCQHTTHandleQuickOperation(), new GoCQHTTHandleQuickOperation(),
new GoCQHTTPSetEssenceMsg(), new GoCQHTTPSetEssenceMsg(),
new GoCQHTTPDelEssenceMsg() new GoCQHTTPDelEssenceMsg(),
new GoCQHTTPDelGroupFile()
] ]
function initActionMap() { function initActionMap() {

View File

@@ -24,7 +24,7 @@ export default class Debug extends BaseAction<Payload, any> {
log('debug call ntqq api', payload) log('debug call ntqq api', payload)
const ntqqApi = [NTQQMsgApi, NTQQFriendApi, NTQQGroupApi, NTQQUserApi, NTQQFileApi, NTQQFileCacheApi, NTQQWindowApi] const ntqqApi = [NTQQMsgApi, NTQQFriendApi, NTQQGroupApi, NTQQUserApi, NTQQFileApi, NTQQFileCacheApi, NTQQWindowApi]
for (const ntqqApiClass of ntqqApi) { for (const ntqqApiClass of ntqqApi) {
log('ntqqApiClass', ntqqApiClass) //log('ntqqApiClass', ntqqApiClass)
const method = ntqqApiClass[payload.method] const method = ntqqApiClass[payload.method]
if (method) { if (method) {
const result = method(...payload.args) const result = method(...payload.args)

View File

@@ -1,10 +1,8 @@
import { GroupNotify, GroupNotifyStatus } from '../../../ntqqapi/types' import { GroupNotify, GroupNotifyStatus } from '../../../ntqqapi/types'
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { uidMaps } from '../../../common/data'
import { NTQQUserApi } from '../../../ntqqapi/api/user' import { NTQQUserApi } from '../../../ntqqapi/api/user'
import { NTQQGroupApi } from '../../../ntqqapi/api/group' import { NTQQGroupApi } from '../../../ntqqapi/api/group'
import { log } from '../../../common/utils/log'
interface OB11GroupRequestNotify { interface OB11GroupRequestNotify {
group_id: number group_id: number
@@ -17,11 +15,10 @@ export default class GetGroupAddRequest extends BaseAction<null, OB11GroupReques
protected async _handle(payload: null): Promise<OB11GroupRequestNotify[]> { protected async _handle(payload: null): Promise<OB11GroupRequestNotify[]> {
const data = await NTQQGroupApi.getGroupIgnoreNotifies() const data = await NTQQGroupApi.getGroupIgnoreNotifies()
log(data) const notifies: GroupNotify[] = data.notifies.filter((notify) => notify.status === GroupNotifyStatus.WAIT_HANDLE)
let notifies: GroupNotify[] = data.notifies.filter((notify) => notify.status === GroupNotifyStatus.WAIT_HANDLE) const returnData: OB11GroupRequestNotify[] = []
let returnData: OB11GroupRequestNotify[] = []
for (const notify of notifies) { for (const notify of notifies) {
const uin = uidMaps[notify.user1.uid] || (await NTQQUserApi.getUserDetailInfo(notify.user1.uid))?.uin const uin = await NTQQUserApi.getUinByUid(notify.user1.uid)
returnData.push({ returnData.push({
group_id: parseInt(notify.group.groupCode), group_id: parseInt(notify.group.groupCode),
user_id: parseInt(uin), user_id: parseInt(uin),

View File

@@ -1,27 +1,24 @@
import { ActionName } from '../types' import { ActionName } from '../types'
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { dbUtil } from '../../../common/db' import { NTQQMsgApi } from '@/ntqqapi/api/msg'
import { NTQQMsgApi } from '../../../ntqqapi/api/msg' import { MessageUnique } from '@/common/utils/MessageUnique'
interface Payload { interface Payload {
message_id: number message_id: number | string
} }
class DeleteMsg extends BaseAction<Payload, void> { class DeleteMsg extends BaseAction<Payload, void> {
actionName = ActionName.DeleteMsg actionName = ActionName.DeleteMsg
protected async _handle(payload: Payload) { protected async _handle(payload: Payload) {
let msg = await dbUtil.getMsgByShortId(payload.message_id) if (!payload.message_id) {
throw Error('message_id不能为空')
}
const msg = await MessageUnique.getMsgIdAndPeerByShortId(+payload.message_id)
if (!msg) { if (!msg) {
throw `消息${payload.message_id}不存在` throw `消息${payload.message_id}不存在`
} }
await NTQQMsgApi.recallMsg( await NTQQMsgApi.recallMsg(msg.Peer, [msg.MsgId])
{
chatType: msg.chatType,
peerUid: msg.peerUid,
},
[msg.msgId],
)
} }
} }

View File

@@ -1,42 +1,42 @@
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { NTQQMsgApi } from '@/ntqqapi/api' import { NTQQMsgApi, NTQQUserApi } from '@/ntqqapi/api'
import { ChatType, RawMessage } from '@/ntqqapi/types' import { ChatType } from '@/ntqqapi/types'
import { dbUtil } from '@/common/db'
import { getUidByUin } from '@/common/data'
import { ActionName } from '../types' import { ActionName } from '../types'
import { Peer } from '@/ntqqapi/types' import { Peer } from '@/ntqqapi/types'
import { MessageUnique } from '@/common/utils/MessageUnique'
interface Payload { interface Payload {
message_id: number message_id: number | string
group_id: number group_id: number | string
user_id?: number user_id?: number | string
} }
interface Response { abstract class ForwardSingleMsg extends BaseAction<Payload, null> {
message_id: number
}
abstract class ForwardSingleMsg extends BaseAction<Payload, Response> {
protected async getTargetPeer(payload: Payload): Promise<Peer> { protected async getTargetPeer(payload: Payload): Promise<Peer> {
if (payload.user_id) { if (payload.user_id) {
return { chatType: ChatType.friend, peerUid: getUidByUin(payload.user_id.toString())! } const peerUid = await NTQQUserApi.getUidByUin(payload.user_id.toString())
if (!peerUid) {
throw new Error(`无法找到私聊对象${payload.user_id}`)
}
return { chatType: ChatType.friend, peerUid }
} }
return { chatType: ChatType.group, peerUid: payload.group_id.toString() } return { chatType: ChatType.group, peerUid: payload.group_id!.toString() }
} }
protected async _handle(payload: Payload): Promise<Response> { protected async _handle(payload: Payload): Promise<null> {
const msg = (await dbUtil.getMsgByShortId(payload.message_id))! if (!payload.message_id) {
throw Error('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 peer = await this.getTargetPeer(payload)
const sentMsg = await NTQQMsgApi.forwardMsg( const ret = await NTQQMsgApi.forwardMsg(msg.Peer, peer, [msg.MsgId])
{ if (ret.result !== 0) {
chatType: msg.chatType, throw new Error(`转发消息失败 ${ret.errMsg}`)
peerUid: msg.peerUid, }
}, return null
peer,
[msg.msgId],
)
const ob11MsgId = await dbUtil.addMsg(sentMsg)
return { message_id: ob11MsgId! }
} }
} }

View File

@@ -2,10 +2,12 @@ import { OB11Message } from '../../types'
import { OB11Constructor } from '../../constructor' import { OB11Constructor } from '../../constructor'
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { dbUtil } from '../../../common/db' import { NTQQMsgApi } from '@/ntqqapi/api'
import { MessageUnique } from '@/common/utils/MessageUnique'
import { getMsgCache } from '@/common/data'
export interface PayloadType { export interface PayloadType {
message_id: number message_id: number | string
} }
export type ReturnDataType = OB11Message export type ReturnDataType = OB11Message
@@ -18,14 +20,22 @@ class GetMsg extends BaseAction<PayloadType, OB11Message> {
if (!payload.message_id) { if (!payload.message_id) {
throw '参数message_id不能为空' throw '参数message_id不能为空'
} }
let msg = await dbUtil.getMsgByShortId(payload.message_id) const msgShortId = MessageUnique.getShortIdByMsgId(payload.message_id.toString())
if (!msg) { const msgIdWithPeer = await MessageUnique.getMsgIdAndPeerByShortId(msgShortId || +payload.message_id)
msg = await dbUtil.getMsgByLongId(payload.message_id.toString()) if (!msgIdWithPeer) {
throw ('消息不存在')
} }
if (!msg) { const peer = {
throw '消息不存在' guildId: '',
peerUid: msgIdWithPeer.Peer.peerUid,
chatType: msgIdWithPeer.Peer.chatType
} }
return await OB11Constructor.message(msg) const msg = getMsgCache(msgIdWithPeer.MsgId) ?? (await NTQQMsgApi.getMsgsByMsgId(peer, [msgIdWithPeer.MsgId])).msgList[0]
const retMsg = await OB11Constructor.message(msg)
retMsg.message_id = MessageUnique.createMsg(peer, msg.msgId)!
retMsg.message_seq = retMsg.message_id
retMsg.real_id = retMsg.message_id
return retMsg
} }
} }

View File

@@ -2,87 +2,52 @@ import {
AtType, AtType,
ChatType, ChatType,
ElementType, ElementType,
Friend,
Group,
GroupMemberRole, GroupMemberRole,
PicSubType,
RawMessage, RawMessage,
SendArkElement,
SendMessageElement, SendMessageElement,
} from '../../../ntqqapi/types' } from '@/ntqqapi/types'
import { friends, getFriend, getGroup, getGroupMember, getUidByUin, selfInfo } from '../../../common/data' import { getGroupMember, getSelfUid, getSelfUin } from '@/common/data'
import { import {
OB11MessageCustomMusic, OB11MessageCustomMusic,
OB11MessageData, OB11MessageData,
OB11MessageDataType, OB11MessageDataType,
OB11MessageFile,
OB11MessageJson, OB11MessageJson,
OB11MessageMixType, OB11MessageMixType,
OB11MessageMusic, OB11MessageMusic,
OB11MessageNode, OB11MessageNode,
OB11MessageVideo,
OB11PostSendMsg, OB11PostSendMsg,
} from '../../types' } from '../../types'
import { NTQQMsgApi } from '../../../ntqqapi/api/msg' import { SendMsgElementConstructor } from '@/ntqqapi/constructor'
import { SendMsgElementConstructor } from '../../../ntqqapi/constructor'
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { ActionName, BaseCheckResult } from '../types' import { ActionName, BaseCheckResult } from '../types'
import * as fs from 'node:fs' import fs from 'node:fs'
import fsPromise from 'node:fs/promises'
import { decodeCQCode } from '../../cqcode' import { decodeCQCode } from '../../cqcode'
import { dbUtil } from '../../../common/db' import { getConfigUtil } from '@/common/config'
import { ALLOW_SEND_TEMP_MSG, getConfigUtil } from '../../../common/config' import { log } from '@/common/utils/log'
import { log } from '../../../common/utils/log' import { sleep } from '@/common/utils/helper'
import { sleep } from '../../../common/utils/helper' import { uri2local } from '@/common/utils'
import { uri2local } from '../../../common/utils' import { NTQQGroupApi, NTQQMsgApi, NTQQUserApi, NTQQFriendApi } from '@/ntqqapi/api'
import { crychic } from '../../../ntqqapi/native/crychic' import { CustomMusicSignPostData, IdMusicSignPostData, MusicSign, MusicSignPostData } from '@/common/utils/sign'
import { NTQQGroupApi } from '../../../ntqqapi/api' import { Peer } from '@/ntqqapi/types/msg'
import { CustomMusicSignPostData, IdMusicSignPostData, MusicSign, MusicSignPostData } from '../../../common/utils/sign' import { MessageUnique } from '@/common/utils/MessageUnique'
import { Peer } from '../../../ntqqapi/types/msg' import { OB11MessageFileBase } from '../../types'
function checkSendMessage(sendMsgList: OB11MessageData[]) {
function checkUri(uri: string): boolean {
const pattern = /^(file:\/\/|http:\/\/|https:\/\/|base64:\/\/)/
return pattern.test(uri)
}
for (let msg of sendMsgList) {
if (msg['type'] && msg['data']) {
let type = msg['type']
let data = msg['data']
if (type === 'text' && !data['text']) {
return 400
}
else if (['image', 'voice', 'record'].includes(type)) {
if (!data['file']) {
return 400
}
else {
if (checkUri(data['file'])) {
return 200
}
else {
return 400
}
}
}
else if (type === 'at' && !data['qq']) {
return 400
}
else if (type === 'reply' && !data['id']) {
return 400
}
}
else {
return 400
}
}
return 200
}
export interface ReturnDataType { export interface ReturnDataType {
message_id: number message_id: number
} }
export enum ContextMode {
Normal = 0,
Private = 1,
Group = 2
}
interface MessageContext {
deleteAfterSentFiles: string[]
peer: Peer
}
export function convertMessage2List(message: OB11MessageMixType, autoEscape = false) { export function convertMessage2List(message: OB11MessageMixType, autoEscape = false) {
if (typeof message === 'string') { if (typeof message === 'string') {
if (autoEscape === true) { if (autoEscape === true) {
@@ -105,9 +70,35 @@ export function convertMessage2List(message: OB11MessageMixType, autoEscape = fa
return message return message
} }
// forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/onebot11/action/msg/SendMsg/create-send-elements.ts#L26
async function handleOb11FileLikeMessage(
{ data: inputdata }: OB11MessageFileBase,
{ deleteAfterSentFiles }: Pick<MessageContext, 'deleteAfterSentFiles'>,
) {
//有的奇怪的框架将url作为参数 而不是file 此时优先url 同时注意可能传入的是非file://开头的目录 By Mlikiowa
const {
path,
isLocal,
fileName,
errMsg,
success,
} = (await uri2local(inputdata?.url || inputdata.file))
if (!success) {
log('文件下载失败', errMsg)
throw Error('文件下载失败' + errMsg)
}
if (!isLocal) { // 只删除http和base64转过来的文件
deleteAfterSentFiles.push(path)
}
return { path, fileName: inputdata.name || fileName }
}
export async function createSendElements( export async function createSendElements(
messageData: OB11MessageData[], messageData: OB11MessageData[],
target: Group | Friend | undefined, peer: Peer,
ignoreTypes: OB11MessageDataType[] = [], ignoreTypes: OB11MessageDataType[] = [],
) { ) {
let sendElements: SendMessageElement[] = [] let sendElements: SendMessageElement[] = []
@@ -125,15 +116,14 @@ export async function createSendElements(
} }
break break
case OB11MessageDataType.at: { case OB11MessageDataType.at: {
if (!target) { if (!peer) {
continue continue
} }
let atQQ = sendMsg.data?.qq if (sendMsg.data?.qq) {
if (atQQ) { const atQQ = String(sendMsg.data.qq)
atQQ = atQQ.toString()
if (atQQ === 'all') { if (atQQ === 'all') {
// todo查询剩余的at全体次数 // todo查询剩余的at全体次数
const groupCode = (target as Group)?.groupCode const groupCode = peer.peerUid
let remainAtAllCount = 1 let remainAtAllCount = 1
let isAdmin: boolean = true let isAdmin: boolean = true
if (groupCode) { if (groupCode) {
@@ -141,21 +131,28 @@ export async function createSendElements(
remainAtAllCount = (await NTQQGroupApi.getGroupAtAllRemainCount(groupCode)).atInfo remainAtAllCount = (await NTQQGroupApi.getGroupAtAllRemainCount(groupCode)).atInfo
.RemainAtAllCountForUin .RemainAtAllCountForUin
log(`${groupCode}剩余at全体次数`, remainAtAllCount) log(`${groupCode}剩余at全体次数`, remainAtAllCount)
const self = await getGroupMember((target as Group)?.groupCode, selfInfo.uin) const self = await getGroupMember(groupCode, getSelfUin())
isAdmin = self?.role === GroupMemberRole.admin || self?.role === GroupMemberRole.owner isAdmin = self?.role === GroupMemberRole.admin || self?.role === GroupMemberRole.owner
} catch (e) { } catch (e) {
} }
} }
if (isAdmin && remainAtAllCount > 0) { if (isAdmin && remainAtAllCount > 0) {
sendElements.push(SendMsgElementConstructor.at(atQQ, atQQ, AtType.atAll, '全体成员')) sendElements.push(SendMsgElementConstructor.at(atQQ, atQQ, AtType.atAll, '@全体成员'))
} }
} }
else { else if (peer.chatType === ChatType.group) {
// const atMember = group?.members.find(m => m.uin == atQQ) const atMember = await getGroupMember(peer.peerUid, atQQ)
const atMember = await getGroupMember((target as Group)?.groupCode, atQQ)
if (atMember) { if (atMember) {
const display = `@${atMember.cardName || atMember.nick}`
sendElements.push( sendElements.push(
SendMsgElementConstructor.at(atQQ, atMember.uid, AtType.atUser, atMember.cardName || atMember.nick), SendMsgElementConstructor.at(atQQ, atMember.uid, AtType.atUser, display),
)
} else {
const atNmae = sendMsg.data?.name
const uid = await NTQQUserApi.getUidByUin(atQQ) || ''
const display = atNmae ? `@${atNmae}` : ''
sendElements.push(
SendMsgElementConstructor.at(atQQ, uid, AtType.atUser, display),
) )
} }
} }
@@ -163,9 +160,16 @@ export async function createSendElements(
} }
break break
case OB11MessageDataType.reply: { case OB11MessageDataType.reply: {
let replyMsgId = sendMsg.data.id if (sendMsg.data?.id) {
if (replyMsgId) { const replyMsgId = await MessageUnique.getMsgIdAndPeerByShortId(+sendMsg.data.id)
const replyMsg = await dbUtil.getMsgByShortId(parseInt(replyMsgId)) if (!replyMsgId) {
log('回复消息不存在', replyMsgId)
continue
}
const replyMsg = (await NTQQMsgApi.getMsgsByMsgId(
replyMsgId.Peer,
[replyMsgId.MsgId!]
)).msgList[0]
if (replyMsg) { if (replyMsg) {
sendElements.push( sendElements.push(
SendMsgElementConstructor.reply( SendMsgElementConstructor.reply(
@@ -197,66 +201,36 @@ export async function createSendElements(
) )
} }
break break
case OB11MessageDataType.image: case OB11MessageDataType.image: {
case OB11MessageDataType.file: const res = await SendMsgElementConstructor.pic(
case OB11MessageDataType.video: (await handleOb11FileLikeMessage(sendMsg, { deleteAfterSentFiles })).path,
case OB11MessageDataType.voice: { sendMsg.data.summary || '',
const data = (sendMsg as OB11MessageFile).data sendMsg.data.subType || 0
let file = data.file )
const payloadFileName = data?.name deleteAfterSentFiles.push(res.picElement.sourcePath)
if (file) { sendElements.push(res)
const cache = await dbUtil.getFileCache(file) }
if (cache) { break
if (fs.existsSync(cache.filePath)) { case OB11MessageDataType.file: {
file = 'file://' + cache.filePath const { path, fileName } = await handleOb11FileLikeMessage(sendMsg, { deleteAfterSentFiles })
} sendElements.push(await SendMsgElementConstructor.file(path, fileName))
else if (cache.downloadFunc) { }
await cache.downloadFunc() break
file = cache.filePath case OB11MessageDataType.video: {
} const { path, fileName } = await handleOb11FileLikeMessage(sendMsg, { deleteAfterSentFiles })
else if (cache.url) { let thumb = sendMsg.data.thumb
file = cache.url if (thumb) {
} const uri2LocalRes = await uri2local(thumb)
log('找到文件缓存', file) if (uri2LocalRes.success) thumb = uri2LocalRes.path
}
const { path, isLocal, fileName, errMsg } = await uri2local(file)
if (errMsg) {
throw errMsg
}
if (path) {
if (!isLocal) {
// 只删除http和base64转过来的文件
deleteAfterSentFiles.push(path)
}
if (sendMsg.type === OB11MessageDataType.file) {
log('发送文件', path, payloadFileName || fileName)
sendElements.push(await SendMsgElementConstructor.file(path, payloadFileName || fileName))
}
else if (sendMsg.type === OB11MessageDataType.video) {
log('发送视频', path, payloadFileName || fileName)
let thumb = sendMsg.data?.thumb
if (thumb) {
let uri2LocalRes = await uri2local(thumb)
if (uri2LocalRes.success) {
thumb = uri2LocalRes.path
}
}
sendElements.push(await SendMsgElementConstructor.video(path, payloadFileName || fileName, thumb))
}
else if (sendMsg.type === OB11MessageDataType.voice) {
sendElements.push(await SendMsgElementConstructor.ptt(path))
}
else if (sendMsg.type === OB11MessageDataType.image) {
sendElements.push(
await SendMsgElementConstructor.pic(
path,
sendMsg.data.summary || '',
<PicSubType>parseInt(sendMsg.data?.subType?.toString()!) || 0,
),
)
}
}
} }
const res = await SendMsgElementConstructor.video(path, fileName, thumb)
deleteAfterSentFiles.push(res.videoElement.filePath)
sendElements.push(res)
}
break
case OB11MessageDataType.voice: {
const { path } = await handleOb11FileLikeMessage(sendMsg, { deleteAfterSentFiles })
sendElements.push(await SendMsgElementConstructor.ptt(path))
} }
break break
case OB11MessageDataType.json: { case OB11MessageDataType.json: {
@@ -265,18 +239,6 @@ export async function createSendElements(
break break
case OB11MessageDataType.poke: { case OB11MessageDataType.poke: {
let qq = sendMsg.data?.qq || sendMsg.data?.id let qq = sendMsg.data?.qq || sendMsg.data?.id
if (qq) {
if ('groupCode' in target!) {
crychic.sendGroupPoke(target.groupCode, qq.toString())
}
else {
if (!qq) {
qq = parseInt(target?.uin!)
}
crychic.sendFriendPoke(qq.toString())
}
sendElements.push(SendMsgElementConstructor.poke('', '')!)
}
} }
break break
case OB11MessageDataType.dice: { case OB11MessageDataType.dice: {
@@ -327,17 +289,40 @@ export async function sendMsg(
log('文件大小计算失败', e, fileElement) log('文件大小计算失败', e, fileElement)
} }
} }
log('发送消息总大小', totalSize, 'bytes') //log('发送消息总大小', totalSize, 'bytes')
let timeout = ((totalSize / 1024 / 100) * 1000) + 5000 // 100kb/s const timeout = 10000 + (totalSize / 1024 / 256 * 1000) // 10s Basic Timeout + PredictTime( For File 512kb/s )
log('设置消息超时时间', timeout) //log('设置消息超时时间', timeout)
const returnMsg = await NTQQMsgApi.sendMsg(peer, sendElements, waitComplete, timeout) const returnMsg = await NTQQMsgApi.sendMsg(peer, sendElements, waitComplete, timeout)
log('消息发送结果', returnMsg) returnMsg.msgShortId = MessageUnique.createMsg(peer, returnMsg.msgId)
returnMsg.msgShortId = await dbUtil.addMsg(returnMsg) log('消息发送', returnMsg.msgShortId)
deleteAfterSentFiles.map((f) => fs.unlink(f, () => { deleteAfterSentFiles.map(path => fsPromise.unlink(path))
}))
return returnMsg return returnMsg
} }
async function createContext(payload: OB11PostSendMsg, contextMode: ContextMode): Promise<Peer> {
// This function determines the type of message by the existence of user_id / group_id,
// not message_type.
// This redundant design of Ob11 here should be blamed.
if ((contextMode === ContextMode.Group || contextMode === ContextMode.Normal) && payload.group_id) {
return {
chatType: ChatType.group,
peerUid: payload.group_id.toString(),
}
}
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 {
chatType: isBuddy ? ChatType.friend : ChatType.temp,
peerUid: Uid!,
guildId: payload.group_id?.toString() || '' //临时主动发起时需要传入群号
}
}
throw '请指定 group_id 或 user_id'
}
export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> { export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
actionName = ActionName.SendMsg actionName = ActionName.SendMsg
@@ -357,20 +342,12 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
message: '音乐消息不可以和其他消息混在一起发送', message: '音乐消息不可以和其他消息混在一起发送',
} }
} }
if (payload.message_type !== 'private' && payload.group_id && !(await getGroup(payload.group_id))) {
return {
valid: false,
message: `${payload.group_id}不存在`,
}
}
if (payload.user_id && payload.message_type !== 'group') { if (payload.user_id && payload.message_type !== 'group') {
if (!(await getFriend(payload.user_id))) { const uid = await NTQQUserApi.getUidByUin(payload.user_id.toString())
if (!ALLOW_SEND_TEMP_MSG && !(await dbUtil.getReceivedTempUinMap())[payload.user_id.toString()]) { const isBuddy = await NTQQFriendApi.isBuddy(uid!)
return { // 此处有问题
valid: false, if (!isBuddy) {
message: `不能发送临时消息`, //return { valid: false, message: '异常消息' }
}
}
} }
} }
return { return {
@@ -379,56 +356,20 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
} }
protected async _handle(payload: OB11PostSendMsg) { protected async _handle(payload: OB11PostSendMsg) {
const peer: Peer = { let contextMode = ContextMode.Normal
chatType: ChatType.friend, if (payload.message_type === 'group') {
peerUid: '', contextMode = ContextMode.Group
} } else if (payload.message_type === 'private') {
let isTempMsg = false contextMode = ContextMode.Private
let group: Group | undefined = undefined
let friend: Friend | undefined = undefined
const genGroupPeer = async () => {
group = await getGroup(payload.group_id?.toString()!)
peer.chatType = ChatType.group
// peer.name = group.name
peer.peerUid = group?.groupCode!
}
const genFriendPeer = () => {
friend = friends.find((f) => f.uin == payload.user_id.toString())
if (friend) {
// peer.name = friend.nickName
peer.peerUid = friend.uid
}
else {
peer.chatType = ChatType.temp
const tempUserUid = getUidByUin(payload.user_id.toString())
if (!tempUserUid) {
throw `找不到私聊对象${payload.user_id}`
}
// peer.name = tempUser.nickName
isTempMsg = true
peer.peerUid = tempUserUid
}
}
if (payload?.group_id && payload.message_type === 'group') {
await genGroupPeer()
}
else if (payload?.user_id) {
genFriendPeer()
}
else if (payload.group_id) {
await genGroupPeer()
}
else {
throw '发送消息参数错误, 请指定group_id或user_id'
} }
const peer = await createContext(payload, contextMode)
const messages = convertMessage2List( const messages = convertMessage2List(
payload.message, payload.message,
payload.auto_escape === true || payload.auto_escape === 'true', payload.auto_escape === true || payload.auto_escape === 'true',
) )
if (this.getSpecialMsgNum(messages, OB11MessageDataType.node)) { if (this.getSpecialMsgNum(messages, OB11MessageDataType.node)) {
try { try {
const returnMsg = await this.handleForwardNode(peer, messages as OB11MessageNode[], group) const returnMsg = await this.handleForwardNode(peer, messages as OB11MessageNode[])
return { message_id: returnMsg?.msgShortId! } return { message_id: returnMsg?.msgShortId! }
} catch (e: any) { } catch (e: any) {
throw '发送转发消息失败 ' + e.toString() throw '发送转发消息失败 ' + e.toString()
@@ -485,15 +426,13 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
} }
} }
// log("send msg:", peer, sendElements) // log("send msg:", peer, sendElements)
const { sendElements, deleteAfterSentFiles } = await createSendElements(messages, group || friend) const { sendElements, deleteAfterSentFiles } = await createSendElements(messages, peer)
if (sendElements.length === 1) { if (sendElements.length === 1) {
if (sendElements[0] === null) { if (sendElements[0] === null) {
return { message_id: 0 } return { message_id: 0 }
} }
} }
const returnMsg = await sendMsg(peer, sendElements, deleteAfterSentFiles) const returnMsg = await sendMsg(peer, sendElements, deleteAfterSentFiles)
deleteAfterSentFiles.map((f) => fs.unlink(f, () => {
}))
return { message_id: returnMsg.msgShortId! } return { message_id: returnMsg.msgShortId! }
} }
@@ -521,12 +460,12 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
const nodeMsg = await NTQQMsgApi.sendMsg( const nodeMsg = await NTQQMsgApi.sendMsg(
{ {
chatType: ChatType.friend, chatType: ChatType.friend,
peerUid: selfInfo.uid, peerUid: getSelfUid(),
}, },
sendElements, sendElements,
true, true,
) )
await sleep(500) await sleep(400)
return nodeMsg return nodeMsg
} catch (e) { } catch (e) {
log(e, '克隆转发消息失败,将忽略本条消息', msg) log(e, '克隆转发消息失败,将忽略本条消息', msg)
@@ -534,32 +473,24 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
} }
// 返回一个合并转发的消息id // 返回一个合并转发的消息id
private async handleForwardNode(destPeer: Peer, messageNodes: OB11MessageNode[], group: Group | undefined) { private async handleForwardNode(destPeer: Peer, messageNodes: OB11MessageNode[]) {
const selfPeer = { const selfPeer = {
chatType: ChatType.friend, chatType: ChatType.friend,
peerUid: selfInfo.uid, peerUid: getSelfUid(),
} }
let nodeMsgIds: string[] = [] let nodeMsgIds: string[] = []
// 先判断一遍是不是id和自定义混用 // 先判断一遍是不是id和自定义混用
let needClone =
messageNodes.filter((node) => node.data.id).length && messageNodes.filter((node) => !node.data.id).length
for (const messageNode of messageNodes) { for (const messageNode of messageNodes) {
// 一个node表示一个人的消息 // 一个node表示一个人的消息
let nodeId = messageNode.data.id let nodeId = messageNode.data.id
// 有nodeId表示一个子转发消息卡片 // 有nodeId表示一个子转发消息卡片
if (nodeId) { if (nodeId) {
let nodeMsg = await dbUtil.getMsgByShortId(parseInt(nodeId)) const nodeMsg = await MessageUnique.getMsgIdAndPeerByShortId(+nodeId) || await MessageUnique.getPeerByMsgId(nodeId)
if (!needClone) { if (!nodeMsg) {
nodeMsgIds.push(nodeMsg?.msgId!) log('转发消息失败,未找到消息', nodeId)
} continue
else {
if (nodeMsg?.peerUid !== selfInfo.uid) {
const cloneMsg = await this.cloneMsg(nodeMsg!)
if (cloneMsg) {
nodeMsgIds.push(cloneMsg.msgId)
}
}
} }
nodeMsgIds.push(nodeMsg.MsgId)
} }
else { else {
// 自定义的消息 // 自定义的消息
@@ -567,7 +498,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
try { try {
const { sendElements, deleteAfterSentFiles } = await createSendElements( const { sendElements, deleteAfterSentFiles } = await createSendElements(
convertMessage2List(messageNode.data.content), convertMessage2List(messageNode.data.content),
group, destPeer
) )
log('开始生成转发节点', sendElements) log('开始生成转发节点', sendElements)
let sendElementsSplit: SendMessageElement[][] = [] let sendElementsSplit: SendMessageElement[][] = []
@@ -593,7 +524,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
for (const eles of sendElementsSplit) { for (const eles of sendElementsSplit) {
const nodeMsg = await sendMsg(selfPeer, eles, [], true) const nodeMsg = await sendMsg(selfPeer, eles, [], true)
nodeMsgIds.push(nodeMsg.msgId) nodeMsgIds.push(nodeMsg.msgId)
await sleep(500) await sleep(400)
log('转发节点生成成功', nodeMsg.msgId) log('转发节点生成成功', nodeMsg.msgId)
} }
deleteAfterSentFiles.map((f) => fs.unlink(f, () => { deleteAfterSentFiles.map((f) => fs.unlink(f, () => {
@@ -605,33 +536,25 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
} }
// 检查srcPeer是否一致不一致则需要克隆成自己的消息, 让所有srcPeer都变成自己的使其保持一致才能够转发 // 检查srcPeer是否一致不一致则需要克隆成自己的消息, 让所有srcPeer都变成自己的使其保持一致才能够转发
let nodeMsgArray: Array<RawMessage> = [] const nodeMsgArray: RawMessage[] = []
let srcPeer: Peer | null = null let srcPeer: Peer | null = null
let needSendSelf = false let needSendSelf = false
for (const [index, msgId] of nodeMsgIds.entries()) { for (const msgId of nodeMsgIds) {
const nodeMsg = await dbUtil.getMsgByLongId(msgId) const nodeMsgPeer = await MessageUnique.getPeerByMsgId(msgId)
if (nodeMsg) { if (nodeMsgPeer) {
nodeMsgArray.push(nodeMsg) const nodeMsg = (await NTQQMsgApi.getMsgsByMsgId(nodeMsgPeer.Peer, [msgId])).msgList[0]
if (!srcPeer) { srcPeer = srcPeer ?? { chatType: nodeMsg.chatType, peerUid: nodeMsg.peerUid }
srcPeer = { chatType: nodeMsg.chatType, peerUid: nodeMsg.peerUid } if (srcPeer.peerUid !== nodeMsg.peerUid) {
}
else if (srcPeer.peerUid !== nodeMsg.peerUid) {
needSendSelf = true needSendSelf = true
srcPeer = selfPeer
} }
nodeMsgArray.push(nodeMsg)
} }
} }
log('nodeMsgArray', nodeMsgArray)
nodeMsgIds = nodeMsgArray.map((msg) => msg.msgId) nodeMsgIds = nodeMsgArray.map((msg) => msg.msgId)
if (needSendSelf) { if (needSendSelf) {
log('需要克隆转发消息') for (const msg of nodeMsgArray) {
for (const [index, msg] of nodeMsgArray.entries()) { if (msg.peerUid === selfPeer.peerUid) continue
if (msg.peerUid !== selfInfo.uid) { await this.cloneMsg(msg)
const cloneMsg = await this.cloneMsg(msg)
if (cloneMsg) {
nodeMsgIds[index] = cloneMsg.msgId
}
}
} }
} }
// elements之间用换行符分隔 // elements之间用换行符分隔
@@ -647,50 +570,10 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
if (nodeMsgIds.length === 0) { if (nodeMsgIds.length === 0) {
throw Error('转发消息失败,节点为空') throw Error('转发消息失败,节点为空')
} }
try { const returnMsg = await NTQQMsgApi.multiForwardMsg(srcPeer!, destPeer, nodeMsgIds)
log('开发转发', nodeMsgIds) returnMsg.msgShortId = MessageUnique.createMsg(destPeer, returnMsg.msgId)
return await NTQQMsgApi.multiForwardMsg(srcPeer!, destPeer, nodeMsgIds) return returnMsg
} catch (e) {
log('forward failed', e)
return null
}
} }
// private genMusicElement(url: string, audio: string, title: string, content: string, image: string): SendArkElement {
// const musicJson = {
// app: 'com.tencent.structmsg',
// config: {
// ctime: 1709689928,
// forward: 1,
// token: '5c1e4905f926dd3a64a4bd3841460351',
// type: 'normal',
// },
// extra: { app_type: 1, appid: 100497308, uin: selfInfo.uin },
// meta: {
// news: {
// action: '',
// android_pkg_name: '',
// app_type: 1,
// appid: 100497308,
// ctime: 1709689928,
// desc: content || title,
// jumpUrl: url,
// musicUrl: audio,
// preview: image,
// source_icon: 'https://p.qpic.cn/qqconnect/0/app_100497308_1626060999/100?max-age=2592000&t=0',
// source_url: '',
// tag: 'QQ音乐',
// title: title,
// uin: selfInfo.uin,
// },
// },
// prompt: content || title,
// ver: '0.0.0.1',
// view: 'news',
// }
// return SendMsgElementConstructor.ark(musicJson)
// }
} }
export default SendMsg export default SendMsg

View File

@@ -1,32 +1,36 @@
import { ActionName } from '../types' import { ActionName } from '../types'
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { dbUtil } from '../../../common/db' import { NTQQMsgApi } from '@/ntqqapi/api/msg'
import { NTQQMsgApi } from '../../../ntqqapi/api/msg' import { MessageUnique } from '@/common/utils/MessageUnique'
interface Payload { interface Payload {
message_id: number message_id: number | string
emoji_id: string emoji_id: number | string
} }
export class SetMsgEmojiLike extends BaseAction<Payload, any> { export class SetMsgEmojiLike extends BaseAction<Payload, any> {
actionName = ActionName.SetMsgEmojiLike actionName = ActionName.SetMsgEmojiLike
protected async _handle(payload: Payload) { protected async _handle(payload: Payload) {
let msg = await dbUtil.getMsgByShortId(payload.message_id) if (!payload.message_id) {
throw Error('message_id不能为空')
}
const msg = await MessageUnique.getMsgIdAndPeerByShortId(+payload.message_id)
if (!msg) { if (!msg) {
throw new Error('msg not found') throw new Error('msg not found')
} }
if (!payload.emoji_id) { if (!payload.emoji_id) {
throw new Error('emojiId not found') throw new Error('emojiId not found')
} }
const msgData = (await NTQQMsgApi.getMsgsByMsgId(msg.Peer, [msg.MsgId])).msgList
if (!msgData || msgData.length == 0 || !msgData[0].msgSeq) {
throw new Error('find msg by msgid error')
}
return await NTQQMsgApi.setEmojiLike( return await NTQQMsgApi.setEmojiLike(
{ msg.Peer,
chatType: msg.chatType, msgData[0].msgSeq,
peerUid: msg.peerUid, payload.emoji_id.toString(),
}, true
msg.msgSeq,
payload.emoji_id,
true,
) )
} }
} }

View File

@@ -4,13 +4,12 @@
import { OB11Message, OB11MessageAt, OB11MessageData, OB11MessageDataType } from '../types' import { OB11Message, OB11MessageAt, OB11MessageData, OB11MessageDataType } from '../types'
import { OB11FriendRequestEvent } from '../event/request/OB11FriendRequest' import { OB11FriendRequestEvent } from '../event/request/OB11FriendRequest'
import { OB11GroupRequestEvent } from '../event/request/OB11GroupRequest' import { OB11GroupRequestEvent } from '../event/request/OB11GroupRequest'
import { dbUtil } from '@/common/db' import { NTQQFriendApi, NTQQGroupApi, NTQQMsgApi, NTQQUserApi } from '@/ntqqapi/api'
import { NTQQFriendApi, NTQQGroupApi, NTQQMsgApi } from '@/ntqqapi/api' import { ChatType, GroupRequestOperateTypes, Peer } from '@/ntqqapi/types'
import { ChatType, Group, GroupRequestOperateTypes, Peer } from '@/ntqqapi/types'
import { getGroup, getUidByUin } from '@/common/data'
import { convertMessage2List, createSendElements, sendMsg } from './msg/SendMsg' import { convertMessage2List, createSendElements, sendMsg } from './msg/SendMsg'
import { isNull, log } from '@/common/utils' import { isNull, log } from '@/common/utils'
import { getConfigUtil } from '@/common/config' import { getConfigUtil } from '@/common/config'
import { MessageUnique } from '@/common/utils/MessageUnique'
interface QuickOperationPrivateMessage { interface QuickOperationPrivateMessage {
@@ -62,16 +61,14 @@ export async function handleQuickOperation(context: QuickOperationEvent, quickAc
} }
async function handleMsg(msg: OB11Message, quickAction: QuickOperationPrivateMessage | QuickOperationGroupMessage) { async function handleMsg(msg: OB11Message, quickAction: QuickOperationPrivateMessage | QuickOperationGroupMessage) {
msg = msg as OB11Message
const rawMessage = await dbUtil.getMsgByShortId(msg.message_id)
const reply = quickAction.reply const reply = quickAction.reply
const ob11Config = getConfigUtil().getConfig().ob11 const ob11Config = getConfigUtil().getConfig().ob11
let peer: Peer = { const peer: Peer = {
chatType: ChatType.friend, chatType: ChatType.friend,
peerUid: msg.user_id.toString(), peerUid: msg.user_id.toString(),
} }
if (msg.message_type == 'private') { if (msg.message_type == 'private') {
peer.peerUid = getUidByUin(msg.user_id.toString())! peer.peerUid = (await NTQQUserApi.getUidByUin(msg.user_id.toString()))!
if (msg.sub_type === 'group') { if (msg.sub_type === 'group') {
peer.chatType = ChatType.temp peer.chatType = ChatType.temp
} }
@@ -81,7 +78,6 @@ async function handleMsg(msg: OB11Message, quickAction: QuickOperationPrivateMes
peer.peerUid = msg.group_id?.toString()! peer.peerUid = msg.group_id?.toString()!
} }
if (reply) { if (reply) {
let group: Group | null = null
let replyMessage: OB11MessageData[] = [] let replyMessage: OB11MessageData[] = []
if (ob11Config.enableQOAutoQuote) { if (ob11Config.enableQOAutoQuote) {
replyMessage.push({ replyMessage.push({
@@ -93,7 +89,6 @@ async function handleMsg(msg: OB11Message, quickAction: QuickOperationPrivateMes
} }
if (msg.message_type == 'group') { if (msg.message_type == 'group') {
group = (await getGroup(msg.group_id?.toString()!))!
if ((quickAction as QuickOperationGroupMessage).at_sender) { if ((quickAction as QuickOperationGroupMessage).at_sender) {
replyMessage.push({ replyMessage.push({
type: 'at', type: 'at',
@@ -104,23 +99,26 @@ async function handleMsg(msg: OB11Message, quickAction: QuickOperationPrivateMes
} }
} }
replyMessage = replyMessage.concat(convertMessage2List(reply, quickAction.auto_escape)) replyMessage = replyMessage.concat(convertMessage2List(reply, quickAction.auto_escape))
const { sendElements, deleteAfterSentFiles } = await createSendElements(replyMessage, group!) const { sendElements, deleteAfterSentFiles } = await createSendElements(replyMessage, peer)
log(`发送消息给`, peer, sendElements)
sendMsg(peer, sendElements, deleteAfterSentFiles, false).then().catch(log) sendMsg(peer, sendElements, deleteAfterSentFiles, false).then().catch(log)
} }
if (msg.message_type === 'group') { if (msg.message_type === 'group') {
const groupMsgQuickAction = quickAction as QuickOperationGroupMessage const groupMsgQuickAction = quickAction as QuickOperationGroupMessage
const rawMessage = await MessageUnique.getMsgIdAndPeerByShortId(+(msg.message_id ?? 0))
if (!rawMessage) return
// handle group msg // handle group msg
if (groupMsgQuickAction.delete) { if (groupMsgQuickAction.delete) {
NTQQMsgApi.recallMsg(peer, [rawMessage?.msgId!]).then().catch(log) NTQQMsgApi.recallMsg(peer, [rawMessage.MsgId]).then().catch(log)
} }
if (groupMsgQuickAction.kick) { if (groupMsgQuickAction.kick) {
NTQQGroupApi.kickMember(peer.peerUid, [rawMessage?.senderUid!]).then().catch(log) const { msgList } = await NTQQMsgApi.getMsgsByMsgId(peer, [rawMessage.MsgId])
NTQQGroupApi.kickMember(peer.peerUid, [msgList[0].senderUid]).then().catch(log)
} }
if (groupMsgQuickAction.ban) { if (groupMsgQuickAction.ban) {
const { msgList } = await NTQQMsgApi.getMsgsByMsgId(peer, [rawMessage.MsgId])
NTQQGroupApi.banMember(peer.peerUid, [ NTQQGroupApi.banMember(peer.peerUid, [
{ {
uid: rawMessage?.senderUid!, uid: msgList[0].senderUid,
timeStamp: groupMsgQuickAction.ban_duration || 60 * 30, timeStamp: groupMsgQuickAction.ban_duration || 60 * 30,
}, },
]).then().catch(log) ]).then().catch(log)

View File

@@ -1,6 +1,6 @@
import { OB11User } from '../../types' import { OB11User } from '../../types'
import { OB11Constructor } from '../../constructor' import { OB11Constructor } from '../../constructor'
import { selfInfo } from '../../../common/data' import { getSelfInfo, getSelfNick } from '../../../common/data'
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
@@ -8,7 +8,10 @@ class GetLoginInfo extends BaseAction<null, OB11User> {
actionName = ActionName.GetLoginInfo actionName = ActionName.GetLoginInfo
protected async _handle(payload: null) { protected async _handle(payload: null) {
return OB11Constructor.selfInfo(selfInfo) return OB11Constructor.selfInfo({
...getSelfInfo(),
nick: await getSelfNick(true)
})
} }
} }

View File

@@ -1,14 +1,14 @@
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { OB11Status } from '../../types' import { OB11Status } from '../../types'
import { ActionName } from '../types' import { ActionName } from '../types'
import { selfInfo } from '../../../common/data' import { getSelfInfo } from '../../../common/data'
export default class GetStatus extends BaseAction<any, OB11Status> { export default class GetStatus extends BaseAction<any, OB11Status> {
actionName = ActionName.GetStatus actionName = ActionName.GetStatus
protected async _handle(payload: any): Promise<OB11Status> { protected async _handle(payload: any): Promise<OB11Status> {
return { return {
online: selfInfo.online!, online: getSelfInfo().online!,
good: true, good: true,
} }
} }

View File

@@ -73,4 +73,5 @@ export enum ActionName {
GetGroupHonorInfo = "get_group_honor_info", GetGroupHonorInfo = "get_group_honor_info",
GoCQHTTP_SetEssenceMsg = 'set_essence_msg', GoCQHTTP_SetEssenceMsg = 'set_essence_msg',
GoCQHTTP_DelEssenceMsg = 'delete_essence_msg', GoCQHTTP_DelEssenceMsg = 'delete_essence_msg',
GoCQHTTP_DelGroupFile = 'delete_group_file',
} }

View File

@@ -1,16 +1,27 @@
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { NTQQUserApi } from '@/ntqqapi/api' import { NTQQUserApi, WebApi } from '@/ntqqapi/api'
import { ActionName } from '../types' import { ActionName } from '../types'
interface Response {
cookies: string
bkn: string
}
interface Payload { interface Payload {
domain: string domain: string
} }
export class GetCookies extends BaseAction<Payload, { cookies: string; bkn: string }> { export class GetCookies extends BaseAction<Payload, Response> {
actionName = ActionName.GetCookies actionName = ActionName.GetCookies
protected async _handle(payload: Payload) { protected async _handle(payload: Payload) {
const domain = payload.domain || 'qun.qq.com' if (!payload.domain) {
return NTQQUserApi.getCookies(domain); throw '缺少参数 domain'
}
const cookiesObject = await NTQQUserApi.getCookies(payload.domain)
//把获取到的cookiesObject转换成 k=v; 格式字符串拼接在一起
const cookies = Object.entries(cookiesObject).map(([key, value]) => `${key}=${value}`).join('; ')
const bkn = cookiesObject.skey ? WebApi.genBkn(cookiesObject.skey) : ''
return { cookies, bkn }
} }
} }

View File

@@ -1,11 +1,9 @@
import BaseAction from '../BaseAction'
import { OB11User } from '../../types' import { OB11User } from '../../types'
import { OB11Constructor } from '../../constructor' import { OB11Constructor } from '../../constructor'
import { friends, rawFriends } from '@/common/data'
import BaseAction from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { NTQQFriendApi } from '@/ntqqapi/api' import { NTQQFriendApi } from '@/ntqqapi/api'
import { CategoryFriend } from '@/ntqqapi/types' import { getBuildVersion } from '@/common/utils/QQBasicInfo'
import { qqPkgInfo } from '@/common/utils/QQBasicInfo'
interface Payload { interface Payload {
no_cache: boolean | string no_cache: boolean | string
@@ -15,26 +13,24 @@ export class GetFriendList extends BaseAction<Payload, OB11User[]> {
actionName = ActionName.GetFriendList actionName = ActionName.GetFriendList
protected async _handle(payload: Payload) { protected async _handle(payload: Payload) {
if (+qqPkgInfo.buildVersion >= 26702) { const refresh = payload?.no_cache === true || payload?.no_cache === 'true'
return OB11Constructor.friendsV2(await NTQQFriendApi.getBuddyV2(payload?.no_cache === true || payload?.no_cache === 'true')) if (getBuildVersion() >= 26702) {
return OB11Constructor.friendsV2(await NTQQFriendApi.getBuddyV2(refresh))
} }
if (friends.length === 0 || payload?.no_cache === true || payload?.no_cache === 'true') { return OB11Constructor.friends(await NTQQFriendApi.getFriends(refresh))
const _friends = await NTQQFriendApi.getFriends(true)
// log('强制刷新好友列表,结果: ', _friends)
if (_friends.length > 0) {
friends.length = 0
friends.push(..._friends)
}
}
return OB11Constructor.friends(friends)
} }
} }
// extend
export class GetFriendWithCategory extends BaseAction<void, Array<CategoryFriend>> { export class GetFriendWithCategory extends BaseAction<void, any> {
actionName = ActionName.GetFriendsWithCategory; actionName = ActionName.GetFriendsWithCategory
protected async _handle(payload: void) { protected async _handle(payload: void) {
return rawFriends; if (getBuildVersion() >= 26702) {
//全新逻辑
return OB11Constructor.friendsV2(await NTQQFriendApi.getBuddyV2ExWithCate(true))
} else {
throw new Error('this ntqq version not support, must be 26702 or later')
}
} }
} }

View File

@@ -1,31 +1,22 @@
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { getFriend, getUidByUin, uidMaps } from '../../../common/data'
import { ActionName } from '../types' import { ActionName } from '../types'
import { NTQQFriendApi } from '../../../ntqqapi/api/friend' import { NTQQUserApi } from '@/ntqqapi/api'
import { log } from '../../../common/utils/log'
interface Payload { interface Payload {
user_id: number user_id: number | string
times: number times: number | string
} }
export default class SendLike extends BaseAction<Payload, null> { export default class SendLike extends BaseAction<Payload, null> {
actionName = ActionName.SendLike actionName = ActionName.SendLike
protected async _handle(payload: Payload): Promise<null> { protected async _handle(payload: Payload): Promise<null> {
log('点赞参数', payload)
try { try {
const qq = payload.user_id.toString() const qq = payload.user_id.toString()
const friend = await getFriend(qq) const uid: string = await NTQQUserApi.getUidByUin(qq) || ''
let uid: string const result = await NTQQUserApi.like(uid, +payload.times || 1)
if (!friend) { if (result?.result !== 0) {
uid = getUidByUin(qq)! throw Error(result?.errMsg)
} else {
uid = friend.uid
}
let result = await NTQQFriendApi.likeFriend(uid, parseInt(payload.times?.toString()) || 1)
if (result.result !== 0) {
throw result.errMsg
} }
} catch (e) { } catch (e) {
throw `点赞失败 ${e}` throw `点赞失败 ${e}`

View File

@@ -4,7 +4,7 @@ import { NTQQFriendApi } from '../../../ntqqapi/api/friend'
interface Payload { interface Payload {
flag: string flag: string
approve: boolean approve?: boolean | string
remark?: string remark?: string
} }
@@ -12,7 +12,7 @@ export default class SetFriendAddRequest extends BaseAction<Payload, null> {
actionName = ActionName.SetFriendAddRequest actionName = ActionName.SetFriendAddRequest
protected async _handle(payload: Payload): Promise<null> { protected async _handle(payload: Payload): Promise<null> {
const approve = payload.approve.toString() === 'true' const approve = payload.approve?.toString() !== 'false'
await NTQQFriendApi.handleFriendRequest(payload.flag, approve) await NTQQFriendApi.handleFriendRequest(payload.flag, approve)
return null return null
} }

View File

@@ -17,26 +17,22 @@ import {
Group, Group,
Peer, Peer,
GroupMember, GroupMember,
PicType,
RawMessage, RawMessage,
SelfInfo, SelfInfo,
Sex, Sex,
TipGroupElementType, TipGroupElementType,
User, User,
VideoElement, FriendV2,
FriendV2 ChatType2
} from '../ntqqapi/types' } from '../ntqqapi/types'
import { deleteGroup, getFriend, getGroupMember, selfInfo, tempGroupCodeMap, uidMaps } from '../common/data' import { getGroupMember, getSelfUin } from '../common/data'
import { EventType } from './event/OB11BaseEvent' import { EventType } from './event/OB11BaseEvent'
import { encodeCQCode } from './cqcode' import { encodeCQCode } from './cqcode'
import { dbUtil } from '../common/db' import { MessageUnique } from '../common/utils/MessageUnique'
import { OB11GroupIncreaseEvent } from './event/notice/OB11GroupIncreaseEvent' import { OB11GroupIncreaseEvent } from './event/notice/OB11GroupIncreaseEvent'
import { OB11GroupBanEvent } from './event/notice/OB11GroupBanEvent' import { OB11GroupBanEvent } from './event/notice/OB11GroupBanEvent'
import { OB11GroupUploadNoticeEvent } from './event/notice/OB11GroupUploadNoticeEvent' import { OB11GroupUploadNoticeEvent } from './event/notice/OB11GroupUploadNoticeEvent'
import { OB11GroupNoticeEvent } from './event/notice/OB11GroupNoticeEvent' import { OB11GroupNoticeEvent } from './event/notice/OB11GroupNoticeEvent'
import { NTQQUserApi } from '../ntqqapi/api/user'
import { NTQQFileApi } from '../ntqqapi/api/file'
import { NTQQMsgApi } from '../ntqqapi/api/msg'
import { calcQQLevel } from '../common/utils/qqlevel' import { calcQQLevel } from '../common/utils/qqlevel'
import { log } from '../common/utils/log' import { log } from '../common/utils/log'
import { isNull, sleep } from '../common/utils/helper' import { isNull, sleep } from '../common/utils/helper'
@@ -44,7 +40,7 @@ import { getConfigUtil } from '../common/config'
import { OB11GroupTitleEvent } from './event/notice/OB11GroupTitleEvent' import { OB11GroupTitleEvent } from './event/notice/OB11GroupTitleEvent'
import { OB11GroupCardEvent } from './event/notice/OB11GroupCardEvent' import { OB11GroupCardEvent } from './event/notice/OB11GroupCardEvent'
import { OB11GroupDecreaseEvent } from './event/notice/OB11GroupDecreaseEvent' import { OB11GroupDecreaseEvent } from './event/notice/OB11GroupDecreaseEvent'
import { NTQQGroupApi } from '../ntqqapi/api' import { NTQQGroupApi, NTQQUserApi, NTQQFileApi, NTQQMsgApi } from '../ntqqapi/api'
import { OB11GroupMsgEmojiLikeEvent } from './event/notice/OB11MsgEmojiLikeEvent' import { OB11GroupMsgEmojiLikeEvent } from './event/notice/OB11MsgEmojiLikeEvent'
import { mFaceCache } from '../ntqqapi/constructor' import { mFaceCache } from '../ntqqapi/constructor'
import { OB11FriendAddNoticeEvent } from './event/notice/OB11FriendAddNoticeEvent' import { OB11FriendAddNoticeEvent } from './event/notice/OB11FriendAddNoticeEvent'
@@ -53,8 +49,7 @@ import { OB11GroupRecallNoticeEvent } from './event/notice/OB11GroupRecallNotice
import { OB11FriendPokeEvent, OB11GroupPokeEvent } from './event/notice/OB11PokeEvent' import { OB11FriendPokeEvent, OB11GroupPokeEvent } from './event/notice/OB11PokeEvent'
import { OB11BaseNoticeEvent } from './event/notice/OB11BaseNoticeEvent' import { OB11BaseNoticeEvent } from './event/notice/OB11BaseNoticeEvent'
import { OB11GroupEssenceEvent } from './event/notice/OB11GroupEssenceEvent' import { OB11GroupEssenceEvent } from './event/notice/OB11GroupEssenceEvent'
import { omit } from 'cosmokit'
let lastRKeyUpdateTime = 0
export class OB11Constructor { export class OB11Constructor {
static async message(msg: RawMessage): Promise<OB11Message> { static async message(msg: RawMessage): Promise<OB11Message> {
@@ -64,15 +59,15 @@ export class OB11Constructor {
debug, debug,
ob11: { messagePostFormat }, ob11: { messagePostFormat },
} = config } = config
const message_type = msg.chatType == ChatType.group ? 'group' : 'private' const selfUin = getSelfUin()
const resMsg: OB11Message = { const resMsg: OB11Message = {
self_id: parseInt(selfInfo.uin), self_id: parseInt(selfUin),
user_id: parseInt(msg.senderUin!), user_id: parseInt(msg.senderUin!),
time: parseInt(msg.msgTime) || Date.now(), time: parseInt(msg.msgTime) || Date.now(),
message_id: msg.msgShortId!, message_id: msg.msgShortId!,
real_id: msg.msgShortId!, real_id: msg.msgShortId!,
message_seq: msg.msgShortId!, message_seq: msg.msgShortId!,
message_type: msg.chatType == ChatType.group ? 'group' : 'private', message_type: msg.chatType === ChatType.group ? 'group' : 'private',
sender: { sender: {
user_id: parseInt(msg.senderUin!), user_id: parseInt(msg.senderUin!),
nickname: msg.sendNickName, nickname: msg.sendNickName,
@@ -83,7 +78,7 @@ export class OB11Constructor {
sub_type: 'friend', sub_type: 'friend',
message: messagePostFormat === 'string' ? '' : [], message: messagePostFormat === 'string' ? '' : [],
message_format: messagePostFormat === 'string' ? 'string' : 'array', message_format: messagePostFormat === 'string' ? 'string' : 'array',
post_type: selfInfo.uin == msg.senderUin ? EventType.MESSAGE_SENT : EventType.MESSAGE, post_type: selfUin == msg.senderUin ? EventType.MESSAGE_SENT : EventType.MESSAGE,
} }
if (debug) { if (debug) {
resMsg.raw = msg resMsg.raw = msg
@@ -99,16 +94,17 @@ export class OB11Constructor {
} }
else if (msg.chatType == ChatType.friend) { else if (msg.chatType == ChatType.friend) {
resMsg.sub_type = 'friend' resMsg.sub_type = 'friend'
const friend = await getFriend(msg.senderUin!) resMsg.sender.nickname = (await NTQQUserApi.getUserDetailInfo(msg.senderUid)).nick
if (friend) {
resMsg.sender.nickname = friend.nick
}
} }
else if (msg.chatType == ChatType.temp) { else if (msg.chatType as unknown as ChatType2 == ChatType2.KCHATTYPETEMPC2CFROMGROUP) {
resMsg.sub_type = 'group' resMsg.sub_type = 'group'
const tempGroupCode = tempGroupCodeMap[msg.peerUin] const ret = await NTQQMsgApi.getTempChatInfo(ChatType2.KCHATTYPETEMPC2CFROMGROUP, msg.senderUid)
if (tempGroupCode) { if (ret?.result === 0) {
resMsg.group_id = parseInt(tempGroupCode) resMsg.group_id = parseInt(ret.tmpChatInfo!.groupCode)
resMsg.sender.nickname = ret.tmpChatInfo!.fromNick
} else {
resMsg.group_id = 284840486 //兜底数据
resMsg.sender.nickname = '临时会话'
} }
} }
@@ -155,115 +151,121 @@ export class OB11Constructor {
} }
else if (element.replyElement) { else if (element.replyElement) {
message_data['type'] = OB11MessageDataType.reply message_data['type'] = OB11MessageDataType.reply
// log("收到回复消息", element.replyElement.replayMsgSeq)
try { try {
const replyMsg = await dbUtil.getMsgBySeqId(element.replyElement.replayMsgSeq) const records = msg.records.find(msgRecord => msgRecord.msgId === element.replyElement.sourceMsgIdInRecords)
// log("找到回复消息", replyMsg.msgShortId, replyMsg.msgId) if (!records) throw new Error('找不到回复消息')
if (replyMsg) { let replyMsg = (await NTQQMsgApi.getMsgsBySeqAndCount({
message_data['data']['id'] = replyMsg.msgShortId?.toString() peerUid: msg.peerUid,
guildId: '',
chatType: msg.chatType,
}, element.replyElement.replayMsgSeq, 1, true, true))?.msgList[0]
if (!replyMsg || records.msgRandom !== replyMsg.msgRandom) {
const peer = {
chatType: msg.chatType,
peerUid: msg.peerUid,
guildId: '',
}
replyMsg = (await NTQQMsgApi.getSingleMsg(peer, element.replyElement.replayMsgSeq))?.msgList[0]
} }
else { // 284840486: 合并消息内侧 消息具体定位不到
continue if ((!replyMsg || records.msgRandom !== replyMsg.msgRandom) && msg.peerUin !== '284840486') {
throw new Error('回复消息消息验证失败')
} }
message_data['data']['id'] = replyMsg && MessageUnique.createMsg({
peerUid: msg.peerUid,
guildId: '',
chatType: msg.chatType,
}, replyMsg.msgId)?.toString()
} catch (e: any) { } catch (e: any) {
log('获取不到引用的消息', e.stack, element.replyElement.replayMsgSeq) log('获取不到引用的消息', e.stack, element.replyElement.replayMsgSeq)
continue
} }
} }
else if (element.picElement) { else if (element.picElement) {
message_data['type'] = OB11MessageDataType.image message_data['type'] = OB11MessageDataType.image
// message_data["data"]["file"] = element.picElement.sourcePath const { picElement } = element
let fileName = element.picElement.fileName /*let fileName = picElement.fileName
const sourcePath = element.picElement.sourcePath const isGif = picElement.picType === PicType.gif
const isGif = element.picElement.picType === PicType.gif
if (isGif && !fileName.endsWith('.gif')) { if (isGif && !fileName.endsWith('.gif')) {
fileName += '.gif' fileName += '.gif'
} }*/
message_data['data']['file'] = fileName message_data['data']['file'] = picElement.fileName
message_data['data']['subType'] = element.picElement.picSubType message_data['data']['subType'] = picElement.picSubType
// message_data["data"]["path"] = element.picElement.sourcePath //message_data['data']['file_id'] = picElement.fileUuid
// let currentRKey = "CAQSKAB6JWENi5LMk0kc62l8Pm3Jn1dsLZHyRLAnNmHGoZ3y_gDZPqZt-64" message_data['data']['url'] = await NTQQFileApi.getImageUrl(picElement)
message_data['data']['file_size'] = picElement.fileSize
message_data['data']['url'] = await NTQQFileApi.getImageUrl(element.picElement, msg.chatType) MessageUnique.addFileCache({
// message_data["data"]["file_id"] = element.picElement.fileUuid peerUid: msg.peerUid,
message_data['data']['file_size'] = element.picElement.fileSize msgId: msg.msgId,
dbUtil msgTime: +msg.msgTime,
.addFileCache(fileName, { chatType: msg.chatType,
fileName, elementId: element.elementId,
elementId: element.elementId, elementType: element.elementType,
filePath: sourcePath, fileName: picElement.fileName,
fileSize: element.picElement.fileSize.toString(), fileSize: String(picElement.fileSize || '0'),
url: message_data['data']['url'], fileUuid: picElement.fileUuid
downloadFunc: async () => { })
await NTQQFileApi.downloadMedia(
msg.msgId,
msg.chatType,
msg.peerUid,
element.elementId,
element.picElement.thumbPath?.get(0) || '',
element.picElement.sourcePath,
)
},
}).then()
} }
else if (element.videoElement || element.fileElement) { else if (element.videoElement) {
const videoOrFileElement = element.videoElement || element.fileElement message_data['type'] = OB11MessageDataType.video
const ob11MessageDataType = element.videoElement ? OB11MessageDataType.video : OB11MessageDataType.file const { videoElement } = element
message_data['type'] = ob11MessageDataType message_data['data']['file'] = videoElement.fileName
message_data['data']['file'] = videoOrFileElement.fileName message_data['data']['path'] = videoElement.filePath
message_data['data']['path'] = videoOrFileElement.filePath //message_data['data']['file_id'] = videoElement.fileUuid
message_data['data']['file_id'] = videoOrFileElement.fileUuid message_data['data']['file_size'] = videoElement.fileSize
message_data['data']['file_size'] = videoOrFileElement.fileSize message_data['data']['url'] = await NTQQFileApi.getVideoUrl({
if (element.videoElement) { chatType: msg.chatType,
message_data['data']['url'] = await NTQQFileApi.getVideoUrl({ peerUid: msg.peerUid,
chatType: msg.chatType, }, msg.msgId, element.elementId)
peerUid: msg.peerUid, MessageUnique.addFileCache({
}, msg.msgId, element.elementId, peerUid: msg.peerUid,
) msgId: msg.msgId,
} msgTime: +msg.msgTime,
dbUtil chatType: msg.chatType,
.addFileCache(videoOrFileElement.fileUuid!, { elementId: element.elementId,
msgId: msg.msgId, elementType: element.elementType,
elementId: element.elementId, fileName: videoElement.fileName,
fileName: videoOrFileElement.fileName, fileSize: String(videoElement.fileSize || '0'),
filePath: videoOrFileElement.filePath, fileUuid: videoElement.fileUuid!
fileSize: videoOrFileElement.fileSize!, })
downloadFunc: async () => { }
await NTQQFileApi.downloadMedia( else if (element.fileElement) {
msg.msgId, message_data['type'] = OB11MessageDataType.file
msg.chatType, const { fileElement } = element
msg.peerUid, message_data['data']['file'] = fileElement.fileName
element.elementId, message_data['data']['path'] = fileElement.filePath
ob11MessageDataType == OB11MessageDataType.video message_data['data']['file_id'] = fileElement.fileUuid
? (videoOrFileElement as VideoElement).thumbPath?.get(0) message_data['data']['file_size'] = fileElement.fileSize
: null, MessageUnique.addFileCache({
videoOrFileElement.filePath, peerUid: msg.peerUid,
) msgId: msg.msgId,
}, msgTime: +msg.msgTime,
}) chatType: msg.chatType,
.then() elementId: element.elementId,
// 怎么拿到url呢 elementType: element.elementType,
fileName: fileElement.fileName,
fileSize: String(fileElement.fileSize || '0'),
fileUuid: fileElement.fileUuid!
})
} }
else if (element.pttElement) { else if (element.pttElement) {
message_data['type'] = OB11MessageDataType.voice message_data['type'] = OB11MessageDataType.voice
message_data['data']['file'] = element.pttElement.fileName const { pttElement } = element
message_data['data']['path'] = element.pttElement.filePath message_data['data']['file'] = pttElement.fileName
// message_data["data"]["file_id"] = element.pttElement.fileUuid message_data['data']['path'] = pttElement.filePath
message_data['data']['file_size'] = element.pttElement.fileSize //message_data['data']['file_id'] = pttElement.fileUuid
dbUtil message_data['data']['file_size'] = pttElement.fileSize
.addFileCache(element.pttElement.fileName, { MessageUnique.addFileCache({
elementId: element.elementId, peerUid: msg.peerUid,
fileName: element.pttElement.fileName, msgId: msg.msgId,
filePath: element.pttElement.filePath, msgTime: +msg.msgTime,
fileSize: element.pttElement.fileSize, chatType: msg.chatType,
}) elementId: element.elementId,
.then() elementType: element.elementType,
fileName: pttElement.fileName,
// log("收到语音消息", msg) fileSize: String(pttElement.fileSize || '0'),
// window.LLAPI.Ptt2Text(message.raw.msgId, message.peer, messages).then(text => { fileUuid: pttElement.fileUuid
// console.log("语音转文字结果", text) })
// }).catch(err => {
// console.log("语音转文字失败", err)
// })
} }
else if (element.arkElement) { else if (element.arkElement) {
message_data['type'] = OB11MessageDataType.json message_data['type'] = OB11MessageDataType.json
@@ -336,7 +338,11 @@ export class OB11Constructor {
//筛选item带有uid的元素 //筛选item带有uid的元素
const poke_uid = pokedetail.filter(item => item.uid) const poke_uid = pokedetail.filter(item => item.uid)
if (poke_uid.length == 2) { if (poke_uid.length == 2) {
return new OB11FriendPokeEvent(parseInt((uidMaps[poke_uid[0].uid])!), parseInt((uidMaps[poke_uid[1].uid])), pokedetail) return new OB11FriendPokeEvent(
parseInt(await NTQQUserApi.getUinByUid(poke_uid[0].uid)),
parseInt(await NTQQUserApi.getUinByUid(poke_uid[1].uid)),
pokedetail
)
} }
} }
//下面得改 上面也是错的grayTipElement.subElementType == GrayTipElementSubType.MEMBER_NEW_TITLE //下面得改 上面也是错的grayTipElement.subElementType == GrayTipElementSubType.MEMBER_NEW_TITLE
@@ -373,7 +379,7 @@ export class OB11Constructor {
const groupElement = grayTipElement?.groupElement const groupElement = grayTipElement?.groupElement
if (groupElement) { if (groupElement) {
// log("收到群提示消息", groupElement) // log("收到群提示消息", groupElement)
if (groupElement.type == TipGroupElementType.memberIncrease) { if (groupElement.type === TipGroupElementType.memberIncrease) {
log('收到群成员增加消息', groupElement) log('收到群成员增加消息', groupElement)
await sleep(1000) await sleep(1000)
const member = await getGroupMember(msg.peerUid, groupElement.memberUid) const member = await getGroupMember(msg.peerUid, groupElement.memberUid)
@@ -421,24 +427,26 @@ export class OB11Constructor {
) )
} }
} }
else if (groupElement.type == TipGroupElementType.kicked) { else if (groupElement.type === TipGroupElementType.kicked) {
log(`收到我被踢出或退群提示, 群${msg.peerUid}`, groupElement) log(`收到我被踢出或退群提示, 群${msg.peerUid}`, groupElement)
deleteGroup(msg.peerUid)
NTQQGroupApi.quitGroup(msg.peerUid).then() NTQQGroupApi.quitGroup(msg.peerUid).then()
try { try {
const adminUin = const adminUin = (await getGroupMember(msg.peerUid, groupElement.adminUid))?.uin || (await NTQQUserApi.getUidByUin(groupElement.adminUid))
(await getGroupMember(msg.peerUid, groupElement.adminUid))?.uin ||
(await NTQQUserApi.getUserDetailInfo(groupElement.adminUid))?.uin
if (adminUin) { if (adminUin) {
return new OB11GroupDecreaseEvent( return new OB11GroupDecreaseEvent(
parseInt(msg.peerUid), parseInt(msg.peerUid),
parseInt(selfInfo.uin), parseInt(getSelfUin()),
parseInt(adminUin), parseInt(adminUin),
'kick_me', 'kick_me'
) )
} }
} catch (e) { } catch (e) {
return new OB11GroupDecreaseEvent(parseInt(msg.peerUid), parseInt(selfInfo.uin), 0, 'leave') return new OB11GroupDecreaseEvent(
parseInt(msg.peerUid),
parseInt(getSelfUin()),
0,
'leave'
)
} }
} }
} }
@@ -472,16 +480,27 @@ export class OB11Constructor {
const senderUin = emojiLikeData.gtip.qq.jp const senderUin = emojiLikeData.gtip.qq.jp
const msgSeq = emojiLikeData.gtip.url.msgseq const msgSeq = emojiLikeData.gtip.url.msgseq
const emojiId = emojiLikeData.gtip.face.id const emojiId = emojiLikeData.gtip.face.id
const msg = await dbUtil.getMsgBySeqId(msgSeq) const replyMsgList = (await NTQQMsgApi.getMsgsBySeqAndCount({
if (!msg) { chatType: ChatType.group,
guildId: '',
peerUid: msg.peerUid,
}, msgSeq, 1, true, true))?.msgList
if (!replyMsgList?.length) {
return return
} }
return new OB11GroupMsgEmojiLikeEvent(parseInt(msg.peerUid), parseInt(senderUin), msg.msgShortId!, [ const likes = [
{ {
emoji_id: emojiId, emoji_id: emojiId,
count: 1, count: 1,
}, },
]) ]
const shortId = MessageUnique.getShortIdByMsgId(replyMsgList[0].msgId)
return new OB11GroupMsgEmojiLikeEvent(
parseInt(msg.peerUid),
parseInt(senderUin),
shortId!,
likes
)
} catch (e: any) { } catch (e: any) {
log('解析表情回应消息失败', e.stack) log('解析表情回应消息失败', e.stack)
} }
@@ -541,7 +560,12 @@ export class OB11Constructor {
//筛选item带有uid的元素 //筛选item带有uid的元素
const poke_uid = pokedetail.filter(item => item.uid) const poke_uid = pokedetail.filter(item => item.uid)
if (poke_uid.length == 2) { if (poke_uid.length == 2) {
return new OB11GroupPokeEvent(parseInt(msg.peerUid), parseInt((uidMaps[poke_uid[0].uid])!), parseInt((uidMaps[poke_uid[1].uid])), pokedetail) return new OB11GroupPokeEvent(
parseInt(msg.peerUid),
parseInt(await NTQQUserApi.getUinByUid(poke_uid[0].uid)),
parseInt(await NTQQUserApi.getUinByUid(poke_uid[1].uid)),
pokedetail
)
} }
} }
if (grayTipElement.jsonGrayTipElement.busiId == 2401) { if (grayTipElement.jsonGrayTipElement.busiId == 2401) {
@@ -549,20 +573,26 @@ export class OB11Constructor {
const searchParams = new URL(json.items[0].jp).searchParams const searchParams = new URL(json.items[0].jp).searchParams
const msgSeq = searchParams.get('msgSeq')! const msgSeq = searchParams.get('msgSeq')!
const Group = searchParams.get('groupCode') const Group = searchParams.get('groupCode')
const Businessid = searchParams.get('businessid')
const Peer: Peer = { const Peer: Peer = {
guildId: '', guildId: '',
chatType: ChatType.group, chatType: ChatType.group,
peerUid: Group! peerUid: Group!
} }
let msgList = (await NTQQMsgApi.getMsgsBySeqAndCount(Peer, msgSeq.toString(), 1, true, true)).msgList const msgList = (await NTQQMsgApi.getMsgsBySeqAndCount(Peer, msgSeq.toString(), 1, true, true))?.msgList
const origMsg = await dbUtil.getMsgByLongId(msgList[0].msgId) if (!msgList?.length) {
const postMsg = await dbUtil.getMsgBySeqId(origMsg?.msgSeq!) ?? origMsg return
// 如果 senderUin 为 0可能是 历史消息 或 自身消息
if (msgList[0].senderUin === '0') {
msgList[0].senderUin = postMsg?.senderUin ?? selfInfo.uin
} }
return new OB11GroupEssenceEvent(parseInt(msg.peerUid), postMsg?.msgShortId!, parseInt(msgList[0].senderUin)) //const origMsg = await dbUtil.getMsgByLongId(msgList[0].msgId)
//const postMsg = await dbUtil.getMsgBySeqId(origMsg?.msgSeq!) ?? origMsg
// 如果 senderUin 为 0可能是 历史消息 或 自身消息
//if (msgList[0].senderUin === '0') {
//msgList[0].senderUin = postMsg?.senderUin ?? getSelfUin()
//}
return new OB11GroupEssenceEvent(
parseInt(msg.peerUid),
MessageUnique.getShortIdByMsgId(msgList[0].msgId)!,
parseInt(msgList[0].senderUin!)
)
// 获取MsgSeq+Peer可获取具体消息 // 获取MsgSeq+Peer可获取具体消息
} }
if (grayTipElement.jsonGrayTipElement.busiId == 2407) { if (grayTipElement.jsonGrayTipElement.busiId == 2407) {
@@ -583,27 +613,26 @@ export class OB11Constructor {
static async RecallEvent( static async RecallEvent(
msg: RawMessage, msg: RawMessage,
shortId: number
): Promise<OB11FriendRecallNoticeEvent | OB11GroupRecallNoticeEvent | undefined> { ): Promise<OB11FriendRecallNoticeEvent | OB11GroupRecallNoticeEvent | undefined> {
let msgElement = msg.elements.find( const msgElement = msg.elements.find(
(element) => element.grayTipElement?.subElementType === GrayTipElementSubType.RECALL, (element) => element.grayTipElement?.subElementType === GrayTipElementSubType.RECALL,
) )
if (!msgElement) { if (!msgElement) {
return return
} }
const isGroup = msg.chatType === ChatType.group
const revokeElement = msgElement.grayTipElement.revokeElement const revokeElement = msgElement.grayTipElement.revokeElement
if (isGroup) { if (msg.chatType === ChatType.group) {
const operator = await getGroupMember(msg.peerUid, revokeElement.operatorUid) const operator = await getGroupMember(msg.peerUid, revokeElement.operatorUid)
const sender = await getGroupMember(msg.peerUid, revokeElement.origMsgSenderUid!)
return new OB11GroupRecallNoticeEvent( return new OB11GroupRecallNoticeEvent(
parseInt(msg.peerUid), parseInt(msg.peerUid),
parseInt(sender?.uin!), parseInt(msg.senderUin!),
parseInt(operator?.uin!), parseInt(operator?.uin || msg.senderUin!),
msg.msgShortId!, shortId,
) )
} }
else { else {
return new OB11FriendRecallNoticeEvent(parseInt(msg.senderUin!), msg.msgShortId!) return new OB11FriendRecallNoticeEvent(parseInt(msg.senderUin!), shortId)
} }
} }
@@ -633,7 +662,7 @@ export class OB11Constructor {
for (const friend of friends) { for (const friend of friends) {
const sexValue = this.sex(friend.baseInfo.sex!) const sexValue = this.sex(friend.baseInfo.sex!)
data.push({ data.push({
...friend.baseInfo, ...omit(friend.baseInfo, ['richBuffer']),
...friend.coreInfo, ...friend.coreInfo,
user_id: parseInt(friend.coreInfo.uin), user_id: parseInt(friend.coreInfo.uin),
nickname: friend.coreInfo.nick, nickname: friend.coreInfo.nick,
@@ -673,7 +702,7 @@ export class OB11Constructor {
sex: OB11Constructor.sex(member.sex!), sex: OB11Constructor.sex(member.sex!),
age: 0, age: 0,
area: '', area: '',
level: 0, level: '0',
qq_level: (member.qqLevel && calcQQLevel(member.qqLevel)) || 0, qq_level: (member.qqLevel && calcQQLevel(member.qqLevel)) || 0,
join_time: 0, // 暂时没法获取 join_time: 0, // 暂时没法获取
last_sent_time: 0, // 暂时没法获取 last_sent_time: 0, // 暂时没法获取

View File

@@ -1,4 +1,4 @@
import { selfInfo } from '../../common/data' import { getSelfUin } from '../../common/data'
export enum EventType { export enum EventType {
META = 'meta_event', META = 'meta_event',
@@ -10,6 +10,6 @@ export enum EventType {
export abstract class OB11BaseEvent { export abstract class OB11BaseEvent {
time = Math.floor(Date.now() / 1000) time = Math.floor(Date.now() / 1000)
self_id = parseInt(selfInfo.uin) self_id = parseInt(getSelfUin())
abstract post_type: EventType abstract post_type: EventType
} }

View File

@@ -10,23 +10,27 @@ abstract class OB11PokeEvent extends OB11BaseNoticeEvent {
export class OB11FriendPokeEvent extends OB11PokeEvent { export class OB11FriendPokeEvent extends OB11PokeEvent {
user_id: number user_id: number
raw_info: any
constructor(user_id: number, target_id: number, raw_message: any) { constructor(user_id: number, target_id: number, raw_message: any) {
super(); super()
this.target_id = target_id; this.target_id = target_id
this.user_id = user_id; this.user_id = user_id
this.raw_message = raw_message; // raw_message nb等框架标准为string
this.raw_info = raw_message
} }
} }
export class OB11GroupPokeEvent extends OB11PokeEvent { export class OB11GroupPokeEvent extends OB11PokeEvent {
user_id: number user_id: number
group_id: number group_id: number
raw_info: any
constructor(group_id: number, user_id: number = 0, target_id: number = 0, raw_message: any) { constructor(group_id: number, user_id: number = 0, target_id: number = 0, raw_message: any) {
super() super()
this.group_id = group_id this.group_id = group_id
this.target_id = target_id this.target_id = target_id
this.user_id = user_id this.user_id = user_id
this.raw_message = raw_message this.raw_info = raw_message
} }
} }

View File

@@ -1,11 +1,11 @@
import { Response } from 'express' import { Response } from 'express'
import { OB11Response } from '../action/OB11Response' import { OB11Response } from '../action/OB11Response'
import { HttpServerBase } from '@/common/server/http' import { HttpServerBase } from '@/common/server/http'
import { actionHandlers, actionMap } from '../action' import { actionMap } from '../action'
import { getConfigUtil } from '@/common/config' import { getConfigUtil } from '@/common/config'
import { postOb11Event } from './post-ob11-event' import { postOb11Event } from './post-ob11-event'
import { OB11HeartbeatEvent } from '../event/meta/OB11HeartbeatEvent' import { OB11HeartbeatEvent } from '../event/meta/OB11HeartbeatEvent'
import { selfInfo } from '@/common/data' import { getSelfInfo } from '@/common/data'
class OB11HTTPServer extends HttpServerBase { class OB11HTTPServer extends HttpServerBase {
name = 'LLOneBot server' name = 'LLOneBot server'
@@ -40,7 +40,7 @@ class HTTPHeart {
} }
this.intervalId = setInterval(() => { this.intervalId = setInterval(() => {
// ws的心跳是ws自己维护的 // ws的心跳是ws自己维护的
postOb11Event(new OB11HeartbeatEvent(selfInfo.online!, true, heartInterval!), false, false) postOb11Event(new OB11HeartbeatEvent(getSelfInfo().online!, true, heartInterval!), false, false)
}, heartInterval) }, heartInterval)
} }

View File

@@ -1,5 +1,5 @@
import { OB11Message } from '../types' import { OB11Message } from '../types'
import { selfInfo } from '@/common/data' import { getSelfUin } from '@/common/data'
import { OB11BaseMetaEvent } from '../event/meta/OB11BaseMetaEvent' import { OB11BaseMetaEvent } from '../event/meta/OB11BaseMetaEvent'
import { OB11BaseNoticeEvent } from '../event/notice/OB11BaseNoticeEvent' import { OB11BaseNoticeEvent } from '../event/notice/OB11BaseNoticeEvent'
import { WebSocket as WebSocketClass } from 'ws' import { WebSocket as WebSocketClass } from 'ws'
@@ -27,17 +27,19 @@ export function unregisterWsEventSender(ws: WebSocketClass) {
export function postWsEvent(event: PostEventType) { export function postWsEvent(event: PostEventType) {
for (const ws of eventWSList) { for (const ws of eventWSList) {
new Promise(() => { new Promise((resolve) => {
wsReply(ws, event) wsReply(ws, event)
}).then().catch(log) resolve(undefined)
}).then()
} }
} }
export function postOb11Event(msg: PostEventType, reportSelf = false, postWs = true) { export function postOb11Event(msg: PostEventType, reportSelf = false, postWs = true) {
const config = getConfigUtil().getConfig() const config = getConfigUtil().getConfig()
const selfUin = getSelfUin()
// 判断msg是否是event // 判断msg是否是event
if (!config.reportSelfMessage && !reportSelf) { if (!config.reportSelfMessage && !reportSelf) {
if (msg.post_type === 'message' && (msg as OB11Message).user_id.toString() == selfInfo.uin) { if (msg.post_type === 'message' && (msg as OB11Message).user_id.toString() == selfUin) {
return return
} }
} }
@@ -48,7 +50,7 @@ export function postOb11Event(msg: PostEventType, reportSelf = false, postWs = t
const sig = hmac.digest('hex') const sig = hmac.digest('hex')
let headers = { let headers = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'x-self-id': selfInfo.uin, 'x-self-id': selfUin,
} }
if (config.ob11.httpSecret) { if (config.ob11.httpSecret) {
headers['x-signature'] = 'sha1=' + sig headers['x-signature'] = 'sha1=' + sig
@@ -60,13 +62,15 @@ export function postOb11Event(msg: PostEventType, reportSelf = false, postWs = t
body: msgStr, body: msgStr,
}).then( }).then(
async (res) => { async (res) => {
log(`新消息事件HTTP上报成功: ${host} `, msgStr) if (msg.post_type) {
log(`HTTP 事件上报: ${host} `, msg.post_type)
}
try { try {
const resJson = await res.json() const resJson = await res.json()
log(`新消息事件HTTP上报返回快速操作: `, JSON.stringify(resJson)) log(`新消息事件HTTP上报返回快速操作: `, JSON.stringify(resJson))
handleQuickOperation(msg as QuickOperationEvent, resJson).then().catch(log); handleQuickOperation(msg as QuickOperationEvent, resJson).then().catch(log);
} catch (e) { } catch (e) {
log(`新消息事件HTTP上报没有返回快速操作不需要处理`) //log(`新消息事件HTTP上报没有返回快速操作不需要处理`)
return return
} }
}, },

View File

@@ -1,4 +1,4 @@
import { selfInfo } from '../../../common/data' import { getSelfInfo } from '../../../common/data'
import { LifeCycleSubType, OB11LifeCycleEvent } from '../../event/meta/OB11LifeCycleEvent' import { LifeCycleSubType, OB11LifeCycleEvent } from '../../event/meta/OB11LifeCycleEvent'
import { ActionName } from '../../action/types' import { ActionName } from '../../action/types'
import { OB11Response } from '../../action/OB11Response' import { OB11Response } from '../../action/OB11Response'
@@ -78,6 +78,7 @@ export class ReverseWebsocket {
private connect() { private connect() {
const { token, heartInterval } = getConfigUtil().getConfig() const { token, heartInterval } = getConfigUtil().getConfig()
const selfInfo = getSelfInfo()
this.websocket = new WebSocketClass(this.url, { this.websocket = new WebSocketClass(this.url, {
maxPayload: 1024 * 1024 * 1024, maxPayload: 1024 * 1024 * 1024,
handshakeTimeout: 2000, handshakeTimeout: 2000,
@@ -103,6 +104,10 @@ export class ReverseWebsocket {
this.websocket.on('error', log) this.websocket.on('error', log)
this.websocket.on('ping',()=>{
this.websocket?.pong()
})
const wsClientInterval = setInterval(() => { const wsClientInterval = setInterval(() => {
postWsEvent(new OB11HeartbeatEvent(selfInfo.online!, true, heartInterval!)) postWsEvent(new OB11HeartbeatEvent(selfInfo.online!, true, heartInterval!))
}, heartInterval) // 心跳包 }, heartInterval) // 心跳包

View File

@@ -1,70 +1,131 @@
import { WebSocket } from 'ws' import BaseAction from '../../action/BaseAction'
import { WebSocket, WebSocketServer } from 'ws'
import { actionMap } from '../../action' import { actionMap } from '../../action'
import { OB11Response } from '../../action/OB11Response' import { OB11Response } from '../../action/OB11Response'
import { postWsEvent, registerWsEventSender, unregisterWsEventSender } from '../post-ob11-event' import { postWsEvent, registerWsEventSender, unregisterWsEventSender } from '../post-ob11-event'
import { ActionName } from '../../action/types' import { ActionName } from '../../action/types'
import BaseAction from '../../action/BaseAction'
import { LifeCycleSubType, OB11LifeCycleEvent } from '../../event/meta/OB11LifeCycleEvent' import { LifeCycleSubType, OB11LifeCycleEvent } from '../../event/meta/OB11LifeCycleEvent'
import { OB11HeartbeatEvent } from '../../event/meta/OB11HeartbeatEvent' import { OB11HeartbeatEvent } from '../../event/meta/OB11HeartbeatEvent'
import { WebsocketServerBase } from '../../../common/server/websocket'
import { IncomingMessage } from 'node:http' import { IncomingMessage } from 'node:http'
import { wsReply } from './reply' import { wsReply } from './reply'
import { selfInfo } from '../../../common/data' import { getSelfInfo } from '@/common/data'
import { log } from '../../../common/utils/log' import { log } from '@/common/utils/log'
import { getConfigUtil } from '../../../common/config' import { getConfigUtil } from '@/common/config'
import { llonebotError } from '@/common/data'
class OB11WebsocketServer extends WebsocketServerBase { export class OB11WebsocketServer {
authorizeFailed(wsClient: WebSocket) { private ws?: WebSocketServer
wsClient.send(JSON.stringify(OB11Response.res(null, 'failed', 1403, 'token验证失败')))
constructor() {
log(`llonebot websocket service started`)
} }
async handleAction(wsClient: WebSocket, actionName: string, params: any, echo?: any) { start(port: number) {
try {
this.ws = new WebSocketServer({ port, maxPayload: 1024 * 1024 * 1024 })
llonebotError.wsServerError = ''
} catch (e: any) {
llonebotError.wsServerError = '正向 WebSocket 服务启动失败, ' + e.toString()
return
}
this.ws?.on('connection', (socket, req) => {
const url = req.url?.split('?').shift()
this.authorize(socket, req)
this.onConnect(socket, url!)
})
}
stop() {
llonebotError.wsServerError = ''
this.ws?.close(err => {
log('ws server close failed!', err)
})
this.ws = undefined
}
restart(port: number) {
this.stop()
this.start(port)
}
private authorize(socket: WebSocket, req: IncomingMessage) {
const { token } = getConfigUtil().getConfig()
const url = req.url?.split('?').shift()
log('ws connect', url)
let clientToken = ''
const authHeader = req.headers['authorization']
if (authHeader) {
clientToken = authHeader.split('Bearer ').pop()!
log('receive ws header token', clientToken)
} else {
const { searchParams } = new URL(`http://localhost${req.url}`)
const urlToken = searchParams.get('access_token')
if (urlToken) {
if (Array.isArray(urlToken)) {
clientToken = urlToken[0]
} else {
clientToken = urlToken
}
log('receive ws url token', clientToken)
}
}
if (token && clientToken !== token) {
this.authorizeFailed(socket)
return socket.close()
}
}
private authorizeFailed(socket: WebSocket) {
socket.send(JSON.stringify(OB11Response.res(null, 'failed', 1403, 'token验证失败')))
}
private async handleAction(socket: WebSocket, actionName: string, params: any, echo?: any) {
const action: BaseAction<any, any> = actionMap.get(actionName)! const action: BaseAction<any, any> = actionMap.get(actionName)!
if (!action) { if (!action) {
return wsReply(wsClient, OB11Response.error('不支持的api ' + actionName, 1404, echo)) return wsReply(socket, OB11Response.error('不支持的api ' + actionName, 1404, echo))
} }
try { try {
let handleResult = await action.websocketHandle(params, echo) const handleResult = await action.websocketHandle(params, echo)
handleResult.echo = echo handleResult.echo = echo
wsReply(wsClient, handleResult) wsReply(socket, handleResult)
} catch (e: any) { } catch (e: any) {
wsReply(wsClient, OB11Response.error(`api处理出错:${e.stack}`, 1200, echo)) wsReply(socket, OB11Response.error(`api处理出错:${e.stack}`, 1200, echo))
} }
} }
onConnect(wsClient: WebSocket, url: string, req: IncomingMessage) { private onConnect(socket: WebSocket, url: string) {
if (url == '/api' || url == '/api/' || url == '/') { if (['/api', '/api/', '/'].includes(url)) {
wsClient.on('message', async (msg) => { socket.on('message', async (msg) => {
let receiveData: { action: ActionName | null; params: any; echo?: any } = { action: null, params: {} } let receiveData: { action: ActionName | null; params: any; echo?: any } = { action: null, params: {} }
let echo = null let echo: any
try { try {
receiveData = JSON.parse(msg.toString()) receiveData = JSON.parse(msg.toString())
echo = receiveData.echo echo = receiveData.echo
log('收到正向Websocket消息', receiveData) log('收到正向Websocket消息', receiveData)
} catch (e) { } catch (e) {
return wsReply(wsClient, OB11Response.error('json解析失败请检查数据格式', 1400, echo)) return wsReply(socket, OB11Response.error('json解析失败请检查数据格式', 1400, echo))
} }
this.handleAction(wsClient, receiveData.action!, receiveData.params, receiveData.echo).then() this.handleAction(socket, receiveData.action!, receiveData.params, receiveData.echo)
}) })
} }
if (url == '/event' || url == '/event/' || url == '/') { if (['/event', '/event/', '/'].includes(url)) {
registerWsEventSender(wsClient) registerWsEventSender(socket)
log('event上报ws客户端已连接') log('event上报ws客户端已连接')
try { try {
wsReply(wsClient, new OB11LifeCycleEvent(LifeCycleSubType.CONNECT)) wsReply(socket, new OB11LifeCycleEvent(LifeCycleSubType.CONNECT))
} catch (e) { } catch (e) {
log('发送生命周期失败', e) log('发送生命周期失败', e)
} }
const { heartInterval } = getConfigUtil().getConfig() const { heartInterval } = getConfigUtil().getConfig()
const wsClientInterval = setInterval(() => { const intervalId = setInterval(() => {
postWsEvent(new OB11HeartbeatEvent(selfInfo.online!, true, heartInterval!)) postWsEvent(new OB11HeartbeatEvent(getSelfInfo().online!, true, heartInterval!))
}, heartInterval) // 心跳包 }, heartInterval) // 心跳包
wsClient.on('close', () => { socket.on('close', () => {
log('event上报ws客户端已断开') log('event上报ws客户端已断开')
clearInterval(wsClientInterval) clearInterval(intervalId)
unregisterWsEventSender(wsClient) unregisterWsEventSender(socket)
}) })
} }
} }

Some files were not shown because too many files have changed in this diff Show More