Compare commits

...

135 Commits

Author SHA1 Message Date
linyuchen
0fb30df1bc chore: version 4.3.1 2024-11-17 15:22:06 +08:00
linyuchen
62e23614fb Merge branch 'dev' 2024-11-17 15:20:45 +08:00
linyuchen
5514bf0bb8 chore: bump version 2024-11-17 15:20:22 +08:00
linyuchen
f5d093cc45 fix: 调用发包接口时检查 QQ 版本,兼容 27333 - 27597 的戳一戳 2024-11-17 15:19:48 +08:00
linyuchen
44c6debd01 Merge branch 'dev' 2024-11-16 23:03:01 +08:00
linyuchen
2c1d12e04b Merge remote-tracking branch 'origin/dev' into dev 2024-11-16 23:01:39 +08:00
linyuchen
110193ea15 fix: 调用发包接口时检查QQ版本 2024-11-16 23:01:24 +08:00
idranme
fafcf058b1 refactor 2024-11-16 19:39:14 +08:00
linyuchen
825c7c8e29 Merge branch 'dev' 2024-11-16 14:49:13 +08:00
linyuchen
c8d5eebe5d feat: new api set_friend_remark, set_friend_category, set_group_remark, set_group_msg_mask, set_restart 2024-11-16 14:48:45 +08:00
linyuchen
466a3e4d66 Merge branch 'dev'
# Conflicts:
#	manifest.json
#	src/version.ts
2024-11-15 11:01:52 +08:00
linyuchen
f6263375f1 chore: version 4.2.2 2024-11-15 11:01:25 +08:00
linyuchen
f79581d97e 🐛修复 hook ipc 时获取不到 callbackId 导致其他插件 ipc 通信失败 2024-11-15 10:58:35 +08:00
linyuchen
56f26e9aa8 chore: version 4.2.1 2024-11-14 20:00:36 +08:00
linyuchen
9e03071629 Merge branch 'dev' 2024-11-14 19:58:27 +08:00
idranme
1f02c98c8f chore 2024-11-14 15:24:46 +08:00
linyuchen
e1e5c278b9 🐛修复 hook ipc 时获取不到 eventName 导致其他插件 ipc 通信失败 2024-11-14 14:51:01 +08:00
idranme
104839f7ea fix 2024-11-14 12:34:48 +08:00
idranme
bb8771a5b4 refactor 2024-11-14 11:40:19 +08:00
linyuchen
4a2523463b Merge remote-tracking branch 'origin/main' 2024-11-13 19:35:04 +08:00
linyuchen
a23a99310a Merge branch 'dev' 2024-11-13 19:34:18 +08:00
linyuchen
5c5105ce88 chore: version 4.2.0 2024-11-13 19:33:29 +08:00
linyuchen
1bf5e41bdc chore: 协议包支持 macOS 2024-11-13 19:27:50 +08:00
linyuchen
cd679cc041 refactor: 设置群员头衔的时候检查是否群主 2024-11-13 19:27:21 +08:00
linyuchen
eabee466bb refactor: 使用 napcat packet 实现戳一戳、群头衔、群打卡 2024-11-12 22:09:50 +08:00
idranme
d3f93257ce feat: get_stranger_info API adds city field 2024-11-10 16:59:58 +08:00
idranme
33f340ca81 chore 2024-11-10 14:18:09 +08:00
idranme
0d27ef7ebc Merge pull request #502 from LLOneBot/dev
release: 4.1.4
2024-11-09 22:29:16 +08:00
idranme
479e8c9d25 optimize 2024-11-09 22:21:04 +08:00
linyuchen
e3dffa24f8 Merge branch 'dev' 2024-11-09 21:40:06 +08:00
linyuchen
30b8793ee1 fix: 修复 IPC 超时 2024-11-09 21:37:41 +08:00
linyuchen
edf7a97269 Merge branch 'dev' 2024-11-08 18:27:06 +08:00
linyuchen
47b068737d chore: bump version, add author 2024-11-08 06:05:17 +08:00
linyuchen
bfb67188ce fix: DownloadFile接口参数url和base64二选一即可 2024-11-08 05:45:21 +08:00
linyuchen
7ad384d407 fix: 发送文件路径包含#%时发送失败 2024-11-08 05:44:55 +08:00
idranme
66335ddf9b Merge pull request #492 from LLOneBot/dev
release: 4.1.2
2024-10-27 12:11:50 +08:00
idranme
f7926c2e1b chore: bump versions 2024-10-27 12:07:21 +08:00
idranme
b669e28038 fix 2024-10-27 12:06:33 +08:00
idranme
70b3005005 Merge pull request #489 from LLOneBot/dev
release: 4.1.1
2024-10-26 00:19:15 +08:00
idranme
94f1d84dd8 chore: bump versions 2024-10-26 00:16:18 +08:00
idranme
aa2b4a160d fix 2024-10-26 00:15:48 +08:00
idranme
9be43de04b fix: forward 2024-10-26 00:14:27 +08:00
idranme
ac5fe4d275 Merge pull request #485 from LLOneBot/dev
release: 4.1.0
2024-10-24 22:11:38 +08:00
idranme
78f04f0ba2 chore: bump versions 2024-10-24 22:09:33 +08:00
idranme
f1027ec0d9 fix 2024-10-24 22:09:12 +08:00
idranme
f1b0be710a refactor 2024-10-24 20:46:21 +08:00
idranme
91ca4e96c4 fix 2024-10-24 17:40:35 +08:00
idranme
c9e39769dd feat: support for fake forward message 2024-10-23 23:30:27 +08:00
linyuchen
8b04833a6a 退出设置界面时检查配置是否改动并提示用户保存配置 2024-10-23 17:10:33 +08:00
linyuchen
4aadcd5288 Update README.md contributors 2024-10-21 17:14:54 +08:00
idranme
2f74de667e Merge pull request #481 from LLOneBot/dev
release: 4.0.13
2024-10-19 18:21:27 +08:00
idranme
2a67ffae24 chore: bump versions 2024-10-19 18:12:07 +08:00
idranme
78def9ebf8 fix 2024-10-19 18:09:33 +08:00
idranme
c6dddcd664 refactor 2024-10-19 18:09:23 +08:00
idranme
5b90a25f8f refactor 2024-10-19 15:57:58 +08:00
idranme
364dfe8b93 feat: get_group_file_system_info API 2024-10-19 10:31:14 +08:00
idranme
0fe725eb32 fix 2024-10-19 10:07:59 +08:00
idranme
8b89fd7a0b Merge pull request #479 from LLOneBot/dev
release: 4.0.12
2024-10-18 21:16:46 +08:00
idranme
1b0c9ad57c chore: bump versions 2024-10-18 21:10:57 +08:00
idranme
2910b8f4e6 optimize 2024-10-18 21:09:51 +08:00
idranme
2453509734 refactor 2024-10-18 00:03:23 +08:00
idranme
8239e9a243 Merge pull request #477 from LLOneBot/dev
release: 4.0.11
2024-10-16 11:44:43 +08:00
idranme
50e5f89f4f chore: bump versions 2024-10-16 11:43:14 +08:00
idranme
be2119a1e6 feat: add save button 2024-10-16 11:32:50 +08:00
idranme
951afea794 Merge pull request #475 from LLOneBot/dev
release: 4.0.10
2024-10-15 21:09:42 +08:00
idranme
0946d9652e chore: bump versions 2024-10-15 20:54:57 +08:00
idranme
a66e48dfb0 optimize 2024-10-15 20:53:25 +08:00
idranme
029842ca08 fix(onebot): group_increase event 2024-10-15 20:22:39 +08:00
idranme
39fda24799 fix: config hot update 2024-10-15 11:51:00 +08:00
idranme
a4beeba528 Merge pull request #474 from LLOneBot/dev
release: 4.0.9
2024-10-13 23:51:30 +08:00
idranme
c837e970df chore: bump versions 2024-10-13 23:48:50 +08:00
idranme
4ced7fa3cf fix 2024-10-13 23:48:22 +08:00
idranme
dbd71d4376 chore 2024-10-13 23:42:01 +08:00
idranme
d43612b2a3 feat(satori): support for receiving llonebot:ark element 2024-10-13 20:31:20 +08:00
idranme
31ad0195d8 fix(onebot): get_group_msg_history API 2024-10-13 19:52:29 +08:00
idranme
9b32140f87 Merge pull request #470 from LLOneBot/dev
release: 4.0.8
2024-10-13 16:36:12 +08:00
idranme
dc5982c6b2 chore: bump versions 2024-10-13 16:34:35 +08:00
idranme
ce46c99330 fix 2024-10-13 16:32:39 +08:00
idranme
d3abaf806f optimize 2024-10-13 16:32:18 +08:00
idranme
e10a67ce05 Merge pull request #468 from LLOneBot/dev
release: 4.0.7
2024-10-13 01:30:01 +08:00
idranme
c8e897abdb chore: bump versions 2024-10-13 01:29:19 +08:00
idranme
e07c06f3e9 Merge pull request #467 from LLOneBot/dev
...
2024-10-13 01:17:06 +08:00
idranme
9c694a11b5 fix: poke 2024-10-13 01:12:12 +08:00
idranme
6e3bb7c9cf fix 2024-10-13 01:10:39 +08:00
idranme
0d8d3ac24f Merge pull request #466 from LLOneBot/dev
release: 4.0.6
2024-10-13 00:48:36 +08:00
idranme
c96d032820 chore: bump versions 2024-10-13 00:47:31 +08:00
idranme
837f48b63a fix: poke 2024-10-13 00:45:30 +08:00
idranme
9d0f9e7096 Merge pull request #465 from LLOneBot/dev
release: 4.0.5
2024-10-12 23:58:07 +08:00
idranme
801d79d79d chore: bump versions 2024-10-12 23:55:32 +08:00
idranme
0d5640046c feat: poke 2024-10-12 23:50:58 +08:00
idranme
e988908784 Merge pull request #463 from LLOneBot/dev
release: 4.0.4
2024-10-11 18:22:37 +08:00
idranme
1cfa736dd5 chore: bump versions 2024-10-11 18:18:51 +08:00
idranme
0081b0b124 refactor 2024-10-11 18:17:52 +08:00
idranme
ba565e7c38 feat(onebot): ocr_image API 2024-10-11 17:42:19 +08:00
idranme
abb468c3f8 optimize 2024-10-11 13:38:59 +08:00
idranme
433a175809 fix: at element 2024-10-11 12:47:12 +08:00
idranme
b40c81c5cb Merge pull request #462 from LLOneBot/dev
release: 4.0.3
2024-10-11 00:52:33 +08:00
idranme
ddf7ffcabe chore: bump versions 2024-10-11 00:50:56 +08:00
idranme
2b0aa6249b fix: at element 2024-10-11 00:50:06 +08:00
idranme
6bb4a8fe69 optimize 2024-10-11 00:31:11 +08:00
idranme
91d78f22f7 refactor 2024-10-09 02:55:12 +08:00
idranme
457ffc0922 Merge pull request #461 from LLOneBot/dev
release: 4.0.2
2024-10-08 21:26:39 +08:00
idranme
e3a2303e45 chore: bump versions 2024-10-08 21:25:23 +08:00
idranme
8465c47d41 fix 2024-10-08 21:22:04 +08:00
idranme
41822eb052 Merge pull request #460 from LLOneBot/dev
release: 4.0.1
2024-10-08 20:46:09 +08:00
idranme
b5578d6278 chore: bump versions 2024-10-08 20:43:56 +08:00
idranme
fecb4c4655 feat(onebot): delete_friend API 2024-10-08 20:40:02 +08:00
idranme
c82b849ead fix 2024-10-08 20:07:12 +08:00
idranme
0bc6e23343 Merge pull request #459 from LLOneBot/dev
release: 4.0.0
2024-10-07 20:26:59 +08:00
idranme
8e9523602b chore: v4.0.0 2024-10-07 20:23:54 +08:00
idranme
48588817fb chore 2024-10-07 19:10:38 +08:00
idranme
4cd9adde1d feat: satori protocol 2024-10-06 10:37:06 +08:00
idranme
8c0cc8beba refactor 2024-10-06 10:28:52 +08:00
idranme
9ec09c6eee Merge pull request #457 from LLOneBot/dev
release: 3.34.1
2024-10-03 15:18:47 +08:00
idranme
4d816b498a chore: v3.34.1 2024-10-03 15:17:57 +08:00
idranme
464efe819d fix 2024-10-03 15:16:48 +08:00
idranme
0876e4645f Merge pull request #456 from LLOneBot/dev
release: 3.34.0
2024-10-01 21:32:24 +08:00
idranme
a2f9128623 chore: v3.34.0 2024-10-01 21:25:19 +08:00
idranme
e313b2b3e6 feat 2024-10-01 21:16:39 +08:00
idranme
a7d86f8fe0 refactor 2024-10-01 21:09:27 +08:00
idranme
496d56f297 feat 2024-09-30 00:49:58 +08:00
idranme
ed2f554d4e refactor 2024-09-28 22:00:05 +08:00
idranme
36d990e328 Merge pull request #452 from LLOneBot/dev
release: 3.33.10
2024-09-28 14:40:11 +08:00
idranme
0ceef4d4c0 chore: v3.33.10 2024-09-28 14:37:44 +08:00
idranme
35bf4f001b feat: _get_group_notice API 2024-09-28 14:35:06 +08:00
idranme
544682fe41 fix 2024-09-28 12:54:02 +08:00
idranme
3da49fbfba optimize 2024-09-27 18:37:47 +08:00
idranme
d5875c9e5b Merge pull request #451 from LLOneBot/dev
release: 3.33.9
2024-09-27 16:53:44 +08:00
idranme
7895644156 chore: v3.33.9 2024-09-27 16:51:49 +08:00
idranme
f092626ede fix 2024-09-27 16:22:50 +08:00
idranme
a58fb31f8e Merge pull request #448 from LLOneBot/dev
release: 3.33.8
2024-09-26 12:57:16 +08:00
idranme
fe85e277f1 chore: v3.33.8 2024-09-26 12:54:30 +08:00
idranme
5217638b46 feat 2024-09-26 01:52:47 +08:00
idranme
f68b707e1c optimize 2024-09-25 22:34:59 +08:00
idranme
c24ce6ec65 adjustment of get_friends_with_category API returns 2024-09-25 22:04:52 +08:00
157 changed files with 15026 additions and 2928 deletions

7
.gitattributes vendored
View File

@@ -1,7 +0,0 @@
* text eol=lf
*.png -text
*.jpg -text
*.ico -text
*.gif -text
*.webp -text

View File

@@ -1,6 +1,6 @@
# LLOneBot # LLOneBot
LiteLoaderQQNT 插件,实现 OneBot 11 协议,用于 QQ 机器人开发 LiteLoaderQQNT 插件,实现 OneBot 11 和 Satori 协议,用于 QQ 机器人开发
> [!CAUTION]\ > [!CAUTION]\
> 请不要在 QQ 官方群聊和任何影响力较大的简中互联网平台(包括但不限于: 哔哩哔哩,微博,知乎,抖音等)发布和讨论任何与本插件存在相关性的信息 > 请不要在 QQ 官方群聊和任何影响力较大的简中互联网平台(包括但不限于: 哔哩哔哩,微博,知乎,抖音等)发布和讨论任何与本插件存在相关性的信息
@@ -23,6 +23,10 @@ TG 群:<https://t.me/+nLZEnpne-pQ1OWFl>
[![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)
## 贡献者
[![Contributors](https://contributors-img.web.app/image?repo=LLOneBot/LLOneBot)](https://github.com/LOneBot/LLOneBot/graphs/contributors)
## 鸣谢 ## 鸣谢
- [NapCatQQ](https://github.com/NapNeko/NapCatQQ) - [NapCatQQ](https://github.com/NapNeko/NapCatQQ)

View File

@@ -39,6 +39,7 @@ const config: ElectronViteConfig = {
...external.map(genCpModule), ...external.map(genCpModule),
{ src: './manifest.json', dest: 'dist' }, { src: './manifest.json', dest: 'dist' },
{ src: './icon.webp', dest: 'dist' }, { src: './icon.webp', dest: 'dist' },
{ src: './src/ntqqapi/native/napcat-protocol-packet/Moehoo/*', dest: 'dist/main/Moehoo' },
], ],
}), }),
], ],

View File

@@ -3,13 +3,17 @@
"type": "extension", "type": "extension",
"name": "LLOneBot", "name": "LLOneBot",
"slug": "LLOneBot", "slug": "LLOneBot",
"description": "实现 OneBot 11 协议,用于 QQ 机器人开发", "description": "实现 OneBot 11 和 Satori 协议,用于 QQ 机器人开发",
"version": "3.33.7", "version": "4.3.1",
"icon": "./icon.webp", "icon": "./icon.webp",
"authors": [ "authors": [
{ {
"name": "linyuchen", "name": "linyuchen",
"link": "https://github.com/linyuchen" "link": "https://github.com/linyuchen"
},
{
"name": "idranme",
"link": "https://github.com/idranme"
} }
], ],
"repository": { "repository": {

View File

@@ -12,36 +12,38 @@
"deploy-win": "cmd /c \"xcopy /C /S /Y dist\\* %LITELOADERQQNT_PROFILE%\\plugins\\LLOneBot\\\"", "deploy-win": "cmd /c \"xcopy /C /S /Y dist\\* %LITELOADERQQNT_PROFILE%\\plugins\\LLOneBot\\\"",
"format": "prettier -cw .", "format": "prettier -cw .",
"check": "tsc", "check": "tsc",
"compile:proto": "pbjs --no-create --no-convert --no-encode --no-verify -t static-module -w es6 -p src/ntqqapi/proto -o src/ntqqapi/proto/compiled.js systemMessage.proto profileLikeTip.proto && pbts -o src/ntqqapi/proto/compiled.d.ts src/ntqqapi/proto/compiled.js" "compile:proto": "pbjs --no-create --no-convert --no-delimited --no-verify -t static-module -w es6 -p src/ntqqapi/proto -o src/ntqqapi/proto/compiled.js profileLikeTip.proto groupNotify.proto message.proto richMedia.proto && pbts -o src/ntqqapi/proto/compiled.d.ts src/ntqqapi/proto/compiled.js"
}, },
"author": "", "author": "",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@minatojs/driver-sqlite": "^4.6.0", "@minatojs/driver-sqlite": "^4.6.0",
"@satorijs/element": "^3.1.7",
"@satorijs/protocol": "^1.4.2",
"compare-versions": "^6.1.1",
"cordis": "^3.18.1", "cordis": "^3.18.1",
"cors": "^2.8.5", "cors": "^2.8.5",
"cosmokit": "^1.6.2", "cosmokit": "^1.6.3",
"express": "^5.0.0", "express": "^5.0.1",
"fast-xml-parser": "^4.5.0",
"file-type": "^19.5.0",
"fluent-ffmpeg": "^2.1.3", "fluent-ffmpeg": "^2.1.3",
"minato": "^3.6.0", "minato": "^3.6.1",
"protobufjs": "^7.4.0", "protobufjs": "^7.4.0",
"silk-wasm": "^3.6.1", "silk-wasm": "^3.6.3",
"ts-case-convert": "^2.1.0",
"ws": "^8.18.0" "ws": "^8.18.0"
}, },
"devDependencies": { "devDependencies": {
"@types/cors": "^2.8.17", "@types/cors": "^2.8.17",
"@types/express": "^4.17.21", "@types/express": "^5.0.0",
"@types/fluent-ffmpeg": "^2.1.26", "@types/fluent-ffmpeg": "^2.1.27",
"@types/node": "^20.14.15", "@types/node": "^20.14.15",
"@types/ws": "^8.5.12", "@types/ws": "^8.5.13",
"electron": "^31.4.0", "electron": "^31.4.0",
"electron-vite": "^2.3.0", "electron-vite": "^2.3.0",
"protobufjs-cli": "^1.1.3", "protobufjs-cli": "^1.1.3",
"typescript": "^5.6.2", "typescript": "^5.6.3",
"vite": "^5.4.7", "vite": "^5.4.10",
"vite-plugin-cp": "^4.0.8" "vite-plugin-cp": "^4.0.8"
}, },
"packageManager": "yarn@4.5.0" "packageManager": "yarn@4.5.1"
} }

View File

@@ -6,13 +6,17 @@ const manifest = {
type: 'extension', type: 'extension',
name: 'LLOneBot', name: 'LLOneBot',
slug: 'LLOneBot', slug: 'LLOneBot',
description: '实现 OneBot 11 协议,用于 QQ 机器人开发', description: '实现 OneBot 11 和 Satori 协议,用于 QQ 机器人开发',
version, version,
icon: './icon.webp', icon: './icon.webp',
authors: [ authors: [
{ {
name: 'linyuchen', name: 'linyuchen',
link: 'https://github.com/linyuchen' link: 'https://github.com/linyuchen'
},
{
"name": "idranme",
"link": "https://github.com/idranme"
} }
], ],
repository: { repository: {

View File

@@ -1,6 +1,5 @@
export const CHANNEL_GET_CONFIG = 'llonebot_get_config' export const CHANNEL_GET_CONFIG = 'llonebot_get_config'
export const CHANNEL_SET_CONFIG = 'llonebot_set_config' export const CHANNEL_SET_CONFIG = 'llonebot_set_config'
export const CHANNEL_SET_CONFIG_CONFIRMED = 'llonebot_set_config_confirmed'
export const CHANNEL_LOG = 'llonebot_log' export const CHANNEL_LOG = 'llonebot_log'
export const CHANNEL_ERROR = 'llonebot_error' export const CHANNEL_ERROR = 'llonebot_error'
export const CHANNEL_UPDATE = 'llonebot_update' export const CHANNEL_UPDATE = 'llonebot_update'

View File

@@ -1,6 +1,6 @@
import fs from 'node:fs' import fs from 'node:fs'
import path from 'node:path' import path from 'node:path'
import { Config, OB11Config } from './types' import { Config, OB11Config, SatoriConfig } from './types'
import { selfInfo, DATA_DIR } from './globalVars' import { selfInfo, DATA_DIR } from './globalVars'
import { mergeNewProperties } from './utils/misc' import { mergeNewProperties } from './utils/misc'
@@ -22,6 +22,7 @@ export class ConfigUtil {
reloadConfig(): Config { reloadConfig(): Config {
const ob11Default: OB11Config = { const ob11Default: OB11Config = {
enable: true,
httpPort: 3000, httpPort: 3000,
httpHosts: [], httpHosts: [],
httpSecret: '', httpSecret: '',
@@ -33,17 +34,24 @@ export class ConfigUtil {
enableWsReverse: false, enableWsReverse: false,
messagePostFormat: 'array', messagePostFormat: 'array',
enableHttpHeart: false, enableHttpHeart: false,
listenLocalhost: false listenLocalhost: false,
reportSelfMessage: false
}
const satoriDefault: SatoriConfig = {
enable: true,
port: 5600,
listen: '0.0.0.0',
token: ''
} }
const defaultConfig: Config = { const defaultConfig: Config = {
enableLLOB: true, enableLLOB: true,
satori: satoriDefault,
ob11: ob11Default, ob11: ob11Default,
heartInterval: 60000, heartInterval: 60000,
token: '', token: '',
enableLocalFile2Url: false, enableLocalFile2Url: false,
debug: false, debug: false,
log: false, log: true,
reportSelfMessage: false,
autoDeleteFile: false, autoDeleteFile: false,
autoDeleteFileSecond: 60, autoDeleteFileSecond: 60,
musicSignUrl: '', musicSignUrl: '',
@@ -66,6 +74,7 @@ export class ConfigUtil {
this.checkOldConfig(jsonData.ob11, jsonData, 'httpPort', 'http') this.checkOldConfig(jsonData.ob11, jsonData, 'httpPort', 'http')
this.checkOldConfig(jsonData.ob11, jsonData, 'httpHosts', 'hosts') this.checkOldConfig(jsonData.ob11, jsonData, 'httpHosts', 'hosts')
this.checkOldConfig(jsonData.ob11, jsonData, 'wsPort', 'wsPort') this.checkOldConfig(jsonData.ob11, jsonData, 'wsPort', 'wsPort')
this.checkOldConfig(jsonData.ob11, jsonData, 'reportSelfMessage', 'reportSelfMessage')
this.config = jsonData this.config = jsonData
return this.config return this.config
} }
@@ -79,8 +88,8 @@ export class ConfigUtil {
private checkOldConfig( private checkOldConfig(
currentConfig: OB11Config, currentConfig: OB11Config,
oldConfig: Config, oldConfig: Config,
currentKey: 'httpPort' | 'httpHosts' | 'wsPort', currentKey: 'httpPort' | 'httpHosts' | 'wsPort' | 'reportSelfMessage',
oldKey: 'http' | 'hosts' | 'wsPort', oldKey: 'http' | 'hosts' | 'wsPort' | 'reportSelfMessage',
) { ) {
// 迁移旧的配置到新配置,避免用户重新填写配置 // 迁移旧的配置到新配置,避免用户重新填写配置
const oldValue = oldConfig[oldKey] const oldValue = oldConfig[oldKey]

View File

@@ -1,4 +1,5 @@
export interface OB11Config { export interface OB11Config {
enable: boolean
httpPort: number httpPort: number
httpHosts: string[] httpHosts: string[]
httpSecret?: string httpSecret?: string
@@ -16,21 +17,24 @@ export interface OB11Config {
*/ */
enableQOAutoQuote?: boolean enableQOAutoQuote?: boolean
listenLocalhost: boolean listenLocalhost: boolean
reportSelfMessage: boolean
} }
export interface CheckVersion { export interface SatoriConfig {
result: boolean enable: boolean
version: string listen: string
port: number
token: string
} }
export interface Config { export interface Config {
enableLLOB: boolean enableLLOB: boolean
satori: SatoriConfig
ob11: OB11Config ob11: OB11Config
token?: string token?: string
heartInterval: number // ms heartInterval: number // ms
enableLocalFile2Url?: boolean // 开启后本地文件路径图片会转成http链接, 语音会转成base64 enableLocalFile2Url?: boolean // 开启后本地文件路径图片会转成http链接, 语音会转成base64
debug?: boolean debug?: boolean
reportSelfMessage?: boolean
log?: boolean log?: boolean
autoDeleteFile?: boolean autoDeleteFile?: boolean
autoDeleteFileSecond?: number autoDeleteFileSecond?: number
@@ -45,6 +49,13 @@ export interface Config {
hosts?: string[] hosts?: string[]
/** @deprecated */ /** @deprecated */
wsPort?: string wsPort?: string
/** @deprecated */
reportSelfMessage?: boolean
}
export interface CheckVersion {
result: boolean
version: string
} }
export interface LLOneBotError { export interface LLOneBotError {

View File

@@ -53,49 +53,44 @@ function convert(ctx: Context, input: Input, options: FFmpegOptions, outputPath?
} }
export async function encodeSilk(ctx: Context, filePath: string) { export async function encodeSilk(ctx: Context, filePath: string) {
try { const file = await fsPromise.readFile(filePath)
const file = await fsPromise.readFile(filePath) if (!isSilk(file)) {
if (!isSilk(file)) { ctx.logger.info(`语音文件${filePath}需要转换成silk`)
ctx.logger.info(`语音文件${filePath}需要转换成silk`) let result: EncodeResult
let result: EncodeResult const allowSampleRate = [8000, 12000, 16000, 24000, 32000, 44100, 48000]
const allowSampleRate = [8000, 12000, 16000, 24000, 32000, 44100, 48000] if (isWav(file) && allowSampleRate.includes(getWavFileInfo(file).fmt.sampleRate)) {
if (isWav(file) && allowSampleRate.includes(getWavFileInfo(file).fmt.sampleRate)) { result = await encode(file, 0)
result = await encode(file, 0)
} else {
const input = await convert(ctx, filePath, {
output: [
'-ar 24000',
'-ac 1',
'-f s16le'
]
})
result = await encode(input, 24000)
}
const pttPath = path.join(TEMP_DIR, randomUUID())
await fsPromise.writeFile(pttPath, result.data)
ctx.logger.info(`语音文件${filePath}转换成功!`, pttPath, `时长:`, result.duration)
return {
converted: true,
path: pttPath,
duration: result.duration / 1000,
}
} else { } else {
const silk = file const input = await convert(ctx, filePath, {
let duration = 1 output: [
try { '-ar 24000',
duration = getDuration(silk) / 1000 '-ac 1',
} catch (e) { '-f s16le'
ctx.logger.warn('获取语音文件时长失败, 默认为1秒', filePath, (e as Error).stack) ]
} })
return { result = await encode(input, 24000)
converted: false, }
path: filePath, const pttPath = path.join(TEMP_DIR, randomUUID())
duration, await fsPromise.writeFile(pttPath, result.data)
} ctx.logger.info(`语音文件${filePath}转换成功!`, pttPath, `时长:`, result.duration)
return {
converted: true,
path: pttPath,
duration: result.duration / 1000,
}
} else {
const silk = file
let duration = 1
try {
duration = getDuration(silk) / 1000
} catch (e) {
ctx.logger.warn('获取语音文件时长失败, 默认为1秒', filePath, (e as Error).stack)
}
return {
converted: false,
path: filePath,
duration,
} }
} catch (err) {
ctx.logger.error('convert silk failed', (err as Error).stack)
return {}
} }
} }

View File

@@ -4,7 +4,7 @@ import path from 'node:path'
import { TEMP_DIR } from '../globalVars' import { TEMP_DIR } from '../globalVars'
import { randomUUID, createHash } from 'node:crypto' import { randomUUID, createHash } from 'node:crypto'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { fileTypeFromFile } from 'file-type' import { Context } from 'cordis'
// 定义一个异步函数来检查文件是否存在 // 定义一个异步函数来检查文件是否存在
export function checkFileReceived(path: string, timeout: number = 3000): Promise<void> { export function checkFileReceived(path: string, timeout: number = 3000): Promise<void> {
@@ -27,9 +27,10 @@ export function checkFileReceived(path: string, timeout: number = 3000): Promise
export function calculateFileMD5(filePath: string): Promise<string> { export function calculateFileMD5(filePath: string): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const hash = createHash('md5')
// 创建一个流式读取器 // 创建一个流式读取器
const stream = fs.createReadStream(filePath) const stream = fs.createReadStream(filePath)
const hash = createHash('md5')
stream.on('data', (data: Buffer) => { stream.on('data', (data: Buffer) => {
// 当读取到数据时,更新哈希对象的状态 // 当读取到数据时,更新哈希对象的状态
@@ -118,12 +119,15 @@ type Uri2LocalRes = {
isLocal: boolean isLocal: boolean
} }
export async function uri2local(uri: string, filename?: string, needExt?: boolean): Promise<Uri2LocalRes> { export async function uri2local(ctx: Context, uri: string, needExt?: boolean): Promise<Uri2LocalRes> {
const { type } = checkUriType(uri) const { type } = checkUriType(uri)
if (type === FileUriType.FileURL) { if (type === FileUriType.FileURL) {
const filePath = fileURLToPath(uri) const fileUri = uri.replace('%', '%25').replace('#', '%23')
const filePath = fileURLToPath(fileUri)
const fileName = path.basename(filePath) const fileName = path.basename(filePath)
// console.log('fileURLToPath', filePath)
// console.log('fileName', fileName)
return { success: true, errMsg: '', fileName, path: filePath, isLocal: true } return { success: true, errMsg: '', fileName, path: filePath, isLocal: true }
} }
@@ -136,15 +140,16 @@ export async function uri2local(uri: string, filename?: string, needExt?: boolea
try { try {
const res = await fetchFile(uri) const res = await fetchFile(uri)
const match = res.url.match(/.+\/([^/?]*)(?=\?)?/) const match = res.url.match(/.+\/([^/?]*)(?=\?)?/)
let filename: string
if (match?.[1]) { if (match?.[1]) {
filename ??= match[1].replace(/[/\\:*?"<>|]/g, '_') filename = match[1].replace(/[/\\:*?"<>|]/g, '_')
} else { } else {
filename ??= randomUUID() filename = randomUUID()
} }
let filePath = path.join(TEMP_DIR, filename) let filePath = path.join(TEMP_DIR, filename)
await fsPromise.writeFile(filePath, res.data) await fsPromise.writeFile(filePath, res.data)
if (needExt && !path.extname(filePath)) { if (needExt && !path.extname(filePath)) {
const ext = (await fileTypeFromFile(filePath))?.ext const ext = (await ctx.ntFileApi.getFileType(filePath)).ext
filename += `.${ext}` filename += `.${ext}`
await fsPromise.rename(filePath, `${filePath}.${ext}`) await fsPromise.rename(filePath, `${filePath}.${ext}`)
filePath = `${filePath}.${ext}` filePath = `${filePath}.${ext}`
@@ -157,12 +162,12 @@ export async function uri2local(uri: string, filename?: string, needExt?: boolea
} }
if (type === FileUriType.OneBotBase64) { if (type === FileUriType.OneBotBase64) {
filename ??= randomUUID() let filename = randomUUID()
let filePath = path.join(TEMP_DIR, filename) let filePath = path.join(TEMP_DIR, filename)
const base64 = uri.replace(/^base64:\/\//, '') const base64 = uri.replace(/^base64:\/\//, '')
await fsPromise.writeFile(filePath, base64, 'base64') await fsPromise.writeFile(filePath, base64, 'base64')
if (needExt) { if (needExt) {
const ext = (await fileTypeFromFile(filePath))?.ext const ext = (await ctx.ntFileApi.getFileType(filePath)).ext
filename += `.${ext}` filename += `.${ext}`
await fsPromise.rename(filePath, `${filePath}.${ext}`) await fsPromise.rename(filePath, `${filePath}.${ext}`)
filePath = `${filePath}.${ext}` filePath = `${filePath}.${ext}`
@@ -174,12 +179,12 @@ export async function uri2local(uri: string, filename?: string, needExt?: boolea
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
const capture = /^data:([\w/.+-]+);base64,(.*)$/.exec(uri) const capture = /^data:([\w/.+-]+);base64,(.*)$/.exec(uri)
if (capture) { if (capture) {
filename ??= randomUUID() let filename = randomUUID()
const [, _type, base64] = capture const [, _type, base64] = capture
let filePath = path.join(TEMP_DIR, filename) let filePath = path.join(TEMP_DIR, filename)
await fsPromise.writeFile(filePath, base64, 'base64') await fsPromise.writeFile(filePath, base64, 'base64')
if (needExt) { if (needExt) {
const ext = (await fileTypeFromFile(filePath))?.ext const ext = (await ctx.ntFileApi.getFileType(filePath)).ext
filename += `.${ext}` filename += `.${ext}`
await fsPromise.rename(filePath, `${filePath}.${ext}`) await fsPromise.rename(filePath, `${filePath}.${ext}`)
filePath = `${filePath}.${ext}` filePath = `${filePath}.${ext}`

View File

@@ -2,24 +2,7 @@ import fs from 'fs'
import path from 'node:path' import path from 'node:path'
import { getConfigUtil } from '../config' import { getConfigUtil } from '../config'
import { LOG_DIR } from '../globalVars' import { LOG_DIR } from '../globalVars'
import { Dict } from 'cosmokit' import { inspect } from 'node:util'
function truncateString(obj: Dict | null, maxLength = 500) {
if (obj !== null && typeof obj === 'object') {
Object.keys(obj).forEach((key) => {
if (typeof obj[key] === 'string') {
// 如果是字符串且超过指定长度,则截断
if (obj[key].length > maxLength) {
obj[key] = obj[key].substring(0, maxLength) + '...'
}
} else if (typeof obj[key] === 'object') {
// 如果是对象或数组,则递归调用
truncateString(obj[key], maxLength)
}
})
}
return obj
}
export const logFileName = `llonebot-${new Date().toLocaleString('zh-CN')}.log`.replace(/\//g, '-').replace(/:/g, '-') export const logFileName = `llonebot-${new Date().toLocaleString('zh-CN')}.log`.replace(/\//g, '-').replace(/:/g, '-')
@@ -29,12 +12,16 @@ export function log(...msg: unknown[]) {
} }
let logMsg = '' let logMsg = ''
for (const msgItem of msg) { for (const msgItem of msg) {
// 判断是否是对象
if (typeof msgItem === 'object') { if (typeof msgItem === 'object') {
logMsg += JSON.stringify(truncateString(msgItem)) + ' ' logMsg += inspect(msgItem, {
continue depth: 10,
compact: true,
breakLength: Infinity,
maxArrayLength: 220
}) + ' '
} else {
logMsg += msgItem + ' '
} }
logMsg += msgItem + ' '
} }
const currentDateTime = new Date().toLocaleString() const currentDateTime = new Date().toLocaleString()
logMsg = `${currentDateTime} ${logMsg}\n\n` logMsg = `${currentDateTime} ${logMsg}\n\n`

View File

@@ -3,25 +3,19 @@ import { writeFile } from 'node:fs/promises'
import { version } from '../../version' import { version } from '../../version'
import { log, fetchFile } from '.' import { log, fetchFile } from '.'
import { TEMP_DIR } from '../globalVars' import { TEMP_DIR } from '../globalVars'
import { compare } from 'compare-versions'
const downloadMirrorHosts = ['https://ghp.ci/'] const downloadMirrorHosts = ['https://ghp.ci/']
const releasesMirrorHosts = ['https://kkgithub.com'] const releasesMirrorHosts = ['https://kkgithub.com']
export async function checkNewVersion() { export async function checkNewVersion() {
const latestVersionText = await getRemoteVersion() const latestVersion = await getRemoteVersion()
const latestVersion = latestVersionText.split('.')
log('LLOneBot latest version', latestVersion) log('LLOneBot latest version', latestVersion)
const currentVersion = version.split('.') if (latestVersion === '') {
//log('llonebot current version', currentVersion) return { result: false, version: latestVersion }
for (const k of [0, 1, 2]) { }
const latest = parseInt(latestVersion[k]) if (compare(latestVersion, version, '>')) {
const current = parseInt(currentVersion[k]) return { result: true, version: latestVersion }
if (latest > current) {
log('')
return { result: true, version: latestVersionText }
} else if (latest < current) {
break
}
} }
return { result: false, version: version } return { result: false, version: version }
} }

View File

@@ -0,0 +1,9 @@
import { BrowserWindow } from 'electron'
import { log } from '@/common/utils'
export function getAllWindowIds(): number[] {
const allWindows = BrowserWindow.getAllWindows();
const ids = allWindows.map(window => window.id);
log('getAllWindowIds', ids);
return ids;
}

View File

@@ -28,7 +28,7 @@ export default class Log {
}, },
} }
Logger.targets.push(target) Logger.targets.push(target)
ctx.on('llonebot/config-updated', input => { ctx.on('llob/config-updated', input => {
enable = input.log! enable = input.log!
}) })
} }

View File

@@ -2,6 +2,10 @@ import path from 'node:path'
import Log from './log' import Log from './log'
import Core from '../ntqqapi/core' import Core from '../ntqqapi/core'
import OneBot11Adapter from '../onebot11/adapter' import OneBot11Adapter from '../onebot11/adapter'
import SatoriAdapter from '../satori/adapter'
import Database from 'minato'
import SQLiteDriver from '@minatojs/driver-sqlite'
import Store from './store'
import { BrowserWindow, dialog, ipcMain } from 'electron' import { BrowserWindow, dialog, ipcMain } from 'electron'
import { Config as LLOBConfig } from '../common/types' import { Config as LLOBConfig } from '../common/types'
import { import {
@@ -11,15 +15,12 @@ import {
CHANNEL_LOG, CHANNEL_LOG,
CHANNEL_SELECT_FILE, CHANNEL_SELECT_FILE,
CHANNEL_SET_CONFIG, CHANNEL_SET_CONFIG,
CHANNEL_UPDATE, CHANNEL_UPDATE
CHANNEL_SET_CONFIG_CONFIRMED
} from '../common/channels' } from '../common/channels'
import { getBuildVersion } from '../common/utils' import { startHook } from '../ntqqapi/hook'
import { hookNTQQApiCall, hookNTQQApiReceive } from '../ntqqapi/hook'
import { checkNewVersion, upgradeLLOneBot } from '../common/utils/upgrade' import { checkNewVersion, upgradeLLOneBot } from '../common/utils/upgrade'
import { getConfigUtil } from '../common/config' import { getConfigUtil } from '../common/config'
import { checkFfmpeg } from '../common/utils/video' import { checkFfmpeg } from '../common/utils/video'
import { getSession } from '../ntqqapi/wrapper'
import { Context } from 'cordis' import { Context } from 'cordis'
import { llonebotError, selfInfo, LOG_DIR, DATA_DIR, TEMP_DIR } from '../common/globalVars' import { llonebotError, selfInfo, LOG_DIR, DATA_DIR, TEMP_DIR } from '../common/globalVars'
import { log, logFileName } from '../common/utils/legacyLog' import { log, logFileName } from '../common/utils/legacyLog'
@@ -33,15 +34,12 @@ import {
NTQQWebApi, NTQQWebApi,
NTQQWindowApi NTQQWindowApi
} from '../ntqqapi/api' } from '../ntqqapi/api'
import { mkdir } from 'node:fs/promises'
import { existsSync, mkdirSync } from 'node:fs' import { existsSync, mkdirSync } from 'node:fs'
import Database from 'minato' import { NTQQSystemApi } from '@/ntqqapi/api/system'
import SQLiteDriver from '@minatojs/driver-sqlite'
import Store from './store'
declare module 'cordis' { declare module 'cordis' {
interface Events { interface Events {
'llonebot/config-updated': (input: LLOBConfig) => void 'llob/config-updated': (input: LLOBConfig) => void
} }
} }
@@ -57,6 +55,30 @@ function onLoad() {
mkdirSync(LOG_DIR) mkdirSync(LOG_DIR)
} }
if (!existsSync(TEMP_DIR)) {
mkdirSync(TEMP_DIR)
}
const dbDir = path.join(DATA_DIR, 'database')
if (!existsSync(dbDir)) {
mkdirSync(dbDir)
}
const ctx = new Context()
ctx.plugin(NTQQFileApi)
ctx.plugin(NTQQFileCacheApi)
ctx.plugin(NTQQFriendApi)
ctx.plugin(NTQQGroupApi)
ctx.plugin(NTQQMsgApi)
ctx.plugin(NTQQUserApi)
ctx.plugin(NTQQWebApi)
ctx.plugin(NTQQWindowApi)
ctx.plugin(Database)
ctx.plugin(NTQQSystemApi)
let started = false
ipcMain.handle(CHANNEL_CHECK_VERSION, async () => { ipcMain.handle(CHANNEL_CHECK_VERSION, async () => {
return checkNewVersion() return checkNewVersion()
}) })
@@ -115,7 +137,9 @@ function onLoad() {
if (!ask) { if (!ask) {
getConfigUtil().setConfig(config) getConfigUtil().setConfig(config)
log('配置已更新', config) log('配置已更新', config)
checkFfmpeg(config.ffmpeg).then() if (started) {
ctx.parallel('llob/config-updated', config)
}
resolve(true) resolve(true)
return return
} }
@@ -132,7 +156,9 @@ function onLoad() {
if (result.response === 0) { if (result.response === 0) {
getConfigUtil().setConfig(config) getConfigUtil().setConfig(config)
log('配置已更新', config) log('配置已更新', config)
checkFfmpeg(config.ffmpeg).then() if (started) {
ctx.parallel('llob/config-updated', config)
}
resolve(true) resolve(true)
} }
}) })
@@ -147,87 +173,66 @@ function onLoad() {
log(arg) log(arg)
}) })
async function start() { const intervalId = setInterval(async () => {
log('process pid', process.pid)
const config = getConfigUtil().getConfig()
if (!config.enableLLOB) {
llonebotError.otherError = 'LLOneBot 未启动'
log('LLOneBot 开关设置为关闭不启动LLOneBot')
return
}
if (!existsSync(TEMP_DIR)) {
await mkdir(TEMP_DIR)
}
const dbDir = path.join(DATA_DIR, 'database')
if (!existsSync(dbDir)) {
await mkdir(dbDir)
}
const ctx = new Context()
ctx.plugin(Log, {
enable: config.log!,
filename: logFileName
})
ctx.plugin(NTQQFileApi)
ctx.plugin(NTQQFileCacheApi)
ctx.plugin(NTQQFriendApi)
ctx.plugin(NTQQGroupApi)
ctx.plugin(NTQQMsgApi)
ctx.plugin(NTQQUserApi)
ctx.plugin(NTQQWebApi)
ctx.plugin(NTQQWindowApi)
ctx.plugin(Core, config)
ctx.plugin(OneBot11Adapter, {
...config.ob11,
heartInterval: config.heartInterval,
token: config.token!,
debug: config.debug!,
reportSelfMessage: config.reportSelfMessage!,
msgCacheExpire: config.msgCacheExpire!,
musicSignUrl: config.musicSignUrl,
enableLocalFile2Url: config.enableLocalFile2Url!,
ffmpeg: config.ffmpeg,
})
ctx.plugin(Database)
ctx.plugin(SQLiteDriver, {
path: path.join(dbDir, `${selfInfo.uin}.db`)
})
ctx.plugin(Store)
ctx.start()
ipcMain.on(CHANNEL_SET_CONFIG_CONFIRMED, (event, config: LLOBConfig) => {
ctx.parallel('llonebot/config-updated', config)
})
}
const buildVersion = getBuildVersion()
const intervalId = setInterval(() => {
const self = Object.assign(selfInfo, { const self = Object.assign(selfInfo, {
uin: globalThis.authData?.uin, uin: globalThis.authData?.uin,
uid: globalThis.authData?.uid, uid: globalThis.authData?.uid,
online: true online: true
}) })
if (self.uin && (buildVersion >= 27187 || getSession())) { if (self.uin) {
clearInterval(intervalId) clearInterval(intervalId)
start() log('process pid', process.pid)
const config = getConfigUtil().getConfig()
if (config.enableLLOB && (config.satori.enable || config.ob11.enable)) {
startHook()
await ctx.sleep(600)
} else {
llonebotError.otherError = 'LLOneBot 未启动'
log('LLOneBot 开关设置为关闭,不启动 LLOneBot')
return
}
ctx.plugin(Log, {
enable: config.log!,
filename: logFileName
})
ctx.plugin(SQLiteDriver, {
path: path.join(dbDir, `${selfInfo.uin}.db`)
})
ctx.plugin(Store, {
msgCacheExpire: config.msgCacheExpire! * 1000
})
ctx.plugin(Core, config)
if (config.ob11.enable) {
ctx.plugin(OneBot11Adapter, {
...config.ob11,
heartInterval: config.heartInterval,
token: config.token!,
debug: config.debug!,
musicSignUrl: config.musicSignUrl,
enableLocalFile2Url: config.enableLocalFile2Url!,
ffmpeg: config.ffmpeg,
})
}
if (config.satori.enable) {
ctx.plugin(SatoriAdapter, {
...config.satori,
ffmpeg: config.ffmpeg,
})
}
ctx.start()
started = true
llonebotError.otherError = ''
} }
}, 600) }, 500)
} }
// 创建窗口时触发 // 创建窗口时触发
function onBrowserWindowCreated(window: BrowserWindow) { function onBrowserWindowCreated(window: BrowserWindow) {
if (![2, 4, 6].includes(window.id)) {
return
}
if (window.id === 2) { if (window.id === 2) {
mainWindow = window mainWindow = window
} }
//log('window create', window.webContents.getURL().toString())
try {
hookNTQQApiCall(window, window.id !== 2)
hookNTQQApiReceive(window, window.id !== 2)
} catch (e) {
log('LLOneBot hook error: ', String(e))
}
} }
try { try {

View File

@@ -1,4 +1,4 @@
import { Peer } from '@/ntqqapi/types' import { Peer, RawMessage } from '@/ntqqapi/types'
import { createHash } from 'node:crypto' import { createHash } from 'node:crypto'
import { LimitedHashTable } from '@/common/utils/table' import { LimitedHashTable } from '@/common/utils/table'
import { FileCacheV2 } from '@/common/types' import { FileCacheV2 } from '@/common/types'
@@ -24,13 +24,15 @@ interface MsgInfo {
peer: Peer peer: Peer
} }
export default class Store extends Service { class Store extends Service {
static inject = ['database', 'model'] static inject = ['database', 'model']
private cache: LimitedHashTable<string, number> private cache: LimitedHashTable<string, number>
private messages: Map<string, RawMessage>
constructor(protected ctx: Context) { constructor(protected ctx: Context, public config: Store.Config) {
super(ctx, 'store', true) super(ctx, 'store', true)
this.cache = new LimitedHashTable(1000) this.cache = new LimitedHashTable(1000)
this.messages = new Map()
this.initDatabase() this.initDatabase()
} }
@@ -60,9 +62,10 @@ export default class Store extends Service {
} }
createMsgShortId(peer: Peer, msgId: string): number { createMsgShortId(peer: Peer, msgId: string): number {
// OneBot 11 要求 message_id 为 int32
const cacheKey = `${msgId}|${peer.chatType}|${peer.peerUid}` const cacheKey = `${msgId}|${peer.chatType}|${peer.peerUid}`
const hash = createHash('md5').update(cacheKey).digest() const hash = createHash('md5').update(cacheKey).digest()
hash[0] &= 0x7f //设置第一个bit为0 保证shortId为正数 hash[0] &= 0x7f //保证shortId为正数
const shortId = hash.readInt32BE() const shortId = hash.readInt32BE()
this.cache.set(cacheKey, shortId) this.cache.set(cacheKey, shortId)
this.ctx.database.upsert('message', [{ this.ctx.database.upsert('message', [{
@@ -123,4 +126,29 @@ export default class Store extends Service {
getFileCacheById(fileUuid: string) { getFileCacheById(fileUuid: string) {
return this.ctx.database.get('file_v2', { fileUuid }) return this.ctx.database.get('file_v2', { fileUuid })
} }
async addMsgCache(msg: RawMessage) {
const expire = this.config.msgCacheExpire
if (expire === 0) {
return
}
const id = msg.msgId
this.messages.set(id, msg)
setTimeout(() => {
this.messages.delete(id)
}, expire)
}
getMsgCache(msgId: string) {
return this.messages.get(msgId)
}
} }
namespace Store {
export interface Config {
/** 单位为毫秒 */
msgCacheExpire: number
}
}
export default Store

View File

@@ -16,10 +16,8 @@ import path from 'node:path'
import { existsSync } from 'node:fs' import { existsSync } from 'node:fs'
import { ReceiveCmdS } from '../hook' import { ReceiveCmdS } from '../hook'
import { RkeyManager } from '@/ntqqapi/helper/rkey' import { RkeyManager } from '@/ntqqapi/helper/rkey'
import { getSession } from '@/ntqqapi/wrapper' import { RichMediaDownloadCompleteNotify, RichMediaUploadCompleteNotify, RMBizType, Peer } from '@/ntqqapi/types/msg'
import { OnRichMediaDownloadCompleteParams, Peer } from '@/ntqqapi/types/msg'
import { calculateFileMD5 } from '@/common/utils/file' import { calculateFileMD5 } from '@/common/utils/file'
import { fileTypeFromFile } from 'file-type'
import { copyFile, stat, unlink } from 'node:fs/promises' import { copyFile, stat, unlink } from 'node:fs/promises'
import { Time } from 'cosmokit' import { Time } from 'cosmokit'
import { Service, Context } from 'cordis' import { Service, Context } from 'cordis'
@@ -39,39 +37,33 @@ export class NTQQFileApi extends Service {
this.rkeyManager = new RkeyManager(ctx, 'https://llob.linyuchen.net/rkey') this.rkeyManager = new RkeyManager(ctx, 'https://llob.linyuchen.net/rkey')
} }
async getVideoUrl(peer: Peer, msgId: string, elementId: string) { async getVideoUrl(peer: Peer, msgId: string, elementId: string): Promise<string | undefined> {
const session = getSession() const data = await invoke('nodeIKernelRichMediaService/getVideoPlayUrlV2', [{
if (session) { peer,
return (await session.getRichMediaService().getVideoPlayUrlV2( msgId,
peer, elemId: elementId,
msgId, videoCodecFormat: 0,
elementId, exParams: {
0, downSourceType: 1,
{ downSourceType: 1, triggerType: 1 } triggerType: 1
)).urlResult.domainUrl[0]?.url
} else {
const data = await invoke('nodeIKernelRichMediaService/getVideoPlayUrlV2', [{
peer,
msgId,
elemId: elementId,
videoCodecFormat: 0,
exParams: {
downSourceType: 1,
triggerType: 1
},
}, null])
if (data.result !== 0) {
this.ctx.logger.warn('getVideoUrl', data)
} }
return data.urlResult.domainUrl[0]?.url }])
if (data.result !== 0) {
this.ctx.logger.warn('getVideoUrl', data)
} }
return data.urlResult.domainUrl[0]?.url
} }
async getFileType(filePath: string) { async getFileType(filePath: string) {
return fileTypeFromFile(filePath) return await invoke<{
ext: string
mime: string
}>(NTMethod.FILE_TYPE, [filePath], {
className: NTClass.FS_API
})
} }
// 上传文件到QQ的文件夹 /** 上传文件到 QQ 的文件夹 */
async uploadFile(filePath: string, elementType = ElementType.Pic, elementSubType = 0) { async uploadFile(filePath: string, elementType = ElementType.Pic, elementSubType = 0) {
const fileMd5 = await calculateFileMD5(filePath) const fileMd5 = await calculateFileMD5(filePath)
let fileName = path.basename(filePath) let fileName = path.basename(filePath)
@@ -121,25 +113,22 @@ export class NTQQFileApi extends Service {
return sourcePath return sourcePath
} }
} }
const data = await invoke<{ notifyInfo: OnRichMediaDownloadCompleteParams }>( const data = await invoke<{ notifyInfo: RichMediaDownloadCompleteNotify }>(
'nodeIKernelMsgService/downloadRichMedia', 'nodeIKernelMsgService/downloadRichMedia',
[ [{
{ getReq: {
getReq: { fileModelId: '0',
fileModelId: '0', downloadSourceType: 0,
downloadSourceType: 0, triggerType: 1,
triggerType: 1, msgId: msgId,
msgId: msgId, chatType: chatType,
chatType: chatType, peerUid: peerUid,
peerUid: peerUid, elementId: elementId,
elementId: elementId, thumbSize: 0,
thumbSize: 0, downloadType: 1,
downloadType: 1, filePath: thumbPath,
filePath: thumbPath,
},
}, },
null, }],
],
{ {
cbCmd: ReceiveCmdS.MEDIA_DOWNLOAD_COMPLETE, cbCmd: ReceiveCmdS.MEDIA_DOWNLOAD_COMPLETE,
cmdCB: payload => payload.notifyInfo.msgId === msgId, cmdCB: payload => payload.notifyInfo.msgId === msgId,
@@ -174,8 +163,8 @@ export class NTQQFileApi extends Service {
if (url) { if (url) {
const parsedUrl = new URL(IMAGE_HTTP_HOST + url) //临时解析拼接 const parsedUrl = new URL(IMAGE_HTTP_HOST + url) //临时解析拼接
const imageAppid = parsedUrl.searchParams.get('appid') const imageAppid = parsedUrl.searchParams.get('appid')
const isNewPic = imageAppid && ['1406', '1407'].includes(imageAppid) const isNTPic = imageAppid && ['1406', '1407'].includes(imageAppid)
if (isNewPic) { if (isNTPic) {
let rkey = parsedUrl.searchParams.get('rkey') let rkey = parsedUrl.searchParams.get('rkey')
if (rkey) { if (rkey) {
return IMAGE_HTTP_HOST_NT + url return IMAGE_HTTP_HOST_NT + url
@@ -196,16 +185,13 @@ export class NTQQFileApi extends Service {
} }
async downloadFileForModelId(peer: Peer, fileModelId: string, timeout = 2 * Time.minute) { async downloadFileForModelId(peer: Peer, fileModelId: string, timeout = 2 * Time.minute) {
const data = await invoke<{ notifyInfo: OnRichMediaDownloadCompleteParams }>( const data = await invoke<{ notifyInfo: RichMediaDownloadCompleteNotify }>(
'nodeIKernelRichMediaService/downloadFileForModelId', 'nodeIKernelRichMediaService/downloadFileForModelId',
[ [{
{ peer,
peer, fileModelIdList: [fileModelId],
fileModelIdList: [fileModelId], save_path: ''
save_path: '' }],
},
null,
],
{ {
cbCmd: ReceiveCmdS.MEDIA_DOWNLOAD_COMPLETE, cbCmd: ReceiveCmdS.MEDIA_DOWNLOAD_COMPLETE,
cmdCB: payload => payload.notifyInfo.fileModelId === fileModelId, cmdCB: payload => payload.notifyInfo.fileModelId === fileModelId,
@@ -215,6 +201,38 @@ export class NTQQFileApi extends Service {
) )
return data.notifyInfo.filePath return data.notifyInfo.filePath
} }
async ocrImage(path: string) {
return await invoke(
'nodeIKernelNodeMiscService/wantWinScreenOCR',
[
{ url: path },
{ timeout: 5000 }
]
)
}
async uploadRMFileWithoutMsg(filePath: string, bizType: RMBizType, peerUid: string) {
const data = await invoke<{
notifyInfo: RichMediaUploadCompleteNotify
}>(
'nodeIKernelRichMediaService/uploadRMFileWithoutMsg',
[{
params: {
filePath,
bizType,
peerUid,
useNTV2: true
}
}],
{
cbCmd: ReceiveCmdS.MEDIA_UPLOAD_COMPLETE,
cmdCB: payload => payload.notifyInfo.filePath === filePath,
timeout: 10 * Time.second,
}
)
return data.notifyInfo
}
} }
export class NTQQFileCacheApi extends Service { export class NTQQFileCacheApi extends Service {
@@ -223,21 +241,19 @@ export class NTQQFileCacheApi extends Service {
} }
async setCacheSilentScan(isSilent: boolean = true) { async setCacheSilentScan(isSilent: boolean = true) {
return await invoke<GeneralCallResult>(NTMethod.CACHE_SET_SILENCE, [{ isSilent }, null]) return await invoke<GeneralCallResult>(NTMethod.CACHE_SET_SILENCE, [{ isSilent }])
} }
getCacheSessionPathList() { getCacheSessionPathList() {
return invoke< return invoke<Array<{
{ key: string
key: string value: string
value: string }>>(NTMethod.CACHE_PATH_SESSION, [], { className: NTClass.OS_API })
}[]
>(NTMethod.CACHE_PATH_SESSION, [], { className: NTClass.OS_API })
} }
scanCache() { scanCache() {
invoke<GeneralCallResult>(ReceiveCmdS.CACHE_SCAN_FINISH, [], { classNameIsRegister: true }) invoke<GeneralCallResult>(ReceiveCmdS.CACHE_SCAN_FINISH, [], { registerEvent: true })
return invoke<CacheScanResult>(NTMethod.CACHE_SCAN, [null, null], { timeout: 300 * Time.second }) return invoke<CacheScanResult>(NTMethod.CACHE_SCAN, [], { timeout: 300 * Time.second })
} }
getHotUpdateCachePath() { getHotUpdateCachePath() {
@@ -257,13 +273,13 @@ export class NTQQFileCacheApi extends Service {
pageSize: pageSize, pageSize: pageSize,
order: 1, order: 1,
lastRecord: _lastRecord, lastRecord: _lastRecord,
}, null]) }])
} }
async clearChatCache(chats: ChatCacheListItemBasic[] = [], fileKeys: string[] = []) { async clearChatCache(chats: ChatCacheListItemBasic[] = [], fileKeys: string[] = []) {
return await invoke<GeneralCallResult>(NTMethod.CACHE_CHAT_CLEAR, [{ return await invoke<GeneralCallResult>(NTMethod.CACHE_CHAT_CLEAR, [{
chats, chats,
fileKeys, fileKeys,
}, null]) }])
} }
} }

View File

@@ -1,8 +1,6 @@
import { Friend, FriendV2, SimpleInfo, CategoryFriend, BuddyListReqType } from '../types' import { Friend, SimpleInfo, CategoryFriend } from '../types'
import { ReceiveCmdS } from '../hook' import { ReceiveCmdS } from '../hook'
import { invoke, NTMethod, NTClass } from '../ntcall' import { invoke, NTMethod, NTClass } from '../ntcall'
import { getSession } from '@/ntqqapi/wrapper'
import { Dict, pick } from 'cosmokit'
import { Service, Context } from 'cordis' import { Service, Context } from 'cordis'
declare module 'cordis' { declare module 'cordis' {
@@ -18,49 +16,32 @@ export class NTQQFriendApi extends Service {
/** 大于或等于 26702 应使用 getBuddyV2 */ /** 大于或等于 26702 应使用 getBuddyV2 */
async getFriends() { async getFriends() {
const data = await invoke<{ const res = await invoke<{
data: { data: {
categoryId: number categoryId: number
categroyName: string categroyName: string
categroyMbCount: number categroyMbCount: number
buddyList: Friend[] buddyList: Friend[]
}[] }[]
}>( }>('getBuddyList', [], {
'getBuddyList', className: NTClass.NODE_STORE_API,
[], cbCmd: ReceiveCmdS.FRIENDS,
{ afterFirstCmd: false
className: NTClass.NODE_STORE_API, })
cbCmd: ReceiveCmdS.FRIENDS, return res.data.flatMap(e => e.buddyList)
afterFirstCmd: false,
}
)
const _friends: Friend[] = []
for (const item of data.data) {
_friends.push(...item.buddyList)
}
return _friends
} }
async handleFriendRequest(friendUid: string, reqTime: string, accept: boolean) { async handleFriendRequest(friendUid: string, reqTime: string, accept: boolean) {
const session = getSession() return await invoke(NTMethod.HANDLE_FRIEND_REQUEST, [{
if (session) { approvalInfo: {
return session.getBuddyService().approvalFriendRequest({
friendUid, friendUid,
reqTime, reqTime,
accept accept,
}) },
} else { }])
return await invoke(NTMethod.HANDLE_FRIEND_REQUEST, [{
approvalInfo: {
friendUid,
reqTime,
accept,
},
}])
}
} }
async getBuddyV2(refresh = false): Promise<FriendV2[]> { async getBuddyV2(refresh = false): Promise<SimpleInfo[]> {
const data = await invoke<{ const data = await invoke<{
buddyCategory: CategoryFriend[] buddyCategory: CategoryFriend[]
userSimpleInfos: Record<string, SimpleInfo> userSimpleInfos: Record<string, SimpleInfo>
@@ -77,7 +58,7 @@ export class NTQQFriendApi extends Service {
return Object.values(data.userSimpleInfos).filter(v => uids.includes(v.uid!)) return Object.values(data.userSimpleInfos).filter(v => uids.includes(v.uid!))
} }
/** uid => uin */ /** uid -> uin */
async getBuddyIdMap(refresh = false): Promise<Map<string, string>> { async getBuddyIdMap(refresh = false): Promise<Map<string, string>> {
const retMap: Map<string, string> = new Map() const retMap: Map<string, string> = new Map()
const data = await invoke<{ const data = await invoke<{
@@ -101,71 +82,48 @@ export class NTQQFriendApi extends Service {
return retMap return retMap
} }
async getBuddyV2ExWithCate(refresh = false) { async getBuddyV2WithCate(refresh = false) {
const session = getSession() const data = await invoke<{
if (session) { buddyCategory: CategoryFriend[]
const uids: string[] = [] userSimpleInfos: Record<string, SimpleInfo>
const categoryMap: Map<string, Dict> = new Map() }>(
const buddyService = session.getBuddyService() 'getBuddyList',
const buddyListV2 = (await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL)).data [refresh],
uids.push( {
...buddyListV2.flatMap(item => { className: NTClass.NODE_STORE_API,
item.buddyUids.forEach(uid => { cbCmd: ReceiveCmdS.FRIENDS,
categoryMap.set(uid, { categoryId: item.categoryId, categroyName: item.categroyName }) afterFirstCmd: false,
})
return item.buddyUids
}))
const data = await session.getProfileService().getCoreAndBaseInfo('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>
}>(
'getBuddyList',
[refresh],
{
className: NTClass.NODE_STORE_API,
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!)) return data
.map(value => {
return {
...value,
categoryId: value.baseInfo.categoryId,
categroyName: category.get(value.baseInfo.categoryId)?.categroyName
}
})
}
} }
async isBuddy(uid: string): Promise<boolean> { async isBuddy(uid: string): Promise<boolean> {
const session = getSession() return await invoke('nodeIKernelBuddyService/isBuddy', [{ uid }])
if (session) {
return session.getBuddyService().isBuddy(uid)
} else {
return await invoke('nodeIKernelBuddyService/isBuddy', [{ uid }, null])
}
} }
async getBuddyRecommendContact(uin: string) { async getBuddyRecommendContact(uin: string) {
const ret = await invoke('nodeIKernelBuddyService/getBuddyRecommendContactArkJson', [{ uin }, null]) const ret = await invoke('nodeIKernelBuddyService/getBuddyRecommendContactArkJson', [{ uin }])
return ret.arkMsg return ret.arkMsg
} }
async setBuddyRemark(uid: string, remark: string) { async setBuddyRemark(uid: string, remark = '') {
return await invoke('nodeIKernelBuddyService/setBuddyRemark', [{ return await invoke('nodeIKernelBuddyService/setBuddyRemark', [{
remarkParams: { uid, remark } remarkParams: { uid, remark }
}, null]) }])
}
async delBuddy(friendUid: string) {
return await invoke('nodeIKernelBuddyService/delBuddy', [{
delInfo: {
friendUid,
tempBlock: false,
tempBothDel: true
}
}])
}
async setBuddyCategory(uid: string, categoryId: number) {
return await invoke('nodeIKernelBuddyService/setBuddyCategory', [{ uid, categoryId }])
} }
} }

View File

@@ -6,16 +6,16 @@ import {
GroupNotifies, GroupNotifies,
GroupRequestOperateTypes, GroupRequestOperateTypes,
GetFileListParam, GetFileListParam,
OnGroupFileInfoUpdateParams, PublishGroupBulletinReq,
PublishGroupBulletinReq GroupAllInfo,
GroupFileInfo,
GroupBulletinListResult,
GroupMsgMask
} from '../types' } from '../types'
import { invoke, NTClass, NTMethod } from '../ntcall' import { invoke, NTClass, NTMethod } from '../ntcall'
import { GeneralCallResult } from '../services' import { GeneralCallResult } from '../services'
import { NTQQWindows } from './window' import { NTQQWindows } from './window'
import { getSession } from '../wrapper'
import { NodeIKernelGroupService } from '../services'
import { Service, Context } from 'cordis' import { Service, Context } from 'cordis'
import { isNumeric } from '@/common/utils/misc'
declare module 'cordis' { declare module 'cordis' {
interface Context { interface Context {
@@ -26,8 +26,6 @@ declare module 'cordis' {
export class NTQQGroupApi extends Service { export class NTQQGroupApi extends Service {
static inject = ['ntWindowApi'] static inject = ['ntWindowApi']
public groupMembers: Map<string, Map<string, GroupMember>> = new Map<string, Map<string, GroupMember>>()
constructor(protected ctx: Context) { constructor(protected ctx: Context) {
super(ctx, 'ntGroupApi', true) super(ctx, 'ntGroupApi', true)
} }
@@ -48,54 +46,45 @@ export class NTQQGroupApi extends Service {
return result.groupList return result.groupList
} }
async getGroupMembers(groupCode: string, num = 3000): Promise<Map<string, GroupMember>> { async getGroupMembers(groupCode: string, num = 3000) {
const session = getSession() const sceneId = await invoke(NTMethod.GROUP_MEMBER_SCENE, [{
let result: Awaited<ReturnType<NodeIKernelGroupService['getNextMemberList']>> groupCode,
if (session) { scene: 'groupMemberList_MainWindow'
const groupService = session.getGroupService() }])
const sceneId = groupService.createMemberListScene(groupCode, 'groupMemberList_MainWindow') const data = await invoke(NTMethod.GROUP_MEMBERS, [{ sceneId, num }])
result = await groupService.getNextMemberList(sceneId, undefined, num) if (data.errCode !== 0) {
} else { throw new Error('获取群成员列表出错,' + data.errMsg)
const sceneId = await invoke(NTMethod.GROUP_MEMBER_SCENE, [{ groupCode, scene: 'groupMemberList_MainWindow' }])
result = await invoke(NTMethod.GROUP_MEMBERS, [{ sceneId, num }, null])
} }
if (result.errCode !== 0) { return data.result.infos
throw ('获取群成员列表出错,' + result.errMsg)
}
return result.result.infos
} }
async getGroupMember(groupCode: string, memberUinOrUid: string) { async getGroupMember(groupCode: string, uid: string, forceUpdate = false) {
if (!this.groupMembers.has(groupCode)) { await invoke('nodeIKernelGroupListener/onMemberInfoChange', [], {
try { registerEvent: true
// 更新群成员列表 })
this.groupMembers.set(groupCode, await this.getGroupMembers(groupCode))
const data = await invoke<{
groupCode: string
members: Map<string, GroupMember>
}>(
'nodeIKernelGroupService/getMemberInfo',
[{
groupCode,
uids: [uid],
forceUpdate
}],
{
cbCmd: 'nodeIKernelGroupListener/onMemberInfoChange',
afterFirstCmd: false,
cmdCB: payload => payload.members.has(uid),
timeout: 2000
} }
catch (e) { )
return return data.members.get(uid)!
}
}
let members = this.groupMembers.get(groupCode)!
const getMember = () => {
let member: GroupMember | undefined = undefined
if (isNumeric(memberUinOrUid)) {
member = Array.from(members.values()).find(member => member.uin === memberUinOrUid)
} else {
member = members.get(memberUinOrUid)
}
return member
}
let member = getMember()
if (!member) {
this.groupMembers.set(groupCode, await this.getGroupMembers(groupCode))
members = this.groupMembers.get(groupCode)!
member = getMember()
}
return member
} }
async getGroupIgnoreNotifies() { async getGroupIgnoreNotifies() {
await this.getSingleScreenNotifies(14) await this.getSingleScreenNotifies(false, 14)
return await this.ctx.ntWindowApi.openWindow<GeneralCallResult & GroupNotifies>( return await this.ctx.ntWindowApi.openWindow<GeneralCallResult & GroupNotifies>(
NTQQWindows.GroupNotifyFilterWindow, NTQQWindows.GroupNotifyFilterWindow,
[], [],
@@ -103,16 +92,18 @@ export class NTQQGroupApi extends Service {
) )
} }
async getSingleScreenNotifies(num: number) { async getSingleScreenNotifies(doubt: boolean, number: number, startSeq = '') {
invoke(ReceiveCmdS.GROUP_NOTIFY, [], { classNameIsRegister: true }) await invoke(ReceiveCmdS.GROUP_NOTIFY, [], { registerEvent: true })
return (await invoke<GroupNotifies>(
const data = await invoke<GroupNotifies>(
'nodeIKernelGroupService/getSingleScreenNotifies', 'nodeIKernelGroupService/getSingleScreenNotifies',
[{ doubt: false, startSeq: '', number: num }, null], [{ doubt, startSeq, number }],
{ {
cbCmd: ReceiveCmdS.GROUP_NOTIFY, cbCmd: ReceiveCmdS.GROUP_NOTIFY,
afterFirstCmd: false, afterFirstCmd: false,
} }
)).notifies )
return data.notifies
} }
async handleGroupRequest(flag: string, operateType: GroupRequestOperateTypes, reason?: string) { async handleGroupRequest(flag: string, operateType: GroupRequestOperateTypes, reason?: string) {
@@ -120,168 +111,98 @@ export class NTQQGroupApi extends Service {
const groupCode = flagitem[0] const groupCode = flagitem[0]
const seq = flagitem[1] const seq = flagitem[1]
const type = parseInt(flagitem[2]) const type = parseInt(flagitem[2])
const session = getSession() const doubt = flagitem[3] === '1'
if (session) { return await invoke(NTMethod.HANDLE_GROUP_REQUEST, [{
return session.getGroupService().operateSysNotify(false, { doubt,
operateType, // 2 拒绝 operateMsg: {
operateType,
targetMsg: { targetMsg: {
seq, // 通知序列号 seq,
type, type,
groupCode, groupCode,
postscript: reason || ' ' // 仅传空值可能导致处理失败,故默认给个空格 postscript: reason || ' ' // 仅传空值可能导致处理失败,故默认给个空格
}
})
} else {
return await invoke(NTMethod.HANDLE_GROUP_REQUEST, [{
doubt: false,
operateMsg: {
operateType,
targetMsg: {
seq,
type,
groupCode,
postscript: reason || ' ' // 仅传空值可能导致处理失败,故默认给个空格
},
}, },
}, null]) },
} }])
} }
async quitGroup(groupCode: string) { async quitGroup(groupCode: string) {
const session = getSession() return await invoke(NTMethod.QUIT_GROUP, [{ groupCode }])
if (session) {
return session.getGroupService().quitGroup(groupCode)
} else {
return await invoke(NTMethod.QUIT_GROUP, [{ groupCode }, null])
}
} }
async kickMember(groupCode: string, kickUids: string[], refuseForever = false, kickReason = '') { async kickMember(groupCode: string, kickUids: string[], refuseForever = false, kickReason = '') {
const session = getSession() return await invoke(NTMethod.KICK_MEMBER, [{ groupCode, kickUids, refuseForever, kickReason }])
if (session) {
return session.getGroupService().kickMember(groupCode, kickUids, refuseForever, kickReason)
} else {
return await invoke(NTMethod.KICK_MEMBER, [{ groupCode, kickUids, refuseForever, kickReason }])
}
} }
/** timeStamp为秒数, 0为解除禁言 */
async banMember(groupCode: string, memList: Array<{ uid: string, timeStamp: number }>) { async banMember(groupCode: string, memList: Array<{ uid: string, timeStamp: number }>) {
// timeStamp为秒数, 0为解除禁言 return await invoke(NTMethod.MUTE_MEMBER, [{ groupCode, memList }])
const session = getSession()
if (session) {
return session.getGroupService().setMemberShutUp(groupCode, memList)
} else {
return await invoke(NTMethod.MUTE_MEMBER, [{ groupCode, memList }])
}
} }
async banGroup(groupCode: string, shutUp: boolean) { async banGroup(groupCode: string, shutUp: boolean) {
const session = getSession() return await invoke(NTMethod.MUTE_GROUP, [{ groupCode, shutUp }])
if (session) {
return session.getGroupService().setGroupShutUp(groupCode, shutUp)
} else {
return await invoke(NTMethod.MUTE_GROUP, [{ groupCode, shutUp }, null])
}
} }
async setMemberCard(groupCode: string, memberUid: string, cardName: string) { async setMemberCard(groupCode: string, memberUid: string, cardName: string) {
const session = getSession() return await invoke(NTMethod.SET_MEMBER_CARD, [{ groupCode, uid: memberUid, cardName }])
if (session) {
return session.getGroupService().modifyMemberCardName(groupCode, memberUid, cardName)
} else {
return await invoke(NTMethod.SET_MEMBER_CARD, [{ groupCode, uid: memberUid, cardName }, null])
}
} }
async setMemberRole(groupCode: string, memberUid: string, role: GroupMemberRole) { async setMemberRole(groupCode: string, memberUid: string, role: GroupMemberRole) {
const session = getSession() return await invoke(NTMethod.SET_MEMBER_ROLE, [{ groupCode, uid: memberUid, role }])
if (session) {
return session.getGroupService().modifyMemberRole(groupCode, memberUid, role)
} else {
return await invoke(NTMethod.SET_MEMBER_ROLE, [{ groupCode, uid: memberUid, role }, null])
}
} }
async setGroupName(groupCode: string, groupName: string) { async setGroupName(groupCode: string, groupName: string) {
const session = getSession() return await invoke(NTMethod.SET_GROUP_NAME, [{ groupCode, groupName }])
if (session) {
return session.getGroupService().modifyGroupName(groupCode, groupName, false)
} else {
return await invoke(NTMethod.SET_GROUP_NAME, [{ groupCode, groupName }, null])
}
} }
async getGroupRemainAtTimes(groupCode: string) { async getGroupRemainAtTimes(groupCode: string) {
return await invoke(NTMethod.GROUP_AT_ALL_REMAIN_COUNT, [{ groupCode }, null]) return await invoke(NTMethod.GROUP_AT_ALL_REMAIN_COUNT, [{ groupCode }])
} }
async removeGroupEssence(groupCode: string, msgId: string) { async removeGroupEssence(groupCode: string, msgId: string) {
const session = getSession() const ntMsgApi = this.ctx.get('ntMsgApi')!
if (session) { const data = await ntMsgApi.getMsgHistory({ chatType: 2, guildId: '', peerUid: groupCode }, msgId, 1, false)
const data = await session.getMsgService().getMsgsIncludeSelf({ chatType: 2, guildId: '', peerUid: groupCode }, msgId, 1, false) return await invoke('nodeIKernelGroupService/removeGroupEssence', [{
return session.getGroupService().removeGroupEssence({ req: {
groupCode: groupCode, groupCode: groupCode,
msgRandom: Number(data?.msgList[0].msgRandom), msgRandom: Number(data?.msgList[0].msgRandom),
msgSeq: Number(data?.msgList[0].msgSeq) msgSeq: Number(data?.msgList[0].msgSeq)
}) }
} else { }])
const ntMsgApi = this.ctx.get('ntMsgApi')!
const data = await ntMsgApi.getMsgHistory({ chatType: 2, guildId: '', peerUid: groupCode }, msgId, 1, false)
return await invoke('nodeIKernelGroupService/removeGroupEssence', [{
req: {
groupCode: groupCode,
msgRandom: Number(data?.msgList[0].msgRandom),
msgSeq: Number(data?.msgList[0].msgSeq)
}
}, null])
}
} }
async addGroupEssence(groupCode: string, msgId: string) { async addGroupEssence(groupCode: string, msgId: string) {
const session = getSession() const ntMsgApi = this.ctx.get('ntMsgApi')!
if (session) { const data = await ntMsgApi.getMsgHistory({ chatType: 2, guildId: '', peerUid: groupCode }, msgId, 1, false)
const data = await session.getMsgService().getMsgsIncludeSelf({ chatType: 2, guildId: '', peerUid: groupCode }, msgId, 1, false) return await invoke('nodeIKernelGroupService/addGroupEssence', [{
return session.getGroupService().addGroupEssence({ req: {
groupCode: groupCode, groupCode: groupCode,
msgRandom: Number(data?.msgList[0].msgRandom), msgRandom: Number(data?.msgList[0].msgRandom),
msgSeq: Number(data?.msgList[0].msgSeq) msgSeq: Number(data?.msgList[0].msgSeq)
}) }
} else { }])
const ntMsgApi = this.ctx.get('ntMsgApi')!
const data = await ntMsgApi.getMsgHistory({ chatType: 2, guildId: '', peerUid: groupCode }, msgId, 1, false)
return await invoke('nodeIKernelGroupService/addGroupEssence', [{
req: {
groupCode: groupCode,
msgRandom: Number(data?.msgList[0].msgRandom),
msgSeq: Number(data?.msgList[0].msgSeq)
}
}, null])
}
} }
async createGroupFileFolder(groupId: string, folderName: string) { async createGroupFileFolder(groupId: string, folderName: string) {
return await invoke('nodeIKernelRichMediaService/createGroupFolder', [{ groupId, folderName }, null]) return await invoke('nodeIKernelRichMediaService/createGroupFolder', [{ groupId, folderName }])
} }
async deleteGroupFileFolder(groupId: string, folderId: string) { async deleteGroupFileFolder(groupId: string, folderId: string) {
return await invoke('nodeIKernelRichMediaService/deleteGroupFolder', [{ groupId, folderId }, null]) return await invoke('nodeIKernelRichMediaService/deleteGroupFolder', [{ groupId, folderId }])
} }
async deleteGroupFile(groupId: string, fileIdList: string[], busIdList: number[]) { async deleteGroupFile(groupId: string, fileIdList: string[], busIdList: number[]) {
return await invoke('nodeIKernelRichMediaService/deleteGroupFile', [{ groupId, busIdList, fileIdList }, null]) return await invoke('nodeIKernelRichMediaService/deleteGroupFile', [{ groupId, busIdList, fileIdList }])
} }
async getGroupFileList(groupId: string, fileListForm: GetFileListParam) { async getGroupFileList(groupId: string, fileListForm: GetFileListParam) {
invoke('nodeIKernelMsgListener/onGroupFileInfoUpdate', [], { classNameIsRegister: true }) invoke('nodeIKernelMsgListener/onGroupFileInfoUpdate', [], { registerEvent: true })
const data = await invoke<{ fileInfo: OnGroupFileInfoUpdateParams }>( const data = await invoke<{ fileInfo: GroupFileInfo }>(
'nodeIKernelRichMediaService/getGroupFileList', 'nodeIKernelRichMediaService/getGroupFileList',
[ [{
{ groupId,
groupId, fileListForm
fileListForm }],
},
null,
],
{ {
cbCmd: 'nodeIKernelMsgListener/onGroupFileInfoUpdate', cbCmd: 'nodeIKernelMsgListener/onGroupFileInfoUpdate',
afterFirstCmd: false, afterFirstCmd: false,
@@ -294,17 +215,17 @@ export class NTQQGroupApi extends Service {
async publishGroupBulletin(groupCode: string, req: PublishGroupBulletinReq) { async publishGroupBulletin(groupCode: string, req: PublishGroupBulletinReq) {
const ntUserApi = this.ctx.get('ntUserApi')! const ntUserApi = this.ctx.get('ntUserApi')!
const psKey = (await ntUserApi.getPSkey(['qun.qq.com'])).domainPskeyMap.get('qun.qq.com')! const psKey = (await ntUserApi.getPSkey(['qun.qq.com'])).domainPskeyMap.get('qun.qq.com')!
return await invoke('nodeIKernelGroupService/publishGroupBulletin', [{ groupCode, psKey, req }, null]) return await invoke('nodeIKernelGroupService/publishGroupBulletin', [{ groupCode, psKey, req }])
} }
async uploadGroupBulletinPic(groupCode: string, path: string) { async uploadGroupBulletinPic(groupCode: string, path: string) {
const ntUserApi = this.ctx.get('ntUserApi')! const ntUserApi = this.ctx.get('ntUserApi')!
const psKey = (await ntUserApi.getPSkey(['qun.qq.com'])).domainPskeyMap.get('qun.qq.com')! const psKey = (await ntUserApi.getPSkey(['qun.qq.com'])).domainPskeyMap.get('qun.qq.com')!
return await invoke('nodeIKernelGroupService/uploadGroupBulletinPic', [{ groupCode, psKey, path }, null]) return await invoke('nodeIKernelGroupService/uploadGroupBulletinPic', [{ groupCode, psKey, path }])
} }
async getGroupRecommendContact(groupCode: string) { async getGroupRecommendContact(groupCode: string) {
const ret = await invoke('nodeIKernelGroupService/getGroupRecommendContactArkJson', [{ groupCode }, null]) const ret = await invoke('nodeIKernelGroupService/getGroupRecommendContactArkJson', [{ groupCode }])
return ret.arkJson return ret.arkJson
} }
@@ -315,7 +236,7 @@ export class NTQQGroupApi extends Service {
msgSeq: +msgSeq, msgSeq: +msgSeq,
msgRandom: +msgRandom msgRandom: +msgRandom
} }
}, null]) }])
} }
async getGroupHonorList(groupCode: string) { async getGroupHonorList(groupCode: string) {
@@ -324,6 +245,109 @@ export class NTQQGroupApi extends Service {
req: { req: {
groupCode: [+groupCode] groupCode: [+groupCode]
} }
}, null]) }])
}
async getGroupAllInfo(groupCode: string) {
invoke('nodeIKernelGroupListener/onGroupAllInfoChange', [], {
registerEvent: true
})
return await invoke<{ groupAll: GroupAllInfo }>(
'nodeIKernelGroupService/getGroupAllInfo',
[{
groupCode,
source: 4
}],
{
cbCmd: 'nodeIKernelGroupListener/onGroupAllInfoChange',
afterFirstCmd: false,
cmdCB: payload => payload.groupAll.groupCode === groupCode
}
)
}
async getGroupBulletinList(groupCode: string) {
invoke('nodeIKernelGroupListener/onGetGroupBulletinListResult', [], {
registerEvent: true
})
const ntUserApi = this.ctx.get('ntUserApi')!
const psKey = (await ntUserApi.getPSkey(['qun.qq.com'])).domainPskeyMap.get('qun.qq.com')!
return await invoke<{
groupCode: string
context: string
result: GroupBulletinListResult
}>(
'nodeIKernelGroupService/getGroupBulletinList',
[{
groupCode,
psKey,
context: '',
req: {
startIndex: -1,
num: 20,
needInstructionsForJoinGroup: 1,
needPublisherInfo: 1
}
}],
{
cbCmd: 'nodeIKernelGroupListener/onGetGroupBulletinListResult',
cmdCB: payload => payload.groupCode === groupCode,
afterFirstCmd: false
}
)
}
async setGroupAvatar(groupCode: string, path: string) {
return await invoke('nodeIKernelGroupService/setHeader', [{ path, groupCode }])
}
async searchMember(groupCode: string, keyword: string) {
await invoke('nodeIKernelGroupListener/onSearchMemberChange', [], {
registerEvent: true
})
const sceneId = await invoke(NTMethod.GROUP_MEMBER_SCENE, [{
groupCode,
scene: 'groupMemberList_MainWindow'
}])
const data = await invoke<{
sceneId: string
keyword: string
infos: Map<string, GroupMember>
}>(
'nodeIKernelGroupService/searchMember',
[{ sceneId, keyword }],
{
cbCmd: 'nodeIKernelGroupListener/onSearchMemberChange',
cmdCB: payload => {
return payload.sceneId === sceneId && payload.keyword === keyword
},
afterFirstCmd: false
}
)
return data.infos
}
async getGroupFileCount(groupId: string) {
return await invoke(
'nodeIKernelRichMediaService/batchGetGroupFileCount',
[{ groupIds: [groupId] }]
)
}
async getGroupFileSpace(groupId: string) {
return await invoke(
'nodeIKernelRichMediaService/getGroupSpace',
[{ groupId }]
)
}
async setGroupMsgMask(groupCode: string, msgMask: GroupMsgMask) {
return await invoke('nodeIKernelGroupService/setGroupMsgMask', [{ groupCode, msgMask }])
}
async setGroupRemark(groupCode: string, groupRemark = '') {
return await invoke('nodeIKernelGroupService/modifyGroupRemark', [{ groupCode, groupRemark }])
} }
} }

View File

@@ -1,7 +1,5 @@
import { invoke, NTMethod } from '../ntcall' import { invoke, NTMethod } from '../ntcall'
import { GeneralCallResult } from '../services'
import { RawMessage, SendMessageElement, Peer, ChatType } from '../types' import { RawMessage, SendMessageElement, Peer, ChatType } from '../types'
import { getSession } from '@/ntqqapi/wrapper'
import { Service, Context } from 'cordis' import { Service, Context } from 'cordis'
import { selfInfo } from '@/common/globalVars' import { selfInfo } from '@/common/globalVars'
@@ -19,98 +17,80 @@ export class NTQQMsgApi extends Service {
} }
async getTempChatInfo(chatType: ChatType, peerUid: string) { async getTempChatInfo(chatType: ChatType, peerUid: string) {
const session = getSession() return await invoke('nodeIKernelMsgService/getTempChatInfo', [{ chatType, peerUid }])
if (session) {
return session.getMsgService().getTempChatInfo(chatType, peerUid)
} else {
return await invoke('nodeIKernelMsgService/getTempChatInfo', [{ chatType, peerUid }, null])
}
} }
async setEmojiLike(peer: Peer, msgSeq: string, emojiId: string, setEmoji: boolean = true) { async setEmojiLike(peer: Peer, msgSeq: string, emojiId: string, setEmoji: boolean) {
// 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
const session = getSession()
const emojiType = emojiId.length > 3 ? '2' : '1' const emojiType = emojiId.length > 3 ? '2' : '1'
if (session) { return await invoke(NTMethod.EMOJI_LIKE, [{ peer, msgSeq, emojiId, emojiType, setEmoji }])
return session.getMsgService().setMsgEmojiLikes(peer, msgSeq, emojiId, emojiType, setEmoji)
} else {
return await invoke(NTMethod.EMOJI_LIKE, [{ peer, msgSeq, emojiId, emojiType, setEmoji }, null])
}
} }
async getMultiMsg(peer: Peer, rootMsgId: string, parentMsgId: string) { async getMultiMsg(peer: Peer, rootMsgId: string, parentMsgId: string) {
const session = getSession() return await invoke(NTMethod.GET_MULTI_MSG, [{ peer, rootMsgId, parentMsgId }])
if (session) {
return session.getMsgService().getMultiMsg(peer, rootMsgId, parentMsgId)
} else {
return await invoke(NTMethod.GET_MULTI_MSG, [{ peer, rootMsgId, parentMsgId }, null])
}
} }
async activateChat(peer: Peer) { async activateChat(peer: Peer) {
return await invoke<GeneralCallResult>(NTMethod.ACTIVE_CHAT_PREVIEW, [{ peer, cnt: 1 }, null]) return await invoke(NTMethod.ACTIVE_CHAT_PREVIEW, [{ peer, cnt: 1 }])
} }
async activateChatAndGetHistory(peer: Peer) { async activateChatAndGetHistory(peer: Peer, cnt: number) {
return await invoke<GeneralCallResult>(NTMethod.ACTIVE_CHAT_HISTORY, [{ peer, cnt: 20 }, null]) // 消息从旧到新
return await invoke(NTMethod.ACTIVE_CHAT_HISTORY, [{ peer, cnt, msgId: '0', queryOrder: true }])
} }
async getAioFirstViewLatestMsgs(peer: Peer, cnt: number) { async getAioFirstViewLatestMsgs(peer: Peer, cnt: number) {
return await invoke('nodeIKernelMsgService/getAioFirstViewLatestMsgs', [{ peer, cnt }, null]) return await invoke('nodeIKernelMsgService/getAioFirstViewLatestMsgs', [{ peer, cnt }])
} }
async getMsgsByMsgId(peer: Peer | undefined, msgIds: string[] | undefined) { async getMsgsByMsgId(peer: Peer, msgIds: string[]) {
if (!peer) throw new Error('peer is not allowed') if (!peer) throw new Error('peer is not allowed')
if (!msgIds) throw new Error('msgIds is not allowed') if (!msgIds) throw new Error('msgIds is not allowed')
const session = getSession() return await invoke('nodeIKernelMsgService/getMsgsByMsgId', [{ peer, msgIds }])
if (session) {
return session.getMsgService().getMsgsByMsgId(peer, msgIds)
} else {
return await invoke('nodeIKernelMsgService/getMsgsByMsgId', [{ peer, msgIds }, null])
}
} }
async getMsgHistory(peer: Peer, msgId: string, cnt: number, isReverseOrder: boolean = false) { async getMsgHistory(peer: Peer, msgId: string, cnt: number, queryOrder = false) {
const session = getSession() // 默认情况下消息时间从新到旧
// 消息时间从旧到新 return await invoke(NTMethod.HISTORY_MSG, [{ peer, msgId, cnt, queryOrder }])
if (session) {
return session.getMsgService().getMsgsIncludeSelf(peer, msgId, cnt, isReverseOrder)
} else {
return await invoke(NTMethod.HISTORY_MSG, [{ peer, msgId, cnt, queryOrder: isReverseOrder }, null])
}
} }
async recallMsg(peer: Peer, msgIds: string[]) { async recallMsg(peer: Peer, msgIds: string[]) {
const session = getSession() return await invoke(NTMethod.RECALL_MSG, [{ peer, msgIds }])
if (session) {
return session.getMsgService().recallMsg(peer, msgIds)
} else {
return await invoke(NTMethod.RECALL_MSG, [{ peer, msgIds }, null])
}
} }
async sendMsg(peer: Peer, msgElements: SendMessageElement[], timeout = 10000) { async sendMsg(peer: Peer, msgElements: SendMessageElement[], timeout = 10000) {
const msgId = await this.generateMsgUniqueId(peer.chatType) const uniqueId = await this.generateMsgUniqueId(peer.chatType)
peer.guildId = msgId const msgAttributeInfos = new Map()
msgAttributeInfos.set(0, {
attrType: 0,
attrId: uniqueId,
vasMsgInfo: {
msgNamePlateInfo: {},
bubbleInfo: {},
avatarPendantInfo: {},
vasFont: {},
iceBreakInfo: {}
}
})
let sentMsgId: string
const data = await invoke<{ msgList: RawMessage[] }>( const data = await invoke<{ msgList: RawMessage[] }>(
'nodeIKernelMsgService/sendMsg', 'nodeIKernelMsgService/sendMsg',
[ [{
{ msgId: '0',
msgId: '0', peer,
peer, msgElements,
msgElements, msgAttributeInfos
msgAttributeInfos: new Map() }],
},
null
],
{ {
cbCmd: 'nodeIKernelMsgListener/onMsgInfoListUpdate', cbCmd: 'nodeIKernelMsgListener/onMsgInfoListUpdate',
afterFirstCmd: false, afterFirstCmd: false,
cmdCB: payload => { cmdCB: payload => {
for (const msgRecord of payload.msgList) { for (const msgRecord of payload.msgList) {
if (msgRecord.guildId === msgId && msgRecord.sendStatus === 2) { if (msgRecord.msgAttrs.get(0)?.attrId === uniqueId && msgRecord.sendStatus === 2) {
sentMsgId = msgRecord.msgId
return true return true
} }
} }
@@ -119,22 +99,38 @@ export class NTQQMsgApi extends Service {
timeout timeout
} }
) )
return data.msgList.find(msgRecord => msgRecord.guildId === msgId)
return data.msgList.find(msgRecord => msgRecord.msgId === sentMsgId)
} }
async forwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) { async forwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) {
const session = getSession() const uniqueId = await this.generateMsgUniqueId(destPeer.chatType)
if (session) { destPeer.guildId = uniqueId
return session.getMsgService().forwardMsg(msgIds, srcPeer, [destPeer], []) const data = await invoke<{ msgList: RawMessage[] }>(
} else { 'nodeIKernelMsgService/forwardMsgWithComment',
return await invoke(NTMethod.FORWARD_MSG, [{ [{
msgIds, msgIds,
srcContact: srcPeer, srcContact: srcPeer,
dstContacts: [destPeer], dstContacts: [destPeer],
commentElements: [], commentElements: [],
msgAttributeInfos: new Map(), msgAttributeInfos: new Map(),
}, null]) }],
} {
cbCmd: 'nodeIKernelMsgListener/onMsgInfoListUpdate',
afterFirstCmd: false,
cmdCB: payload => {
for (const msgRecord of payload.msgList) {
if (msgRecord.guildId === uniqueId && msgRecord.sendStatus === 2) {
return true
}
}
return false
},
timeout: 3000
}
)
delete destPeer.guildId
return data.msgList.filter(msgRecord => msgRecord.guildId === uniqueId)
} }
async multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]): Promise<RawMessage> { async multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]): Promise<RawMessage> {
@@ -145,66 +141,46 @@ export class NTQQMsgApi extends Service {
const selfUid = selfInfo.uid const selfUid = selfInfo.uid
const data = await invoke<{ msgList: RawMessage[] }>( const data = await invoke<{ msgList: RawMessage[] }>(
'nodeIKernelMsgService/multiForwardMsgWithComment', 'nodeIKernelMsgService/multiForwardMsgWithComment',
[ [{
{ msgInfos,
msgInfos, srcContact: srcPeer,
srcContact: srcPeer, dstContact: destPeer,
dstContact: destPeer, commentElements: [],
commentElements: [], msgAttributeInfos: new Map(),
msgAttributeInfos: new Map(), }],
},
null,
],
{ {
cbCmd: 'nodeIKernelMsgListener/onMsgInfoListUpdate', cbCmd: 'nodeIKernelMsgListener/onMsgInfoListUpdate',
afterFirstCmd: false, afterFirstCmd: false,
cmdCB: payload => { cmdCB: payload => {
for (const msgRecord of payload.msgList) { for (const msgRecord of payload.msgList) {
if (msgRecord.peerUid == destPeer.peerUid && msgRecord.senderUid == selfUid) { if (
msgRecord.msgType === 11 &&
msgRecord.subMsgType === 7 &&
msgRecord.peerUid === destPeer.peerUid &&
msgRecord.senderUid === selfUid
) {
const element = msgRecord.elements[0]
const data = JSON.parse(element.arkElement!.bytesData)
if (data.app !== 'com.tencent.multimsg' || !data.meta.detail.resid) {
continue
}
return true return true
} }
} }
return false return false
}, }
} }
) )
for (const msg of data.msgList) { return data.msgList.find(msgRecord => {
const arkElement = msg.elements.find(ele => ele.arkElement) const { arkElement } = msgRecord.elements[0]
if (!arkElement) { if (arkElement?.bytesData.includes('com.tencent.multimsg')) {
continue return true
} }
const forwardData = 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('转发消息超时')
}
async getMsgsBySeqAndCount(peer: Peer, msgSeq: string, count: number, desc: boolean, z: boolean) {
const session = getSession()
if (session) {
return await session.getMsgService().getMsgsBySeqAndCount(peer, msgSeq, count, desc, z)
} else {
return await invoke('nodeIKernelMsgService/getMsgsBySeqAndCount', [{
peer,
cnt: count,
msgSeq,
queryOrder: desc
}, null])
}
} }
async getSingleMsg(peer: Peer, msgSeq: string) { async getSingleMsg(peer: Peer, msgSeq: string) {
const session = getSession() return await invoke('nodeIKernelMsgService/getSingleMsg', [{ peer, msgSeq }])
if (session) {
return await session.getMsgService().getSingleMsg(peer, msgSeq)
} else {
return await invoke('nodeIKernelMsgService/getSingleMsg', [{ peer, msgSeq }, null])
}
} }
async queryFirstMsgBySeq(peer: Peer, msgSeq: string) { async queryFirstMsgBySeq(peer: Peer, msgSeq: string) {
@@ -222,7 +198,7 @@ export class NTQQMsgApi extends Service {
isIncludeCurrent: true, isIncludeCurrent: true,
pageLimit: 1, pageLimit: 1,
} }
}, null]) }])
} }
async queryMsgsWithFilterExBySeq(peer: Peer, msgSeq: string, filterMsgTime: string, filterSendersUid: string[] = []) { async queryMsgsWithFilterExBySeq(peer: Peer, msgSeq: string, filterMsgTime: string, filterSendersUid: string[] = []) {
@@ -240,11 +216,11 @@ export class NTQQMsgApi extends Service {
isIncludeCurrent: true, isIncludeCurrent: true,
pageLimit: 1, pageLimit: 1,
} }
}, null]) }])
} }
async setMsgRead(peer: Peer) { async setMsgRead(peer: Peer) {
return await invoke('nodeIKernelMsgService/setMsgRead', [{ peer }, null]) return await invoke('nodeIKernelMsgService/setMsgRead', [{ peer }])
} }
async getMsgEmojiLikesList(peer: Peer, msgSeq: string, emojiId: string, emojiType: string, count: number) { async getMsgEmojiLikesList(peer: Peer, msgSeq: string, emojiId: string, emojiType: string, count: number) {
@@ -254,7 +230,7 @@ export class NTQQMsgApi extends Service {
emojiId, emojiId,
emojiType, emojiType,
cnt: count cnt: count
}, null]) }])
} }
async fetchFavEmojiList(count: number) { async fetchFavEmojiList(count: number) {
@@ -263,11 +239,12 @@ export class NTQQMsgApi extends Service {
count, count,
backwardFetch: true, backwardFetch: true,
forceRefresh: true forceRefresh: true
}, null]) }])
} }
async generateMsgUniqueId(chatType: number) { async generateMsgUniqueId(chatType: number) {
const uniqueId = await invoke('nodeIKernelMsgService/generateMsgUniqueId', [{ chatType }]) const time = await this.getServerTime()
const uniqueId = await invoke('nodeIKernelMsgService/generateMsgUniqueId', [{ chatType, time }])
if (typeof uniqueId === 'string') { if (typeof uniqueId === 'string') {
return uniqueId return uniqueId
} else { } else {
@@ -275,4 +252,36 @@ export class NTQQMsgApi extends Service {
return `${Date.now()}${random}` return `${Date.now()}${random}`
} }
} }
async queryMsgsById(chatType: ChatType, msgId: string) {
const msgTime = this.getMsgTimeFromId(msgId)
return await invoke('nodeIKernelMsgService/queryMsgsWithFilterEx', [{
msgId,
msgTime: '0',
msgSeq: '0',
params: {
chatInfo: {
peerUid: '',
chatType
},
filterMsgToTime: msgTime,
filterMsgFromTime: msgTime,
isIncludeCurrent: true,
pageLimit: 1,
}
}])
}
getMsgTimeFromId(msgId: string) {
// 小概率相差1毫秒
return String(BigInt(msgId) >> 32n)
}
async getServerTime() {
return await invoke('nodeIKernelMSFService/getServerTime', [])
}
async fetchUnitedCommendConfig(groups: string[]) {
return await invoke('nodeIKernelUnitedConfigService/fetchUnitedCommendConfig', [{ groups }])
}
} }

38
src/ntqqapi/api/system.ts Normal file
View File

@@ -0,0 +1,38 @@
import { Context, Service } from 'cordis'
import { invoke, NTClass } from '@/ntqqapi/ntcall'
declare module 'cordis' {
interface Context {
ntSystemApi: NTQQSystemApi
}
}
export class NTQQSystemApi extends Service {
constructor(protected ctx: Context) {
super(ctx, 'ntSystemApi', true)
}
async restart() {
// todo: 调用此接口后会将 NTQQ 设置里面的自动登录和无需手机确认打开,重启后将状态恢复到之前的状态
// 设置自动登录
await this.setSettingAutoLogin(true)
// 退出账号
invoke('quitAccount', [], {
className: NTClass.BUSINESS_API
}).then()
invoke('notifyQQClose', [{ type: 1 }], { className: NTClass.QQ_EX_API }).then()
// 等待登录界面,模拟点击登录按钮?还是直接调用登录方法?
}
async getSettingAutoLogin() {
// 查询是否自动登录
return invoke('nodeIKernelNodeMiscService/queryAutoRun', [])
}
async setSettingAutoLogin(state: boolean) {
await invoke('nodeIKernelSettingService/setNeedConfirmSwitch', [{ state: 1 }]) // 1不需要手机确认2需要手机确认
await invoke('nodeIKernelSettingService/setAutoLoginSwitch', [{ state }])
}
}

View File

@@ -1,9 +1,8 @@
import { User, UserDetailInfoByUin, UserDetailInfoByUinV2, UserDetailInfoListenerArg, UserDetailSource, ProfileBizType } from '../types' import { User, UserDetailInfoByUin, UserDetailInfoByUinV2, UserDetailInfo, UserDetailSource, ProfileBizType, SimpleInfo } from '../types'
import { invoke } from '../ntcall' import { invoke, NTClass } from '../ntcall'
import { getBuildVersion } from '@/common/utils' import { getBuildVersion } from '@/common/utils'
import { getSession } from '@/ntqqapi/wrapper'
import { RequestUtil } from '@/common/utils/request' import { RequestUtil } from '@/common/utils/request'
import { Time } from 'cosmokit' import { isNullable, pick, Time } from 'cosmokit'
import { Service, Context } from 'cordis' import { Service, Context } from 'cordis'
import { selfInfo } from '@/common/globalVars' import { selfInfo } from '@/common/globalVars'
@@ -20,31 +19,25 @@ export class NTQQUserApi extends Service {
super(ctx, 'ntUserApi', true) super(ctx, 'ntUserApi', true)
} }
async setQQAvatar(path: string) { async setSelfAvatar(path: string) {
return await invoke( return await invoke(
'nodeIKernelProfileService/setHeader', 'nodeIKernelProfileService/setHeader',
[ [{ path }],
{ path },
null,
],
{ {
timeout: 10 * Time.second, // 10秒不一定够 timeout: 10 * Time.second // 10秒不一定够
} }
) )
} }
async fetchUserDetailInfo(uid: string) { async fetchUserDetailInfo(uid: string) {
const result = await invoke<{ info: UserDetailInfoListenerArg }>( const result = await invoke<{ info: UserDetailInfo }>(
'nodeIKernelProfileService/fetchUserDetailInfo', 'nodeIKernelProfileService/fetchUserDetailInfo',
[ [{
{ callFrom: 'BuddyProfileStore',
callFrom: 'BuddyProfileStore', uid: [uid],
uid: [uid], source: UserDetailSource.KSERVER,
source: UserDetailSource.KSERVER, bizList: [ProfileBizType.KALL]
bizList: [ProfileBizType.KALL] }],
},
null
],
{ {
cbCmd: 'nodeIKernelProfileListener/onUserDetailInfoChanged', cbCmd: 'nodeIKernelProfileListener/onUserDetailInfoChanged',
afterFirstCmd: false, afterFirstCmd: false,
@@ -70,13 +63,10 @@ export class NTQQUserApi extends Service {
} }
const result = await invoke<{ info: User }>( const result = await invoke<{ info: User }>(
'nodeIKernelProfileService/getUserDetailInfoWithBizInfo', 'nodeIKernelProfileService/getUserDetailInfoWithBizInfo',
[ [{
{ uid,
uid, bizList: [0]
bizList: [0] }],
},
null,
],
{ {
cbCmd: 'nodeIKernelProfileListener/onProfileDetailInfoChanged', cbCmd: 'nodeIKernelProfileListener/onProfileDetailInfoChanged',
afterFirstCmd: false, afterFirstCmd: false,
@@ -98,103 +88,77 @@ export class NTQQUserApi extends Service {
} }
async getPSkey(domains: string[]) { async getPSkey(domains: string[]) {
return await invoke('nodeIKernelTipOffService/getPskey', [{ domains, isForNewPCQQ: true }, null]) return await invoke('nodeIKernelTipOffService/getPskey', [{ domains, isForNewPCQQ: true }])
} }
async like(uid: string, count = 1) { async like(uid: string, count = 1) {
const session = getSession() return await invoke(
if (session) { 'nodeIKernelProfileLikeService/setBuddyProfileLike',
return session.getProfileLikeService().setBuddyProfileLike({ [{
friendUid: uid, doLikeUserInfo: {
sourceId: 71, friendUid: uid,
doLikeCount: count, sourceId: 71,
doLikeTollCount: 0 doLikeCount: count,
}) doLikeTollCount: 0
} else { }
return await invoke( }]
'nodeIKernelProfileLikeService/setBuddyProfileLike', )
[
{
doLikeUserInfo: {
friendUid: uid,
sourceId: 71,
doLikeCount: count,
doLikeTollCount: 0
}
},
null,
],
)
}
} }
async getUidByUinV1(uin: string) { async getUidByUinV1(uin: string, groupCode?: string) {
const session = getSession() let uid = (await invoke('nodeIKernelUixConvertService/getUid', [{ uins: [uin] }])).uidInfo.get(uin)
// 通用转换开始尝试
let uid = (await session?.getUixConvertService().getUid([uin]))?.uidInfo.get(uin)
if (!uid) { if (!uid) {
for (const membersList of this.ctx.ntGroupApi.groupMembers.values()) { //从群友列表转 const friends = await this.ctx.ntFriendApi.getFriends()
for (const member of membersList.values()) { uid = friends.find(item => item.uin === uin)?.uid
if (member.uin === uin) { }
uid = member.uid if (!uid && groupCode) {
break let member = await this.ctx.ntGroupApi.searchMember(groupCode, uin)
} if (member.size === 0) {
} await this.ctx.ntGroupApi.getGroupMembers(groupCode, 1)
if (uid) break await this.ctx.sleep(30)
member = await this.ctx.ntGroupApi.searchMember(groupCode, uin)
} }
uid = Array.from(member.values()).find(e => e.uin === uin)?.uid
} }
if (!uid) { if (!uid) {
const unveifyUid = (await this.getUserDetailInfoByUin(uin)).info.uid //特殊转换 const unveifyUid = (await this.getUserDetailInfoByUin(uin)).info.uid
if (unveifyUid.indexOf('*') === -1) { if (!unveifyUid.includes('*')) {
uid = unveifyUid uid = unveifyUid
} }
} }
if (!uid) {
const friends = await this.ctx.ntFriendApi.getFriends() //从好友列表转
uid = friends.find(item => item.uin === uin)?.uid
}
return uid return uid
} }
async getUidByUinV2(uin: string, groupCode?: string) { async getUidByUinV2(uin: string) {
let uid = (await invoke('nodeIKernelGroupService/getUidByUins', [{ uin: [uin] }])).uids.get(uin) let uid = (await invoke('nodeIKernelGroupService/getUidByUins', [{ uinList: [uin] }])).uids.get(uin)
if (uid) return uid if (uid) return uid
uid = (await invoke('nodeIKernelProfileService/getUidByUin', [{ callFrom: 'FriendsServiceImpl', uin: [uin] }])).get(uin) uid = (await invoke('nodeIKernelProfileService/getUidByUin', [{ callFrom: 'FriendsServiceImpl', uin: [uin] }])).get(uin)
if (uid) return uid if (uid) return uid
uid = (await invoke('nodeIKernelUixConvertService/getUid', [{ uins: [uin] }])).uidInfo.get(uin) uid = (await invoke('nodeIKernelUixConvertService/getUid', [{ uins: [uin] }])).uidInfo.get(uin)
if (uid) return uid if (uid) return uid
const unveifyUid = (await this.getUserDetailInfoByUinV2(uin)).detail.uid const unveifyUid = (await this.getUserDetailInfoByUinV2(uin)).detail.uid
if (!unveifyUid.includes('*')) return unveifyUid //if (!unveifyUid.includes('*')) return unveifyUid
if (groupCode) { return unveifyUid
const member = await this.ctx.ntGroupApi.getGroupMember(groupCode, uin)
return member?.uid
}
} }
async getUidByUin(uin: string, groupCode?: string) { async getUidByUin(uin: string, groupCode?: string) {
if (getBuildVersion() >= 26702) { if (getBuildVersion() >= 26702) {
return this.getUidByUinV2(uin, groupCode) return this.getUidByUinV2(uin)
} }
return this.getUidByUinV1(uin) return this.getUidByUinV1(uin, groupCode)
} }
async getUserDetailInfoByUinV2(uin: string) { async getUserDetailInfoByUinV2(uin: string) {
return await invoke<UserDetailInfoByUinV2>( return await invoke<UserDetailInfoByUinV2>(
'nodeIKernelProfileService/getUserDetailInfoByUin', 'nodeIKernelProfileService/getUserDetailInfoByUin',
[ [{ uin }]
{ uin },
null,
],
) )
} }
async getUserDetailInfoByUin(uin: string) { async getUserDetailInfoByUin(uin: string) {
return await invoke<UserDetailInfoByUin>( return await invoke<UserDetailInfoByUin>(
'nodeIKernelProfileService/getUserDetailInfoByUin', 'nodeIKernelProfileService/getUserDetailInfoByUin',
[ [{ uin }]
{ uin },
null,
],
) )
} }
@@ -202,31 +166,21 @@ export class NTQQUserApi extends Service {
const ret = await invoke('nodeIKernelUixConvertService/getUin', [{ uids: [uid] }]) const ret = await invoke('nodeIKernelUixConvertService/getUin', [{ uids: [uid] }])
let uin = ret.uinInfo.get(uid) let uin = ret.uinInfo.get(uid)
if (!uin) { if (!uin) {
uin = (await this.getUserDetailInfo(uid)).uin //从QQ Native 转换 uin = (await this.getUserDetailInfo(uid)).uin
} }
return uin return uin
} }
async getUinByUidV2(uid: string) { async getUinByUidV2(uid: string) {
const session = getSession() let uin = (await invoke('nodeIKernelGroupService/getUinByUids', [{ uidList: [uid] }])).uins.get(uid)
if (session) { if (uin && uin !== '0') return uin
let uin = (await session.getGroupService().getUinByUids([uid])).uins.get(uid) uin = (await invoke('nodeIKernelProfileService/getUinByUid', [{ callFrom: 'FriendsServiceImpl', uid: [uid] }])).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
} else {
let uin = (await invoke('nodeIKernelGroupService/getUinByUids', [{ uid: [uid] }])).uins.get(uid)
if (uin) return uin
uin = (await invoke('nodeIKernelProfileService/getUinByUid', [{ callFrom: 'FriendsServiceImpl', uid: [uid] }])).get(uid)
if (uin) return uin
uin = (await invoke('nodeIKernelUixConvertService/getUin', [{ uids: [uid] }])).uinInfo.get(uid)
if (uin) return uin
}
let uin = (await this.ctx.ntFriendApi.getBuddyIdMap(true)).get(uid)
if (uin) return uin if (uin) return uin
uin = (await this.getUserDetailInfo(uid)).uin //从QQ Native 转换 uin = (await invoke('nodeIKernelUixConvertService/getUin', [{ uids: [uid] }])).uinInfo.get(uid)
if (uin) return uin
uin = (await this.ctx.ntFriendApi.getBuddyIdMap()).get(uid)
if (uin) return uin
uin = (await this.getUserDetailInfo(uid)).uin
return uin return uin
} }
@@ -238,21 +192,13 @@ export class NTQQUserApi extends Service {
} }
async forceFetchClientKey() { async forceFetchClientKey() {
const session = getSession() return await invoke('nodeIKernelTicketService/forceFetchClientKey', [{ url: '' }])
if (session) {
return await session.getTicketService().forceFetchClientKey('')
} else {
return await invoke('nodeIKernelTicketService/forceFetchClientKey', [{ url: '' }, null])
}
} }
async getSelfNick(refresh = false) { async getSelfNick(refresh = true) {
if ((refresh || !selfInfo.nick) && selfInfo.uid) { if ((refresh || !selfInfo.nick) && selfInfo.uid) {
const userInfo = await this.getUserDetailInfo(selfInfo.uid) const data = await this.getUserSimpleInfo(selfInfo.uid)
if (userInfo) { selfInfo.nick = data.nick
Object.assign(selfInfo, { nick: userInfo.nick })
return userInfo.nick
}
} }
return selfInfo.nick return selfInfo.nick
} }
@@ -264,7 +210,7 @@ export class NTQQUserApi extends Service {
extStatus, extStatus,
batteryStatus, batteryStatus,
} }
}, null]) }])
} }
async getProfileLike(uid: string) { async getProfileLike(uid: string) {
@@ -279,6 +225,77 @@ export class NTQQUserApi extends Service {
start: 0, start: 0,
limit: 20, limit: 20,
} }
}, null]) }])
}
async getUserSimpleInfoV2(uid: string, force = true) {
const data = await invoke<{ profiles: Record<string, SimpleInfo> }>(
'nodeIKernelProfileService/getUserSimpleInfo',
[{
uids: [uid],
force
}],
{
cbCmd: 'onProfileSimpleChanged',
afterFirstCmd: false,
cmdCB: payload => !isNullable(payload.profiles[uid]),
}
)
return data.profiles[uid].coreInfo
}
async getUserSimpleInfo(uid: string, force = true) {
if (getBuildVersion() >= 26702) {
return this.getUserSimpleInfoV2(uid, force)
}
const data = await invoke<{ profiles: Map<string, User> }>(
'nodeIKernelProfileService/getUserSimpleInfo',
[{
uids: [uid],
force
}],
{
cbCmd: 'nodeIKernelProfileListener/onProfileSimpleChanged',
afterFirstCmd: false,
cmdCB: payload => payload.profiles.has(uid),
}
)
const profile = data.profiles.get(uid)!
return pick(profile, ['nick', 'remark', 'uid', 'uin'])
}
async getCoreAndBaseInfo(uids: string[]) {
return await invoke(
'nodeIKernelProfileService/getCoreAndBaseInfo',
[{
uids,
callFrom: 'nodeStore'
}]
)
}
async getRobotUinRange() {
const data = await invoke(
'nodeIKernelRobotService/getRobotUinRange',
[{
req: {
justFetchMsgConfig: '1',
type: 1,
version: 0,
aioKeywordVersion: 0
}
}]
)
return data.response.robotUinRanges
}
async quitAccount() {
return await invoke(
'quitAccount',
[],
{
className: NTClass.BUSINESS_API,
}
)
} }
} }

View File

@@ -35,7 +35,7 @@ export class NTQQWindowApi extends Service {
super(ctx, 'ntWindowApi', true) super(ctx, 'ntWindowApi', true)
} }
// 打开窗口并获取对应的下发事件 /** 打开窗口并获取对应的下发事件 */
async openWindow<R = GeneralCallResult>( async openWindow<R = GeneralCallResult>(
ntQQWindow: NTQQWindow, ntQQWindow: NTQQWindow,
args: unknown[], args: unknown[],
@@ -53,7 +53,6 @@ export class NTQQWindowApi extends Service {
) )
setTimeout(() => { setTimeout(() => {
for (const w of BrowserWindow.getAllWindows()) { for (const w of BrowserWindow.getAllWindows()) {
// log("close window", w.webContents.getURL())
if (w.webContents.getURL().indexOf(ntQQWindow.windowUrlHash) != -1) { if (w.webContents.getURL().indexOf(ntQQWindow.windowUrlHash) != -1) {
w.close() w.close()
} }

View File

@@ -2,7 +2,6 @@ import { unlink } from 'node:fs/promises'
import { Service, Context } from 'cordis' import { Service, Context } from 'cordis'
import { registerCallHook, registerReceiveHook, ReceiveCmdS } from './hook' import { registerCallHook, registerReceiveHook, ReceiveCmdS } from './hook'
import { Config as LLOBConfig } from '../common/types' import { Config as LLOBConfig } from '../common/types'
import { llonebotError } from '../common/globalVars'
import { isNumeric } from '../common/utils/misc' import { isNumeric } from '../common/utils/misc'
import { NTMethod } from './ntcall' import { NTMethod } from './ntcall'
import { import {
@@ -13,39 +12,50 @@ import {
GroupMember, GroupMember,
CategoryFriend, CategoryFriend,
SimpleInfo, SimpleInfo,
ChatType ChatType,
BuddyReqType,
GrayTipElementSubType
} from './types' } from './types'
import { selfInfo } from '../common/globalVars' import { selfInfo } from '../common/globalVars'
import { version } from '../version' import { version } from '../version'
import { invoke } from './ntcall' import { invoke } from './ntcall'
import { Native } from './native/crychic'
import { initWrapperSession, NTQQPacketApi } from './native/napcat-protocol-packet'
declare module 'cordis' { declare module 'cordis' {
interface Context { interface Context {
app: Core app: Core
} }
interface Events { interface Events {
'nt/message-created': (input: RawMessage[]) => void 'nt/message-created': (input: RawMessage) => void
'nt/message-deleted': (input: RawMessage) => void 'nt/message-deleted': (input: RawMessage) => void
'nt/message-sent': (input: RawMessage) => void 'nt/message-sent': (input: RawMessage) => void
'nt/group-notify': (input: GroupNotify[]) => void 'nt/group-notify': (input: { notify: GroupNotify, doubt: boolean }) => void
'nt/friend-request': (input: FriendRequest[]) => void 'nt/friend-request': (input: FriendRequest) => void
'nt/group-member-info-updated': (input: { groupCode: string, members: GroupMember[] }) => void 'nt/group-member-info-updated': (input: { groupCode: string, members: GroupMember[] }) => void
'nt/system-message-created': (input: Uint8Array) => void 'nt/system-message-created': (input: Uint8Array) => void
} }
} }
class Core extends Service { class Core extends Service {
static inject = ['ntMsgApi', 'ntFriendApi', 'ntGroupApi'] static inject = ['ntMsgApi', 'ntFriendApi', 'ntGroupApi', 'store']
public startTime = 0
public native
public packet!: NTQQPacketApi
constructor(protected ctx: Context, public config: Core.Config) { constructor(protected ctx: Context, public config: Core.Config) {
super(ctx, 'app', true) super(ctx, 'app', true)
this.native = new Native(ctx)
initWrapperSession().then(session => {
this.packet = new NTQQPacketApi(session)
})
} }
public start() { public start() {
llonebotError.otherError = '' this.startTime = Date.now()
this.registerListener() this.registerListener()
this.ctx.logger.info(`LLOneBot/${version}`) this.ctx.logger.info(`LLOneBot/${version}`)
this.ctx.on('llonebot/config-updated', input => { this.ctx.on('llob/config-updated', input => {
Object.assign(this.config, input) Object.assign(this.config, input)
}) })
} }
@@ -117,16 +127,18 @@ class Core extends Service {
activatedPeerUids.push(contact.id) activatedPeerUids.push(contact.id)
const peer = { peerUid: contact.id, chatType: contact.chatType } const peer = { peerUid: contact.id, chatType: contact.chatType }
if (contact.chatType === ChatType.TempC2CFromGroup) { if (contact.chatType === ChatType.TempC2CFromGroup) {
this.ctx.ntMsgApi.activateChatAndGetHistory(peer).then(() => { this.ctx.ntMsgApi.activateChatAndGetHistory(peer, 2).then(res => {
this.ctx.ntMsgApi.getMsgHistory(peer, '', 20).then(({ msgList }) => { for (const msg of res.msgList) {
const lastTempMsg = msgList.at(-1) if (Date.now() / 1000 - Number(msg.msgTime) > 3) {
if (Date.now() / 1000 - Number(lastTempMsg?.msgTime) < 5) { continue
this.ctx.parallel('nt/message-created', [lastTempMsg!])
} }
}) if (msg.senderUin && msg.senderUin !== '0') {
this.ctx.store.addMsgCache(msg)
}
this.ctx.parallel('nt/message-created', msg)
}
}) })
} } else {
else {
this.ctx.ntMsgApi.activateChat(peer) this.ctx.ntMsgApi.activateChat(peer)
} }
} }
@@ -161,7 +173,17 @@ class Core extends Service {
}) })
registerReceiveHook<{ msgList: RawMessage[] }>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], payload => { registerReceiveHook<{ msgList: RawMessage[] }>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], payload => {
this.ctx.parallel('nt/message-created', payload.msgList) const startTime = this.startTime / 1000
for (const message of payload.msgList) {
// 过滤启动之前的消息
if (parseInt(message.msgTime) < startTime) {
continue
}
if (message.senderUin && message.senderUin !== '0') {
this.ctx.store.addMsgCache(message)
}
this.ctx.parallel('nt/message-created', message)
}
}) })
const sentMsgIds = new Map<string, boolean>() const sentMsgIds = new Map<string, boolean>()
@@ -169,24 +191,30 @@ class Core extends Service {
registerReceiveHook<{ msgList: RawMessage[] }>([ReceiveCmdS.UPDATE_MSG], payload => { registerReceiveHook<{ msgList: RawMessage[] }>([ReceiveCmdS.UPDATE_MSG], payload => {
for (const msg of payload.msgList) { for (const msg of payload.msgList) {
if (msg.recallTime !== '0' && !recallMsgIds.includes(msg.msgId)) { if (
msg.recallTime !== '0' &&
msg.msgType === 5 &&
msg.subMsgType === 4 &&
msg.elements[0]?.grayTipElement?.subElementType === GrayTipElementSubType.Revoke &&
!recallMsgIds.includes(msg.msgId)
) {
recallMsgIds.shift()
recallMsgIds.push(msg.msgId) recallMsgIds.push(msg.msgId)
this.ctx.parallel('nt/message-deleted', msg) this.ctx.parallel('nt/message-deleted', msg)
} else if (sentMsgIds.get(msg.msgId)) { } else if (sentMsgIds.get(msg.msgId)) {
sentMsgIds.delete(msg.msgId) if (msg.sendStatus === 2) {
this.ctx.parallel('nt/message-sent', msg) sentMsgIds.delete(msg.msgId)
this.ctx.parallel('nt/message-sent', msg)
}
} }
} }
}) })
registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, payload => { registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, payload => {
if (!this.config.reportSelfMessage) {
return
}
sentMsgIds.set(payload.msgRecord.msgId, true) sentMsgIds.set(payload.msgRecord.msgId, true)
}) })
const groupNotifyFlags: string[] = [] const groupNotifyIgnore: string[] = []
registerReceiveHook<{ registerReceiveHook<{
doubt: boolean doubt: boolean
oldestUnreadSeq: string oldestUnreadSeq: string
@@ -195,27 +223,34 @@ class Core extends Service {
if (payload.unreadCount) { if (payload.unreadCount) {
let notifies: GroupNotify[] let notifies: GroupNotify[]
try { try {
notifies = (await this.ctx.ntGroupApi.getSingleScreenNotifies(14)).slice(0, payload.unreadCount) notifies = await this.ctx.ntGroupApi.getSingleScreenNotifies(payload.doubt, payload.unreadCount)
} catch (e) { } catch (e) {
return return
} }
const list = notifies.filter(v => { for (const notify of notifies) {
const flag = v.group.groupCode + '|' + v.seq + '|' + v.type const notifyTime = Math.trunc(+notify.seq / 1000)
if (groupNotifyFlags.includes(flag)) { if (groupNotifyIgnore.includes(notify.seq) || notifyTime < this.startTime) {
return false continue
} }
groupNotifyFlags.push(flag) groupNotifyIgnore.push(notify.seq)
return true this.ctx.parallel('nt/group-notify', { notify, doubt: payload.doubt })
}) }
this.ctx.parallel('nt/group-notify', list)
} }
}) })
registerReceiveHook<FriendRequestNotify>(ReceiveCmdS.FRIEND_REQUEST, payload => { registerReceiveHook<FriendRequestNotify>(ReceiveCmdS.FRIEND_REQUEST, payload => {
this.ctx.parallel('nt/friend-request', payload.data.buddyReqs) for (const req of payload.data.buddyReqs) {
if (!!req.isInitiator || (req.isDecide && req.reqType !== BuddyReqType.MeInitiatorWaitPeerConfirm)) {
continue
}
if (+req.reqTime < this.startTime / 1000) {
continue
}
this.ctx.parallel('nt/friend-request', req)
}
}) })
invoke('nodeIKernelMsgListener/onRecvSysMsg', [], { classNameIsRegister: true }) invoke('nodeIKernelMsgListener/onRecvSysMsg', [], { registerEvent: true })
registerReceiveHook<{ registerReceiveHook<{
msgBuf: number[] msgBuf: number[]

View File

@@ -16,16 +16,14 @@ import {
SendTextElement, SendTextElement,
SendVideoElement, SendVideoElement,
} from './types' } from './types'
import { stat, writeFile, copyFile, unlink } from 'node:fs/promises' import { stat, writeFile, copyFile, unlink, access } from 'node:fs/promises'
import { calculateFileMD5 } from '../common/utils/file' import { calculateFileMD5 } from '../common/utils/file'
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 { Context } from 'cordis' import { Context } from 'cordis'
import { isNullable } from 'cosmokit' import { isNullable } from 'cosmokit'
//export const mFaceCache = new Map<string, string>() // emojiId -> faceName export namespace SendElement {
export namespace SendElementEntities {
export function text(content: string): SendTextElement { export function text(content: string): SendTextElement {
return { return {
elementType: ElementType.Text, elementType: ElementType.Text,
@@ -54,15 +52,15 @@ export namespace SendElementEntities {
} }
} }
export function reply(msgSeq: string, msgId: string, senderUin: string, senderUinStr: string): SendReplyElement { export function reply(msgSeq: string, msgId: string, senderUin: string): SendReplyElement {
return { return {
elementType: ElementType.Reply, elementType: ElementType.Reply,
elementId: '', elementId: '',
replyElement: { replyElement: {
replayMsgSeq: msgSeq, // raw.msgSeq replayMsgSeq: msgSeq,
replayMsgId: msgId, // raw.msgId replayMsgId: msgId,
senderUin: senderUin, senderUin: senderUin,
senderUinStr: senderUinStr, senderUinStr: senderUin,
}, },
} }
} }
@@ -117,25 +115,17 @@ export namespace SendElementEntities {
} }
export async function video(ctx: Context, filePath: string, fileName = '', diyThumbPath = ''): Promise<SendVideoElement> { export async function video(ctx: Context, filePath: string, fileName = '', diyThumbPath = ''): Promise<SendVideoElement> {
try { await access(filePath)
await stat(filePath)
} catch (e) {
throw `文件${filePath}异常,不存在`
}
ctx.logger.info('复制视频到QQ目录', filePath)
const { fileName: _fileName, path, fileSize, md5 } = await ctx.ntFileApi.uploadFile(filePath, ElementType.Video) const { fileName: _fileName, path, fileSize, md5 } = await ctx.ntFileApi.uploadFile(filePath, ElementType.Video)
ctx.logger.info('复制视频到QQ目录完成', path)
if (fileSize === 0) { if (fileSize === 0) {
throw '文件异常大小为0' throw new Error('文件异常,大小为 0')
} }
const maxMB = 100; const maxMB = 100
if (fileSize > 1024 * 1024 * maxMB) { if (fileSize > 1024 * 1024 * maxMB) {
throw `视频过大,最大支持${maxMB}MB当前文件大小${fileSize}B` throw new Error(`视频过大,最大支持${maxMB}MB当前文件大小${fileSize}B`)
} }
let thumbDir = path.replace(`${pathLib.sep}Ori${pathLib.sep}`, `${pathLib.sep}Thumb${pathLib.sep}`) const thumbDir = pathLib.dirname(path.replaceAll('\\', '/').replace(`/Ori/`, `/Thumb/`))
thumbDir = pathLib.dirname(thumbDir)
// log("thumb 目录", thumb)
let videoInfo = { let videoInfo = {
width: 1920, width: 1920,
height: 1080, height: 1080,
@@ -196,7 +186,6 @@ export namespace SendElementEntities {
const _thumbPath = await createThumb const _thumbPath = await createThumb
ctx.logger.info('生成视频缩略图', _thumbPath) ctx.logger.info('生成视频缩略图', _thumbPath)
const thumbSize = (await stat(_thumbPath)).size const thumbSize = (await stat(_thumbPath)).size
// log("生成缩略图", _thumbPath)
thumbPath.set(0, _thumbPath) thumbPath.set(0, _thumbPath)
const thumbMd5 = await calculateFileMD5(_thumbPath) const thumbMd5 = await calculateFileMD5(_thumbPath)
const element: SendVideoElement = { const element: SendVideoElement = {
@@ -221,13 +210,9 @@ export namespace SendElementEntities {
export async function ptt(ctx: Context, pttPath: string): Promise<SendPttElement> { export async function ptt(ctx: Context, pttPath: string): Promise<SendPttElement> {
const { converted, path: silkPath, duration } = await encodeSilk(ctx, pttPath) const { converted, path: silkPath, duration } = await encodeSilk(ctx, pttPath)
if (!silkPath) {
throw '语音转换失败, 请检查语音文件是否正常'
}
// log("生成语音", silkPath, duration);
const { md5, fileName, path, fileSize } = await ctx.ntFileApi.uploadFile(silkPath, ElementType.Ptt) const { md5, fileName, path, fileSize } = await ctx.ntFileApi.uploadFile(silkPath, ElementType.Ptt)
if (fileSize === 0) { if (fileSize === 0) {
throw '文件异常大小为0' throw new Error('文件异常,大小为 0')
} }
if (converted) { if (converted) {
unlink(silkPath) unlink(silkPath)
@@ -253,19 +238,19 @@ export namespace SendElementEntities {
} }
} }
export function face(faceId: number): SendFaceElement { export function face(faceId: number, faceType?: number): SendFaceElement {
// 从face_config.json中获取表情名称 // 从face_config.json中获取表情名称
const sysFaces = faceConfig.sysface const sysFaces = faceConfig.sysface
const emojiFaces = faceConfig.emoji const face = sysFaces.find(face => face.QSid === String(faceId))
const face = sysFaces.find((face) => face.QSid === faceId.toString()) if (!faceType) {
faceId = parseInt(faceId.toString()) if (faceId < 222) {
// let faceType = parseInt(faceId.toString().substring(0, 1)); faceType = 1
let faceType = 1 } else {
if (faceId >= 222) { faceType = 2
faceType = 2 }
} if (face?.AniStickerType) {
if (face?.AniStickerType) { faceType = 3
faceType = 3; }
} }
return { return {
elementType: ElementType.Face, elementType: ElementType.Face,

View File

@@ -1,5 +1,15 @@
{ {
"sysface": [ "sysface": [
{
"QSid": "419",
"QDes": "/火车",
"IQLid": "419",
"AQLid": "419",
"EMCode": "10419",
"AniStickerType": 3,
"AniStickerPackId": "1",
"AniStickerId": "47"
},
{ {
"QSid": "392", "QSid": "392",
"QDes": "/龙年快乐", "QDes": "/龙年快乐",
@@ -3662,4 +3672,4 @@
"EMCode": "401016" "EMCode": "401016"
} }
] ]
} }

View File

@@ -31,30 +31,18 @@ export class RkeyManager {
isExpired(): boolean { isExpired(): boolean {
const now = new Date().getTime() / 1000 const now = new Date().getTime() / 1000
// console.log(`now: ${now}, expired_time: ${this.rkeyData.expired_time}`)
return now > this.rkeyData.expired_time return now > this.rkeyData.expired_time
} }
async refreshRkey() { async refreshRkey() {
//刷新rkey
this.rkeyData = await this.fetchServerRkey() this.rkeyData = await this.fetchServerRkey()
} }
async fetchServerRkey() { async fetchServerRkey(): Promise<ServerRkeyData> {
return new Promise<ServerRkeyData>((resolve, reject) => { const response = await fetch(this.serverUrl)
fetch(this.serverUrl) if (!response.ok) {
.then(response => { throw new Error(response.statusText)
if (!response.ok) { }
return reject(response.statusText) // 请求失败,返回错误信息 return response.json()
}
return response.json() // 解析 JSON 格式的响应体
})
.then(data => {
resolve(data)
})
.catch(error => {
reject(error)
})
})
} }
} }

View File

@@ -1,7 +1,7 @@
import type { BrowserWindow } from 'electron' import { NTMethod } from './ntcall'
import { NTClass, NTMethod } from './ntcall'
import { log } from '@/common/utils' import { log } from '@/common/utils'
import { randomUUID } from 'node:crypto' import { randomUUID } from 'node:crypto'
import { ipcMain } from 'electron'
import { Dict } from 'cosmokit' import { Dict } from 'cosmokit'
export const hookApiCallbacks: Record<string, (res: any) => void> = {} export const hookApiCallbacks: Record<string, (res: any) => void> = {}
@@ -26,59 +26,73 @@ export enum ReceiveCmdS {
SELF_STATUS = 'nodeIKernelProfileListener/onSelfStatusChanged', SELF_STATUS = 'nodeIKernelProfileListener/onSelfStatusChanged',
CACHE_SCAN_FINISH = 'nodeIKernelStorageCleanListener/onFinishScan', CACHE_SCAN_FINISH = 'nodeIKernelStorageCleanListener/onFinishScan',
MEDIA_UPLOAD_COMPLETE = 'nodeIKernelMsgListener/onRichMediaUploadComplete', MEDIA_UPLOAD_COMPLETE = 'nodeIKernelMsgListener/onRichMediaUploadComplete',
SKEY_UPDATE = 'onSkeyUpdate',
} }
type NTReturnData = [
{
type: 'request'
eventName: NTClass
callbackId?: string
},
{
cmdName: ReceiveCmdS
cmdType: 'event'
payload: unknown
}[]
]
const logHook = false const logHook = false
const receiveHooks: Array<{ const receiveHooks: Map<string, {
method: ReceiveCmdS[] method: ReceiveCmdS[]
hookFunc: (payload: any) => void | Promise<void> hookFunc: (payload: any) => void | Promise<void>
id: string }> = new Map()
}> = []
const callHooks: Array<{ const callHooks: Array<{
method: NTMethod[] method: NTMethod[]
hookFunc: (callParams: unknown[]) => void | Promise<void> hookFunc: (callParams: unknown[]) => void | Promise<void>
}> = [] }> = []
export function hookNTQQApiReceive(window: BrowserWindow, onlyLog: boolean) { export function startHook() {
window.webContents.send = new Proxy(window.webContents.send, { log('start hook')
apply(target, thisArg, args: [channel: string, ...args: NTReturnData]) {
try { const senderExclude = Symbol()
if (logHook && !args[1]?.eventName?.startsWith('ns-LoggerApi')) {
log('received ntqq api message', args) ipcMain.emit = new Proxy(ipcMain.emit, {
} apply(target, thisArg, args: [channel: string, ...args: any]) {
} catch { } if (args[2]?.eventName?.startsWith('ns-LoggerApi')) {
if (!onlyLog) { return target.apply(thisArg, args)
if (args[2] instanceof Array) { }
for (const receiveData of args[2]) { if (logHook) {
const ntMethodName = receiveData.cmdName log('request', args)
for (const hook of receiveHooks) { }
if (hook.method.includes(ntMethodName)) { const event = args[1]
Promise.resolve(hook.hookFunc(receiveData.payload)) if (event.sender && !event.sender[senderExclude]) {
event.sender[senderExclude] = true
event.sender.send = new Proxy(event.sender.send, {
apply(target, thisArg, args: [channel: string, meta: Dict, data: Dict[]]) {
if (args[1]?.eventName?.startsWith('ns-LoggerApi')) {
return target.apply(thisArg, args)
}
if (logHook) {
log('received', args)
}
const callbackId = args[1]?.callbackId
if (callbackId) {
if (hookApiCallbacks[callbackId]) {
Promise.resolve(hookApiCallbacks[callbackId](args[2]))
delete hookApiCallbacks[callbackId]
}
} else if (args[2]) {
if (['IPC_DOWN_2', 'IPC_DOWN_3'].includes(args[0])) {
for (const receiveData of args[2]) {
for (const hook of receiveHooks.values()) {
if (hook.method.includes(receiveData.cmdName)) {
Promise.resolve(hook.hookFunc(receiveData.payload))
}
}
}
} }
} }
return target.apply(thisArg, args)
} }
} })
if (args[1]?.callbackId) { }
const callbackId = args[1].callbackId
if (hookApiCallbacks[callbackId]) { if (args[3]?.length) {
Promise.resolve(hookApiCallbacks[callbackId](args[2])) const method = args[3][0]
delete hookApiCallbacks[callbackId] const callParams = args[3].slice(1)
for (const hook of callHooks) {
if (hook.method.includes(method)) {
Promise.resolve(hook.hookFunc(callParams))
} }
} }
} }
@@ -87,60 +101,6 @@ export function hookNTQQApiReceive(window: BrowserWindow, onlyLog: boolean) {
}) })
} }
export function hookNTQQApiCall(window: BrowserWindow, onlyLog: boolean) {
const webContents = window.webContents as Dict
const ipc_message_proxy = webContents._events['-ipc-message']?.[0] || webContents._events['-ipc-message']
const proxyIpcMsg = new Proxy(ipc_message_proxy, {
apply(target, thisArg, args) {
const isLogger = args[3]?.[0]?.eventName?.startsWith('ns-LoggerApi')
if (!isLogger) {
try {
logHook && log('call NTQQ api', thisArg, args)
} catch (e) { }
if (!onlyLog) {
try {
const _args: unknown[] = args[3][1]
const cmdName = _args[0] as NTMethod
const callParams = _args.slice(1)
callHooks.forEach((hook) => {
if (hook.method.includes(cmdName)) {
Promise.resolve(hook.hookFunc(callParams))
}
})
} catch { }
}
}
return target.apply(thisArg, args)
},
})
if (webContents._events['-ipc-message']?.[0]) {
webContents._events['-ipc-message'][0] = proxyIpcMsg
} else {
webContents._events['-ipc-message'] = proxyIpcMsg
}
/*const ipc_invoke_proxy = webContents._events['-ipc-invoke']?.[0] || webContents._events['-ipc-invoke']
const proxyIpcInvoke = new Proxy(ipc_invoke_proxy, {
apply(target, thisArg, args) {
//HOOK_LOG && log('call NTQQ invoke api', thisArg, args)
args[0]['_replyChannel']['sendReply'] = new Proxy(args[0]['_replyChannel']['sendReply'], {
apply(sendtarget, sendthisArg, sendargs) {
sendtarget.apply(sendthisArg, sendargs)
},
})
const ret = target.apply(thisArg, args)
//HOOK_LOG && log('call NTQQ invoke api return', ret)
return ret
},
})
if (webContents._events['-ipc-invoke']?.[0]) {
webContents._events['-ipc-invoke'][0] = proxyIpcInvoke
} else {
webContents._events['-ipc-invoke'] = proxyIpcInvoke
}*/
}
export function registerReceiveHook<PayloadType>( export function registerReceiveHook<PayloadType>(
method: string | string[], method: string | string[],
hookFunc: (payload: PayloadType) => void, hookFunc: (payload: PayloadType) => void,
@@ -149,10 +109,9 @@ export function registerReceiveHook<PayloadType>(
if (!Array.isArray(method)) { if (!Array.isArray(method)) {
method = [method] method = [method]
} }
receiveHooks.push({ receiveHooks.set(id, {
method: method as ReceiveCmdS[], method: method as ReceiveCmdS[],
hookFunc, hookFunc,
id,
}) })
return id return id
} }
@@ -171,6 +130,5 @@ export function registerCallHook(
} }
export function removeReceiveHook(id: string) { export function removeReceiveHook(id: string) {
const index = receiveHooks.findIndex((h) => h.id === id) receiveHooks.delete(id)
receiveHooks.splice(index, 1)
} }

Binary file not shown.

View File

@@ -0,0 +1,99 @@
import { Context } from 'cordis'
import { Dict } from 'cosmokit'
import { getBuildVersion } from '../../../common/utils/misc'
import { TEMP_DIR } from '../../../common/globalVars'
import { copyFile } from 'fs/promises'
import { ChatType, Peer } from '../../types'
import path from 'node:path'
import addon from './external/crychic-win32-x64.node?asset'
export class Native {
public activated = false
private crychic?: Dict
private seq = 0
private cb: Map<number, (res: any) => void> = new Map()
constructor(private ctx: Context) {
ctx.on('ready', () => {
this.start()
})
}
checkPlatform() {
return process.platform === 'win32' && process.arch === 'x64'
}
checkVersion() {
const version = getBuildVersion()
// 27333—27597
return version >= 27333 && version < 28060
}
async start() {
if (this.crychic) {
return
}
if (!this.checkPlatform()) {
return
}
if (!this.checkVersion()) {
return
}
const handler = async (name: string, ...e: unknown[]) => {
if (name === 'cb') {
this.cb.get(e[0] as number)?.(e[1])
}
}
try {
const fileName = path.basename(addon)
const dest = path.join(TEMP_DIR, fileName)
try {
await copyFile(addon, dest)
} catch (e) {
// resource busy or locked?
this.ctx.logger.warn(e)
}
this.crychic = require(dest)
this.crychic!.setCryHandler(handler)
this.crychic!.init()
this.activated = true
} catch (e) {
this.ctx.logger.warn('crychic 加载失败', e)
}
}
async sendFriendPoke(uin: number) {
if (!this.crychic) return
this.crychic.sendFriendPoke(uin)
await this.ctx.ntMsgApi.fetchUnitedCommendConfig(['100243'])
}
async sendGroupPoke(groupCode: number, memberUin: number) {
if (!this.crychic) return
this.crychic.sendGroupPoke(memberUin, groupCode)
await this.ctx.ntMsgApi.fetchUnitedCommendConfig(['100243'])
}
uploadForward(peer: Peer, transmit: Uint8Array) {
return new Promise<string>(async (resolve, reject) => {
if (!this.crychic) return
let groupCode = 0
const uid = peer.peerUid
const isGroup = peer.chatType === ChatType.Group
if (isGroup) {
groupCode = +uid
}
const seq = ++this.seq
this.cb.set(seq, (resid: string) => {
this.cb.delete(seq)
resolve(resid)
})
setTimeout(() => {
this.cb.delete(seq)
reject(new Error('fake forward timeout'))
}, 5000)
this.crychic.uploadForward(seq, isGroup, uid, groupCode, transmit)
await this.ctx.ntMsgApi.fetchUnitedCommendConfig(['100243'])
})
}
}

View File

@@ -0,0 +1,17 @@
import { WrapperSession } from './wrapper-session/types';
export { initWrapperSession } from './wrapper-session';
export declare function checkSupportVersion(): void;
export declare class NTQQPacketApi {
private qqVersion;
private readonly packetClient;
private readonly packer;
private logger;
private readonly wrapperSession;
constructor(wrapperSession: WrapperSession);
private InitSendPacket;
private sendPacket;
private sendOidbPacket;
sendPokePacket(peer: number, group?: number): Promise<void>;
sendGroupSignPacket(selfUin: string, groupCode: string): Promise<void>;
sendSetSpecialTittlePacket(groupCode: string, uid: string, tittle: string): Promise<void>;
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
export declare function initWrapperSession(): Promise<any>;

View File

@@ -0,0 +1,6 @@
export interface MsgService {
sendSsoCmdReqByContend: (cmd: string, trace_id: string) => Promise<unknown>;
}
export type WrapperSession = {
getMsgService(): MsgService;
};

View File

@@ -1,6 +1,6 @@
import { ipcMain } from 'electron' import { ipcMain } from 'electron'
import { hookApiCallbacks, registerReceiveHook, removeReceiveHook } from './hook' import { hookApiCallbacks, registerReceiveHook, removeReceiveHook } from './hook'
import { log } from '../common/utils/legacyLog' import { log } from '../common/utils'
import { randomUUID } from 'node:crypto' import { randomUUID } from 'node:crypto'
import { import {
GeneralCallResult, GeneralCallResult,
@@ -13,7 +13,9 @@ import {
NodeIKernelUixConvertService, NodeIKernelUixConvertService,
NodeIKernelRichMediaService, NodeIKernelRichMediaService,
NodeIKernelTicketService, NodeIKernelTicketService,
NodeIKernelTipOffService NodeIKernelTipOffService,
NodeIKernelRobotService,
NodeIKernelNodeMiscService
} from './services' } from './services'
export enum NTClass { export enum NTClass {
@@ -27,7 +29,8 @@ export enum NTClass {
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' NODE_STORE_API = 'ns-NodeStoreApi',
QQ_EX_API = 'ns-QQEXApi',
} }
export enum NTMethod { export enum NTMethod {
@@ -39,7 +42,6 @@ export enum NTMethod {
MEDIA_FILE_PATH = 'nodeIKernelMsgService/getRichMediaFilePathForGuild', MEDIA_FILE_PATH = 'nodeIKernelMsgService/getRichMediaFilePathForGuild',
RECALL_MSG = 'nodeIKernelMsgService/recallMsg', RECALL_MSG = 'nodeIKernelMsgService/recallMsg',
EMOJI_LIKE = 'nodeIKernelMsgService/setMsgEmojiLikes', EMOJI_LIKE = 'nodeIKernelMsgService/setMsgEmojiLikes',
FORWARD_MSG = 'nodeIKernelMsgService/forwardMsgWithComment',
SELF_INFO = 'fetchAuthData', SELF_INFO = 'fetchAuthData',
FILE_TYPE = 'getFileType', FILE_TYPE = 'getFileType',
@@ -93,37 +95,57 @@ interface NTService {
nodeIKernelRichMediaService: NodeIKernelRichMediaService nodeIKernelRichMediaService: NodeIKernelRichMediaService
nodeIKernelTicketService: NodeIKernelTicketService nodeIKernelTicketService: NodeIKernelTicketService
nodeIKernelTipOffService: NodeIKernelTipOffService nodeIKernelTipOffService: NodeIKernelTipOffService
nodeIKernelRobotService: NodeIKernelRobotService
nodeIKernelNodeMiscService: NodeIKernelNodeMiscService
} }
interface InvokeOptions<ReturnType> { interface InvokeOptions<ReturnType> {
className?: NTClass className?: NTClass
channel?: NTChannel channel?: NTChannel
classNameIsRegister?: boolean registerEvent?: boolean
cbCmd?: string | string[] cbCmd?: string | string[]
cmdCB?: (payload: ReturnType, result: unknown) => boolean cmdCB?: (payload: ReturnType, result: unknown) => boolean
afterFirstCmd?: boolean // 是否在methodName调用完之后再去hook cbCmd afterFirstCmd?: boolean // 是否在methodName调用完之后再去hook cbCmd
timeout?: number timeout?: number
} }
let channel: NTChannel
function getChannel() {
if (channel) {
return channel
}
if (ipcMain.eventNames().includes(NTChannel.IPC_UP_2)) {
return channel = NTChannel.IPC_UP_2
} else {
return channel = NTChannel.IPC_UP_3
}
}
export function invoke< export function invoke<
R extends Awaited<ReturnType<Extract<NTService[S][M], (...args: any) => unknown>>>, R extends Awaited<ReturnType<Extract<NTService[S][M], (...args: any) => unknown>>>,
S extends keyof NTService = any, S extends keyof NTService = any,
M extends keyof NTService[S] & string = any M extends keyof NTService[S] & string = any
>(method: Extract<unknown, `${S}/${M}`> | string, args: unknown[], options: InvokeOptions<R> = {}) { >(method: Extract<unknown, `${S}/${M}`> | string, args: unknown[], options: InvokeOptions<R> = {}) {
const className = options.className ?? NTClass.NT_API const className = options.className ?? NTClass.NT_API
const channel = options.channel ?? NTChannel.IPC_UP_2 const channel = options.channel ?? getChannel()
const timeout = options.timeout ?? 5000 const timeout = options.timeout ?? 5000
const afterFirstCmd = options.afterFirstCmd ?? true const afterFirstCmd = options.afterFirstCmd ?? true
let eventName = className + '-' + channel[channel.length - 1] let eventName = className + '-' + channel[channel.length - 1]
if (options.classNameIsRegister) { if (options.registerEvent) {
eventName += '-register' eventName += '-register'
} }
return new Promise<R>((resolve, reject) => { return new Promise<R>((resolve, reject) => {
const apiArgs = [method, ...args] const apiArgs = [method, ...args]
const callbackId = randomUUID() const callbackId = randomUUID()
let eventId: string
const timeoutId = setTimeout(() => { const timeoutId = setTimeout(() => {
log(`ntqq api timeout ${channel}, ${eventName}, ${method}`, apiArgs) if (eventId) {
reject(`ntqq api timeout ${channel}, ${eventName}, ${method}, ${apiArgs}`) removeReceiveHook(eventId)
}
log(`ntqq api timeout ${channel}, ${eventName}, ${method}`, args)
reject(`ntqq api timeout ${channel}, ${eventName}, ${method}, ${JSON.stringify(args)}`)
}, timeout) }, timeout)
if (!options.cbCmd) { if (!options.cbCmd) {
@@ -137,19 +159,15 @@ export function invoke<
let result: unknown let result: unknown
// 这里的callback比较特殊QQ后端先返回是否调用成功再返回一条结果数据 // 这里的callback比较特殊QQ后端先返回是否调用成功再返回一条结果数据
const secondCallback = () => { const secondCallback = () => {
const hookId = registerReceiveHook<R>(options.cbCmd!, (payload) => { eventId = registerReceiveHook<R>(options.cbCmd!, (payload) => {
if (options.cmdCB) { if (options.cmdCB) {
if (options.cmdCB(payload, result)) { if (!options.cmdCB(payload, result)) {
removeReceiveHook(hookId) return
clearTimeout(timeoutId)
resolve(payload)
} }
} }
else { removeReceiveHook(eventId)
removeReceiveHook(hookId) clearTimeout(timeoutId)
clearTimeout(timeoutId) resolve(payload)
resolve(payload)
}
}) })
} }
!afterFirstCmd && secondCallback() !afterFirstCmd && secondCallback()
@@ -159,9 +177,12 @@ export function invoke<
afterFirstCmd && secondCallback() afterFirstCmd && secondCallback()
} }
else { else {
log('ntqq api call failed,', method, res)
clearTimeout(timeoutId) clearTimeout(timeoutId)
reject(`ntqq api call failed, ${method}, ${res.errMsg}`) if (eventId) {
removeReceiveHook(eventId)
}
log('ntqq api call failed,', method, args, res)
reject(`ntqq api call failed, ${method}, ${JSON.stringify(res)}`)
} }
} }
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,14 @@
syntax = "proto3";
package SysMsg;
message GroupMemberChange {
uint32 groupCode = 1;
string memberUid = 3;
uint32 type = 4; // 130:主动 131:被动
string adminUid = 5;
}
message GroupInvite {
uint32 groupCode = 1;
string operatorUid = 5;
}

View File

@@ -0,0 +1,134 @@
syntax = "proto3";
package Msg;
message RoutingHead {
optional uint64 fromUin = 1;
optional string fromUid = 2;
optional uint32 fromAppid = 3;
optional uint32 fromInstid = 4;
optional uint64 toUin = 5;
optional string toUid = 6;
optional C2c c2c = 7;
optional Group group = 8;
}
message C2c {
optional string friendName = 6;
}
message Group {
optional uint64 groupCode = 1;
optional uint32 groupType = 2;
optional uint64 groupInfoSeq = 3;
optional string groupCard = 4;
optional uint32 groupCardType = 5;
optional uint32 groupLevel = 6;
optional string groupName = 7;
optional string extGroupKeyInfo = 8;
optional uint32 msgFlag = 9;
}
message ContentHead {
optional uint64 msgType = 1;
optional uint64 subType = 2;
optional uint32 c2cCmd = 3;
optional uint64 random = 4;
optional uint64 msgSeq = 5;
optional uint64 msgTime = 6;
optional uint32 pkgNum = 7;
optional uint32 pkgIndex = 8;
optional uint32 divSeq = 9;
optional uint32 autoReply = 10;
optional uint64 ntMsgSeq = 11;
optional uint64 msgUid = 12;
optional ContentHeadField15 field15 = 15;
}
message ContentHeadField15 {
optional uint32 field1 = 1;
optional uint32 field2 = 2;
optional uint32 field3 = 3;
optional string field4 = 4;
optional string field5 = 5;
}
message Message {
optional RoutingHead routingHead = 1;
optional ContentHead contentHead = 2;
optional MessageBody body = 3;
}
message MessageBody {
optional RichText richText = 1;
optional bytes msgContent = 2;
optional bytes msgEncryptContent = 3;
}
message RichText {
optional Attr attr = 1;
repeated Elem elems = 2;
}
message Elem {
optional Text text = 1;
optional Face face = 2;
optional LightAppElem lightApp = 51;
optional CommonElem commonElem = 53;
}
message Text {
optional string str = 1;
optional string link = 2;
optional bytes attr6Buf = 3;
optional bytes attr7Buf = 4;
optional bytes buf = 11;
optional bytes pbReserve = 12;
}
message Face {
optional uint32 index = 1;
optional bytes old = 2;
optional bytes buf = 11;
}
message LightAppElem {
optional bytes data = 1;
optional bytes msgResid = 2;
}
message CommonElem {
required uint32 serviceType = 1;
optional bytes pbElem = 2;
optional uint32 businessType = 3;
}
message Attr {
optional int32 codePage = 1;
optional int32 time = 2;
optional int32 random = 3;
optional int32 color = 4;
optional int32 size = 5;
optional int32 effect = 6;
optional int32 charSet = 7;
optional int32 pitchAndFamily = 8;
optional string fontName = 9;
optional bytes reserveData = 10;
}
message MarkdownElem {
string content = 1;
}
message PbMultiMsgItem {
string fileName = 1;
PbMultiMsgNew buffer = 2;
}
message PbMultiMsgNew {
repeated Message msg = 1;
}
message PbMultiMsgTransmit {
repeated Message msg = 1;
repeated PbMultiMsgItem pbItemList = 2;
}

View File

@@ -0,0 +1,76 @@
syntax = "proto3";
package RichMedia;
message MsgInfo {
repeated MsgInfoBody msgInfoBody = 1;
ExtBizInfo extBizInfo = 2;
}
message MsgInfoBody {
IndexNode index = 1;
PicInfo pic = 2;
bool fileExist = 5;
}
message IndexNode {
FileInfo info = 1;
string fileUuid = 2;
uint32 storeID = 3;
uint32 uploadTime = 4;
uint32 expire = 5;
uint32 type = 6; //0
}
message FileInfo {
uint32 fileSize = 1;
string md5HexStr = 2;
string sha1HexStr = 3;
string fileName = 4;
FileType fileType = 5;
uint32 width = 6;
uint32 height = 7;
uint32 time = 8;
uint32 original = 9;
}
message FileType {
uint32 type = 1;
uint32 picFormat = 2;
uint32 videoFormat = 3;
uint32 pttFormat = 4;
}
message PicInfo {
string urlPath = 1;
PicUrlExtParams ext = 2;
string domain = 3;
}
message PicUrlExtParams {
string originalParam = 1;
string bigParam = 2;
string thumbParam = 3;
}
message ExtBizInfo {
PicExtBizInfo pic = 1;
VideoExtBizInfo video = 2;
uint32 busiType = 10;
}
message PicExtBizInfo {
uint32 bizType = 1;
string summary = 2;
}
message VideoExtBizInfo {
bytes pbReserve = 3;
}
message PicFileIdInfo {
bytes sha1 = 2;
uint32 size = 3;
uint32 appid = 4;
uint32 time = 5;
uint32 expire = 10;
}

View File

@@ -1,30 +0,0 @@
syntax = "proto3";
package SysMsg;
message SystemMessage {
repeated SystemMessageHeader header = 1;
repeated SystemMessageMsgSpec msgSpec = 2;
SystemMessageBodyWrapper bodyWrapper = 3;
}
message SystemMessageHeader {
uint32 peerNumber = 1;
string peerString = 2;
uint32 uin = 5;
optional string uid = 6;
}
message SystemMessageMsgSpec {
uint32 msgType = 1;
uint32 subType = 2;
uint32 subSubType = 3;
uint32 msgSeq = 5;
uint32 time = 6;
//uint64 msgId = 12;
uint32 other = 13;
}
message SystemMessageBodyWrapper {
bytes body = 2;
// Find the first [08], or ignore the first 7 bytes?
}

View File

@@ -13,7 +13,7 @@ export interface NodeIKernelBuddyService {
}[] }[]
}> }>
setBuddyRemark(uid: number, remark: string): void setBuddyRemark(arg: unknown): Promise<GeneralCallResult>
isBuddy(uid: string): boolean isBuddy(uid: string): boolean

View File

@@ -123,4 +123,8 @@ export interface NodeIKernelGroupService {
addGroupEssence(param: { groupCode: string, msgRandom: number, msgSeq: number }): Promise<unknown> addGroupEssence(param: { groupCode: string, msgRandom: number, msgSeq: number }): Promise<unknown>
removeGroupEssence(param: { groupCode: string, msgRandom: number, msgSeq: number }): Promise<unknown> removeGroupEssence(param: { groupCode: string, msgRandom: number, msgSeq: number }): Promise<unknown>
setHeader(args: unknown[]): Promise<GeneralCallResult>
searchMember(sceneId: string, keyword: string): Promise<void>
} }

View File

@@ -10,7 +10,9 @@ export interface NodeIKernelMsgService {
setStatus(args: { status: number, extStatus: number, batteryStatus: number }): Promise<GeneralCallResult> setStatus(args: { status: number, extStatus: number, batteryStatus: number }): Promise<GeneralCallResult>
forwardMsg(msgIds: string[], srcContact: Peer, dstContacts: Peer[], commentElements: MessageElement[]): Promise<GeneralCallResult> forwardMsg(msgIds: string[], srcContact: Peer, dstContacts: Peer[], commentElements: MessageElement[]): Promise<GeneralCallResult & {
detailErr: Map<unknown, unknown>
}>
forwardMsgWithComment(...args: unknown[]): Promise<GeneralCallResult> forwardMsgWithComment(...args: unknown[]): Promise<GeneralCallResult>
@@ -18,6 +20,10 @@ export interface NodeIKernelMsgService {
getAioFirstViewLatestMsgs(peer: Peer, num: number): Promise<GeneralCallResult & { msgList: RawMessage[] }> getAioFirstViewLatestMsgs(peer: Peer, num: number): Promise<GeneralCallResult & { msgList: RawMessage[] }>
getAioFirstViewLatestMsgsAndAddActiveChat(...args: unknown[]): Promise<GeneralCallResult & { msgList: RawMessage[] }>
getMsgsIncludeSelfAndAddActiveChat(...args: unknown[]): Promise<GeneralCallResult & { msgList: RawMessage[] }>
getMsgsIncludeSelf(peer: Peer, msgId: string, count: number, queryOrder: boolean): Promise<GeneralCallResult & { msgList: RawMessage[] }> getMsgsIncludeSelf(peer: Peer, msgId: string, count: number, queryOrder: boolean): Promise<GeneralCallResult & { msgList: RawMessage[] }>
getMsgsBySeqAndCount(peer: Peer, seq: string, count: number, desc: boolean, unknownArg: boolean): Promise<GeneralCallResult & { msgList: RawMessage[] }> getMsgsBySeqAndCount(peer: Peer, seq: string, count: number, desc: boolean, unknownArg: boolean): Promise<GeneralCallResult & { msgList: RawMessage[] }>
@@ -71,7 +77,7 @@ export interface NodeIKernelMsgService {
downloadRichMedia(...args: unknown[]): unknown downloadRichMedia(...args: unknown[]): unknown
setMsgEmojiLikes(...args: unknown[]): unknown setMsgEmojiLikes(...args: unknown[]): Promise<GeneralCallResult>
getMsgEmojiLikesList(peer: Peer, msgSeq: string, emojiId: string, emojiType: string, cookie: string, bForward: boolean, number: number): Promise<{ getMsgEmojiLikesList(peer: Peer, msgSeq: string, emojiId: string, emojiType: string, cookie: string, bForward: boolean, number: number): Promise<{
result: number result: number
@@ -89,4 +95,6 @@ export interface NodeIKernelMsgService {
getMultiMsg(...args: unknown[]): Promise<GeneralCallResult & { msgList: RawMessage[] }> getMultiMsg(...args: unknown[]): Promise<GeneralCallResult & { msgList: RawMessage[] }>
getTempChatInfo(chatType: number, uid: string): Promise<TmpChatInfoApi> getTempChatInfo(chatType: number, uid: string): Promise<TmpChatInfoApi>
sendSsoCmdReqByContend(ssoCmd: string, content: string): Promise<GeneralCallResult & { rsp: string }>
} }

View File

@@ -0,0 +1,17 @@
export interface NodeIKernelNodeMiscService {
wantWinScreenOCR(...args: unknown[]): Promise<{
code: number
errMsg: string
result: {
text: string
[key: `pt${number}`]: {
x: string
y: string
}
charBox: unknown[]
score: ''
}[]
}>
queryAutoRun(): Promise<boolean>
}

View File

@@ -3,7 +3,7 @@ import { GeneralCallResult } from './common'
import { Dict } from 'cosmokit' import { Dict } from 'cosmokit'
export interface NodeIKernelProfileLikeService { export interface NodeIKernelProfileLikeService {
setBuddyProfileLike(...args: unknown[]): { result: number, errMsg: string, succCounts: number } setBuddyProfileLike(...args: unknown[]): GeneralCallResult & { succCounts: number }
getBuddyProfileLike(req: BuddyProfileLikeReq): Promise<GeneralCallResult & { getBuddyProfileLike(req: BuddyProfileLikeReq): Promise<GeneralCallResult & {
info: { info: {

View File

@@ -39,5 +39,21 @@ export interface NodeIKernelRichMediaService {
failFileIdList: Array<unknown> failFileIdList: Array<unknown>
} }
}> }>
batchGetGroupFileCount(groupIds: string[]): Promise<GeneralCallResult & {
groupCodes: string[]
groupFileCounts: number[]
}>
getGroupSpace(groupId: string): Promise<GeneralCallResult & {
groupSpaceResult: {
retCode: number
retMsg: string
clientWording: string
totalSpace: string
usedSpace: string
allUpload: boolean
}
}>
} }

View File

@@ -0,0 +1,13 @@
import { GeneralCallResult } from './common'
export interface NodeIKernelRobotService {
getRobotUinRange(req: unknown): Promise<GeneralCallResult & {
response: {
version: number
robotUinRanges: {
minUin: string
maxUin: string
}[]
}
}>
}

View File

@@ -9,3 +9,5 @@ export * from './NodeIKernelUixConvertService'
export * from './NodeIKernelRichMediaService' export * from './NodeIKernelRichMediaService'
export * from './NodeIKernelTicketService' export * from './NodeIKernelTicketService'
export * from './NodeIKernelTipOffService' export * from './NodeIKernelTipOffService'
export * from './NodeIKernelRobotService'
export * from './NodeIKernelNodeMiscService'

View File

@@ -1,12 +1,3 @@
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
@@ -22,9 +13,9 @@ export interface Group {
hasModifyConfGroupName: boolean hasModifyConfGroupName: boolean
remarkName: string remarkName: string
hasMemo: boolean hasMemo: boolean
groupShutupExpireTime: string //"0", groupShutupExpireTime: string
personShutupExpireTime: string //"0", personShutupExpireTime: string
discussToGroupUin: string //"0", discussToGroupUin: string
discussToGroupMaxMsgSeq: number discussToGroupMaxMsgSeq: number
discussToGroupTime: number discussToGroupTime: number
groupFlagExt: number //1073938496, groupFlagExt: number //1073938496,
@@ -32,39 +23,57 @@ export interface Group {
groupCreditLevel: number //0, groupCreditLevel: number //0,
groupFlagExt3: number //0, groupFlagExt3: number //0,
groupOwnerId: { groupOwnerId: {
memberUin: string //"0", memberUin: string
memberUid: string //"u_fbf8N7aeuZEnUiJAbQ9R8Q" memberUid: string
} }
members: GroupMember[] // 原始数据是没有这个的,为了方便自己加了这个字段
createTime: string createTime: string
} }
export enum GroupMemberRole { export enum GroupMemberRole {
normal = 2, Normal = 2,
admin = 3, Admin = 3,
owner = 4, Owner = 4,
} }
export interface GroupMember { export interface GroupMember {
memberSpecialTitle?: string uid: string
avatarPath: string
cardName: string
cardType: number
isDelete: boolean
nick: string
qid: string qid: string
uin: string
nick: string
remark: string remark: string
role: GroupMemberRole // 群主:4, 管理员:3群员:2 cardType: number
shutUpTime: number // 禁言时间,单位是什么暂时不清楚 cardName: string
uid: string // 加密的字符串 role: GroupMemberRole
uin: string // QQ号 avatarPath: string
shutUpTime: number
isDelete: boolean
isSpecialConcerned: boolean
isSpecialShield: boolean
isRobot: boolean isRobot: boolean
sex?: Sex groupHonor: Uint8Array
qqLevel?: QQLevel memberRealLevel: number
isChangeRole: boolean memberLevel: number
globalGroupLevel: number
globalGroupPoint: number
memberTitleId: number
memberSpecialTitle: string
specialTitleExpireTime: string
userShowFlag: number
userShowFlagNew: number
richFlag: number
mssVipType: number
bigClubLevel: number
bigClubFlag: number
autoRemark: string
creditLevel: number
joinTime: number joinTime: number
lastSpeakTime: number lastSpeakTime: number
memberLevel: number memberFlag: number
memberFlagExt: number
memberMobileFlag: number
memberFlagExt2: number
isSpecialShielded: boolean
cardNameId: number
} }
export interface PublishGroupBulletinReq { export interface PublishGroupBulletinReq {
@@ -78,3 +87,104 @@ export interface PublishGroupBulletinReq {
pinned: number pinned: number
confirmRequired: number confirmRequired: number
} }
export interface GroupAllInfo {
groupCode: string
ownerUid: string
groupFlag: number
groupFlagExt: number
maxMemberNum: number
memberNum: number
groupOption: number
classExt: number
groupName: string
fingerMemo: string
groupQuestion: string
certType: number
shutUpAllTimestamp: number
shutUpMeTimestamp: number //解除禁言时间
groupTypeFlag: number
privilegeFlag: number
groupSecLevel: number
groupFlagExt3: number
isConfGroup: number
isModifyConfGroupFace: number
isModifyConfGroupName: number
noFigerOpenFlag: number
noCodeFingerOpenFlag: number
groupFlagExt4: number
groupMemo: string
cmdUinMsgSeq: number
cmdUinJoinTime: number
cmdUinUinFlag: number
cmdUinMsgMask: number
groupSecLevelInfo: number
cmdUinPrivilege: number
cmdUinFlagEx2: number
appealDeadline: number
remarkName: number
isTop: boolean
richFingerMemo: string
groupAnswer: string
joinGroupAuth: string
isAllowModifyConfGroupName: number
}
export interface GroupBulletinListResult {
groupCode: string
srvCode: number
readOnly: number
role: number
inst: unknown[]
feeds: {
uin: string
feedId: string
publishTime: string
msg: {
text: string
textFace: string
pics: {
id: string
width: number
height: number
}[]
title: string
}
type: number
fn: number
cn: number
vn: number
settings: {
isShowEditCard: number
remindTs: number
tipWindowType: number
confirmRequired: number
}
pinned: number
readNum: number
is_read: number
is_all_confirm: number
}[]
groupInfo: {
groupCode: string
classId: number
}
gln: number
tst: number
publisherInfos: {
uin: string
nick: string
avatar: string
}[]
server_time: string
svrt: string
nextIndex: number
jointime: string
}
export enum GroupMsgMask {
AllowNotify = 1, // 允许提醒
AllowNotNotify = 4, // 接受消息不提醒
BoxNotNotify = 2, // 收进群助手不提醒
NotAllow = 3, // 屏蔽
}

View File

@@ -80,7 +80,7 @@ export interface SendVideoElement {
export interface SendArkElement { export interface SendArkElement {
elementType: ElementType.Ark elementType: ElementType.Ark
elementId: '' elementId: ''
arkElement: ArkElement arkElement: Partial<ArkElement>
} }
export type SendMessageElement = export type SendMessageElement =
@@ -197,6 +197,60 @@ export interface PicElement {
md5HexStr?: string md5HexStr?: string
} }
export interface TipAioOpGrayTipElement {
operateType: number
peerUid: string
fromGrpCodeOfTmpChat: string
}
export enum TipGroupElementType {
MemberIncrease = 1,
Kicked = 3, // 被移出群
Ban = 8,
}
export interface TipGroupElement {
type: TipGroupElementType // 1是表示有人加入群, 自己加入群也会收到这个
role: number
groupName: string // 暂时获取不到
memberUid: string
memberNick: string
memberRemark: string
adminUid: string
adminNick: string
adminRemark: string
createGroup: null
memberAdd?: {
showType: number
otherAdd?: {
uid: string
name: string
}
otherAddByOtherQRCode?: unknown
otherAddByYourQRCode?: unknown
youAddByOtherQRCode?: unknown
otherInviteOther?: unknown
otherInviteYou?: unknown
youInviteOther?: unknown
}
shutUp?: {
curTime: string
duration: string // 禁言时间,秒
admin: {
uid: string
card: string
name: string
role: GroupMemberRole
}
member: {
uid: string
card: string
name: string
role: GroupMemberRole
}
}
}
export enum GrayTipElementSubType { export enum GrayTipElementSubType {
Revoke = 1, Revoke = 1,
Proclamation = 2, Proclamation = 2,
@@ -234,14 +288,18 @@ export interface GrayTipElement {
xmlElement?: { xmlElement?: {
templId: string templId: string
content: string content: string
templParam: Map<string, string>
members: Map<string, string> // uid -> remark
} }
jsonGrayTipElement?: { jsonGrayTipElement?: {
busiId: string busiId: string
jsonStr: string jsonStr: string
xmlToJsonParam?: {
templParam: Map<string, string>
}
} }
} }
export enum FaceIndex { export enum FaceIndex {
Dice = 358, Dice = 358,
RPS = 359, // 石头剪刀布 RPS = 359, // 石头剪刀布
@@ -268,6 +326,10 @@ export interface MarketFaceElement {
key: string key: string
imageWidth?: number imageWidth?: number
imageHeight?: number imageHeight?: number
supportSize?: {
width: number
height: number
}[]
} }
export interface VideoElement { export interface VideoElement {
@@ -326,58 +388,6 @@ export interface InlineKeyboardElement {
] ]
} }
export interface TipAioOpGrayTipElement {
// 这是什么提示来着?
operateType: number
peerUid: string
fromGrpCodeOfTmpChat: string
}
export enum TipGroupElementType {
MemberIncrease = 1,
Kicked = 3, // 被移出群
Ban = 8,
}
export interface TipGroupElement {
type: TipGroupElementType // 1是表示有人加入群, 自己加入群也会收到这个
role: 0 // 暂时不知
groupName: string // 暂时获取不到
memberUid: string
memberNick: string
memberRemark: string
adminUid: string
adminNick: string
adminRemark: string
createGroup: null
memberAdd?: {
showType: 1
otherAdd: null
otherAddByOtherQRCode: null
otherAddByYourQRCode: null
youAddByOtherQRCode: null
otherInviteOther: null
otherInviteYou: null
youInviteOther: null
}
shutUp?: {
curTime: string
duration: string // 禁言时间,秒
admin: {
uid: string
card: string
name: string
role: GroupMemberRole
}
member: {
uid: string
card: string
name: string
role: GroupMemberRole
}
}
}
export interface StructLongMsgElement { export interface StructLongMsgElement {
xmlContent: string xmlContent: string
resId: string resId: string
@@ -409,11 +419,30 @@ export interface RawMessage {
guildId: string guildId: string
sendNickName: string sendNickName: string
sendMemberName?: string // 发送者群名片 sendMemberName?: string // 发送者群名片
sendRemarkName?: string // 发送者好友备注
chatType: ChatType chatType: ChatType
sendStatus?: number // 消息状态别人发的2是已撤回自己发的2是已发送 sendStatus?: number // 消息状态别人发的2是已撤回自己发的2是已发送
recallTime: string // 撤回时间, "0"是没有撤回 recallTime: string // 撤回时间, "0"是没有撤回
records: RawMessage[] records: RawMessage[]
elements: MessageElement[] elements: MessageElement[]
peerName: string
multiTransInfo?: {
status: number
msgId: number
friendFlag: number
fromFaceUrl: string
}
emojiLikesList: {
emojiId: string
emojiType: string
likesCnt: string
isClicked: boolean
}[]
msgAttrs: Map<number, {
attrType: number
attrId: string
}>
isOnlineMsg: boolean
} }
export interface Peer { export interface Peer {
@@ -455,7 +484,7 @@ export interface MessageElement {
actionBarElement?: unknown actionBarElement?: unknown
} }
export interface OnRichMediaDownloadCompleteParams { export interface RichMediaDownloadCompleteNotify {
fileModelId: string fileModelId: string
msgElementId: string msgElementId: string
msgId: string msgId: string
@@ -478,7 +507,7 @@ export interface OnRichMediaDownloadCompleteParams {
userUsedSpacePerDay: unknown userUsedSpacePerDay: unknown
} }
export interface OnGroupFileInfoUpdateParams { export interface GroupFileInfo {
retCode: number retCode: number
retMsg: string retMsg: string
clientWording: string clientWording: string
@@ -559,3 +588,31 @@ export interface GetFileListParam {
showOnlinedocFolder: number showOnlinedocFolder: number
folderId?: string folderId?: string
} }
export interface RichMediaUploadCompleteNotify {
fileId: string
fileDownType: number
filePath: string
totalSize: string
trasferStatus: number
commonFileInfo: {
uuid: string
fileName: string
fileSize: string
md5: string
sha: string
}
}
export enum RMBizType {
Unknown,
C2CFile,
GroupFile,
C2CPic,
GroupPic,
DiscPic,
C2CVideo,
GroupVideo,
C2CPtt,
GroupPtt,
}

View File

@@ -1,19 +1,19 @@
export enum GroupNotifyType { export enum GroupNotifyType {
INVITED_BY_MEMBER = 1, InvitedByMember = 1,
REFUSE_INVITED, RefuseInvited,
REFUSED_BY_ADMINI_STRATOR, RefusedByAdminiStrator,
AGREED_TOJOIN_DIRECT, // 有人接受了邀请入群 AgreedTojoinDirect, // 有人接受了邀请入群
INVITED_NEED_ADMINI_STRATOR_PASS, // 有人邀请了别人入群 InvitedNeedAdminiStratorPass, // 有人邀请了别人入群
AGREED_TO_JOIN_BY_ADMINI_STRATOR, AgreedToJoinByAdminiStrator,
REQUEST_JOIN_NEED_ADMINI_STRATOR_PASS, RequestJoinNeedAdminiStratorPass,
SET_ADMIN, SetAdmin,
KICK_MEMBER_NOTIFY_ADMIN, KickMemberNotifyAdmin,
KICK_MEMBER_NOTIFY_KICKED, KickMemberNotifyKicked,
MEMBER_LEAVE_NOTIFY_ADMIN, // 主动退出 MemberLeaveNotifyAdmin, // 主动退出
CANCEL_ADMIN_NOTIFY_CANCELED, // 我被取消管理员 CancelAdminNotifyCanceled, // 我被取消管理员
CANCEL_ADMIN_NOTIFY_ADMIN, // 其他人取消管理员 CancelAdminNotifyAdmin, // 其他人取消管理员
TRANSFER_GROUP_NOTIFY_OLDOWNER, TransferGroupNotifyOldowner,
TRANSFER_GROUP_NOTIFY_ADMIN TransferGroupNotifyAdmin
} }
export interface GroupNotifies { export interface GroupNotifies {
@@ -23,11 +23,11 @@ export interface GroupNotifies {
} }
export enum GroupNotifyStatus { export enum GroupNotifyStatus {
KINIT, // 初始化 Init, // 初始化
KUNHANDLE, // 未处理 Unhandle, // 未处理
KAGREED, // 同意 Agreed, // 同意
KREFUSED, // 拒绝 Refused, // 拒绝
KIGNORED // 忽略 Ignored // 忽略
} }
export interface GroupNotify { export interface GroupNotify {
@@ -56,20 +56,8 @@ export enum GroupRequestOperateTypes {
} }
export enum BuddyReqType { export enum BuddyReqType {
KMEINITIATOR, MsgInfo = 12,
KPEERINITIATOR, MeInitiatorWaitPeerConfirm = 13,
KMEAGREED,
KMEAGREEDANDADDED,
KPEERAGREED,
KPEERAGREEDANDADDED,
KPEERREFUSED,
KMEREFUSED,
KMEIGNORED,
KMEAGREEANYONE,
KMESETQUESTION,
KMEAGREEANDADDFAILED,
KMSGINFO,
KMEINITIATORWAITPEERCONFIRM
} }
export interface FriendRequest { export interface FriendRequest {
@@ -91,41 +79,3 @@ 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

@@ -1,7 +1,8 @@
export enum Sex { export enum Sex {
male = 0, Unknown = 0,
female = 2, Male = 1,
unknown = 255, Female = 2,
Hidden = 255
} }
export interface QQLevel { export interface QQLevel {
@@ -67,6 +68,7 @@ export interface User {
recommendImgFlag?: number recommendImgFlag?: number
disableEmojiShortCuts?: number disableEmojiShortCuts?: number
pendantId?: string pendantId?: string
age?: number
} }
export interface SelfInfo extends User { export interface SelfInfo extends User {
@@ -100,12 +102,12 @@ export interface BaseInfo {
birthday_month: number birthday_month: number
birthday_day: number birthday_day: number
age: number age: number
sex: number sex: Sex
eMail: string eMail: string
phoneNum: string phoneNum: string
categoryId: number categoryId: number
richTime: number richTime: number
richBuffer: string richBuffer: Uint8Array
} }
interface MusicInfo { interface MusicInfo {
@@ -221,11 +223,6 @@ interface RelationFlags {
isHidePrivilegeIcon: number isHidePrivilegeIcon: number
} }
export interface FriendV2 extends SimpleInfo {
categoryId?: number
categroyName?: string
}
interface CommonExt { interface CommonExt {
constellation: number constellation: number
shengXiao: number shengXiao: number
@@ -255,7 +252,7 @@ interface PhotoWall {
picList: Pic[] picList: Pic[]
} }
export interface UserDetailInfoListenerArg { export interface UserDetailInfo {
uid: string uid: string
uin: string uin: string
simpleInfo: SimpleInfo simpleInfo: SimpleInfo
@@ -300,7 +297,7 @@ export interface UserDetailInfoByUin {
birthday_year: number birthday_year: number
birthday_month: number birthday_month: number
birthday_day: number birthday_day: number
sex: number //0 sex: number
topTime: string topTime: string
constellation: number constellation: number
shengXiao: number shengXiao: number

View File

@@ -1,54 +0,0 @@
import {
NodeIKernelBuddyService,
NodeIKernelGroupService,
NodeIKernelProfileService,
NodeIKernelProfileLikeService,
NodeIKernelMSFService,
NodeIKernelMsgService,
NodeIKernelUixConvertService,
NodeIKernelRichMediaService,
NodeIKernelTicketService,
NodeIKernelTipOffService
} from './services'
import { constants } from 'node:os'
import { Dict } from 'cosmokit'
const Process = require('node:process')
export interface NodeIQQNTWrapperSession {
getBuddyService(): NodeIKernelBuddyService
getGroupService(): NodeIKernelGroupService
getProfileService(): NodeIKernelProfileService
getProfileLikeService(): NodeIKernelProfileLikeService
getMsgService(): NodeIKernelMsgService
getMSFService(): NodeIKernelMSFService
getUixConvertService(): NodeIKernelUixConvertService
getRichMediaService(): NodeIKernelRichMediaService
getTicketService(): NodeIKernelTicketService
getTipOffService(): NodeIKernelTipOffService
}
export interface WrapperApi {
NodeIQQNTWrapperSession?: NodeIQQNTWrapperSession
}
const wrapperApi: WrapperApi = {}
Process.dlopenOrig = Process.dlopen
Process.dlopen = function (module: Dict, filename: string, flags = constants.dlopen.RTLD_LAZY) {
const dlopenRet = this.dlopenOrig(module, filename, flags)
for (const export_name in module.exports) {
module.exports[export_name] = new Proxy(module.exports[export_name], {
construct: (target, args) => {
const ret = new target(...args)
if (export_name === 'NodeIQQNTWrapperSession') wrapperApi.NodeIQQNTWrapperSession = ret
return ret
}
})
}
return dlopenRet
}
export function getSession() {
return wrapperApi['NodeIQQNTWrapperSession']
}

View File

@@ -0,0 +1,21 @@
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
interface Payload {
user_id: number | string
}
export class DeleteFriend extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_DeleteFriend
payloadSchema = Schema.object({
user_id: Schema.union([Number, String]).required()
})
protected async _handle(payload: Payload) {
const uin = payload.user_id.toString()
const uid = await this.ctx.ntUserApi.getUidByUin(uin)
if (!uid) throw new Error('无法获取用户信息')
await this.ctx.ntFriendApi.delBuddy(uid)
return null
}
}

View File

@@ -23,8 +23,8 @@ interface FileResponse {
export class DownloadFile extends BaseAction<Payload, FileResponse> { export class DownloadFile extends BaseAction<Payload, FileResponse> {
actionName = ActionName.GoCQHTTP_DownloadFile actionName = ActionName.GoCQHTTP_DownloadFile
payloadSchema = Schema.object({ payloadSchema = Schema.object({
url: String, url: Schema.string(),
base64: String, base64: Schema.string(),
headers: Schema.union([String, Schema.array(String)]) headers: Schema.union([String, Schema.array(String)])
}) })

View File

@@ -34,25 +34,23 @@ export class GetForwardMsg extends BaseAction<Payload, Response> {
if (data?.result !== 0) { if (data?.result !== 0) {
throw Error('找不到相关的聊天记录' + data?.errMsg) throw Error('找不到相关的聊天记录' + data?.errMsg)
} }
const msgList = data.msgList const messages: (OB11ForwardMessage | undefined)[] = await Promise.all(
const messages = await Promise.all( data.msgList.map(async (msg) => {
msgList.map(async (msg) => { const res = await OB11Entities.message(this.ctx, msg)
const resMsg = await OB11Entities.message(this.ctx, msg) if (res) {
if (!resMsg) return return {
resMsg.message_id = this.ctx.store.createMsgShortId({ content: res.message,
chatType: msg.chatType, sender: {
peerUid: msg.peerUid, nickname: res.sender.nickname,
}, msg.msgId) user_id: res.sender.user_id
return resMsg },
time: res.time,
message_format: res.message_format,
message_type: res.message_type
}
}
}) })
) )
const forwardMessages = filterNullable(messages) return { messages: filterNullable(messages) }
.map(v => {
const msg = v as Partial<OB11ForwardMessage>
msg.content = msg.message
delete msg.message
return msg as OB11ForwardMessage
})
return { messages: forwardMessages }
} }
} }

View File

@@ -0,0 +1,32 @@
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
interface Payload {
group_id: number | string
}
interface Response {
file_count: number
limit_count: number
used_space: number
total_space: number
}
export class GetGroupFileSystemInfo extends BaseAction<Payload, Response> {
actionName = ActionName.GoCQHTTP_GetGroupFileSystemInfo
payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required()
})
async _handle(payload: Payload) {
const groupId = payload.group_id.toString()
const { groupFileCounts } = await this.ctx.ntGroupApi.getGroupFileCount(groupId)
const { groupSpaceResult } = await this.ctx.ntGroupApi.getGroupFileSpace(groupId)
return {
file_count: groupFileCounts[0],
limit_count: 10000,
used_space: +groupSpaceResult.usedSpace,
total_space: +groupSpaceResult.totalSpace,
}
}
}

View File

@@ -2,6 +2,7 @@ import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { pathToFileURL } from 'node:url' import { pathToFileURL } from 'node:url'
import { ChatType } from '@/ntqqapi/types' import { ChatType } from '@/ntqqapi/types'
import { GroupFileInfo } from '@/ntqqapi/types'
export interface Payload { export interface Payload {
group_id: number | string group_id: number | string
@@ -30,23 +31,7 @@ export class GetGroupFileUrl extends BaseAction<Payload, Response> {
} }
} else { } else {
const groupId = payload.group_id.toString() const groupId = payload.group_id.toString()
let modelId: string | undefined const modelId = await this.search(groupId, payload.file_id)
let nextIndex: number | undefined
while (nextIndex !== 0) {
const res = await this.ctx.ntGroupApi.getGroupFileList(groupId, {
sortType: 1,
fileCount: 50,
startIndex: nextIndex ?? 0,
sortOrder: 2,
showOnlinedocFolder: 0,
})
const file = res.item.find(item => item.fileInfo?.fileId === payload.file_id)
if (file) {
modelId = file.fileInfo?.fileModelId
break
}
nextIndex = res.nextIndex
}
if (modelId) { if (modelId) {
const peer = { const peer = {
chatType: ChatType.Group, chatType: ChatType.Group,
@@ -61,4 +46,37 @@ export class GetGroupFileUrl extends BaseAction<Payload, Response> {
throw new Error('file not found') throw new Error('file not found')
} }
} }
private async search(groupId: string, fileId: string, folderId?: string) {
let modelId: string | undefined
let nextIndex: number | undefined
const folders: GroupFileInfo['item'] = []
while (nextIndex !== 0) {
const res = await this.ctx.ntGroupApi.getGroupFileList(groupId, {
sortType: 1,
fileCount: 100,
startIndex: nextIndex ?? 0,
sortOrder: 2,
showOnlinedocFolder: 0,
folderId
})
const file = res.item.find(item => item.fileInfo?.fileId === fileId)
if (file) {
modelId = file.fileInfo?.fileModelId
break
}
folders.push(...res.item.filter(item => item.folderInfo?.totalFileCount))
nextIndex = res.nextIndex
}
if (!modelId) {
for (const item of folders) {
const res = await this.search(groupId, fileId, item.folderInfo?.folderId)
if (res) {
modelId = res
break
}
}
}
return modelId
}
} }

View File

@@ -1,7 +1,7 @@
import { BaseAction, Schema } from '../BaseAction' import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { OB11GroupFile, OB11GroupFileFolder } from '@/onebot11/types' import { OB11GroupFile, OB11GroupFileFolder } from '@/onebot11/types'
import { OnGroupFileInfoUpdateParams } from '@/ntqqapi/types' import { GroupFileInfo } from '@/ntqqapi/types'
interface Payload { interface Payload {
group_id: number | string group_id: number | string
@@ -22,7 +22,7 @@ export class GetGroupFilesByFolder extends BaseAction<Payload, Response> {
async _handle(payload: Payload) { async _handle(payload: Payload) {
const groupId = payload.group_id.toString() const groupId = payload.group_id.toString()
const data: OnGroupFileInfoUpdateParams['item'] = [] const data: GroupFileInfo['item'] = []
let nextIndex: number | undefined let nextIndex: number | undefined
while (nextIndex !== 0) { while (nextIndex !== 0) {

View File

@@ -30,12 +30,12 @@ export class GetGroupMsgHistory extends BaseAction<Payload, Response> {
const { count, reverseOrder } = payload const { count, reverseOrder } = payload
const peer = { chatType: ChatType.Group, peerUid: payload.group_id.toString() } const peer = { chatType: ChatType.Group, peerUid: payload.group_id.toString() }
let msgList: RawMessage[] let msgList: RawMessage[]
if (!payload.message_seq || payload.message_seq === '0') { if (!payload.message_seq || +payload.message_seq === 0) {
msgList = (await this.ctx.ntMsgApi.getAioFirstViewLatestMsgs(peer, +count)).msgList msgList = (await this.ctx.ntMsgApi.getAioFirstViewLatestMsgs(peer, +count)).msgList
} else { } else {
const startMsgId = (await this.ctx.store.getMsgInfoByShortId(+payload.message_seq))?.msgId const startMsgId = (await this.ctx.store.getMsgInfoByShortId(+payload.message_seq))?.msgId
if (!startMsgId) throw new Error(`消息${payload.message_seq}不存在`) if (!startMsgId) throw new Error(`消息${payload.message_seq}不存在`)
msgList = (await this.ctx.ntMsgApi.getMsgHistory(peer, startMsgId, +count)).msgList msgList = (await this.ctx.ntMsgApi.getMsgHistory(peer, startMsgId, +count, true)).msgList
} }
if (!msgList?.length) throw new Error('未找到消息') if (!msgList?.length) throw new Error('未找到消息')
if (reverseOrder) msgList.reverse() if (reverseOrder) msgList.reverse()

View File

@@ -0,0 +1,48 @@
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
interface Payload {
group_id: number | string
}
interface Notice {
sender_id: number
publish_time: number
message: {
text: string
images: {
height: string
width: string
id: string
}[]
}
}
export class GetGroupNotice extends BaseAction<Payload, Notice[]> {
actionName = ActionName.GoCQHTTP_GetGroupNotice
payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required()
})
protected async _handle(payload: Payload) {
const data = await this.ctx.ntGroupApi.getGroupBulletinList(payload.group_id.toString())
const result: Notice[] = []
for (const feed of data.result.feeds) {
result.push({
sender_id: +feed.uin,
publish_time: +feed.publishTime,
message: {
text: feed.msg.text,
images: feed.msg.pics.map(image => {
return {
height: String(image.height),
width: String(image.width),
id: image.id
}
})
}
})
}
return result
}
}

View File

@@ -1,7 +1,7 @@
import { BaseAction, Schema } from '../BaseAction' import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { OB11GroupFile, OB11GroupFileFolder } from '../../types' import { OB11GroupFile, OB11GroupFileFolder } from '../../types'
import { OnGroupFileInfoUpdateParams } from '@/ntqqapi/types' import { GroupFileInfo } from '@/ntqqapi/types'
interface Payload { interface Payload {
group_id: number | string group_id: number | string
@@ -20,7 +20,7 @@ export class GetGroupRootFiles extends BaseAction<Payload, Response> {
async _handle(payload: Payload) { async _handle(payload: Payload) {
const groupId = payload.group_id.toString() const groupId = payload.group_id.toString()
const data: OnGroupFileInfoUpdateParams['item'] = [] const data: GroupFileInfo['item'] = []
let nextIndex: number | undefined let nextIndex: number | undefined
while (nextIndex !== 0) { while (nextIndex !== 0) {

View File

@@ -28,7 +28,7 @@ export class GetGroupSystemMsg extends BaseAction<void, Response> {
actionName = ActionName.GoCQHTTP_GetGroupSystemMsg actionName = ActionName.GoCQHTTP_GetGroupSystemMsg
async _handle() { async _handle() {
const singleScreenNotifies = await this.ctx.ntGroupApi.getSingleScreenNotifies(10) const singleScreenNotifies = await this.ctx.ntGroupApi.getSingleScreenNotifies(false, 10)
const data: Response = { invited_requests: [], join_requests: [] } const data: Response = { invited_requests: [], join_requests: [] }
for (const notify of singleScreenNotifies) { for (const notify of singleScreenNotifies) {
if (notify.type == 1) { if (notify.type == 1) {
@@ -38,7 +38,7 @@ export class GetGroupSystemMsg extends BaseAction<void, Response> {
invitor_nick: notify.user1.nickName, invitor_nick: notify.user1.nickName,
group_id: +notify.group.groupCode, group_id: +notify.group.groupCode,
group_name: notify.group.groupName, group_name: notify.group.groupName,
checked: notify.status !== GroupNotifyStatus.KUNHANDLE, checked: notify.status !== GroupNotifyStatus.Unhandle,
actor: notify.user2?.uid ? Number(await this.ctx.ntUserApi.getUinByUid(notify.user2.uid)) : 0 actor: notify.user2?.uid ? Number(await this.ctx.ntUserApi.getUinByUid(notify.user2.uid)) : 0
}) })
} else if (notify.type == 7) { } else if (notify.type == 7) {
@@ -49,7 +49,7 @@ export class GetGroupSystemMsg extends BaseAction<void, Response> {
message: notify.postscript, message: notify.postscript,
group_id: +notify.group.groupCode, group_id: +notify.group.groupCode,
group_name: notify.group.groupName, group_name: notify.group.groupName,
checked: notify.status !== GroupNotifyStatus.KUNHANDLE, checked: notify.status !== GroupNotifyStatus.Unhandle,
actor: notify.user2?.uid ? Number(await this.ctx.ntUserApi.getUinByUid(notify.user2.uid)) : 0 actor: notify.user2?.uid ? Number(await this.ctx.ntUserApi.getUinByUid(notify.user2.uid)) : 0
}) })
} }

View File

@@ -3,59 +3,54 @@ import { OB11User } from '../../types'
import { OB11Entities } from '../../entities' import { OB11Entities } from '../../entities'
import { ActionName } from '../types' import { ActionName } from '../types'
import { getBuildVersion } from '@/common/utils' import { getBuildVersion } from '@/common/utils'
import { OB11UserSex } from '../../types'
import { calcQQLevel } from '@/common/utils/misc' import { calcQQLevel } from '@/common/utils/misc'
interface Payload { interface Payload {
user_id: number | string user_id: number | string
} }
export class GetStrangerInfo extends BaseAction<Payload, OB11User> { interface Response extends OB11User {
reg_time: number
long_nick: string
city: string
}
export class GetStrangerInfo extends BaseAction<Payload, Response> {
actionName = ActionName.GoCQHTTP_GetStrangerInfo actionName = ActionName.GoCQHTTP_GetStrangerInfo
payloadSchema = Schema.object({ payloadSchema = Schema.object({
user_id: Schema.union([Number, String]).required() user_id: Schema.union([Number, String]).required()
}) })
protected async _handle(payload: Payload): Promise<OB11User> { protected async _handle(payload: Payload) {
if (!(getBuildVersion() >= 26702)) { const uin = payload.user_id.toString()
const user_id = payload.user_id.toString() if (getBuildVersion() >= 26702) {
const extendData = await this.ctx.ntUserApi.getUserDetailInfoByUin(user_id) const data = await this.ctx.ntUserApi.getUserDetailInfoByUinV2(uin)
const uid = (await this.ctx.ntUserApi.getUidByUin(user_id))! return {
if (!uid || uid.indexOf('*') != -1) { user_id: parseInt(data.detail.uin) || 0,
const ret = { nickname: data.detail.simpleInfo.coreInfo.nick,
...extendData, sex: OB11Entities.sex(data.detail.simpleInfo.baseInfo.sex),
user_id: parseInt(extendData.info.uin) || 0, age: data.detail.simpleInfo.baseInfo.age,
nickname: extendData.info.nick, qid: data.detail.simpleInfo.baseInfo.qid,
sex: OB11UserSex.unknown, level: data.detail.commonExt.qqLevel && calcQQLevel(data.detail.commonExt.qqLevel) || 0,
age: (extendData.info.birthday_year == 0) ? 0 : new Date().getFullYear() - extendData.info.birthday_year, login_days: 0,
qid: extendData.info.qid, reg_time: data.detail.commonExt.regTime,
level: extendData.info.qqLevel && calcQQLevel(extendData.info.qqLevel) || 0, long_nick: data.detail.simpleInfo.baseInfo.longNick,
login_days: 0, city: data.detail.commonExt.city
uid: ''
}
return ret
} }
const data = { ...extendData, ...(await this.ctx.ntUserApi.getUserDetailInfo(uid)) }
return OB11Entities.stranger(data)
} else { } else {
const user_id = payload.user_id.toString() const data = await this.ctx.ntUserApi.getUserDetailInfoByUin(uin)
const extendData = await this.ctx.ntUserApi.getUserDetailInfoByUinV2(user_id) return {
const uid = (await this.ctx.ntUserApi.getUidByUin(user_id))! user_id: parseInt(data.info.uin) || 0,
if (!uid || uid.indexOf('*') != -1) { nickname: data.info.nick,
const ret = { sex: OB11Entities.sex(data.info.sex),
...extendData, age: data.info.birthday_year === 0 ? 0 : new Date().getFullYear() - data.info.birthday_year,
user_id: parseInt(extendData.detail.uin) || 0, qid: data.info.qid,
nickname: extendData.detail.simpleInfo.coreInfo.nick, level: data.info.qqLevel && calcQQLevel(data.info.qqLevel) || 0,
sex: OB11UserSex.unknown, login_days: 0,
age: 0, reg_time: data.info.regTime,
level: extendData.detail.commonExt.qqLevel && calcQQLevel(extendData.detail.commonExt.qqLevel) || 0, long_nick: data.info.longNick,
login_days: 0, city: data.info.city
uid: ''
}
return ret
} }
const data = { ...extendData, ...(await this.ctx.ntUserApi.getUserDetailInfo(uid)) }
return OB11Entities.stranger(data)
} }
} }
} }

View File

@@ -0,0 +1,63 @@
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
import { uri2local } from '@/common/utils/file'
import { access, unlink } from 'node:fs/promises'
interface Payload {
image: string
}
interface TextDetection {
text: string
confidence: number
coordinates: {
x: number //int32
y: number
}[]
}
interface Response {
texts: TextDetection[]
language: string
}
export class OCRImage extends BaseAction<Payload, Response> {
actionName = ActionName.GoCQHTTP_OCRImage
payloadSchema = Schema.object({
image: Schema.string().required()
})
protected async _handle(payload: Payload) {
const { errMsg, isLocal, path, success } = await uri2local(this.ctx, payload.image, true)
if (!success) {
throw new Error(errMsg)
}
await access(path)
const data = await this.ctx.ntFileApi.ocrImage(path)
if (!isLocal) {
unlink(path)
}
const texts = data.result.map(item => {
const ret: TextDetection = {
text: item.text,
confidence: 1,
coordinates: []
}
for (let i = 0; i < 4; i++) {
const pt = item[`pt${i + 1}`]
ret.coordinates.push({
x: parseInt(pt.x),
y: parseInt(pt.y)
})
}
return ret
})
return {
texts,
language: ''
}
}
}

View File

@@ -1,34 +1,42 @@
import { unlink } from 'node:fs/promises' import { unlink } from 'node:fs/promises'
import { OB11MessageNode } from '../../types' import { OB11MessageData, OB11MessageNode } from '../../types'
import { ActionName } from '../types' import { ActionName } from '../types'
import { BaseAction, Schema } from '../BaseAction' import { BaseAction, Schema } from '../BaseAction'
import { Peer } from '@/ntqqapi/types/msg' import { Peer } from '@/ntqqapi/types/msg'
import { ChatType, ElementType, RawMessage, SendMessageElement } from '@/ntqqapi/types' import { ChatType, ElementType, RawMessage, SendMessageElement } from '@/ntqqapi/types'
import { selfInfo } from '@/common/globalVars' import { selfInfo } from '@/common/globalVars'
import { convertMessage2List, createSendElements, sendMsg, createPeer, CreatePeerMode } from '../../helper/createMessage' import { message2List, createSendElements, sendMsg, createPeer, CreatePeerMode } from '../../helper/createMessage'
import { MessageEncoder } from '@/onebot11/helper/createMultiMessage'
import { Msg } from '@/ntqqapi/proto/compiled'
interface Payload { interface Payload {
user_id?: string | number user_id?: string | number
group_id?: string | number group_id?: string | number
messages: OB11MessageNode[] messages?: OB11MessageNode[]
message?: OB11MessageNode[]
message_type?: 'group' | 'private' message_type?: 'group' | 'private'
} }
interface Response { interface Response {
message_id: number message_id: number
forward_id?: string forward_id: string
} }
export class SendForwardMsg extends BaseAction<Payload, Response> { export class SendForwardMsg extends BaseAction<Payload, Response> {
actionName = ActionName.GoCQHTTP_SendForwardMsg actionName = ActionName.SendForwardMsg
payloadSchema = Schema.object({ payloadSchema = Schema.object({
user_id: Schema.union([Number, String]), user_id: Schema.union([Number, String]),
group_id: Schema.union([Number, String]), group_id: Schema.union([Number, String]),
messages: Schema.array(Schema.any()).required(), messages: Schema.array(Schema.any()),
message: Schema.array(Schema.any()),
message_type: Schema.union(['group', 'private']) message_type: Schema.union(['group', 'private'])
}) })
protected async _handle(payload: Payload) { protected async _handle(payload: Payload) {
const messages = payload.messages ?? payload.message
if (!messages) {
throw new Error('未指定消息内容')
}
let contextMode = CreatePeerMode.Normal let contextMode = CreatePeerMode.Normal
if (payload.message_type === 'group') { if (payload.message_type === 'group') {
contextMode = CreatePeerMode.Group contextMode = CreatePeerMode.Group
@@ -36,9 +44,90 @@ export class SendForwardMsg extends BaseAction<Payload, Response> {
contextMode = CreatePeerMode.Private contextMode = CreatePeerMode.Private
} }
const peer = await createPeer(this.ctx, payload, contextMode) const peer = await createPeer(this.ctx, payload, contextMode)
const msg = await this.handleForwardNode(peer, payload.messages)
const msgShortId = this.ctx.store.createMsgShortId({ chatType: msg.chatType, peerUid: msg.peerUid }, msg.msgId) const nodes = this.parseNodeContent(messages)
return { message_id: msgShortId } let fake = true
for (const node of nodes) {
if (node.data.id) {
fake = false
break
}
if (node.data.content?.some(e => {
return !MessageEncoder.support.includes(e.type)
})) {
fake = false
break
}
}
if (fake && this.ctx.app.native.activated) {
return await this.handleFakeForwardNode(peer, nodes)
} else {
return await this.handleForwardNode(peer, nodes)
}
}
private parseNodeContent(nodes: OB11MessageNode[]) {
return nodes.map(e => {
return {
type: e.type,
data: {
...e.data,
content: e.data.content ? message2List(e.data.content) : undefined
}
}
})
}
private async handleFakeForwardNode(peer: Peer, nodes: OB11MessageNode[]): Promise<Response> {
const encoder = new MessageEncoder(this.ctx, peer)
const raw = await encoder.generate(nodes)
const transmit = Msg.PbMultiMsgTransmit.encode({ pbItemList: raw.multiMsgItems }).finish()
const resid = await this.ctx.app.native.uploadForward(peer, transmit.subarray(1))
const uuid = crypto.randomUUID()
try {
const msg = await this.ctx.ntMsgApi.sendMsg(peer, [{
elementType: 10,
elementId: '',
arkElement: {
bytesData: JSON.stringify({
app: 'com.tencent.multimsg',
config: {
autosize: 1,
forward: 1,
round: 1,
type: 'normal',
width: 300
},
desc: '[聊天记录]',
extra: JSON.stringify({
filename: uuid,
tsum: raw.tsum,
}),
meta: {
detail: {
news: raw.news,
resid,
source: raw.source,
summary: raw.summary,
uniseq: uuid,
}
},
prompt: '[聊天记录]',
ver: '0.0.0.5',
view: 'contact'
})
}
}], 1800)
const msgShortId = this.ctx.store.createMsgShortId({
chatType: msg!.chatType,
peerUid: msg!.peerUid
}, msg!.msgId)
return { message_id: msgShortId, forward_id: resid }
} catch (e) {
this.ctx.logger.error('合并转发失败', e)
throw new Error(`发送伪造合并转发消息失败 (res_id: ${resid} `)
}
} }
private async cloneMsg(msg: RawMessage): Promise<RawMessage | undefined> { private async cloneMsg(msg: RawMessage): Promise<RawMessage | undefined> {
@@ -65,7 +154,7 @@ export class SendForwardMsg extends BaseAction<Payload, Response> {
} }
// 返回一个合并转发的消息id // 返回一个合并转发的消息id
private async handleForwardNode(destPeer: Peer, messageNodes: OB11MessageNode[]) { private async handleForwardNode(destPeer: Peer, messageNodes: OB11MessageNode[]): Promise<Response> {
const selfPeer = { const selfPeer = {
chatType: ChatType.C2C, chatType: ChatType.C2C,
peerUid: selfInfo.uid, peerUid: selfInfo.uid,
@@ -90,7 +179,7 @@ export class SendForwardMsg extends BaseAction<Payload, Response> {
try { try {
const { sendElements, deleteAfterSentFiles } = await createSendElements( const { sendElements, deleteAfterSentFiles } = await createSendElements(
this.ctx, this.ctx,
convertMessage2List(messageNode.data.content), messageNode.data.content as OB11MessageData[],
destPeer destPeer
) )
this.ctx.logger.info('开始生成转发节点', sendElements) this.ctx.logger.info('开始生成转发节点', sendElements)
@@ -157,8 +246,13 @@ export class SendForwardMsg extends BaseAction<Payload, Response> {
if (retMsgIds.length === 0) { if (retMsgIds.length === 0) {
throw Error('转发消息失败,节点为空') throw Error('转发消息失败,节点为空')
} }
const returnMsg = await this.ctx.ntMsgApi.multiForwardMsg(srcPeer!, destPeer, retMsgIds) const msg = await this.ctx.ntMsgApi.multiForwardMsg(srcPeer!, destPeer, retMsgIds)
return returnMsg const resid = JSON.parse(msg.elements[0].arkElement!.bytesData).meta.detail.resid
const msgShortId = this.ctx.store.createMsgShortId({
chatType: msg.chatType,
peerUid: msg.peerUid
}, msg.msgId)
return { message_id: msgShortId, forward_id: resid }
} }
} }

View File

@@ -28,7 +28,7 @@ export class SendGroupNotice extends BaseAction<Payload, null> {
let picInfo: { id: string, width: number, height: number } | undefined let picInfo: { id: string, width: number, height: number } | undefined
if (payload.image) { if (payload.image) {
const { path, isLocal, success, errMsg } = await uri2local(payload.image, undefined, true) const { path, isLocal, success, errMsg } = await uri2local(this.ctx, payload.image, true)
if (!success) { if (!success) {
throw new Error(`设置群公告失败, 错误信息: uri2local: ${errMsg}`) throw new Error(`设置群公告失败, 错误信息: uri2local: ${errMsg}`)
} }

View File

@@ -0,0 +1,19 @@
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
import { selfInfo } from '@/common/globalVars'
interface Payload {
group_id: number | string
}
export class SendGroupSign extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_SendGroupSign
payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required(),
})
async _handle(payload: Payload) {
await this.ctx.app.packet.sendGroupSignPacket(selfInfo.uin, payload.group_id.toString())
return null
}
}

View File

@@ -0,0 +1,30 @@
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
import { selfInfo } from '@/common/globalVars'
import { GroupMemberRole } from '@/ntqqapi/types'
interface Payload {
group_id: number | string
user_id: number | string
special_title?: string
}
export class SetGroupSpecialTitle extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_SetGroupSpecialTitle
payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required(),
user_id: Schema.union([Number, String]).required(),
special_title: Schema.string()
})
async _handle(payload: Payload) {
const uid = await this.ctx.ntUserApi.getUidByUin(payload.user_id.toString(), payload.group_id.toString())
if (!uid) throw new Error(`用户${payload.user_id}的uid获取失败`)
const self = await this.ctx.ntGroupApi.getGroupMember(payload.group_id.toString(), selfInfo.uid, false)
if (self.role !== GroupMemberRole.Owner){
throw new Error(`不是群${payload.group_id}的群主,无法设置群头衔`)
}
await this.ctx.app.packet.sendSetSpecialTittlePacket(payload.group_id.toString(), uid, payload.special_title || "")
return null
}
}

View File

@@ -1,6 +1,6 @@
import { BaseAction, Schema } from '../BaseAction' import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { SendElementEntities } from '@/ntqqapi/entities' import { SendElement } from '@/ntqqapi/entities'
import { uri2local } from '@/common/utils' import { uri2local } from '@/common/utils'
import { sendMsg, createPeer, CreatePeerMode } from '../../helper/createMessage' import { sendMsg, createPeer, CreatePeerMode } from '../../helper/createMessage'
@@ -23,11 +23,15 @@ export class UploadGroupFile extends BaseAction<Payload, null> {
}) })
protected async _handle(payload: Payload): Promise<null> { protected async _handle(payload: Payload): Promise<null> {
const { success, errMsg, path, fileName } = await uri2local(payload.file) const { success, errMsg, path, fileName } = await uri2local(this.ctx, payload.file)
if (!success) { if (!success) {
throw new Error(errMsg) throw new Error(errMsg)
} }
const file = await SendElementEntities.file(this.ctx, path, payload.name || fileName, payload.folder ?? payload.folder_id) const name = payload.name || fileName
if (name.includes('/') || name.includes('\\')) {
throw new Error(`文件名 ${name} 不合法`)
}
const file = await SendElement.file(this.ctx, path, name, payload.folder ?? payload.folder_id)
const peer = await createPeer(this.ctx, payload, CreatePeerMode.Group) const peer = await createPeer(this.ctx, payload, CreatePeerMode.Group)
await sendMsg(this.ctx, peer, [file], []) await sendMsg(this.ctx, peer, [file], [])
return null return null

View File

@@ -1,6 +1,6 @@
import { BaseAction, Schema } from '../BaseAction' import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { SendElementEntities } from '@/ntqqapi/entities' import { SendElement } from '@/ntqqapi/entities'
import { uri2local } from '@/common/utils' import { uri2local } from '@/common/utils'
import { sendMsg, createPeer, CreatePeerMode } from '../../helper/createMessage' import { sendMsg, createPeer, CreatePeerMode } from '../../helper/createMessage'
@@ -19,11 +19,15 @@ export class UploadPrivateFile extends BaseAction<UploadPrivateFilePayload, null
}) })
protected async _handle(payload: UploadPrivateFilePayload): Promise<null> { protected async _handle(payload: UploadPrivateFilePayload): Promise<null> {
const { success, errMsg, path, fileName } = await uri2local(payload.file) const { success, errMsg, path, fileName } = await uri2local(this.ctx, payload.file)
if (!success) { if (!success) {
throw new Error(errMsg) throw new Error(errMsg)
} }
const sendFileEle = await SendElementEntities.file(this.ctx, path, payload.name || fileName) const name = payload.name || fileName
if (name.includes('/') || name.includes('\\')) {
throw new Error(`文件名 ${name} 不合法`)
}
const sendFileEle = await SendElement.file(this.ctx, path, name)
const peer = await createPeer(this.ctx, payload, CreatePeerMode.Private) const peer = await createPeer(this.ctx, payload, CreatePeerMode.Private)
await sendMsg(this.ctx, peer, [sendFileEle], []) await sendMsg(this.ctx, peer, [sendFileEle], [])
return null return null

View File

@@ -2,32 +2,36 @@ import { BaseAction, Schema } from '../BaseAction'
import { OB11GroupMember } from '../../types' import { OB11GroupMember } from '../../types'
import { OB11Entities } from '../../entities' import { OB11Entities } from '../../entities'
import { ActionName } from '../types' import { ActionName } from '../types'
import { isNullable } from 'cosmokit' import { calcQQLevel, parseBool } from '@/common/utils/misc'
interface Payload { interface Payload {
group_id: number | string group_id: number | string
user_id: number | string user_id: number | string
no_cache: boolean
} }
class GetGroupMemberInfo extends BaseAction<Payload, OB11GroupMember> { class GetGroupMemberInfo extends BaseAction<Payload, OB11GroupMember> {
actionName = ActionName.GetGroupMemberInfo actionName = ActionName.GetGroupMemberInfo
payloadSchema = Schema.object({ payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required(), group_id: Schema.union([Number, String]).required(),
user_id: Schema.union([Number, String]).required() user_id: Schema.union([Number, String]).required(),
no_cache: Schema.union([Boolean, Schema.transform(String, parseBool)]).default(false)
}) })
protected async _handle(payload: Payload) { protected async _handle(payload: Payload) {
const groupCode = payload.group_id.toString() const groupCode = payload.group_id.toString()
const member = await this.ctx.ntGroupApi.getGroupMember(groupCode, payload.user_id.toString()) const uid = await this.ctx.ntUserApi.getUidByUin(payload.user_id.toString(), groupCode)
if (!uid) throw new Error('无法获取用户信息')
const member = await this.ctx.ntGroupApi.getGroupMember(groupCode, uid, payload.no_cache)
if (member) { if (member) {
if (isNullable(member.sex)) { const ret = OB11Entities.groupMember(+groupCode, member)
const info = await this.ctx.ntUserApi.getUserDetailInfo(member.uid) const date = Math.trunc(Date.now() / 1000)
Object.assign(member, info)
}
const ret = OB11Entities.groupMember(groupCode, member)
const date = Math.round(Date.now() / 1000)
ret.last_sent_time ??= date ret.last_sent_time ??= date
ret.join_time ??= date ret.join_time ??= date
const info = await this.ctx.ntUserApi.getUserDetailInfo(member.uid)
ret.sex = OB11Entities.sex(info.sex!)
ret.qq_level = info.qqLevel && calcQQLevel(info.qqLevel) || 0
ret.age = info.age ?? 0
return ret return ret
} }
throw new Error(`群成员${payload.user_id}不存在`) throw new Error(`群成员${payload.user_id}不存在`)

View File

@@ -17,19 +17,26 @@ class GetGroupMemberList extends BaseAction<Payload, OB11GroupMember[]> {
protected async _handle(payload: Payload) { protected async _handle(payload: Payload) {
const groupCode = payload.group_id.toString() const groupCode = payload.group_id.toString()
let groupMembers = await this.ctx.ntGroupApi.getGroupMembers(groupCode) let groupMembers = await this.ctx.ntGroupApi.getGroupMembers(groupCode)
if (groupMembers.size === 0) { for (let i = 0; i < 5; i++) {
await this.ctx.sleep(100) if (groupMembers.size > 0) {
break
}
await this.ctx.sleep(60)
groupMembers = await this.ctx.ntGroupApi.getGroupMembers(groupCode) groupMembers = await this.ctx.ntGroupApi.getGroupMembers(groupCode)
} }
const groupMembersArr = Array.from(groupMembers.values())
const date = Math.round(Date.now() / 1000)
return groupMembersArr.map(item => { const date = Math.trunc(Date.now() / 1000)
const member = OB11Entities.groupMember(groupCode, item) const groupId = Number(payload.group_id)
const ret: OB11GroupMember[] = []
for (const item of groupMembers.values()) {
const member = OB11Entities.groupMember(groupId, item)
member.join_time ??= date member.join_time ??= date
member.last_sent_time ??= date member.last_sent_time ??= date
return member ret.push(member)
}) }
return ret
} }
} }

View File

@@ -25,7 +25,7 @@ export default class SetGroupAdmin extends BaseAction<Payload, null> {
await this.ctx.ntGroupApi.setMemberRole( await this.ctx.ntGroupApi.setMemberRole(
groupCode, groupCode,
uid, uid,
payload.enable ? GroupMemberRole.admin : GroupMemberRole.normal payload.enable ? GroupMemberRole.Admin : GroupMemberRole.Normal
) )
return null return null
} }

View File

@@ -4,7 +4,7 @@ import { ActionName } from '../types'
interface Payload { interface Payload {
group_id: number | string group_id: number | string
user_id: number | string user_id: number | string
duration: number duration: number | string
} }
export default class SetGroupBan extends BaseAction<Payload, null> { export default class SetGroupBan extends BaseAction<Payload, null> {
@@ -12,7 +12,7 @@ export default class SetGroupBan extends BaseAction<Payload, null> {
payloadSchema = Schema.object({ payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required(), group_id: Schema.union([Number, String]).required(),
user_id: Schema.union([Number, String]).required(), user_id: Schema.union([Number, String]).required(),
duration: Schema.number().default(30 * 60) duration: Schema.union([Number, String]).default(30 * 60)
}) })
protected async _handle(payload: Payload): Promise<null> { protected async _handle(payload: Payload): Promise<null> {
@@ -21,7 +21,7 @@ export default class SetGroupBan extends BaseAction<Payload, null> {
const uid = await this.ctx.ntUserApi.getUidByUin(uin, groupCode) const uid = await this.ctx.ntUserApi.getUidByUin(uin, groupCode)
if (!uid) throw new Error('无法获取用户信息') if (!uid) throw new Error('无法获取用户信息')
await this.ctx.ntGroupApi.banMember(groupCode, [ await this.ctx.ntGroupApi.banMember(groupCode, [
{ uid, timeStamp: payload.duration }, { uid, timeStamp: +payload.duration },
]) ])
return null return null
} }

View File

@@ -45,7 +45,7 @@ import { GetGroupMsgHistory } from './go-cqhttp/GetGroupMsgHistory'
import GetFile from './file/GetFile' import GetFile from './file/GetFile'
import { GetForwardMsg } from './go-cqhttp/GetForwardMsg' import { GetForwardMsg } from './go-cqhttp/GetForwardMsg'
import { GetCookies } from './user/GetCookie' import { GetCookies } from './user/GetCookie'
import { SetMsgEmojiLike } from './msg/SetMsgEmojiLike' import { SetMsgEmojiLike } from './llonebot/SetMsgEmojiLike'
import { ForwardFriendSingleMsg, ForwardGroupSingleMsg } from './msg/ForwardSingleMsg' import { ForwardFriendSingleMsg, ForwardGroupSingleMsg } from './msg/ForwardSingleMsg'
import { GetEssenceMsgList } from './go-cqhttp/GetGroupEssence' import { GetEssenceMsgList } from './go-cqhttp/GetGroupEssence'
import { GetGroupHonorInfo } from './group/GetGroupHonorInfo' import { GetGroupHonorInfo } from './group/GetGroupHonorInfo'
@@ -70,6 +70,21 @@ import { GetFriendWithCategory } from './llonebot/GetFriendWithCategory'
import { UploadGroupFile } from './go-cqhttp/UploadGroupFile' import { UploadGroupFile } from './go-cqhttp/UploadGroupFile'
import { UploadPrivateFile } from './go-cqhttp/UploadPrivateFile' import { UploadPrivateFile } from './go-cqhttp/UploadPrivateFile'
import { GetGroupFileUrl } from './go-cqhttp/GetGroupFileUrl' import { GetGroupFileUrl } from './go-cqhttp/GetGroupFileUrl'
import { GetGroupNotice } from './go-cqhttp/GetGroupNotice'
import { GetRobotUinRange } from './llonebot/GetRobotUinRange'
import { DeleteFriend } from './go-cqhttp/DeleteFriend'
import { OCRImage } from './go-cqhttp/OCRImage'
import { GroupPoke } from './llonebot/GroupPoke'
import { FriendPoke } from './llonebot/FriendPoke'
import { GetGroupFileSystemInfo } from './go-cqhttp/GetGroupFileSystemInfo'
import { GetCredentials } from './system/GetCredentials'
import { SetGroupSpecialTitle } from '@/onebot11/action/go-cqhttp/SetGroupSpecialTitle'
import { SendGroupSign } from '@/onebot11/action/go-cqhttp/SendGroupSign'
import { SetRestart } from '@/onebot11/action/system/SetRestart'
import { SetFriendCategory } from '@/onebot11/action/llonebot/SetFriendCategory'
import { SetFriendRemark } from '@/onebot11/action/llonebot/SetFriendRemark'
import { SetGroupMsgMask } from '@/onebot11/action/llonebot/SetGroupMsgMask'
import { SetGroupRemark } from '@/onebot11/action/llonebot/SetGroupRemark'
export function initActionMap(adapter: Adapter) { export function initActionMap(adapter: Adapter) {
const actionHandlers = [ const actionHandlers = [
@@ -86,6 +101,14 @@ export function initActionMap(adapter: Adapter) {
new GetFriendMsgHistory(adapter), new GetFriendMsgHistory(adapter),
new FetchEmojiLike(adapter), new FetchEmojiLike(adapter),
new FetchCustomFace(adapter), new FetchCustomFace(adapter),
new SetMsgEmojiLike(adapter),
new GetRobotUinRange(adapter),
new GroupPoke(adapter),
new FriendPoke(adapter),
new SetFriendCategory(adapter),
new SetFriendRemark(adapter),
new SetGroupMsgMask(adapter),
new SetGroupRemark(adapter),
// onebot11 // onebot11
new SendLike(adapter), new SendLike(adapter),
new GetMsg(adapter), new GetMsg(adapter),
@@ -116,9 +139,10 @@ export function initActionMap(adapter: Adapter) {
new GetRecord(adapter), new GetRecord(adapter),
new CleanCache(adapter), new CleanCache(adapter),
new GetCookies(adapter), new GetCookies(adapter),
new SetMsgEmojiLike(adapter),
new ForwardFriendSingleMsg(adapter), new ForwardFriendSingleMsg(adapter),
new ForwardGroupSingleMsg(adapter), new ForwardGroupSingleMsg(adapter),
new GetCredentials(adapter),
new SetRestart(adapter),
// go-cqhttp // go-cqhttp
new GetEssenceMsgList(adapter), new GetEssenceMsgList(adapter),
new GetGroupHonorInfo(adapter), new GetGroupHonorInfo(adapter),
@@ -144,9 +168,15 @@ export function initActionMap(adapter: Adapter) {
new GetGroupRootFiles(adapter), new GetGroupRootFiles(adapter),
new SendGroupNotice(adapter), new SendGroupNotice(adapter),
new GetGroupFilesByFolder(adapter), new GetGroupFilesByFolder(adapter),
new GetGroupFileUrl(adapter) new GetGroupFileUrl(adapter),
new GetGroupNotice(adapter),
new DeleteFriend(adapter),
new OCRImage(adapter),
new GetGroupFileSystemInfo(adapter),
new SetGroupSpecialTitle(adapter),
new SendGroupSign(adapter),
] ]
const actionMap = new Map<string, BaseAction<any, unknown>>() const actionMap = new Map()
for (const action of actionHandlers) { for (const action of actionHandlers) {
actionMap.set(action.actionName, action) actionMap.set(action.actionName, action)
actionMap.set(action.actionName + '_async', action) actionMap.set(action.actionName + '_async', action)

View File

@@ -16,10 +16,8 @@ export default class Debug extends BaseAction<Payload, unknown> {
for (const ntqqApiClass of ntqqApi) { for (const ntqqApiClass of ntqqApi) {
const method = ntqqApiClass[payload.method as keyof typeof ntqqApiClass] const method = ntqqApiClass[payload.method as keyof typeof ntqqApiClass]
if (method && method instanceof Function) { if (method && method instanceof Function) {
const result = method.apply(ntqqApiClass, payload.args) const result = await method.apply(ntqqApiClass, payload.args)
if (method.constructor.name === 'AsyncFunction') { this.ctx.logger.info('debug', result)
return await result
}
return result return result
} }
} }

View File

@@ -0,0 +1,24 @@
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
import { getBuildVersion } from '@/common/utils'
interface Payload {
user_id: number | string
}
export class FriendPoke extends BaseAction<Payload, null> {
actionName = ActionName.FriendPoke
payloadSchema = Schema.object({
user_id: Schema.union([Number, String]).required()
})
async _handle(payload: Payload) {
if (!this.ctx.app.native.checkPlatform() || !this.ctx.app.native.checkVersion()) {
await this.ctx.app.packet.sendPokePacket(+payload.user_id)
}
else{
await this.ctx.app.native.sendFriendPoke(+payload.user_id)
}
return null
}
}

View File

@@ -4,14 +4,35 @@ import { OB11Entities } from '../../entities'
import { ActionName } from '../types' import { ActionName } from '../types'
import { getBuildVersion } from '@/common/utils' import { getBuildVersion } from '@/common/utils'
export class GetFriendWithCategory extends BaseAction<void, OB11User[]> { interface Category {
categoryId: number
categorySortId: number
categoryName: string
categoryMbCount: number
onlineCount: number
buddyList: OB11User[]
}
export class GetFriendWithCategory extends BaseAction<void, Category[]> {
actionName = ActionName.GetFriendsWithCategory actionName = ActionName.GetFriendsWithCategory
protected async _handle() { protected async _handle() {
if (getBuildVersion() >= 26702) { if (getBuildVersion() < 26702) {
return OB11Entities.friendsV2(await this.ctx.ntFriendApi.getBuddyV2ExWithCate(true))
} else {
throw new Error('this ntqq version not support, must be 26702 or later') throw new Error('this ntqq version not support, must be 26702 or later')
} }
const data = await this.ctx.ntFriendApi.getBuddyV2WithCate(true)
return data.buddyCategory.map(item => {
return {
categoryId: item.categoryId,
categorySortId: item.categorySortId,
categoryName: item.categroyName,
categoryMbCount: item.categroyMbCount,
onlineCount: item.onlineCount,
buddyList: item.buddyUids.map(uid => {
const info = data.userSimpleInfos[uid]
return OB11Entities.friendV2(info)
})
}
})
} }
} }

View File

@@ -13,7 +13,7 @@ export default class GetGroupAddRequest extends BaseAction<null, OB11GroupReques
protected async _handle(): Promise<OB11GroupRequestNotify[]> { protected async _handle(): Promise<OB11GroupRequestNotify[]> {
const data = await this.ctx.ntGroupApi.getGroupIgnoreNotifies() const data = await this.ctx.ntGroupApi.getGroupIgnoreNotifies()
const notifies = data.notifies.filter(notify => notify.status === GroupNotifyStatus.KUNHANDLE) const notifies = data.notifies.filter(notify => notify.status === GroupNotifyStatus.Unhandle)
const returnData: OB11GroupRequestNotify[] = [] const returnData: OB11GroupRequestNotify[] = []
for (const notify of notifies) { for (const notify of notifies) {
const uin = await this.ctx.ntUserApi.getUinByUid(notify.user1.uid) const uin = await this.ctx.ntUserApi.getUinByUid(notify.user1.uid)

View File

@@ -0,0 +1,11 @@
import { BaseAction } from '../BaseAction'
import { ActionName } from '../types'
import { Dict } from 'cosmokit'
export class GetRobotUinRange extends BaseAction<void, Dict[]> {
actionName = ActionName.GetRobotUinRange
async _handle() {
return await this.ctx.ntUserApi.getRobotUinRange()
}
}

View File

@@ -0,0 +1,26 @@
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
import { getBuildVersion } from '@/common/utils'
interface Payload {
group_id: number | string
user_id: number | string
}
export class GroupPoke extends BaseAction<Payload, null> {
actionName = ActionName.GroupPoke
payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required(),
user_id: Schema.union([Number, String]).required()
})
async _handle(payload: Payload) {
if (!this.ctx.app.native.checkPlatform() || !this.ctx.app.native.checkVersion()) {
await this.ctx.app.packet.sendPokePacket(+payload.user_id, +payload.group_id)
}
else{
await this.ctx.app.native.sendGroupPoke(+payload.group_id, +payload.user_id)
}
return null
}
}

View File

@@ -0,0 +1,21 @@
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
interface Payload {
user_id: number | string
category_id: number | string
}
export class SetFriendCategory extends BaseAction<Payload, unknown> {
actionName = ActionName.SetFriendCategory
payloadSchema = Schema.object({
user_id: Schema.union([Number, String]).required(),
category_id: Schema.union([Number, String]).required()
})
protected async _handle(payload: Payload) {
const uid = await this.ctx.ntUserApi.getUidByUin(payload.user_id.toString())
if (!uid) throw new Error('无法获取好友信息')
return this.ctx.ntFriendApi.setBuddyCategory(uid, +payload.category_id)
}
}

View File

@@ -0,0 +1,21 @@
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
interface Payload {
user_id: number | string
remark?: string
}
export class SetFriendRemark extends BaseAction<Payload, unknown> {
actionName = ActionName.SetFriendRemark
payloadSchema = Schema.object({
user_id: Schema.union([Number, String]).required(),
remark: Schema.string()
})
protected async _handle(payload: Payload) {
const uid = await this.ctx.ntUserApi.getUidByUin(payload.user_id.toString())
if (!uid) throw new Error('无法获取好友信息')
return this.ctx.ntFriendApi.setBuddyRemark(uid, payload.remark || '')
}
}

View File

@@ -0,0 +1,19 @@
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
interface Payload {
group_id: number | string
mask: number | string // 1, 2, 3, 4
}
export class SetGroupMsgMask extends BaseAction<Payload, unknown> {
actionName = ActionName.SetGroupMsgMask
payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required(),
mask: Schema.union([Number, String]).required()
})
protected async _handle(payload: Payload) {
return this.ctx.ntGroupApi.setGroupMsgMask(payload.group_id.toString(), +payload.mask)
}
}

View File

@@ -0,0 +1,15 @@
import { BaseAction } from '../BaseAction'
import { ActionName } from '../types'
interface Payload {
group_id: string,
remark?: string
}
export class SetGroupRemark extends BaseAction<Payload, unknown> {
actionName = ActionName.SetGroupRemark
protected async _handle(payload: Payload): Promise<unknown>{
return this.ctx.ntGroupApi.setGroupRemark(payload.group_id.toString(), payload.remark)
}
}

View File

@@ -11,13 +11,13 @@ export default class SetAvatar extends BaseAction<Payload, null> {
actionName = ActionName.SetQQAvatar actionName = ActionName.SetQQAvatar
protected async _handle(payload: Payload): Promise<null> { protected async _handle(payload: Payload): Promise<null> {
const { path, isLocal, errMsg } = await uri2local(payload.file) const { path, isLocal, errMsg } = await uri2local(this.ctx, payload.file)
if (errMsg) { if (errMsg) {
throw new Error(errMsg) throw new Error(errMsg)
} }
if (path) { if (path) {
await checkFileReceived(path, 5000) // 文件不存在QQ会崩溃需要提前判断 await checkFileReceived(path, 5000) // 文件不存在QQ会崩溃需要提前判断
const ret = await this.ctx.ntUserApi.setQQAvatar(path) const ret = await this.ctx.ntUserApi.setSelfAvatar(path)
if (!isLocal) { if (!isLocal) {
unlink(path) unlink(path)
} }

View File

@@ -19,8 +19,8 @@ abstract class ForwardSingleMsg extends BaseAction<Payload, null> {
} }
const peer = await createPeer(this.ctx, payload) const peer = await createPeer(this.ctx, payload)
const ret = await this.ctx.ntMsgApi.forwardMsg(msg.peer, peer, [msg.msgId]) const ret = await this.ctx.ntMsgApi.forwardMsg(msg.peer, peer, [msg.msgId])
if (ret.result !== 0) { if (ret.length === 0) {
throw new Error(`转发消息失败 ${ret.errMsg}`) throw new Error(`转发消息失败`)
} }
return null return null
} }

View File

@@ -25,14 +25,11 @@ class GetMsg extends BaseAction<PayloadType, OB11Message> {
peerUid: msgInfo.peer.peerUid, peerUid: msgInfo.peer.peerUid,
chatType: msgInfo.peer.chatType chatType: msgInfo.peer.chatType
} }
const msg = this.adapter.getMsgCache(msgInfo.msgId) ?? (await this.ctx.ntMsgApi.getMsgsByMsgId(peer, [msgInfo.msgId])).msgList[0] const msg = this.ctx.store.getMsgCache(msgInfo.msgId) ?? (await this.ctx.ntMsgApi.getMsgsByMsgId(peer, [msgInfo.msgId])).msgList[0]
const retMsg = await OB11Entities.message(this.ctx, msg) const retMsg = await OB11Entities.message(this.ctx, msg)
if (!retMsg) { if (!retMsg) {
throw new Error('消息为空') throw new Error('消息为空')
} }
retMsg.message_id = this.ctx.store.createMsgShortId(peer, msg.msgId)
retMsg.message_seq = retMsg.message_id
retMsg.real_id = retMsg.message_id
return retMsg return retMsg
} }
} }

View File

@@ -9,7 +9,7 @@ import {
import { BaseAction } from '../BaseAction' import { BaseAction } from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { CustomMusicSignPostData, IdMusicSignPostData, MusicSign, MusicSignPostData } from '@/common/utils/sign' import { CustomMusicSignPostData, IdMusicSignPostData, MusicSign, MusicSignPostData } from '@/common/utils/sign'
import { convertMessage2List, createSendElements, sendMsg, createPeer, CreatePeerMode } from '../../helper/createMessage' import { message2List, createSendElements, sendMsg, createPeer, CreatePeerMode } from '../../helper/createMessage'
interface ReturnData { interface ReturnData {
message_id: number message_id: number
@@ -26,14 +26,14 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnData> {
contextMode = CreatePeerMode.Private contextMode = CreatePeerMode.Private
} }
const peer = await createPeer(this.ctx, payload, contextMode) const peer = await createPeer(this.ctx, payload, contextMode)
const messages = convertMessage2List( const messages = message2List(
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)) {
throw new Error('请使用 /send_group_forward_msg 或 /send_private_forward_msg 进行合并转发') throw new Error('请使用 /send_group_forward_msg 或 /send_private_forward_msg 进行合并转发')
} }
else if (this.getSpecialMsgNum(messages, OB11MessageDataType.music)) { else if (this.getSpecialMsgNum(messages, OB11MessageDataType.Music)) {
const music = messages[0] as OB11MessageMusic const music = messages[0] as OB11MessageMusic
if (music) { if (music) {
const { musicSignUrl } = this.adapter.config const { musicSignUrl } = this.adapter.config
@@ -78,7 +78,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnData> {
throw `签名音乐消息失败:${e}` throw `签名音乐消息失败:${e}`
} }
messages[0] = { messages[0] = {
type: OB11MessageDataType.json, type: OB11MessageDataType.Json,
data: { data: jsonContent }, data: { data: jsonContent },
} as OB11MessageJson } as OB11MessageJson
} }

View File

@@ -7,62 +7,52 @@ import { ChatCacheListItemBasic, CacheFileType } from '@/ntqqapi/types'
export default class CleanCache extends BaseAction<void, void> { export default class CleanCache extends BaseAction<void, void> {
actionName = ActionName.CleanCache actionName = ActionName.CleanCache
protected _handle(): Promise<void> { protected async _handle(): Promise<void> {
return new Promise<void>(async (res, rej) => { const cacheFilePaths: string[] = []
try {
// dbUtil.clearCache()
const cacheFilePaths: string[] = []
await this.ctx.ntFileCacheApi.setCacheSilentScan(false) await this.ctx.ntFileCacheApi.setCacheSilentScan(false)
cacheFilePaths.push(await this.ctx.ntFileCacheApi.getHotUpdateCachePath()) cacheFilePaths.push(await this.ctx.ntFileCacheApi.getHotUpdateCachePath())
cacheFilePaths.push(await this.ctx.ntFileCacheApi.getDesktopTmpPath()) cacheFilePaths.push(await this.ctx.ntFileCacheApi.getDesktopTmpPath())
const list = await this.ctx.ntFileCacheApi.getCacheSessionPathList() const list = await this.ctx.ntFileCacheApi.getCacheSessionPathList()
list.forEach((e) => cacheFilePaths.push(e.value)) list.forEach((e) => cacheFilePaths.push(e.value))
// await NTQQApi.addCacheScannedPaths(); // XXX: 调用就崩溃,原因目前还未知 // await NTQQApi.addCacheScannedPaths(); // XXX: 调用就崩溃,原因目前还未知
const cacheScanResult = await this.ctx.ntFileCacheApi.scanCache() const cacheScanResult = await this.ctx.ntFileCacheApi.scanCache()
const cacheSize = parseInt(cacheScanResult.size[6]) const cacheSize = parseInt(cacheScanResult.size[6])
if (cacheScanResult.result !== 0) { if (cacheScanResult.result !== 0) {
throw 'Something went wrong while scanning cache. Code: ' + cacheScanResult.result throw 'Something went wrong while scanning cache. Code: ' + cacheScanResult.result
} }
await this.ctx.ntFileCacheApi.setCacheSilentScan(true) await this.ctx.ntFileCacheApi.setCacheSilentScan(true)
if (cacheSize > 0 && cacheFilePaths.length > 2) { if (cacheSize > 0 && cacheFilePaths.length > 2) {
// 存在缓存文件且大小不为 0 时执行清理动作 // 存在缓存文件且大小不为 0 时执行清理动作
// await NTQQApi.clearCache([ 'tmp', 'hotUpdate', ...cacheScanResult ]) // XXX: 也是调用就崩溃,调用 fs 删除得了 // await NTQQApi.clearCache([ 'tmp', 'hotUpdate', ...cacheScanResult ]) // XXX: 也是调用就崩溃,调用 fs 删除得了
deleteCachePath(cacheFilePaths) deleteCachePath(cacheFilePaths)
} }
// 获取聊天记录列表 // 获取聊天记录列表
// NOTE: 以防有人不需要删除聊天记录,暂时先注释掉,日后加个开关 // NOTE: 以防有人不需要删除聊天记录,暂时先注释掉,日后加个开关
// const privateChatCache = await getCacheList(ChatType.friend); // 私聊消息 // const privateChatCache = await getCacheList(ChatType.friend); // 私聊消息
// const groupChatCache = await getCacheList(ChatType.group); // 群聊消息 // const groupChatCache = await getCacheList(ChatType.group); // 群聊消息
// const chatCacheList = [ ...privateChatCache, ...groupChatCache ]; // const chatCacheList = [ ...privateChatCache, ...groupChatCache ];
const chatCacheList: ChatCacheListItemBasic[] = [] const chatCacheList: ChatCacheListItemBasic[] = []
// 获取聊天缓存文件列表 // 获取聊天缓存文件列表
const cacheFileList: string[] = [] const cacheFileList: string[] = []
for (const name in CacheFileType) { for (const name in CacheFileType) {
if (!isNaN(parseInt(name))) continue if (!isNaN(parseInt(name))) continue
const fileTypeAny: any = CacheFileType[name] const fileType = CacheFileType[name] as unknown as CacheFileType
const fileType: CacheFileType = fileTypeAny
cacheFileList.push(...(await this.ctx.ntFileCacheApi.getFileCacheInfo(fileType)).infos.map((file) => file.fileKey)) cacheFileList.push(...(await this.ctx.ntFileCacheApi.getFileCacheInfo(fileType)).infos.map((file) => file.fileKey))
} }
// 一并清除 // 一并清除
await this.ctx.ntFileCacheApi.clearChatCache(chatCacheList, cacheFileList) await this.ctx.ntFileCacheApi.clearChatCache(chatCacheList, cacheFileList)
res()
} catch (e) {
console.error('清理缓存时发生了错误')
rej(e)
}
})
} }
} }

View File

@@ -0,0 +1,26 @@
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
interface Payload {
domain: string
}
interface Response {
cookies: string
csrf_token: number
}
export class GetCredentials extends BaseAction<Payload, Response> {
actionName = ActionName.GetCredentials
payloadSchema = Schema.object({
domain: Schema.string().required()
})
protected async _handle(payload: Payload) {
const cookiesObject = await this.ctx.ntUserApi.getCookies(payload.domain)
//把获取到的cookiesObject转换成 k=v; 格式字符串拼接在一起
const cookies = Object.entries(cookiesObject).map(([key, value]) => `${key}=${value}`).join('; ')
const bkn = cookiesObject.skey ? this.ctx.ntWebApi.genBkn(cookiesObject.skey) : ''
return { cookies, csrf_token: +bkn }
}
}

View File

@@ -0,0 +1,10 @@
import { BaseAction } from '@/onebot11/action/BaseAction'
import { ActionName } from '@/onebot11/action/types'
export class SetRestart extends BaseAction<null, void> {
actionName = ActionName.SetRestart
protected async _handle() {
await this.ctx.ntSystemApi.restart()
}
}

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