Compare commits

...

157 Commits

Author SHA1 Message Date
idranme
6b03b01a24 Merge pull request #319 from LLOneBot/dev
chore: v3.28.0
2024-08-06 02:08:51 +08:00
idranme
18f01b7f21 chore: v3.28.0 2024-08-06 02:08:00 +08:00
idranme
897f691d6c make ts happy 2024-08-06 01:47:51 +08:00
idranme
a9902d9109 sync 2024-08-05 22:49:48 +08:00
idranme
5d78fdd6a4 fix 2024-08-05 22:07:04 +08:00
idranme
72eb013371 fix 2024-08-05 20:44:28 +08:00
idranme
808777c044 fix: import path 2024-08-05 19:18:15 +08:00
idranme
a2d1379866 sync 2024-08-05 19:09:41 +08:00
idranme
c41a8556fa Change description 2024-08-05 00:23:41 +08:00
idranme
fa2df2a3cd opt 2024-08-04 23:11:59 +08:00
idranme
b28dd3a723 Update publish.yml 2024-08-04 22:44:20 +08:00
idranme
6ffa41e0d6 prioritise local versions 2024-08-04 22:14:07 +08:00
idranme
85df3794e8 optimise 2024-08-04 22:07:55 +08:00
idranme
4bee2ba062 reduce icon size 2024-08-04 20:35:31 +08:00
idranme
4bf992c4a9 chore: deps 2024-08-04 20:31:29 +08:00
idranme
898e856150 poke require >=25765 2024-08-04 20:22:07 +08:00
idranme
c86797afc8 chore: remove unused eslint 2024-08-04 19:54:32 +08:00
idranme
799593b788 chore: support yarn berry 2024-08-04 19:48:17 +08:00
idranme
74d9a083aa Update README.md 2024-08-04 19:28:13 +08:00
idranme
cae525429a Update README.md 2024-08-04 19:21:44 +08:00
idranme
cc0d1e2a9b Merge pull request #316 from idranme/uuid
refa: deps
2024-08-04 18:36:01 +08:00
idranme
34ecfcfa16 Merge branch 'dev' into uuid 2024-08-04 18:35:11 +08:00
idranme
79c5041216 Merge pull request #318 from LLOneBot/dev 2024-08-04 18:07:03 +08:00
idranme
8fb53260ab chore: v3.27.4 2024-08-04 10:05:03 +00:00
idranme
07d9ac823a Merge pull request #317 from LLOneBot/dev
chore: v3.27.4
2024-08-04 17:48:24 +08:00
idranme
b571ef434c chore 2024-08-02 20:50:34 +00:00
idranme
c1f4dcd6a6 chore 2024-08-02 20:40:50 +00:00
idranme
4c5befbe44 chore 2024-08-02 20:39:26 +00:00
linyuchen
296cd4d0a3 Merge pull request #315 from idranme/main
feat: at segment add name
2024-08-02 23:11:14 +08:00
linyuchen
e77a2ca34a Merge pull request #311 from cnxysoft/dev
BUG修复
2024-08-02 23:09:35 +08:00
idranme
f3af0d18bc refa: deps 2024-08-02 12:00:13 +00:00
idranme
406e3c7e6b opt 2024-08-02 10:49:30 +00:00
idranme
3f5ca8ebfa chore 2024-08-02 10:31:37 +00:00
idranme
6e8389e833 chore 2024-08-02 10:26:18 +00:00
idranme
71aedca4c6 feat: the name attribute of the at message segment 2024-08-02 10:23:48 +00:00
Alen
6410689549 BUG修复
尝试修复设精事件shortId和senderId
2024-08-01 21:56:44 +08:00
linyuchen
6d0e2269cc Merge pull request #304 from cnxysoft/dev
功能更新
2024-07-28 14:52:58 +08:00
linyuchen
2e28fc678c Merge branch 'dev' into dev 2024-07-28 14:52:17 +08:00
linyuchen
8204f4407f Merge pull request #300 from super1207/dev
Merge branch 'dev' of https://github.com/LLOneBot/LLOneBot into dev
2024-07-26 09:57:37 +08:00
Alen
9f1d4c4db2 功能修改
修改群管变更事件获取渠道,让所有群角色都能收到群管变更通知
2024-07-25 17:25:40 +08:00
Alen
8ba47635d3 功能更新
1.增加设精事件上报(目前上报的shortId经常出错,实际消息体却是正确的,待解决)
2.增加设精/取消设精api接口
3.poke事件增加raw信息上报
2024-07-25 01:02:48 +08:00
Alen
5fa2427c51 修改poke事件
新增poke事件支持上传raw信息
2024-07-24 19:04:07 +08:00
Alen
aa8739d016 Merge remote-tracking branch 'upstream/main' into dev 2024-07-24 11:48:55 +08:00
super1207
79f0329da7 Merge branch 'dev' of https://github.com/LLOneBot/LLOneBot into dev 2024-07-20 18:01:30 +08:00
super1207
7a33a36f44 add get_event api 2024-07-20 17:58:00 +08:00
linyuchen
808424d08e Merge branch 'main' into dev 2024-07-20 17:08:59 +08:00
linyuchen
d0967785de chore: v3.27.3 2024-07-20 16:58:03 +08:00
linyuchen
eccabb8189 Merge pull request #299 from Natsukage/main
fix: skip problematic name-value pairs in encodeCQCode to prevent undefined errors
2024-07-20 15:25:27 +08:00
夏影
c9374ff515 fix: skip problematic name-value pairs in encodeCQCode to prevent undefined errors
Added logic to skip name-value pairs in encodeCQCode when value cannot be converted to string, preventing errors caused by undefined values. This ensures the function can handle such cases gracefully and continue processing other valid data.
2024-07-20 00:49:34 +08:00
Alen
92c4889924 Merge remote-tracking branch 'upstream/main' 2024-07-16 23:19:32 +08:00
linyuchen
f9454039a1 fix: old poke event 2024-07-16 21:52:15 +08:00
linyuchen
bc4511e175 chore: v3.27.2 2024-07-16 21:43:50 +08:00
linyuchen
f191103f99 Merge pull request #294 from cnxysoft/dev
修复戳一戳
2024-07-16 21:38:17 +08:00
linyuchen
408463f63b Merge branch 'dev' into dev 2024-07-16 21:21:50 +08:00
Alen
fb96c4272e 修复戳一戳
取缔FriendAddEvent,并入Private Event处理
2024-07-16 21:01:19 +08:00
Alen
c6b302d5a8 修复好友戳一戳
取缔FriendAddEvent,并入Private Event处理
2024-07-16 20:27:44 +08:00
linyuchen
1dd468e2ff fix: #290 2024-07-13 16:25:00 +08:00
linyuchen
2a1aa8c649 feat: image subType 2024-07-13 14:26:23 +08:00
linyuchen
1633734e08 Merge branch 'dev' 2024-07-13 14:09:45 +08:00
linyuchen
dff92e6f27 chore: version 3.27.0
feat: support poke
feat: LLOneBot global switch
2024-07-13 14:09:03 +08:00
linyuchen
dba5e30d5d doc: plugin description 2024-07-10 13:48:05 +08:00
linyuchen
2d04ab2e72 fix: crychic crash 2024-07-10 13:47:44 +08:00
linyuchen
1a015ac8d3 Merge pull request #262 from LLOneBot/dev
get_record 支持 out_format 进行转码,和其他小修复
2024-06-21 17:39:53 +08:00
linyuchen
6390620ddd chore: version 3.26.7 2024-06-21 17:33:48 +08:00
linyuchen
0d19005dc3 refactor: remove duplicate import 2024-06-21 17:28:17 +08:00
linyuchen
c6479dd2c4 Merge remote-tracking branch 'origin/dev' into dev 2024-06-21 16:21:15 +08:00
linyuchen
8871331b7c 🐛 fix: ws echo #261 2024-06-21 16:20:59 +08:00
linyuchen
e01148b86a 🐛 fix: ws echo 2024-06-21 16:20:26 +08:00
linyuchen
2f87e3818e Merge pull request #260 from idranme/main
perf: audio
2024-06-21 10:36:29 +08:00
linyuchen
2c8a594c38 Merge branch 'dev' into main 2024-06-21 10:36:14 +08:00
idranme
1508dab7fe perf: audio 2024-06-18 19:15:56 +00:00
linyuchen
958b21e47e fix: wait get_file download complete 2024-06-17 17:41:23 +08:00
linyuchen
781c3311ae fix: get_file cache not found 2024-06-17 16:20:37 +08:00
linyuchen
52850d172e feat: decode silk 2024-06-17 16:05:38 +08:00
linyuchen
52a065542e chore: v3.26.6 2024-06-10 14:38:20 +08:00
linyuchen
fd10469685 feat: video url 2024-06-10 14:35:00 +08:00
linyuchen
a2ee75b113 refactor: sent msg status waiter 2024-06-09 15:27:33 +08:00
linyuchen
0f7f243b98 Merge pull request #250 from Bluefissure/reverse-ws-ua
feat: add ua to reverse websocket headers
2024-06-06 17:35:21 +08:00
Bluefissure
97d7996a50 fix: add version to ua 2024-06-06 08:53:37 +00:00
Bluefissure
b658d164f9 feat: add ua to reverse websocket headers 2024-06-06 08:48:18 +00:00
linyuchen
f150ae478b chore: v3.26.5 2024-06-01 20:19:05 +08:00
linyuchen
d1f68553f1 fix: 加载卡顿,群成员名片变动 2024-06-01 20:18:38 +08:00
linyuchen
f47f0800de Merge remote-tracking branch 'origin/main' 2024-05-29 16:56:08 +08:00
linyuchen
b7ddefc950 fix: QZone cookies 2024-05-29 16:38:22 +08:00
linyuchen
25b3325a44 fix: comment 2024-05-29 16:28:46 +08:00
linyuchen
c281b87bab merge main 2024-05-29 16:27:06 +08:00
linyuchen
c0946ddda2 chore: version 3.26.4 2024-05-29 16:26:04 +08:00
linyuchen
1128cf679c refactor: send file timeout 2024-05-29 16:25:42 +08:00
linyuchen
ff65a42350 Merge pull request #242 from LLOneBot/dev
feat: support qzone cookies
2024-05-29 16:24:32 +08:00
手瓜一十雪
c459587dcd refactor: get cookies 2024-05-29 12:03:35 +08:00
手瓜一十雪
6f8ea9677f feat: support qzone cookies 2024-05-28 17:14:24 +08:00
手瓜一十雪
38197527fa Merge branch 'main' into dev 2024-05-28 17:11:13 +08:00
手瓜一十雪
21b2bd2c8e feat: cookies 2024-05-28 17:11:07 +08:00
linyuchen
25158eee55 chore: version 3.26.3 2024-05-28 16:41:28 +08:00
linyuchen
1aa804f255 chore: version 3.26.3 2024-05-28 16:41:22 +08:00
linyuchen
fbe101339d fix: #237 2024-05-28 16:40:51 +08:00
linyuchen
a4aeb8171d fix: QQ package.json on macOS 2024-05-28 15:42:22 +08:00
linyuchen
27f98a459c fix: member info change on version 24108 2024-05-28 15:31:59 +08:00
linyuchen
e6b0eaa46d Merge pull request #235 from LLOneBot/dev
快速操作回复自动引用原消息开关
2024-05-24 17:14:54 +08:00
linyuchen
f336317a33 chore: version 3.26.2 2024-05-24 17:12:35 +08:00
linyuchen
17b44cc0fa refactor: #226 Quick operation reply automatically quotes the original message switch 2024-05-24 17:10:41 +08:00
linyuchen
debe3a8597 chore: version 3.26.1 2024-05-24 08:54:23 +08:00
linyuchen
f36c5e849f Merge pull request #234 from LLOneBot/dev
fix: #215 get_forward_msg params missing id(onebot11)
2024-05-24 08:52:34 +08:00
linyuchen
abbd6797c4 fix: #215 get_forward_msg params missing id(onebot11) 2024-05-24 08:50:22 +08:00
linyuchen
fdb7784a7d Merge pull request #233 from LLOneBot/dev
[Feature] OneBot11消息构造添加raw字段,单条转发消息接口返回message_id
2024-05-24 08:40:44 +08:00
linyuchen
92b49015b0 feat: Forward single msg return message_id 2024-05-24 08:36:42 +08:00
linyuchen
1765ffff7b style: format 2024-05-24 08:15:08 +08:00
linyuchen
3024316b5b feat: #232 /get_msg, /get_group_msg_history add raw message 2024-05-24 08:11:38 +08:00
linyuchen
9a0d89bfbf Update README.md 2024-05-19 07:52:12 +08:00
linyuchen
807ef3b700 Merge pull request #228 from LLOneBot/dev
feat: Quick operation reply auto quote original message
2024-05-18 16:53:37 +08:00
linyuchen
948f10d4e3 feat: Quick operation reply auto quote original message 2024-05-18 16:51:34 +08:00
linyuchen
0f99b5cb87 Merge pull request #227 from LLOneBot/dev
fix: Send msg timeout minimum
2024-05-18 16:36:30 +08:00
linyuchen
6413b0ff82 fix: Send msg timeout minimum 2024-05-18 16:34:12 +08:00
linyuchen
39713d8e11 Merge branch 'main' into dev 2024-05-18 16:31:22 +08:00
linyuchen
739a497af6 chore: v3.26.0 2024-05-18 13:16:45 +08:00
linyuchen
de2fe9b0aa Merge pull request #225 from LLOneBot/dev
Feature: #209,New API get_friends_with_category
2024-05-18 13:11:30 +08:00
linyuchen
44448895a0 feat: 209 2024-05-18 13:09:45 +08:00
linyuchen
cfd9097769 feat: 209 2024-05-18 13:08:44 +08:00
linyuchen
627042fd25 Merge pull request #224 from LLOneBot/dev
Fix: #219,发送视频图片进行文件大小判断,超时时间根据文件大小(512kb/s)动态调整
2024-05-18 12:53:42 +08:00
linyuchen
b51ce24d0c fix: #219 2024-05-18 12:50:11 +08:00
linyuchen
fc0881eccc Merge pull request #223 from LLOneBot/dev
fix: #218
2024-05-18 12:13:23 +08:00
linyuchen
6b8509d2b2 fix: #218 2024-05-18 12:12:16 +08:00
linyuchen
cf1d67a5cf Merge pull request #222 from LLOneBot/dev
Feature: websocket .handle_quick_operation
2024-05-18 11:47:56 +08:00
linyuchen
473ebd25b8 fix: promise catch 2024-05-18 11:46:51 +08:00
linyuchen
d4427cfff4 feat: .handle_quick_operation of websocket 2024-05-18 11:45:42 +08:00
linyuchen
9d2e9786cc chore: v3.25.0 2024-05-15 23:03:19 +08:00
linyuchen
9968f714c7 chore: v3.25.0 2024-05-15 23:03:04 +08:00
linyuchen
bd212c4bf3 remove debug 2024-05-15 22:45:13 +08:00
linyuchen
32c7f904db fix: Http download headers 2024-05-15 22:44:15 +08:00
linyuchen
2ef017282f feat: get_group_honor_info 2024-05-15 22:33:55 +08:00
手瓜一十雪
9672f67a23 feat: new Api GetGroupEssence&GetGroupHonorInfo 2024-05-15 21:12:10 +08:00
手瓜一十雪
6e5cfd827c feat: webapi 2024-05-15 21:05:33 +08:00
手瓜一十雪
5402bef4a9 Merge branch 'main' into dev 2024-05-15 20:56:52 +08:00
linyuchen
4194512cce fix: Get cookies miss uin 2024-05-15 19:47:11 +08:00
linyuchen
b3aad8b0d9 fix: Check pic fil name ext 2024-05-15 19:19:10 +08:00
linyuchen
1489c6df25 feat: New face 2024-05-15 18:47:38 +08:00
linyuchen
2e225045e6 feat: Get cookies support domain 2024-05-15 17:57:15 +08:00
手瓜一十雪
11ed06148c fix: checkVersion Mirror 2024-05-14 14:43:52 +08:00
linyuchen
a3fc018186 fix: Compatible with win7 2024-05-12 20:36:27 +08:00
linyuchen
9692bf6ec6 refactor: Rename native node module dirname 2024-05-11 14:56:01 +08:00
linyuchen
9b3916307a fix: All images are the first image in single msg
fix: remote rkey
2024-05-11 14:52:59 +08:00
linyuchen
fdf96b479c Merge branch 'main' into dev
# Conflicts:
#	src/ntqqapi/external/cpmodule.ts
#	src/ntqqapi/external/crychic/index.ts
#	src/ntqqapi/external/moehook/hook.ts
#	src/onebot11/action/msg/SendMsg.ts
#	tsconfig.json
2024-05-10 20:28:44 +08:00
linyuchen
25c7a6096d refactor: path alias
fix: moehook
2024-05-10 20:23:30 +08:00
student_2333
627955e7fd chore: format 2024-05-10 13:34:49 +08:00
student_2333
43e9b070a9 fix: try 2 fix cannot parse msg err 2024-05-10 13:33:48 +08:00
linyuchen
78bb36a2bb fix: Music sign return null then throw exception 2024-05-07 17:46:47 +08:00
linyuchen
58e6e3cbda fix: Music sign return null then throw exception 2024-05-07 17:39:44 +08:00
linyuchen
1da086ce0a chore: v3.24.2 2024-05-05 20:20:30 +08:00
linyuchen
e9d43a9449 fix: http download filename special character 2024-05-05 20:06:07 +08:00
linyuchen
ce31052661 refactor: OB11Message add message_seq filed 2024-05-05 19:42:48 +08:00
linyuchen
3fd9b0a183 fix: 表情回应兼容int类型的emoji_id 2024-05-05 13:07:07 +08:00
linyuchen
7e1dee8e07 fix: msg db cache missing shortId 2024-05-04 23:35:19 +08:00
linyuchen
f2854fdf00 fix: report self recall twice 2024-05-04 20:30:39 +08:00
linyuchen
1fad95a55b chore: Version 3.24.1 2024-05-04 11:34:41 +08:00
linyuchen
5342e1521c Merge remote-tracking branch 'origin/main'
# Conflicts:
#	src/ntqqapi/external/moehook/MoeHoo-linux-x64.node
2024-05-03 21:26:31 +08:00
student_2333
3c532526df chore: sync external files 2024-05-01 15:25:49 +08:00
student_2333
05c6cae86f fix: reference before define 2024-05-01 11:10:42 +08:00
119 changed files with 7720 additions and 1659 deletions

View File

@@ -9,10 +9,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: setup node
uses: actions/setup-node@v2
uses: actions/setup-node@v4
with:
node-version: 18

17
.gitignore vendored
View File

@@ -1,6 +1,15 @@
node_modules/
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
package-lock.json
dist/
out/
yarn.lock
node_modules
dist
out
.idea/
.DS_Store
.DS_Store

1
.yarnrc.yml Normal file
View File

@@ -0,0 +1 @@
nodeLinker: node-modules

View File

@@ -1,9 +1,9 @@
# LLOneBot API
# LLOneBot
LiteLoaderQQNT插件使你的NTQQ支持OneBot11协议进行QQ机器人开发
LiteLoaderQQNT 插件,实现 OneBot 11 协议,用以 QQ 机器人开发
> [!CAUTION]\
> **请不要在 QQ 官方群聊和任何影响力较大的简中互联网平台(包括但不限于:B站微博知乎抖音等发布和讨论*任何*与本插件存在相关性的信息**
> **请不要在 QQ 官方群聊和任何影响力较大的简中互联网平台(包括但不限于: B站微博知乎抖音等发布和讨论*任何*与本插件存在相关性的信息**
TG群<https://t.me/+nLZEnpne-pQ1OWFl>
@@ -13,13 +13,13 @@ TG群<https://t.me/+nLZEnpne-pQ1OWFl>
## 设置界面
<img src="./doc/image/setting.png" width="500px" alt="图片名称"/>
<img src="./doc/image/setting.png" width="400px" alt="设置界面"/>
## HTTP 调用示例
![](doc/image/example.jpg)
<img src="./doc/image/example.jpg" width="500px" alt="HTTP调用示例"/>
## 支持的 api 和功能详情
## 支持的 API
<https://llonebot.github.io/zh-CN/develop/api>
@@ -35,13 +35,8 @@ TG群<https://t.me/+nLZEnpne-pQ1OWFl>
- [x] 群禁言事件上报
- [x] 优化加群成功事件上报
- [x] 清理缓存api
- [ ] 无头模式
- [ ] 框架对接文档
## onebot11文档
<https://11.onebot.dev/>
## Stargazers over time
[![Stargazers over time](https://starchart.cc/LLOneBot/LLOneBot.svg?variant=adaptive)](https://starchart.cc/LLOneBot/LLOneBot)

View File

@@ -1,5 +1,6 @@
import cp from 'vite-plugin-cp'
import './scripts/gen-version'
import path from 'node:path'
import './scripts/gen-manifest'
const external = [
'silk-wasm',
@@ -31,9 +32,11 @@ let config = {
external,
input: 'src/main/main.ts',
},
minify: true,
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'./lib-cov/fluent-ffmpeg': './lib/fluent-ffmpeg',
},
},
@@ -42,10 +45,10 @@ let config = {
targets: [
...external.map(genCpModule),
{ src: './manifest.json', dest: 'dist' },
{ src: './icon.jpg', dest: 'dist' },
{ src: './src/ntqqapi/external/crychic/crychic-win32-x64.node', dest: 'dist/main/' },
{ src: './src/ntqqapi/external/moehook/MoeHoo-win32-x64.node', dest: 'dist/main/' },
{ src: './src/ntqqapi/external/moehook/MoeHoo-linux-x64.node', dest: 'dist/main/' },
{ src: './icon.webp', dest: 'dist' },
// { src: './src/ntqqapi/native/crychic/crychic-win32-x64.node', dest: 'dist/main/' },
// { src: './src/ntqqapi/native/moehook/MoeHoo-win32-x64.node', dest: 'dist/main/' },
// { src: './src/ntqqapi/native/moehook/MoeHoo-linux-x64.node', dest: 'dist/main/' },
],
}),
],

BIN
icon.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

BIN
icon.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -1,11 +1,11 @@
{
"manifest_version": 4,
"type": "extension",
"name": "LLOneBot v3.24.0",
"name": "LLOneBot",
"slug": "LLOneBot",
"description": "使你的NTQQ支持OneBot11协议进行QQ机器人开发, 不支持商店在线更新",
"version": "3.24.0",
"icon": "./icon.jpg",
"description": "实现 OneBot 11 协议,用以 QQ 机器人开发",
"version": "3.28.0",
"icon": "./icon.webp",
"authors": [
{
"name": "linyuchen",

View File

@@ -10,39 +10,33 @@
"deploy-mac": "cp -r dist/* ~/Library/Containers/com.tencent.qq/Data/LiteLoaderQQNT/plugins/LLOneBot/",
"build-win": "npm run build && npm run deploy-win",
"deploy-win": "cmd /c \"xcopy /C /S /Y dist\\* %USERPROFILE%\\documents\\LiteLoaderQQNT\\plugins\\LLOneBot\\\"",
"format": "prettier -cw ."
"format": "prettier -cw .",
"check": "tsc"
},
"author": "",
"license": "MIT",
"dependencies": {
"compressing": "^1.10.0",
"compressing": "^1.10.1",
"cors": "^2.8.5",
"express": "^4.18.2",
"fast-xml-parser": "^4.3.6",
"fast-xml-parser": "^4.4.1",
"file-type": "^19.0.0",
"fluent-ffmpeg": "^2.1.2",
"level": "^8.0.1",
"silk-wasm": "^3.3.4",
"utf-8-validate": "^6.0.3",
"uuid": "^9.0.1",
"ws": "^8.16.0"
"silk-wasm": "^3.6.1",
"ws": "^8.18.0"
},
"devDependencies": {
"@types/cors": "^2.8.17",
"@types/express": "^4.17.20",
"@types/fluent-ffmpeg": "^2.1.24",
"@types/node": "^20.11.24",
"@types/uuid": "^9.0.8",
"@types/ws": "^8.5.10",
"@typescript-eslint/eslint-plugin": "^6.4.0",
"@types/ws": "^8.5.12",
"electron": "^29.0.1",
"electron-vite": "^2.0.0",
"eslint": "^8.0.1",
"eslint-plugin-import": "^2.25.2",
"eslint-plugin-n": "^15.0.0 || ^16.0.0",
"eslint-plugin-promise": "^6.0.0",
"ts-node": "^10.9.2",
"typescript": "*",
"vite": "^5.1.4",
"electron-vite": "^2.3.0",
"typescript": "^5.5.4",
"vite": "^5.3.5",
"vite-plugin-cp": "^4.0.8"
}
},
"packageManager": "yarn@4.4.0"
}

38
scripts/gen-manifest.ts Normal file
View File

@@ -0,0 +1,38 @@
import { version } from '../src/version'
import { writeFileSync } from 'node:fs'
const manifest = {
manifest_version: 4,
type: 'extension',
name: 'LLOneBot',
slug: 'LLOneBot',
description: '实现 OneBot 11 协议,用以 QQ 机器人开发',
version,
icon: './icon.webp',
authors: [
{
name: 'linyuchen',
link: 'https://github.com/linyuchen'
}
],
repository: {
repo: 'linyuchen/LiteLoaderQQNT-OneBotApi',
branch: 'main',
release: {
tag: 'latest',
name: 'LLOneBot.zip'
}
},
platform: [
'win32',
'linux',
'darwin'
],
injects: {
renderer: './renderer/index.js',
main: './main/main.cjs',
preload: './preload/preload.cjs'
}
}
writeFileSync('manifest.json', JSON.stringify(manifest, null, 2))

View File

@@ -1,22 +0,0 @@
import fs from 'fs'
import path from 'path'
import { version } from '../src/version'
const manifestPath = path.join(__dirname, '../manifest.json')
function readManifest(): any {
if (fs.existsSync(manifestPath)) {
return JSON.parse(fs.readFileSync(manifestPath, 'utf-8'))
}
}
function writeManifest(manifest: any) {
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2))
}
const manifest = readManifest()
if (version !== manifest.version) {
manifest.version = version
manifest.name = `LLOneBot v${version}`
writeManifest(manifest)
}

View File

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

View File

@@ -1,7 +1,5 @@
import fs from 'fs'
import fsPromise from 'fs/promises'
import fs from 'node:fs'
import { Config, OB11Config } from './types'
import { mergeNewProperties } from './utils/helper'
import path from 'node:path'
import { selfInfo } from './data'
@@ -40,8 +38,10 @@ export class ConfigUtil {
enableWsReverse: false,
messagePostFormat: 'array',
enableHttpHeart: false,
enableQOAutoQuote: false
}
let defaultConfig: Config = {
enableLLOB: true,
ob11: ob11Default,
heartInterval: 60000,
token: '',
@@ -51,7 +51,6 @@ export class ConfigUtil {
reportSelfMessage: false,
autoDeleteFile: false,
autoDeleteFileSecond: 60,
enablePoke: false,
musicSignUrl: '',
}
@@ -82,9 +81,12 @@ export class ConfigUtil {
fs.writeFileSync(this.configPath, JSON.stringify(config, null, 2), 'utf-8')
}
private checkOldConfig(currentConfig: Config | OB11Config,
oldConfig: Config | OB11Config,
currentKey: string, oldKey: string) {
private checkOldConfig(
currentConfig: Config | OB11Config,
oldConfig: Config | OB11Config,
currentKey: string,
oldKey: string,
) {
// 迁移旧的配置到新配置,避免用户重新填写配置
const oldValue = oldConfig[oldKey]
if (oldValue) {

View File

@@ -1,9 +1,18 @@
import { type Friend, type FriendRequest, type Group, type GroupMember, type SelfInfo } from '../ntqqapi/types'
import {
CategoryFriend,
type Friend,
type FriendRequest,
type Group,
type GroupMember,
type SelfInfo,
User,
} from '../ntqqapi/types'
import { type FileCache, type LLOneBotError } from './types'
import { NTQQGroupApi } from '../ntqqapi/api/group'
import { log } from './utils/log'
import { isNumeric } from './utils/helper'
import { NTQQFriendApi } from '../ntqqapi/api'
import { WebApiGroupMember } from '@/ntqqapi/api/webapi'
export const selfInfo: SelfInfo = {
uid: '',
@@ -11,6 +20,10 @@ export const selfInfo: SelfInfo = {
nick: '',
online: true,
}
export const WebGroupData = {
GroupData: new Map<string, Array<WebApiGroupMember>>(),
GroupTime: new Map<string, number>(),
}
export let groups: Group[] = []
export let friends: Friend[] = []
export let friendRequests: Map<number, FriendRequest> = new Map<number, FriendRequest>()
@@ -27,13 +40,13 @@ export async function getFriend(uinOrUid: string): Promise<Friend | undefined> {
let friend = friends.find((friend) => friend[filterKey] === filterValue.toString())
if (!friend) {
try {
const _friends = (await NTQQFriendApi.getFriends(true))
friend = _friends.find(friend => friend[filterKey] === filterValue.toString())
if (friend){
const _friends = await NTQQFriendApi.getFriends(true)
friend = _friends.find((friend) => friend[filterKey] === filterValue.toString())
if (friend) {
friends.push(friend)
}
} catch (e) {
log("刷新好友列表失败", e.stack.toString())
} catch (e: any) {
log('刷新好友列表失败', e.stack.toString())
}
}
return friend
@@ -107,3 +120,5 @@ export function getUidByUin(uin: string) {
}
export let tempGroupCodeMap: Record<string, string> = {} // peerUid => 群号
export let rawFriends: CategoryFriend[] = []

View File

@@ -14,9 +14,9 @@ class DBUtil {
public readonly DB_KEY_PREFIX_FILE = 'file_'
public readonly DB_KEY_PREFIX_GROUP_NOTIFY = 'group_notify_'
private readonly DB_KEY_RECEIVED_TEMP_UIN_MAP = 'received_temp_uin_map'
public db: Level
public db: Level | undefined
public cache: Record<string, RawMessage | string | FileCache | GroupNotify | ReceiveTempUinMap> = {} // <msg_id_ | msg_short_id_ | msg_seq_id_><id>: RawMessage
private currentShortId: number
private currentShortId: number | undefined
/*
* 数据库结构
@@ -44,12 +44,12 @@ class DBUtil {
this.db = new Level(DB_PATH, { valueEncoding: 'json' })
console.log('llonebot init db success')
resolve(null)
} catch (e) {
} catch (e: any) {
console.log('init db fail', e.stack.toString())
setTimeout(initDB, 300)
}
}
initDB()
setTimeout(initDB)
}).then()
const expiredMilliSecond = 1000 * 60 * 60
@@ -72,13 +72,13 @@ class DBUtil {
public async getReceivedTempUinMap(): Promise<ReceiveTempUinMap> {
try {
this.cache[this.DB_KEY_RECEIVED_TEMP_UIN_MAP] = JSON.parse(await this.db.get(this.DB_KEY_RECEIVED_TEMP_UIN_MAP))
} catch (e) {}
this.cache[this.DB_KEY_RECEIVED_TEMP_UIN_MAP] = JSON.parse(await this.db?.get(this.DB_KEY_RECEIVED_TEMP_UIN_MAP)!)
} catch (e) { }
return (this.cache[this.DB_KEY_RECEIVED_TEMP_UIN_MAP] || {}) as ReceiveTempUinMap
}
public setReceivedTempUinMap(data: ReceiveTempUinMap) {
this.cache[this.DB_KEY_RECEIVED_TEMP_UIN_MAP] = data
this.db.put(this.DB_KEY_RECEIVED_TEMP_UIN_MAP, JSON.stringify(data)).then()
this.db?.put(this.DB_KEY_RECEIVED_TEMP_UIN_MAP, JSON.stringify(data)).then()
}
private addCache(msg: RawMessage) {
const longIdKey = this.DB_KEY_PREFIX_MSG_ID + msg.msgId
@@ -91,30 +91,30 @@ class DBUtil {
this.cache = {}
}
async getMsgByShortId(shortMsgId: number): Promise<RawMessage> {
async getMsgByShortId(shortMsgId: number): Promise<RawMessage | undefined> {
const shortMsgIdKey = this.DB_KEY_PREFIX_MSG_SHORT_ID + shortMsgId
if (this.cache[shortMsgIdKey]) {
// log("getMsgByShortId cache", shortMsgIdKey, this.cache[shortMsgIdKey])
return this.cache[shortMsgIdKey] as RawMessage
}
try {
const longId = await this.db.get(shortMsgIdKey)
const msg = await this.getMsgByLongId(longId)
this.addCache(msg)
const longId = await this.db?.get(shortMsgIdKey)
const msg = await this.getMsgByLongId(longId!)
this.addCache(msg!)
return msg
} catch (e) {
} catch (e: any) {
log('getMsgByShortId db error', e.stack.toString())
}
}
async getMsgByLongId(longId: string): Promise<RawMessage> {
async getMsgByLongId(longId: string): Promise<RawMessage | undefined> {
const longIdKey = this.DB_KEY_PREFIX_MSG_ID + longId
if (this.cache[longIdKey]) {
return this.cache[longIdKey] as RawMessage
}
try {
const data = await this.db.get(longIdKey)
const msg = JSON.parse(data)
const data = await this.db?.get(longIdKey)
const msg = JSON.parse(data!)
this.addCache(msg)
return msg
} catch (e) {
@@ -122,17 +122,17 @@ class DBUtil {
}
}
async getMsgBySeqId(seqId: string): Promise<RawMessage> {
async getMsgBySeqId(seqId: string): Promise<RawMessage | undefined> {
const seqIdKey = this.DB_KEY_PREFIX_MSG_SEQ_ID + seqId
if (this.cache[seqIdKey]) {
return this.cache[seqIdKey] as RawMessage
}
try {
const longId = await this.db.get(seqIdKey)
const msg = await this.getMsgByLongId(longId)
this.addCache(msg)
const longId = await this.db?.get(seqIdKey)
const msg = await this.getMsgByLongId(longId!)
this.addCache(msg!)
return msg
} catch (e) {
} catch (e: any) {
log('getMsgBySeqId db error', e.stack.toString())
}
}
@@ -141,7 +141,7 @@ class DBUtil {
// 有则更新,无则添加
// log("addMsg", msg.msgId, msg.msgSeq, msg.msgShortId);
const longIdKey = this.DB_KEY_PREFIX_MSG_ID + msg.msgId
let existMsg = this.cache[longIdKey] as RawMessage
let existMsg: RawMessage | undefined = this.cache[longIdKey] as RawMessage
if (!existMsg) {
try {
existMsg = await this.getMsgByLongId(msg.msgId)
@@ -154,20 +154,20 @@ class DBUtil {
this.updateMsg(msg).then()
return existMsg.msgShortId
}
this.addCache(msg)
const shortMsgId = await this.genMsgShortId()
const shortIdKey = this.DB_KEY_PREFIX_MSG_SHORT_ID + shortMsgId
const seqIdKey = this.DB_KEY_PREFIX_MSG_SEQ_ID + msg.msgSeq
msg.msgShortId = shortMsgId
this.addCache(msg)
// log("新增消息记录", msg.msgId)
this.db.put(shortIdKey, msg.msgId).then().catch()
this.db.put(longIdKey, JSON.stringify(msg)).then().catch()
this.db?.put(shortIdKey, msg.msgId).then().catch()
this.db?.put(longIdKey, JSON.stringify(msg)).then().catch()
try {
await this.db.get(seqIdKey)
await this.db?.get(seqIdKey)
} catch (e) {
// log("新的seqId", seqIdKey)
this.db.put(seqIdKey, msg.msgId).then().catch()
this.db?.put(seqIdKey, msg.msgId).then().catch()
}
if (!this.cache[seqIdKey]) {
this.cache[seqIdKey] = msg
@@ -178,7 +178,7 @@ class DBUtil {
async updateMsg(msg: RawMessage) {
const longIdKey = this.DB_KEY_PREFIX_MSG_ID + msg.msgId
let existMsg = this.cache[longIdKey] as RawMessage
let existMsg: RawMessage | undefined = this.cache[longIdKey] as RawMessage
if (!existMsg) {
try {
existMsg = await this.getMsgByLongId(msg.msgId)
@@ -187,18 +187,18 @@ class DBUtil {
}
}
Object.assign(existMsg, msg)
this.db.put(longIdKey, JSON.stringify(existMsg)).then().catch()
const shortIdKey = this.DB_KEY_PREFIX_MSG_SHORT_ID + existMsg.msgShortId
Object.assign(existMsg!, msg)
this.db?.put(longIdKey, JSON.stringify(existMsg)).then().catch()
const shortIdKey = this.DB_KEY_PREFIX_MSG_SHORT_ID + existMsg?.msgShortId
const seqIdKey = this.DB_KEY_PREFIX_MSG_SEQ_ID + msg.msgSeq
if (!this.cache[seqIdKey]) {
this.cache[seqIdKey] = existMsg
this.cache[seqIdKey] = existMsg!
}
this.db.put(shortIdKey, msg.msgId).then().catch()
this.db?.put(shortIdKey, msg.msgId).then().catch()
try {
await this.db.get(seqIdKey)
await this.db?.get(seqIdKey)
} catch (e) {
this.db.put(seqIdKey, msg.msgId).then().catch()
this.db?.put(seqIdKey, msg.msgId).then().catch()
// log("更新seqId error", e.stack, seqIdKey);
}
// log("更新消息", existMsg.msgSeq, existMsg.msgShortId, existMsg.msgId);
@@ -208,15 +208,15 @@ class DBUtil {
const key = 'msg_current_short_id'
if (this.currentShortId === undefined) {
try {
let id: string = await this.db.get(key)
this.currentShortId = parseInt(id)
const id = await this.db?.get(key)
this.currentShortId = parseInt(id!)
} catch (e) {
this.currentShortId = -2147483640
}
}
this.currentShortId++
this.db.put(key, this.currentShortId.toString()).then().catch()
this.db?.put(key, this.currentShortId.toString()).then().catch()
return this.currentShortId
}
@@ -229,8 +229,8 @@ class DBUtil {
delete cacheDBData['downloadFunc']
this.cache[fileNameOrUuid] = data
try {
await this.db.put(key, JSON.stringify(cacheDBData))
} catch (e) {
await this.db?.put(key, JSON.stringify(cacheDBData))
} catch (e: any) {
log('addFileCache db error', e.stack.toString())
}
}
@@ -241,8 +241,8 @@ class DBUtil {
return this.cache[key] as FileCache
}
try {
let data = await this.db.get(key)
return JSON.parse(data)
const data = await this.db?.get(key)
return JSON.parse(data!)
} catch (e) {
// log("getFileCache db error", e.stack.toString())
}
@@ -255,7 +255,7 @@ class DBUtil {
return
}
this.cache[key] = notify
this.db.put(key, JSON.stringify(notify)).then().catch()
this.db?.put(key, JSON.stringify(notify)).then().catch()
}
async getGroupNotify(seq: string): Promise<GroupNotify | undefined> {
@@ -264,8 +264,8 @@ class DBUtil {
return this.cache[key] as GroupNotify
}
try {
let data = await this.db.get(key)
return JSON.parse(data)
const data = await this.db?.get(key)
return JSON.parse(data!)
} catch (e) {
// log("getGroupNotify db error", e.stack.toString())
}

View File

@@ -1,5 +1,5 @@
import express, { Express, Request, Response } from 'express'
import http from 'http'
import http from 'node:http'
import cors from 'cors'
import { log } from '../utils/log'
import { getConfigUtil } from '../config'
@@ -10,7 +10,7 @@ type RegisterHandler = (res: Response, payload: any) => Promise<any>
export abstract class HttpServerBase {
name: string = 'LLOneBot'
private readonly expressAPP: Express
private server: http.Server = null
private server: http.Server | null = null
constructor() {
this.expressAPP = express()
@@ -38,7 +38,7 @@ export abstract class HttpServerBase {
let clientToken = ''
const authHeader = req.get('authorization')
if (authHeader) {
clientToken = authHeader.split('Bearer ').pop()
clientToken = authHeader.split('Bearer ').pop()!
log('receive http header token', clientToken)
} else if (req.query.access_token) {
if (Array.isArray(req.query.access_token)) {
@@ -62,7 +62,7 @@ export abstract class HttpServerBase {
})
this.listen(port)
llonebotError.httpServerError = ''
} catch (e) {
} catch (e: any) {
log('HTTP服务启动失败', e.toString())
llonebotError.httpServerError = 'HTTP服务启动失败, ' + e.toString()
}
@@ -103,7 +103,7 @@ export abstract class HttpServerBase {
log('收到http请求', url, payload)
try {
res.send(await handler(res, payload))
} catch (e) {
} catch (e: any) {
this.handleFailed(res, payload, e.stack.toString())
}
})

View File

@@ -6,9 +6,9 @@ import { getConfigUtil } from '../config'
import { llonebotError } from '../data'
class WebsocketClientBase {
private wsClient: WebSocket
private wsClient: WebSocket | undefined
constructor() {}
constructor() { }
send(msg: string) {
if (this.wsClient && this.wsClient.readyState == WebSocket.OPEN) {
@@ -16,11 +16,11 @@ class WebsocketClientBase {
}
}
onMessage(msg: string) {}
onMessage(msg: string) { }
}
export class WebsocketServerBase {
private ws: WebSocketServer = null
private ws: WebSocketServer | null = null
constructor() {
console.log(`llonebot websocket service started`)
@@ -30,22 +30,22 @@ export class WebsocketServerBase {
try {
this.ws = new WebSocketServer({ port, maxPayload: 1024 * 1024 * 1024 })
llonebotError.wsServerError = ''
} catch (e) {
} catch (e: any) {
llonebotError.wsServerError = '正向ws服务启动失败, ' + e.toString()
}
this.ws.on('connection', (wsClient, req) => {
const url = req.url.split('?').shift()
this.ws?.on('connection', (wsClient, req) => {
const url = req.url?.split('?').shift()
this.authorize(wsClient, req)
this.onConnect(wsClient, url, req)
this.onConnect(wsClient, url!, req)
wsClient.on('message', async (msg) => {
this.onMessage(wsClient, url, msg.toString())
this.onMessage(wsClient, url!, msg.toString())
})
})
}
stop() {
llonebotError.wsServerError = ''
this.ws.close((err) => {
this.ws?.close((err) => {
log('ws server close failed!', err)
})
this.ws = null
@@ -83,11 +83,11 @@ export class WebsocketServerBase {
}
}
authorizeFailed(wsClient: WebSocket) {}
authorizeFailed(wsClient: WebSocket) { }
onConnect(wsClient: WebSocket, url: string, req: IncomingMessage) {}
onConnect(wsClient: WebSocket, url: string, req: IncomingMessage) { }
onMessage(wsClient: WebSocket, url: string, msg: string) {}
onMessage(wsClient: WebSocket, url: string, msg: string) { }
sendHeart() {}
sendHeart() { }
}

View File

@@ -10,12 +10,14 @@ export interface OB11Config {
enableWsReverse?: boolean
messagePostFormat?: 'array' | 'string'
enableHttpHeart?: boolean
enableQOAutoQuote: boolean // 快速操作回复自动引用原消息
}
export interface CheckVersion {
result: boolean,
result: boolean
version: string
}
export interface Config {
enableLLOB: boolean
ob11: OB11Config
token?: string
heartInterval?: number // ms
@@ -26,7 +28,6 @@ export interface Config {
autoDeleteFile?: boolean
autoDeleteFileSecond?: number
ffmpeg?: string // ffmpeg路径
enablePoke?: boolean
musicSignUrl?: string
ignoreBeforeLoginMsg?: boolean
}
@@ -45,5 +46,6 @@ export interface FileCache {
fileUuid?: string
url?: string
msgId?: string
elementId: string
downloadFunc?: () => Promise<void>
}

View File

@@ -0,0 +1,231 @@
import { NodeIQQNTWrapperSession } from '@/ntqqapi/wrapper'
import { randomUUID } from 'node:crypto'
interface Internal_MapKey {
timeout: number
createtime: number
func: (...arg: any[]) => any
checker: ((...args: any[]) => boolean) | undefined
}
export class ListenerClassBase {
[key: string]: string
}
export interface ListenerIBase {
new(listener: any): ListenerClassBase
}
export class NTEventWrapper {
private ListenerMap: { [key: string]: ListenerIBase } | undefined//ListenerName-Unique -> Listener构造函数
private WrapperSession: NodeIQQNTWrapperSession | undefined//WrapperSession
private ListenerManger: Map<string, ListenerClassBase> = new Map<string, ListenerClassBase>() //ListenerName-Unique -> Listener实例
private EventTask = new Map<string, Map<string, Map<string, Internal_MapKey>>>()//tasks ListenerMainName -> ListenerSubName-> uuid -> {timeout,createtime,func}
constructor() {
}
createProxyDispatch(ListenerMainName: string) {
const current = this
return new Proxy({}, {
get(target: any, prop: any, receiver: any) {
// console.log('get', prop, typeof target[prop])
if (typeof target[prop] === 'undefined') {
// 如果方法不存在返回一个函数这个函数调用existentMethod
return (...args: any[]) => {
current.DispatcherListener.apply(current, [ListenerMainName, prop, ...args]).then()
}
}
// 如果方法存在,正常返回
return Reflect.get(target, prop, receiver)
}
})
}
init({ ListenerMap, WrapperSession }: { ListenerMap: { [key: string]: typeof ListenerClassBase }, WrapperSession: NodeIQQNTWrapperSession }) {
this.ListenerMap = ListenerMap
this.WrapperSession = WrapperSession
}
CreatEventFunction<T extends (...args: any) => any>(eventName: string): T | undefined {
const eventNameArr = eventName.split('/')
type eventType = {
[key: string]: () => { [key: string]: (...params: Parameters<T>) => Promise<ReturnType<T>> }
}
if (eventNameArr.length > 1) {
const serviceName = 'get' + eventNameArr[0].replace('NodeIKernel', '')
const eventName = eventNameArr[1]
//getNodeIKernelGroupListener,GroupService
//console.log('2', eventName)
const services = (this.WrapperSession as unknown as eventType)[serviceName]()
let event = services[eventName]
//重新绑定this
event = event.bind(services)
if (event) {
return event as T
}
return undefined
}
}
CreatListenerFunction<T>(listenerMainName: string, uniqueCode: string = ''): T {
const ListenerType = this.ListenerMap![listenerMainName]
let Listener = this.ListenerManger.get(listenerMainName + uniqueCode)
if (!Listener && ListenerType) {
Listener = new ListenerType(this.createProxyDispatch(listenerMainName))
const ServiceSubName = listenerMainName.match(/^NodeIKernel(.*?)Listener$/)![1]
const Service = 'NodeIKernel' + ServiceSubName + 'Service/addKernel' + ServiceSubName + 'Listener'
const addfunc = this.CreatEventFunction<(listener: T) => number>(Service)
addfunc!(Listener as T)
//console.log(addfunc!(Listener as T))
this.ListenerManger.set(listenerMainName + uniqueCode, Listener)
}
return Listener as T
}
//统一回调清理事件
async DispatcherListener(ListenerMainName: string, ListenerSubName: string, ...args: any[]) {
//console.log("[EventDispatcher]",ListenerMainName, ListenerSubName, ...args)
this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.forEach((task, uuid) => {
//console.log(task.func, uuid, task.createtime, task.timeout)
if (task.createtime + task.timeout < Date.now()) {
this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.delete(uuid)
return
}
if (task.checker && task.checker(...args)) {
task.func(...args)
}
})
}
async CallNoListenerEvent<EventType extends (...args: any[]) => Promise<any> | any>(EventName = '', timeout: number = 3000, ...args: Parameters<EventType>) {
return new Promise<Awaited<ReturnType<EventType>>>(async (resolve, reject) => {
const EventFunc = this.CreatEventFunction<EventType>(EventName)
let complete = false
const Timeouter = setTimeout(() => {
if (!complete) {
reject(new Error('NTEvent EventName:' + EventName + ' timeout'))
}
}, timeout)
const retData = await EventFunc!(...args)
complete = true
resolve(retData)
})
}
async RegisterListen<ListenerType extends (...args: any[]) => void>(ListenerName = '', waitTimes = 1, timeout = 5000, checker: (...args: Parameters<ListenerType>) => boolean) {
return new Promise<Parameters<ListenerType>>((resolve, reject) => {
const ListenerNameList = ListenerName.split('/')
const ListenerMainName = ListenerNameList[0]
const ListenerSubName = ListenerNameList[1]
const id = randomUUID()
let complete = 0
let retData: Parameters<ListenerType> | undefined = undefined
const databack = () => {
if (complete == 0) {
reject(new Error(' ListenerName:' + ListenerName + ' timeout'))
} else {
resolve(retData!)
}
}
const Timeouter = setTimeout(databack, timeout)
const eventCallbak = {
timeout: timeout,
createtime: Date.now(),
checker: checker,
func: (...args: Parameters<ListenerType>) => {
complete++
retData = args
if (complete >= waitTimes) {
clearTimeout(Timeouter)
databack()
}
}
}
if (!this.EventTask.get(ListenerMainName)) {
this.EventTask.set(ListenerMainName, new Map())
}
if (!(this.EventTask.get(ListenerMainName)?.get(ListenerSubName))) {
this.EventTask.get(ListenerMainName)?.set(ListenerSubName, new Map())
}
this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.set(id, eventCallbak)
this.CreatListenerFunction(ListenerMainName)
})
}
async CallNormalEvent<EventType extends (...args: any[]) => Promise<any>, ListenerType extends (...args: any[]) => void>
(EventName = '', ListenerName = '', waitTimes = 1, timeout: number = 3000, checker: (...args: Parameters<ListenerType>) => boolean, ...args: Parameters<EventType>) {
return new Promise<[EventRet: Awaited<ReturnType<EventType>>, ...Parameters<ListenerType>]>(async (resolve, reject) => {
const id = randomUUID()
let complete = 0
let retData: Parameters<ListenerType> | undefined = undefined
let retEvent: any = {}
const databack = () => {
if (complete == 0) {
reject(new Error('Timeout: NTEvent EventName:' + EventName + ' ListenerName:' + ListenerName + ' EventRet:\n' + JSON.stringify(retEvent, null, 4) + '\n'))
} else {
resolve([retEvent as Awaited<ReturnType<EventType>>, ...retData!])
}
}
const ListenerNameList = ListenerName.split('/')
const ListenerMainName = ListenerNameList[0]
const ListenerSubName = ListenerNameList[1]
const Timeouter = setTimeout(databack, timeout)
const eventCallbak = {
timeout: timeout,
createtime: Date.now(),
checker: checker,
func: (...args: any[]) => {
complete++
//console.log('func', ...args)
retData = args as Parameters<ListenerType>
if (complete >= waitTimes) {
clearTimeout(Timeouter)
databack()
}
}
}
if (!this.EventTask.get(ListenerMainName)) {
this.EventTask.set(ListenerMainName, new Map())
}
if (!(this.EventTask.get(ListenerMainName)?.get(ListenerSubName))) {
this.EventTask.get(ListenerMainName)?.set(ListenerSubName, new Map())
}
this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.set(id, eventCallbak)
this.CreatListenerFunction(ListenerMainName)
const EventFunc = this.CreatEventFunction<EventType>(EventName)
retEvent = await EventFunc!(...(args as any[]))
})
}
}
export const NTEventDispatch = new NTEventWrapper()
// 示例代码 快速创建事件
// let NTEvent = new NTEventWrapper()
// let TestEvent = NTEvent.CreatEventFunction<(force: boolean) => Promise<Number>>('NodeIKernelProfileLikeService/GetTest')
// if (TestEvent) {
// TestEvent(true)
// }
// 示例代码 快速创建监听Listener类
// let NTEvent = new NTEventWrapper()
// NTEvent.CreatListenerFunction<NodeIKernelMsgListener>('NodeIKernelMsgListener', 'core')
// 调用接口
//let NTEvent = new NTEventWrapper()
//let ret = await NTEvent.CallNormalEvent<(force: boolean) => Promise<Number>, (data1: string, data2: number) => void>('NodeIKernelProfileLikeService/GetTest', 'NodeIKernelMsgListener/onAddSendMsg', 1, 3000, true)
// 注册监听 解除监听
// NTEventDispatch.RigisterListener('NodeIKernelMsgListener/onAddSendMsg','core',cb)
// NTEventDispatch.UnRigisterListener('NodeIKernelMsgListener/onAddSendMsg','core')
// let GetTest = NTEventDispatch.CreatEvent('NodeIKernelProfileLikeService/GetTest','NodeIKernelMsgListener/onAddSendMsg',Mode)
// GetTest('test')
// always模式
// NTEventDispatch.CreatEvent('NodeIKernelProfileLikeService/GetTest','NodeIKernelMsgListener/onAddSendMsg',Mode,(...args:any[])=>{ console.log(args) })

View File

@@ -0,0 +1,83 @@
import path from 'node:path'
import fs from 'node:fs'
import os from 'node:os'
import { systemPlatform } from './system'
export const exePath = process.execPath
function getPKGPath() {
let p = path.join(path.dirname(exePath), 'resources', 'app', 'package.json')
if (systemPlatform === 'darwin') {
p = path.join(path.dirname(path.dirname(exePath)), 'Resources', 'app', 'package.json')
}
return p
}
export const pkgInfoPath = getPKGPath()
let configVersionInfoPath: string
if (os.platform() !== 'linux') {
configVersionInfoPath = path.join(path.dirname(exePath), 'resources', 'app', 'versions', 'config.json')
}
else {
const userPath = os.homedir()
const appDataPath = path.resolve(userPath, './.config/QQ')
configVersionInfoPath = path.resolve(appDataPath, './versions/config.json')
}
if (typeof configVersionInfoPath !== 'string') {
throw new Error('Something went wrong when load QQ info path')
}
export { configVersionInfoPath }
type QQPkgInfo = {
version: string;
buildVersion: string;
platform: string;
eleArch: string;
}
type QQVersionConfigInfo = {
baseVersion: string;
curVersion: string;
prevVersion: string;
onErrorVersions: Array<any>;
buildId: string;
}
let _qqVersionConfigInfo: QQVersionConfigInfo = {
'baseVersion': '9.9.9-23361',
'curVersion': '9.9.9-23361',
'prevVersion': '',
'onErrorVersions': [],
'buildId': '23361',
}
if (fs.existsSync(configVersionInfoPath)) {
try {
const _ = JSON.parse(fs.readFileSync(configVersionInfoPath).toString())
_qqVersionConfigInfo = Object.assign(_qqVersionConfigInfo, _)
} catch (e) {
console.error('Load QQ version config info failed, Use default version', e)
}
}
export const qqVersionConfigInfo: QQVersionConfigInfo = _qqVersionConfigInfo
export const qqPkgInfo: QQPkgInfo = require(pkgInfoPath)
// platform_type: 3,
// app_type: 4,
// app_version: '9.9.9-23159',
// qua: 'V1_WIN_NQ_9.9.9_23159_GW_B',
// appid: '537213764',
// platVer: '10.0.26100',
// clientVer: '9.9.9-23159',
let _appid: string = '537213803' // 默认为 Windows 平台的 appid
if (systemPlatform === 'linux') {
_appid = '537213827'
}
// todo: mac 平台的 appid
export const appid = _appid
export const isQQ998: boolean = qqPkgInfo.buildVersion >= '22106'

View File

@@ -1,12 +1,12 @@
import fs from 'fs'
import { encode, getDuration, getWavFileInfo, isWav } from 'silk-wasm'
import fsPromise from 'fs/promises'
import { decode, encode, getDuration, getWavFileInfo, isWav, isSilk } from 'silk-wasm'
import { log } from './log'
import path from 'node:path'
import { DATA_DIR, TEMP_DIR } from './index'
import { v4 as uuidv4 } from 'uuid'
import { TEMP_DIR } from './index'
import { getConfigUtil } from '../config'
import { spawn } from 'node:child_process'
import { randomUUID } from 'node:crypto'
export async function encodeSilk(filePath: string) {
function getFileHeader(filePath: string) {
@@ -60,10 +60,11 @@ export async function encodeSilk(filePath: string) {
// }
try {
const pttPath = path.join(TEMP_DIR, uuidv4())
if (getFileHeader(filePath) !== '02232153494c4b') {
const file = await fsPromise.readFile(filePath)
const pttPath = path.join(TEMP_DIR, randomUUID())
if (!isSilk(file)) {
log(`语音文件${filePath}需要转换成silk`)
const _isWav = await isWavFile(filePath)
const _isWav = isWav(file)
const pcmPath = pttPath + '.pcm'
let sampleRate = 0
const convert = () => {
@@ -79,7 +80,8 @@ export async function encodeSilk(filePath: string) {
if (code == null || EXIT_CODES.includes(code)) {
sampleRate = 24000
const data = fs.readFileSync(pcmPath)
fs.unlink(pcmPath, (err) => {})
fs.unlink(pcmPath, (err) => {
})
return resolve(data)
}
log(`FFmpeg exit: code=${code ?? 'unknown'} sig=${signal ?? 'unknown'}`)
@@ -91,7 +93,7 @@ export async function encodeSilk(filePath: string) {
if (!_isWav) {
input = await convert()
} else {
input = fs.readFileSync(filePath)
input = file
const allowSampleRate = [8000, 12000, 16000, 24000, 32000, 44100, 48000]
const { fmt } = getWavFileInfo(input)
// log(`wav文件信息`, fmt)
@@ -108,11 +110,11 @@ export async function encodeSilk(filePath: string) {
duration: silk.duration / 1000,
}
} else {
const silk = fs.readFileSync(filePath)
const silk = file
let duration = 0
try {
duration = getDuration(silk) / 1000
} catch (e) {
} catch (e: any) {
log('获取语音文件时长失败, 使用文件大小推测时长', filePath, e.stack)
duration = await guessDuration(filePath)
}
@@ -123,8 +125,46 @@ export async function encodeSilk(filePath: string) {
duration,
}
}
} catch (error) {
} catch (error: any) {
log('convert silk failed', error.stack)
return {}
}
}
export async function decodeSilk(inputFilePath: string, outFormat: 'mp3' | 'amr' | 'wma' | 'm4a' | 'spx' | 'ogg' | 'wav' | 'flac' = 'mp3') {
const silkArrayBuffer = await fsPromise.readFile(inputFilePath)
const data = (await decode(silkArrayBuffer, 24000)).data
const fileName = path.join(TEMP_DIR, path.basename(inputFilePath))
const outPCMPath = fileName + '.pcm'
const outFilePath = fileName + '.' + outFormat
await fsPromise.writeFile(outPCMPath, data)
const convert = () => {
return new Promise<string>((resolve, reject) => {
const ffmpegPath = getConfigUtil().getConfig().ffmpeg || process.env.FFMPEG_PATH || 'ffmpeg'
const cp = spawn(ffmpegPath, [
'-y',
'-f', 's16le', // PCM format
'-ar', '24000', // Sample rate
'-ac', '1', // Number of audio channels
'-i', outPCMPath,
outFilePath,
])
cp.on('error', (err) => {
log(`FFmpeg处理转换出错: `, err.message)
return reject(err)
})
cp.on('exit', (code, signal) => {
const EXIT_CODES = [0, 255]
if (code == null || EXIT_CODES.includes(code)) {
fs.unlink(outPCMPath, (err) => {
})
return resolve(outFilePath)
}
const exitErr = `FFmpeg exit: code=${code ?? 'unknown'} sig=${signal ?? 'unknown'}`
log(exitErr)
reject(Error(`FFmpeg处理转换失败,${exitErr}`))
})
})
}
return convert()
}

View File

@@ -1,13 +1,10 @@
import fs from 'fs'
import fsPromise from 'fs/promises'
import crypto from 'crypto'
import util from 'util'
import fs from 'node:fs'
import fsPromise from 'node:fs/promises'
import path from 'node:path'
import { v4 as uuidv4 } from 'uuid'
import { log, TEMP_DIR } from './index'
import { dbUtil } from '../db'
import * as fileType from 'file-type'
import { net } from 'electron'
import { randomUUID, createHash } from 'node:crypto'
export function isGIF(path: string) {
const buffer = Buffer.alloc(4)
@@ -37,7 +34,6 @@ export function checkFileReceived(path: string, timeout: number = 3000): Promise
}
export async function file2base64(path: string) {
const readFile = util.promisify(fs.readFile)
let result = {
err: '',
data: '',
@@ -53,10 +49,10 @@ export async function file2base64(path: string) {
result.err = e.toString()
return result
}
const data = await readFile(path)
const data = await fsPromise.readFile(path)
// 转换为Base64编码
result.data = data.toString('base64')
} catch (err) {
} catch (err: any) {
result.err = err.toString()
}
return result
@@ -66,7 +62,7 @@ export function calculateFileMD5(filePath: string): Promise<string> {
return new Promise((resolve, reject) => {
// 创建一个流式读取器
const stream = fs.createReadStream(filePath)
const hash = crypto.createHash('md5')
const hash = createHash('md5')
stream.on('data', (data: Buffer) => {
// 当读取到数据时,更新哈希对象的状态
@@ -91,7 +87,6 @@ export interface HttpDownloadOptions {
headers?: Record<string, string> | string
}
export async function httpDownload(options: string | HttpDownloadOptions): Promise<Buffer> {
let chunks: Buffer[] = []
let url: string
let headers: Record<string, string> = {
'User-Agent':
@@ -109,12 +104,10 @@ export async function httpDownload(options: string | HttpDownloadOptions): Promi
}
}
}
const fetchRes = await net.fetch(url, headers)
const fetchRes = await fetch(url, { headers })
if (!fetchRes.ok) throw new Error(`下载文件失败: ${fetchRes.statusText}`)
const blob = await fetchRes.blob()
let buffer = await blob.arrayBuffer()
return Buffer.from(buffer)
return Buffer.from(await fetchRes.arrayBuffer())
}
type Uri2LocalRes = {
@@ -126,7 +119,7 @@ type Uri2LocalRes = {
isLocal: boolean
}
export async function uri2local(uri: string, fileName: string = null): Promise<Uri2LocalRes> {
export async function uri2local(uri: string, fileName: string | null = null): Promise<Uri2LocalRes> {
let res = {
success: false,
errMsg: '',
@@ -136,13 +129,13 @@ export async function uri2local(uri: string, fileName: string = null): Promise<U
isLocal: false,
}
if (!fileName) {
fileName = uuidv4()
fileName = randomUUID()
}
let filePath = path.join(TEMP_DIR, fileName)
let url = null
let url: URL | null = null
try {
url = new URL(uri)
} catch (e) {
} catch (e: any) {
res.errMsg = `uri ${uri} 解析失败,` + e.toString() + ` 可能${uri}不存在`
return res
}
@@ -153,17 +146,17 @@ export async function uri2local(uri: string, fileName: string = null): Promise<U
let base64Data = uri.split('base64://')[1]
try {
const buffer = Buffer.from(base64Data, 'base64')
fs.writeFileSync(filePath, buffer)
await fsPromise.writeFile(filePath, buffer)
} catch (e: any) {
res.errMsg = `base64文件下载失败,` + e.toString()
return res
}
} else if (url.protocol == 'http:' || url.protocol == 'https:') {
// 下载文件
let buffer: Buffer = null
let buffer: Buffer | null = null
try {
buffer = await httpDownload(uri)
} catch (e) {
} catch (e: any) {
res.errMsg = `${url}下载失败,` + e.toString()
return res
}
@@ -176,9 +169,10 @@ export async function uri2local(uri: string, fileName: string = null): Promise<U
// res.ext = pathInfo.ext
}
}
fileName = fileName.replace(/[/\\:*?"<>|]/g, '_')
res.fileName = fileName
filePath = path.join(TEMP_DIR, uuidv4() + fileName)
fs.writeFileSync(filePath, buffer)
filePath = path.join(TEMP_DIR, randomUUID() + fileName)
await fsPromise.writeFile(filePath, buffer)
} catch (e: any) {
res.errMsg = `${url}下载失败,` + e.toString()
return res
@@ -214,10 +208,10 @@ export async function uri2local(uri: string, fileName: string = null): Promise<U
// }
if (!res.isLocal && !res.ext) {
try {
let ext: string = (await fileType.fileTypeFromFile(filePath)).ext
const ext = (await fileType.fileTypeFromFile(filePath))?.ext
if (ext) {
log('获取文件类型', ext, filePath)
fs.renameSync(filePath, filePath + `.${ext}`)
await fsPromise.rename(filePath, filePath + `.${ext}`)
filePath += `.${ext}`
res.fileName += `.${ext}`
res.ext = ext

View File

@@ -41,7 +41,7 @@ export function mergeNewProperties(newObj: any, oldObj: any) {
})
}
export function isNull(value: any) {
export function isNull(value: any): value is null | undefined | void {
return value === undefined || value === null
}
@@ -65,3 +65,33 @@ export function wrapText(str: string, maxLength: number): string {
return result
}
/**
* 函数缓存装饰器根据方法名、参数、自定义key生成缓存键在一定时间内返回缓存结果
* @param ttl 超时时间,单位毫秒
* @param customKey 自定义缓存键前缀,可为空,防止方法名参数名一致时导致缓存键冲突
* @returns 处理后缓存或调用原方法的结果
*/
export function cacheFunc(ttl: number, customKey: string = '') {
const cache = new Map<string, { expiry: number; value: any }>();
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor): PropertyDescriptor {
const originalMethod = descriptor.value;
const className = target.constructor.name; // 获取类名
const methodName = propertyKey; // 获取方法名
descriptor.value = async function (...args: any[]) {
const cacheKey = `${customKey}${className}.${methodName}:${JSON.stringify(args)}`;
const cached = cache.get(cacheKey);
if (cached && cached.expiry > Date.now()) {
return cached.value;
} else {
const result = await originalMethod.apply(this, args);
cache.set(cacheKey, { value: result, expiry: Date.now() + ttl });
return result;
}
};
return descriptor;
};
}

View File

@@ -5,7 +5,7 @@ export * from './file'
export * from './helper'
export * from './log'
export * from './qqlevel'
export * from './qqpkg'
export * from './QQBasicInfo'
export * from './upgrade'
export const DATA_DIR = global.LiteLoader.plugins['LLOneBot'].path.data
export const TEMP_DIR = path.join(DATA_DIR, 'temp')
@@ -16,3 +16,4 @@ if (!fs.existsSync(TEMP_DIR)) {
export { getVideoInfo } from './video'
export { checkFfmpeg } from './video'
export { encodeSilk } from './audio'
export { isQQ998 } from './QQBasicInfo'

View File

@@ -1,12 +0,0 @@
import path from 'path'
type QQPkgInfo = {
version: string
buildVersion: string
platform: string
eleArch: string
}
export const qqPkgInfo: QQPkgInfo = require(path.join(process.resourcesPath, 'app/package.json'))
export const isQQ998: boolean = qqPkgInfo.buildVersion >= '22106'

107
src/common/utils/request.ts Normal file
View File

@@ -0,0 +1,107 @@
import https from 'node:https';
import http from 'node:http';
import { log } from '@/common/utils/log'
export class RequestUtil {
// 适用于获取服务器下发cookies时获取仅GET
static async HttpsGetCookies(url: string): Promise<{ [key: string]: string }> {
const client = url.startsWith('https') ? https : http;
return new Promise((resolve, reject) => {
client.get(url, (res) => {
let cookies: { [key: string]: string } = {};
const handleRedirect = (res: http.IncomingMessage) => {
//console.log(res.headers.location);
if (res.statusCode === 301 || res.statusCode === 302) {
if (res.headers.location) {
const redirectUrl = new URL(res.headers.location, url);
RequestUtil.HttpsGetCookies(redirectUrl.href).then((redirectCookies) => {
// 合并重定向过程中的cookies
log('redirectCookies', redirectCookies)
cookies = { ...cookies, ...redirectCookies };
resolve(cookies);
});
} else {
resolve(cookies);
}
} else {
resolve(cookies);
}
};
res.on('data', () => { }); // Necessary to consume the stream
res.on('end', () => {
handleRedirect(res);
});
if (res.headers['set-cookie']) {
// console.log(res.headers['set-cookie']);
log('set-cookie', url, res.headers['set-cookie']);
res.headers['set-cookie'].forEach((cookie) => {
const parts = cookie.split(';')[0].split('=');
const key = parts[0];
const value = parts[1];
if (key && value && key.length > 0 && value.length > 0) {
cookies[key] = value;
}
});
}
}).on('error', (err) => {
reject(err);
});
});
}
// 请求和回复都是JSON data传原始内容 自动编码json
static async HttpGetJson<T>(url: string, method: string = 'GET', data?: any, headers: Record<string, string> = {}, isJsonRet: boolean = true, isArgJson: boolean = true): Promise<T> {
let option = new URL(url);
const protocol = url.startsWith('https://') ? https : http;
const options = {
hostname: option.hostname,
port: option.port,
path: option.href,
method: method,
headers: headers
};
return new Promise((resolve, reject) => {
const req = protocol.request(options, (res: any) => {
let responseBody = '';
res.on('data', (chunk: string | Buffer) => {
responseBody += chunk.toString();
});
res.on('end', () => {
try {
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
if (isJsonRet) {
const responseJson = JSON.parse(responseBody);
resolve(responseJson as T);
} else {
resolve(responseBody as T);
}
} else {
reject(new Error(`Unexpected status code: ${res.statusCode}`));
}
} catch (parseError) {
reject(parseError);
}
});
});
req.on('error', (error: any) => {
reject(error);
});
if (method === 'POST' || method === 'PUT' || method === 'PATCH') {
if (isArgJson) {
req.write(JSON.stringify(data));
} else {
req.write(data);
}
}
req.end();
});
}
// 请求返回都是原始内容
static async HttpGetText(url: string, method: string = 'GET', data?: any, headers: Record<string, string> = {}) {
return this.HttpGetJson<string>(url, method, data, headers, false, false);
}
}

View File

@@ -0,0 +1,10 @@
import os from 'node:os';
import path from 'node:path';
export const systemPlatform = os.platform();
export const cpuArch = os.arch();
export const systemVersion = os.release();
// export const hostname = os.hostname(); // win7不支持
const homeDir = os.homedir();
export const downloadsPath = path.join(homeDir, 'Downloads');
export const systemName = os.type();

View File

@@ -5,7 +5,7 @@ import { copyFolder, httpDownload, log, PLUGIN_DIR, TEMP_DIR } from '.'
import compressing from 'compressing'
const downloadMirrorHosts = ['https://mirror.ghproxy.com/']
const checkVersionMirrorHosts = ['https://521github.com']
const checkVersionMirrorHosts = ['https://kkgithub.com']
export async function checkNewVersion() {
const latestVersionText = await getRemoteVersion()
@@ -91,7 +91,7 @@ export async function getRemoteVersionByMirror(mirrorGithub: string) {
releasePage = (await httpDownload(mirrorGithub + '/LLOneBot/LLOneBot/releases')).toString()
// log("releasePage", releasePage);
if (releasePage === 'error') return ''
return releasePage.match(new RegExp('(?<=(tag/v)).*?(?=("))'))[0]
return releasePage.match(new RegExp('(?<=(tag/v)).*?(?=("))'))?.[0]
} catch {}
return ''
}

View File

@@ -31,10 +31,10 @@ export async function getVideoInfo(filePath: string) {
console.log('未找到视频流信息。')
}
resolve({
width: videoStream.width,
height: videoStream.height,
time: parseInt(videoStream.duration),
format: metadata.format.format_name,
width: videoStream?.width!,
height: videoStream?.height!,
time: parseInt(videoStream?.duration!),
format: metadata.format.format_name!,
size,
filePath,
})
@@ -67,7 +67,7 @@ export async function encodeMp4(filePath: string) {
return videoInfo
}
export function checkFfmpeg(newPath: string = null): Promise<boolean> {
export function checkFfmpeg(newPath: string | null = null): Promise<boolean> {
return new Promise((resolve, reject) => {
log('开始检查ffmpeg', newPath)
if (newPath) {

2
src/global.d.ts vendored
View File

@@ -3,6 +3,6 @@ import { type LLOneBot } from './preload'
declare global {
interface Window {
llonebot: LLOneBot
LiteLoader: any
LiteLoader: Record<string, any>
}
}

View File

@@ -13,7 +13,7 @@ import {
CHANNEL_UPDATE,
} from '../common/channels'
import { ob11WebsocketServer } from '../onebot11/server/ws/WebsocketServer'
import { DATA_DIR } from '../common/utils'
import { DATA_DIR, qqPkgInfo } from '../common/utils'
import {
friendRequests,
getFriend,
@@ -25,7 +25,7 @@ import {
selfInfo,
uidMaps,
} from '../common/data'
import { hookNTQQApiCall, hookNTQQApiReceive, ReceiveCmdS, registerReceiveHook } from '../ntqqapi/hook'
import { hookNTQQApiCall, hookNTQQApiReceive, ReceiveCmdS, registerReceiveHook, startHook } from '../ntqqapi/hook'
import { OB11Constructor } from '../onebot11/constructor'
import {
ChatType,
@@ -36,11 +36,8 @@ import {
RawMessage,
} from '../ntqqapi/types'
import { httpHeart, ob11HTTPServer } from '../onebot11/server/http'
import { OB11FriendRecallNoticeEvent } from '../onebot11/event/notice/OB11FriendRecallNoticeEvent'
import { OB11GroupRecallNoticeEvent } from '../onebot11/event/notice/OB11GroupRecallNoticeEvent'
import { postOB11Event } from '../onebot11/server/postOB11Event'
import { postOb11Event } from '../onebot11/server/post-ob11-event'
import { ob11ReverseWebsockets } from '../onebot11/server/ws/ReverseWebsocket'
import { OB11GroupAdminNoticeEvent } from '../onebot11/event/notice/OB11GroupAdminNoticeEvent'
import { OB11GroupRequestEvent } from '../onebot11/event/request/OB11GroupRequest'
import { OB11FriendRequestEvent } from '../onebot11/event/request/OB11FriendRequest'
import * as path from 'node:path'
@@ -48,15 +45,15 @@ import { dbUtil } from '../common/db'
import { setConfig } from './setConfig'
import { NTQQUserApi } from '../ntqqapi/api/user'
import { NTQQGroupApi } from '../ntqqapi/api/group'
import { crychic } from '../ntqqapi/external/crychic'
import { OB11FriendPokeEvent, OB11GroupPokeEvent } from '../onebot11/event/notice/OB11PokeEvent'
import { checkNewVersion, upgradeLLOneBot } from '../common/utils/upgrade'
import { log } from '../common/utils/log'
import { getConfigUtil } from '../common/config'
import { checkFfmpeg } from '../common/utils/video'
import { GroupDecreaseSubType, OB11GroupDecreaseEvent } from '../onebot11/event/notice/OB11GroupDecreaseEvent'
let running = false
import '../ntqqapi/wrapper'
import { sentMessages } from '@/ntqqapi/api'
import { NTEventDispatch } from '../common/utils/EventTask'
import { wrapperApi, wrapperConstructor } from '../ntqqapi/wrapper'
let mainWindow: BrowserWindow | null = null
@@ -126,7 +123,7 @@ function onLoad() {
return
}
dialog
.showMessageBox(mainWindow, {
.showMessageBox(mainWindow!, {
type: 'question',
buttons: ['确认', '取消'],
defaultId: 0, // 默认选中的按钮0 代表第一个按钮,即 "确认"
@@ -141,7 +138,8 @@ function onLoad() {
.catch((e) => {
log('保存设置失败', e.stack)
})
} else {
}
else {
}
})
.catch((err) => {
@@ -168,12 +166,8 @@ function onLoad() {
OB11Constructor.message(message)
.then((msg) => {
if (debug) {
msg.raw = message
} else {
if (msg.message.length === 0) {
return
}
if (!debug && msg.message.length === 0) {
return
}
const isSelfMsg = msg.user_id.toString() == selfInfo.uin
if (isSelfMsg && !reportSelfMessage) {
@@ -182,81 +176,70 @@ function onLoad() {
if (isSelfMsg) {
msg.target_id = parseInt(message.peerUin)
}
postOB11Event(msg)
postOb11Event(msg)
// log("post msg", msg)
})
.catch((e) => log('constructMessage error: ', e.stack.toString()))
OB11Constructor.GroupEvent(message).then((groupEvent) => {
if (groupEvent) {
// log("post group event", groupEvent);
postOB11Event(groupEvent)
postOb11Event(groupEvent)
}
})
OB11Constructor.FriendAddEvent(message).then((friendAddEvent) => {
if (friendAddEvent) {
// log("post friend add event", friendAddEvent);
postOB11Event(friendAddEvent)
OB11Constructor.PrivateEvent(message).then((privateEvent) => {
log(message)
if (privateEvent) {
// log("post private event", privateEvent);
postOb11Event(privateEvent)
}
})
// OB11Constructor.FriendAddEvent(message).then((friendAddEvent) => {
// log(message)
// if (friendAddEvent) {
// // log("post friend add event", friendAddEvent);
// postOb11Event(friendAddEvent)
// }
// })
}
}
async function startReceiveHook() {
if (getConfigUtil().getConfig().enablePoke) {
crychic.loadNode()
crychic.registerPokeHandler((id, isGroup) => {
log(`收到戳一戳消息了!是否群聊:${isGroup}id:${id}`)
let pokeEvent: OB11FriendPokeEvent | OB11GroupPokeEvent
if (isGroup) {
pokeEvent = new OB11GroupPokeEvent(parseInt(id))
} else {
pokeEvent = new OB11FriendPokeEvent(parseInt(id))
}
postOB11Event(pokeEvent)
})
}
startHook()
registerReceiveHook<{
msgList: Array<RawMessage>
}>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], async (payload) => {
try {
await postReceiveMsg(payload.msgList)
} catch (e) {
} catch (e: any) {
log('report message error: ', e.stack.toString())
}
})
const recallMsgIds: string[] = [] // 避免重复上报
registerReceiveHook<{ msgList: Array<RawMessage> }>([ReceiveCmdS.UPDATE_MSG], async (payload) => {
for (const message of payload.msgList) {
// log("message update", message)
const sentMessage = sentMessages[message.msgId]
if (sentMessage) {
Object.assign(sentMessage, message)
}
log('message update', message.msgId, message)
if (message.recallTime != '0') {
//todo: 这个判断方法不太好,应该使用灰色消息元素来判断
// 撤回消息上报
if (recallMsgIds.includes(message.msgId)) {
continue
}
recallMsgIds.push(message.msgId)
const oriMessage = await dbUtil.getMsgByLongId(message.msgId)
if (!oriMessage) {
continue
}
oriMessage.recallTime = message.recallTime
dbUtil.updateMsg(oriMessage).then()
if (message.chatType == ChatType.friend) {
const friendRecallEvent = new OB11FriendRecallNoticeEvent(
parseInt(message.senderUin),
oriMessage.msgShortId,
)
postOB11Event(friendRecallEvent)
} else if (message.chatType == ChatType.group) {
let operatorId = message.senderUin
for (const element of message.elements) {
const operatorUid = element.grayTipElement?.revokeElement.operatorUid
const operator = await getGroupMember(message.peerUin, operatorUid)
operatorId = operator.uin
message.msgShortId = oriMessage.msgShortId
OB11Constructor.RecallEvent(message).then((recallEvent) => {
if (recallEvent) {
log('post recall event', recallEvent)
postOb11Event(recallEvent)
}
const groupRecallEvent = new OB11GroupRecallNoticeEvent(
parseInt(message.peerUin),
parseInt(message.senderUin),
parseInt(operatorId),
oriMessage.msgShortId,
)
postOB11Event(groupRecallEvent)
}
})
// 不让入库覆盖原来消息,不然就获取不到撤回的消息内容了
continue
}
@@ -271,7 +254,7 @@ function onLoad() {
// log("reportSelfMessage", payload)
try {
await postReceiveMsg([payload.msgRecord])
} catch (e) {
} catch (e: any) {
log('report self message error: ', e.stack.toString())
}
})
@@ -311,23 +294,36 @@ function onLoad() {
// if (notify.user2.uid) {
// member2 = await getGroupMember(notify.group.groupCode, null, notify.user2.uid);
// }
if ([GroupNotifyTypes.ADMIN_SET, GroupNotifyTypes.ADMIN_UNSET, GroupNotifyTypes.ADMIN_UNSET_OTHER].includes(notify.type)) {
const member1 = await getGroupMember(notify.group.groupCode, notify.user1.uid)
log('有管理员变动通知')
refreshGroupMembers(notify.group.groupCode).then()
let groupAdminNoticeEvent = new OB11GroupAdminNoticeEvent()
groupAdminNoticeEvent.group_id = parseInt(notify.group.groupCode)
log('开始获取变动的管理员')
if (member1) {
log('变动管理员获取成功')
groupAdminNoticeEvent.user_id = parseInt(member1.uin)
groupAdminNoticeEvent.sub_type = [GroupNotifyTypes.ADMIN_UNSET, GroupNotifyTypes.ADMIN_UNSET_OTHER].includes(notify.type) ? 'unset' : 'set'
// member1.role = notify.type == GroupNotifyTypes.ADMIN_SET ? GroupMemberRole.admin : GroupMemberRole.normal;
postOB11Event(groupAdminNoticeEvent, true)
} else {
log('获取群通知的成员信息失败', notify, getGroup(notify.group.groupCode))
}
} else if (notify.type == GroupNotifyTypes.MEMBER_EXIT || notify.type == GroupNotifyTypes.KICK_MEMBER) {
// 原本的群管变更通知事件处理
// if (
// [GroupNotifyTypes.ADMIN_SET, GroupNotifyTypes.ADMIN_UNSET, GroupNotifyTypes.ADMIN_UNSET_OTHER].includes(
// notify.type,
// )
// ) {
// const member1 = await getGroupMember(notify.group.groupCode, notify.user1.uid)
// log('有管理员变动通知')
// refreshGroupMembers(notify.group.groupCode).then()
// let groupAdminNoticeEvent = new OB11GroupAdminNoticeEvent()
// groupAdminNoticeEvent.group_id = parseInt(notify.group.groupCode)
// log('开始获取变动的管理员')
// if (member1) {
// log('变动管理员获取成功')
// groupAdminNoticeEvent.user_id = parseInt(member1.uin)
// groupAdminNoticeEvent.sub_type = [
// GroupNotifyTypes.ADMIN_UNSET,
// GroupNotifyTypes.ADMIN_UNSET_OTHER,
// ].includes(notify.type)
// ? 'unset'
// : 'set'
// // member1.role = notify.type == GroupNotifyTypes.ADMIN_SET ? GroupMemberRole.admin : GroupMemberRole.normal;
// postOb11Event(groupAdminNoticeEvent, true)
// }
// else {
// log('获取群通知的成员信息失败', notify, getGroup(notify.group.groupCode))
// }
// }
// else
if (notify.type == GroupNotifyTypes.MEMBER_EXIT || notify.type == GroupNotifyTypes.KICK_MEMBER) {
log('有成员退出通知', notify)
try {
const member1 = await NTQQUserApi.getUserDetailInfo(notify.user1.uid)
@@ -336,7 +332,7 @@ function onLoad() {
if (notify.user2.uid) {
// 是被踢的
const member2 = await getGroupMember(notify.group.groupCode, notify.user2.uid)
operatorId = member2.uin
operatorId = member2?.uin!
subType = 'kick'
}
let groupDecreaseEvent = new OB11GroupDecreaseEvent(
@@ -345,63 +341,86 @@ function onLoad() {
parseInt(operatorId),
subType,
)
postOB11Event(groupDecreaseEvent, true)
} catch (e) {
postOb11Event(groupDecreaseEvent, true)
} catch (e: any) {
log('获取群通知的成员信息失败', notify, e.stack.toString())
}
} else if ([GroupNotifyTypes.JOIN_REQUEST].includes(notify.type)) {
log('有加群请求')
let groupRequestEvent = new OB11GroupRequestEvent()
groupRequestEvent.group_id = parseInt(notify.group.groupCode)
let requestQQ = ''
try {
requestQQ = (await NTQQUserApi.getUserDetailInfo(notify.user1.uid)).uin
} catch (e) {
log('获取加群人QQ号失败', e)
}
groupRequestEvent.user_id = parseInt(requestQQ) || 0
groupRequestEvent.sub_type = 'add'
groupRequestEvent.comment = notify.postscript
groupRequestEvent.flag = notify.seq
postOB11Event(groupRequestEvent)
} else if (notify.type == GroupNotifyTypes.INVITE_ME) {
log('收到邀请我加群通知')
let groupInviteEvent = new OB11GroupRequestEvent()
groupInviteEvent.group_id = parseInt(notify.group.groupCode)
let user_id = (await getFriend(notify.user2.uid))?.uin
if (!user_id) {
user_id = (await NTQQUserApi.getUserDetailInfo(notify.user2.uid))?.uin
}
groupInviteEvent.user_id = parseInt(user_id)
groupInviteEvent.sub_type = 'invite'
groupInviteEvent.flag = notify.seq
postOB11Event(groupInviteEvent)
}
} catch (e) {
else if ([GroupNotifyTypes.JOIN_REQUEST, GroupNotifyTypes.JOIN_REQUEST_BY_INVITED].includes(notify.type)) {
log('有加群请求')
let requestQQ = uidMaps[notify.user1.uid]
if (!requestQQ) {
try {
requestQQ = (await NTQQUserApi.getUserDetailInfo(notify.user1.uid)).uin
} catch (e) {
log('获取加群人QQ号失败', e)
}
}
let invitorId: number
if (notify.type == GroupNotifyTypes.JOIN_REQUEST_BY_INVITED) {
// groupRequestEvent.sub_type = 'invite'
let invitorQQ = uidMaps[notify.user2.uid]
if (!invitorQQ) {
try {
let invitor = (await NTQQUserApi.getUserDetailInfo(notify.user2.uid))
invitorId = parseInt(invitor.uin)
} catch (e) {
invitorId = 0
log('获取邀请人QQ号失败', e)
}
}
}
const groupRequestEvent = new OB11GroupRequestEvent(
parseInt(notify.group.groupCode),
parseInt(requestQQ) || 0,
notify.seq,
notify.postscript,
invitorId!,
'add'
)
postOb11Event(groupRequestEvent)
}
else if (notify.type == GroupNotifyTypes.INVITE_ME) {
log('收到邀请我加群通知')
let userId = uidMaps[notify.user2.uid]
if (!userId) {
userId = (await NTQQUserApi.getUserDetailInfo(notify.user2.uid))?.uin
}
const groupInviteEvent = new OB11GroupRequestEvent(
parseInt(notify.group.groupCode),
parseInt(userId),
notify.seq,
undefined,
undefined,
'invite'
)
postOb11Event(groupInviteEvent)
}
} catch (e: any) {
log('解析群通知失败', e.stack.toString())
}
}
} else if (payload.doubt) {
}
else if (payload.doubt) {
// 可能有群管理员变动
}
})
registerReceiveHook<FriendRequestNotify>(ReceiveCmdS.FRIEND_REQUEST, async (payload) => {
for (const req of payload.data.buddyReqs) {
let flag = req.friendUid + req.reqTime
const flag = req.friendUid + req.reqTime
if (req.isUnread && parseInt(req.reqTime) > startTime / 1000) {
friendRequests[flag] = req
log('有新的好友请求', req)
let friendRequestEvent = new OB11FriendRequestEvent()
let userId: number
try {
let requester = await NTQQUserApi.getUserDetailInfo(req.friendUid)
friendRequestEvent.user_id = parseInt(requester.uin)
const requester = await NTQQUserApi.getUserDetailInfo(req.friendUid)
userId = parseInt(requester.uin)
} catch (e) {
log('获取加好友者QQ号失败', e)
}
friendRequestEvent.flag = flag
friendRequestEvent.comment = req.extWords
postOB11Event(friendRequestEvent)
const friendRequestEvent = new OB11FriendRequestEvent(userId!, req.extWords, flag)
postOb11Event(friendRequestEvent)
}
}
})
@@ -411,6 +430,11 @@ function onLoad() {
async function start() {
log('llonebot pid', process.pid)
const config = getConfigUtil().getConfig()
if (!config.enableLLOB) {
log('LLOneBot 开关设置为关闭不启动LLOneBot')
return
}
llonebotError.otherError = ''
startTime = Date.now()
dbUtil.getReceivedTempUinMap().then((m) => {
@@ -418,9 +442,34 @@ function onLoad() {
uidMaps[value] = key
}
})
startReceiveHook().then()
NTQQGroupApi.getGroups(true).then()
const config = getConfigUtil().getConfig()
NTEventDispatch.init({ ListenerMap: wrapperConstructor, WrapperSession: wrapperApi.NodeIQQNTWrapperSession! })
try {
log('start get groups')
const _groups = await NTQQGroupApi.getGroups()
log('_groups', _groups)
await Promise.all(
_groups.map(async (group) => {
try {
const members = await NTQQGroupApi.getGroupMembers(group.groupCode)
group.members = members
groups.push(group)
} catch (e) {
log('获取群成员失败', e)
}
})
)
}
catch (e) {
log('获取群列表失败', e)
}
finally {
log('start activate group member info')
NTQQGroupApi.activateMemberInfoChange().then().catch(log)
NTQQGroupApi.activateMemberListChange().then().catch(log)
startReceiveHook().then()
}
if (config.ob11.enableHttp) {
ob11HTTPServer.start(config.ob11.httpPort)
}
@@ -464,7 +513,7 @@ function onLoad() {
selfInfo.nick = userInfo.nick
return
}
} catch (e) {
} catch (e: any) {
log('get self nickname failed', e.stack)
}
if (getSelfNickCount < 10) {
@@ -474,7 +523,8 @@ function onLoad() {
getUserNick().then()
start().then()
} else {
}
else {
setTimeout(init, 1000)
}
}
@@ -491,7 +541,7 @@ function onBrowserWindowCreated(window: BrowserWindow) {
try {
hookNTQQApiCall(window)
hookNTQQApiReceive(window)
} catch (e) {
} catch (e: any) {
log('LLOneBot hook error: ', e.toString())
}
}

View File

@@ -7,26 +7,33 @@ import {
ChatCacheList,
ChatCacheListItemBasic,
ChatType,
ElementType, IMAGE_HTTP_HOST, IMAGE_HTTP_HOST_NT, RawMessage,
ElementType,
IMAGE_HTTP_HOST,
IMAGE_HTTP_HOST_NT, PicElement,
} from '../types'
import path from 'path'
import fs from 'fs'
import path from 'node:path'
import fs from 'node:fs'
import { ReceiveCmdS } from '../hook'
import { log } from '../../common/utils'
import https from 'https'
import { sleep } from '../../common/utils'
import { hookApi } from '../external/moehook/hook'
let privateImageRKey = ''
let groupImageRKey = ''
let lastGetPrivateRKeyTime = 0
let lastGetGroupRKeyTime = 0
const rkeyExpireTime = 1000 * 60 * 30
import { log } from '@/common/utils'
import { rkeyManager } from '@/ntqqapi/api/rkey'
import { wrapperApi } from '@/ntqqapi/wrapper'
import { Peer } from '@/ntqqapi/types/msg'
export class NTQQFileApi {
static async getVideoUrl(peer: Peer, msgId: string, elementId: string): Promise<string> {
const session = wrapperApi.NodeIQQNTWrapperSession
return (await session?.getRichMediaService().getVideoPlayUrlV2(peer,
msgId,
elementId,
0,
{ downSourceType: 1, triggerType: 1 })).urlResult?.domainUrl[0]?.url;
}
static async getFileType(filePath: string) {
return await callNTQQApi<{ ext: string }>({
className: NTQQApiClass.FS_API, methodName: NTQQApiMethod.FILE_TYPE, args: [filePath],
className: NTQQApiClass.FS_API,
methodName: NTQQApiMethod.FILE_TYPE,
args: [filePath],
})
}
@@ -42,16 +49,20 @@ export class NTQQFileApi {
return await callNTQQApi<string>({
className: NTQQApiClass.FS_API,
methodName: NTQQApiMethod.FILE_COPY,
args: [{
fromPath: filePath,
toPath: destPath,
}],
args: [
{
fromPath: filePath,
toPath: destPath,
},
],
})
}
static async getFileSize(filePath: string) {
return await callNTQQApi<number>({
className: NTQQApiClass.FS_API, methodName: NTQQApiMethod.FILE_SIZE, args: [filePath],
className: NTQQApiClass.FS_API,
methodName: NTQQApiMethod.FILE_SIZE,
args: [filePath],
})
}
@@ -70,18 +81,20 @@ export class NTQQFileApi {
}
const mediaPath = await callNTQQApi<string>({
methodName: NTQQApiMethod.MEDIA_FILE_PATH,
args: [{
path_info: {
md5HexStr: md5,
fileName: fileName,
elementType: elementType,
elementSubType,
thumbSize: 0,
needCreate: true,
downloadType: 1,
file_uuid: '',
args: [
{
path_info: {
md5HexStr: md5,
fileName: fileName,
elementType: elementType,
elementSubType,
thumbSize: 0,
needCreate: true,
downloadType: 1,
file_uuid: '',
},
},
}],
],
})
log('media path', mediaPath)
await NTQQFileApi.copyFile(filePath, mediaPath)
@@ -95,7 +108,15 @@ export class NTQQFileApi {
}
}
static async downloadMedia(msgId: string, chatType: ChatType, peerUid: string, elementId: string, thumbPath: string, sourcePath: string, force: boolean = false) {
static async downloadMedia(
msgId: string,
chatType: ChatType,
peerUid: string,
elementId: string,
thumbPath: string,
sourcePath: string,
force: boolean = false,
) {
// 用于下载收到的消息中的图片等
if (sourcePath && fs.existsSync(sourcePath)) {
if (force) {
@@ -126,7 +147,7 @@ export class NTQQFileApi {
methodName: NTQQApiMethod.DOWNLOAD_MEDIA,
args: apiParams,
cbCmd: ReceiveCmdS.MEDIA_DOWNLOAD_COMPLETE,
cmdCB: (payload: { notifyInfo: { filePath: string, msgId: string } }) => {
cmdCB: (payload: { notifyInfo: { filePath: string; msgId: string } }) => {
log('media 下载完成判断', payload.notifyInfo.msgId, msgId)
return payload.notifyInfo.msgId == msgId
},
@@ -135,21 +156,19 @@ export class NTQQFileApi {
}
static async getImageSize(filePath: string) {
return await callNTQQApi<{ width: number, height: number }>({
className: NTQQApiClass.FS_API, methodName: NTQQApiMethod.IMAGE_SIZE, args: [filePath],
return await callNTQQApi<{ width: number; height: number }>({
className: NTQQApiClass.FS_API,
methodName: NTQQApiMethod.IMAGE_SIZE,
args: [filePath],
})
}
static async getImageUrl(msg: RawMessage) {
const isPrivateImage = msg.chatType !== ChatType.group
const msgElement = msg.elements.find(e => !!e.picElement)
if (!msgElement) {
return ''
}
const url = msgElement.picElement.originImageUrl // 没有域名
const md5HexStr = msgElement.picElement.md5HexStr
const fileMd5 = msgElement.picElement.md5HexStr
const fileUuid = msgElement.picElement.fileUuid
static async getImageUrl(picElement: PicElement, chatType: ChatType) {
const isPrivateImage = chatType !== ChatType.group
const url = picElement.originImageUrl // 没有域名
const md5HexStr = picElement.md5HexStr
const fileMd5 = picElement.md5HexStr
const fileUuid = picElement.fileUuid
if (url) {
if (url.startsWith('/download')) {
// console.log('rkey', rkey);
@@ -157,70 +176,9 @@ export class NTQQFileApi {
return IMAGE_HTTP_HOST_NT + url
}
if (!hookApi.isAvailable()) {
log('hookApi is not available')
return ''
}
const saveRKey = (rkey: string) => {
if (isPrivateImage) {
privateImageRKey = rkey
lastGetPrivateRKeyTime = Date.now()
} else {
groupImageRKey = rkey
lastGetGroupRKeyTime = Date.now()
}
}
const refreshRKey = async () => {
log('获取图片rkey...')
NTQQFileApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid, msgElement.elementId, '', msgElement.picElement.sourcePath, false).then().catch(() => {
})
await sleep(1000)
const _rkey = hookApi.getRKey()
if (_rkey) {
const imageUrl = IMAGE_HTTP_HOST_NT + url + _rkey
// 验证_rkey是否有效
try {
await new Promise((res, rej) => {
https.get(imageUrl, response => {
if (response.statusCode !== 200) {
rej('图片rkey获取失败')
} else {
res(response)
}
}).on('error', e => {
rej(e)
})
})
log('图片rkey获取成功', _rkey)
saveRKey(_rkey)
return _rkey
}catch (e) {
log('图片rkey有误', imageUrl)
}
}
}
const existsRKey = isPrivateImage ? privateImageRKey : groupImageRKey
const lastGetRKeyTime = isPrivateImage ? lastGetPrivateRKeyTime : lastGetGroupRKeyTime
if ((Date.now() - lastGetRKeyTime > rkeyExpireTime)) {
// rkey过期
const newRKey = await refreshRKey()
if (newRKey) {
return IMAGE_HTTP_HOST_NT + url + `${newRKey}`
} else {
log('图片rkey获取失败', url)
if(existsRKey){
return IMAGE_HTTP_HOST_NT + url + `${existsRKey}`
}
return ''
}
}
// 使用未过期的rkey
if (existsRKey) {
return IMAGE_HTTP_HOST_NT + url + `${existsRKey}`
}
const rkeyData = await rkeyManager.getRkey();
const existsRKey = isPrivateImage ? rkeyData.private_rkey : rkeyData.group_rkey;
return IMAGE_HTTP_HOST_NT + url + `${existsRKey}`
} else {
// 老的图片url不需要rkey
return IMAGE_HTTP_HOST + url
@@ -229,47 +187,58 @@ export class NTQQFileApi {
// 没有url需要自己拼接
return `${IMAGE_HTTP_HOST}/gchatpic_new/0/0-0-${(fileMd5 || md5HexStr)!.toUpperCase()}/0`
}
log('图片url获取失败', msg)
log('图片url获取失败', picElement)
return ''
}
}
export class NTQQFileCacheApi {
static async setCacheSilentScan(isSilent: boolean = true) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.CACHE_SET_SILENCE,
args: [{
isSilent,
}, null],
args: [
{
isSilent,
},
null,
],
})
}
static getCacheSessionPathList() {
return callNTQQApi<{
key: string,
value: string
}[]>({
return callNTQQApi<
{
key: string
value: string
}[]
>({
className: NTQQApiClass.OS_API,
methodName: NTQQApiMethod.CACHE_PATH_SESSION,
})
}
static clearCache(cacheKeys: Array<string> = ['tmp', 'hotUpdate']) {
return callNTQQApi<any>({ // TODO: 目前还不知道真正的返回值是什么
return callNTQQApi<any>({
// TODO: 目前还不知道真正的返回值是什么
methodName: NTQQApiMethod.CACHE_CLEAR,
args: [{
keys: cacheKeys,
}, null],
args: [
{
keys: cacheKeys,
},
null,
],
})
}
static addCacheScannedPaths(pathMap: object = {}) {
return callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.CACHE_ADD_SCANNED_PATH,
args: [{
pathMap: { ...pathMap },
}, null],
args: [
{
pathMap: { ...pathMap },
},
null,
],
})
}
@@ -303,14 +272,18 @@ export class NTQQFileCacheApi {
return new Promise<ChatCacheList>((res, rej) => {
callNTQQApi<ChatCacheList>({
methodName: NTQQApiMethod.CACHE_CHAT_GET,
args: [{
chatType: type,
pageSize,
order: 1,
pageIndex,
}, null],
}).then(list => res(list))
.catch(e => rej(e))
args: [
{
chatType: type,
pageSize,
order: 1,
pageIndex,
},
null,
],
})
.then((list) => res(list))
.catch((e) => rej(e))
})
}
@@ -319,24 +292,29 @@ export class NTQQFileCacheApi {
return callNTQQApi<CacheFileList>({
methodName: NTQQApiMethod.CACHE_FILE_GET,
args: [{
fileType: fileType,
restart: true,
pageSize: pageSize,
order: 1,
lastRecord: _lastRecord,
}, null],
args: [
{
fileType: fileType,
restart: true,
pageSize: pageSize,
order: 1,
lastRecord: _lastRecord,
},
null,
],
})
}
static async clearChatCache(chats: ChatCacheListItemBasic[] = [], fileKeys: string[] = []) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.CACHE_CHAT_CLEAR,
args: [{
chats,
fileKeys,
}, null],
args: [
{
chats,
fileKeys,
},
null,
],
})
}
}

View File

@@ -1,8 +1,10 @@
import { Friend, FriendRequest } from '../types'
import { Friend, FriendRequest, FriendV2 } from '../types'
import { ReceiveCmdS } from '../hook'
import { callNTQQApi, GeneralCallResult, NTQQApiMethod } from '../ntcall'
import { friendRequests } from '../../common/data'
import { log } from '../../common/utils'
import { wrapperApi } from '@/ntqqapi/wrapper'
import { BuddyListReqType, NodeIKernelProfileService } from '../services'
import { NTEventDispatch } from '../../common/utils/EventTask'
export class NTQQFriendApi {
static async getFriends(forced = false) {
@@ -26,6 +28,7 @@ export class NTQQFriendApi {
}
return _friends
}
static async likeFriend(uid: string, count = 1) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.LIKE_FRIEND,
@@ -42,6 +45,7 @@ export class NTQQFriendApi {
],
})
}
static async handleFriendRequest(flag: string, accept: boolean) {
const request: FriendRequest = friendRequests[flag]
if (!request) {
@@ -62,4 +66,16 @@ export class NTQQFriendApi {
delete friendRequests[flag]
return result
}
static async getBuddyV2(refresh = false): Promise<FriendV2[]> {
const uids: string[] = []
const session = wrapperApi.NodeIQQNTWrapperSession
const buddyService = session?.getBuddyService()
const buddyListV2 = refresh ? await buddyService?.getBuddyListV2('0', BuddyListReqType.KNOMAL) : await buddyService?.getBuddyListV2('0', BuddyListReqType.KNOMAL)
uids.push(...buddyListV2?.data.flatMap(item => item.buddyUids)!)
const data = await NTEventDispatch.CallNoListenerEvent<NodeIKernelProfileService['getCoreAndBaseInfo']>(
'NodeIKernelProfileService/getCoreAndBaseInfo', 5000, 'nodeStore', uids
)
return Array.from(data.values())
}
}

View File

@@ -5,19 +5,56 @@ import { deleteGroup, uidMaps } from '../../common/data'
import { dbUtil } from '../../common/db'
import { log } from '../../common/utils/log'
import { NTQQWindowApi, NTQQWindows } from './window'
import { wrapperApi } from '../wrapper'
export class NTQQGroupApi {
static async activateMemberListChange() {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.ACTIVATE_MEMBER_LIST_CHANGE,
classNameIsRegister: true,
args: [],
})
}
static async activateMemberInfoChange() {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.ACTIVATE_MEMBER_INFO_CHANGE,
classNameIsRegister: true,
args: [],
})
}
static async getGroupAllInfo(groupCode: string, source: number = 4) {
return await callNTQQApi<GeneralCallResult & Group>({
methodName: NTQQApiMethod.GET_GROUP_ALL_INFO,
args: [
{
groupCode,
source
},
null,
],
})
}
static async getGroups(forced = false) {
let cbCmd = ReceiveCmdS.GROUPS
if (process.platform != 'win32') {
cbCmd = ReceiveCmdS.GROUPS_STORE
}
// let cbCmd = ReceiveCmdS.GROUPS
// if (process.platform != 'win32') {
// cbCmd = ReceiveCmdS.GROUPS_STORE
// }
const result = await callNTQQApi<{
updateType: number
groupList: Group[]
}>({ methodName: NTQQApiMethod.GROUPS, args: [{ force_update: forced }, undefined], cbCmd })
}>({
methodName: NTQQApiMethod.GROUPS,
args: [{ force_update: forced }, undefined],
cbCmd: [ReceiveCmdS.GROUPS, ReceiveCmdS.GROUPS_STORE],
afterFirstCmd: false,
})
log('get groups result', result)
return result.groupList
}
static async getGroupMembers(groupQQ: string, num = 3000): Promise<GroupMember[]> {
const sceneId = await callNTQQApi({
methodName: NTQQApiMethod.GROUP_MEMBER_SCENE,
@@ -28,7 +65,7 @@ export class NTQQGroupApi {
},
],
})
// log("get group member sceneId", sceneId);
// log("get group member sceneId", sceneId)
try {
const result = await callNTQQApi<{
result: { infos: any }
@@ -49,8 +86,8 @@ export class NTQQGroupApi {
for (const member of members) {
uidMaps[member.uid] = member.uin
}
// log(uidMaps);
// log("members info", values);
// log(uidMaps)
// log("members info", values)
log(`get group ${groupQQ} members success`)
return members
} catch (e) {
@@ -58,6 +95,21 @@ export class NTQQGroupApi {
return []
}
}
static async getGroupMembersInfo(groupCode: string, uids: string[], forceUpdate: boolean = false) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.GROUP_MEMBERS_INFO,
args: [
{
forceUpdate,
groupCode,
uids
},
null,
],
})
}
static async getGroupNotifies() {
// 获取管理员变更
// 加群通知,退出通知,需要管理员权限
@@ -72,16 +124,22 @@ export class NTQQGroupApi {
args: [{ doubt: false, startSeq: '', number: 14 }, null],
})
}
static async getGroupIgnoreNotifies() {
await NTQQGroupApi.getGroupNotifies()
return await NTQQWindowApi.openWindow(NTQQWindows.GroupNotifyFilterWindow, [], ReceiveCmdS.GROUP_NOTIFY)
return await NTQQWindowApi.openWindow<GeneralCallResult & GroupNotifies>(
NTQQWindows.GroupNotifyFilterWindow,
[],
ReceiveCmdS.GROUP_NOTIFY,
)
}
static async handleGroupRequest(seq: string, operateType: GroupRequestOperateTypes, reason?: string) {
const notify: GroupNotify = await dbUtil.getGroupNotify(seq)
const notify = await dbUtil.getGroupNotify(seq)
if (!notify) {
throw `${seq}对应的加群通知不存在`
}
// delete groupNotifies[seq];
// delete groupNotifies[seq]
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.HANDLE_GROUP_REQUEST,
args: [
@@ -101,6 +159,7 @@ export class NTQQGroupApi {
],
})
}
static async quitGroup(groupQQ: string) {
const result = await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.QUIT_GROUP,
@@ -111,6 +170,7 @@ export class NTQQGroupApi {
}
return result
}
static async kickMember(
groupQQ: string,
kickUids: string[],
@@ -129,7 +189,8 @@ export class NTQQGroupApi {
],
})
}
static async banMember(groupQQ: string, memList: Array<{ uid: string; timeStamp: number }>) {
static async banMember(groupQQ: string, memList: Array<{ uid: string, timeStamp: number }>) {
// timeStamp为秒数, 0为解除禁言
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.MUTE_MEMBER,
@@ -141,6 +202,7 @@ export class NTQQGroupApi {
],
})
}
static async banGroup(groupQQ: string, shutUp: boolean) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.MUTE_GROUP,
@@ -153,8 +215,10 @@ export class NTQQGroupApi {
],
})
}
static async setMemberCard(groupQQ: string, memberUid: string, cardName: string) {
return await callNTQQApi<GeneralCallResult>({
NTQQGroupApi.activateMemberListChange().then().catch(log)
const res = await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_MEMBER_CARD,
args: [
{
@@ -165,7 +229,10 @@ export class NTQQGroupApi {
null,
],
})
NTQQGroupApi.getGroupMembersInfo(groupQQ, [memberUid], true).then().catch(log)
return res
}
static async setMemberRole(groupQQ: string, memberUid: string, role: GroupMemberRole) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_MEMBER_ROLE,
@@ -179,6 +246,7 @@ export class NTQQGroupApi {
],
})
}
static async setGroupName(groupQQ: string, groupName: string) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_GROUP_NAME,
@@ -228,5 +296,34 @@ export class NTQQGroupApi {
],
})
}
static publishGroupBulletin(groupQQ: string, title: string, content: string) {}
static publishGroupBulletin(groupQQ: string, title: string, content: string) { }
static async removeGroupEssence(GroupCode: string, msgId: string) {
const session = wrapperApi.NodeIQQNTWrapperSession
// 代码没测过
// 需要 ob11msgid->msgId + (peer) -> msgSeq + msgRandom
let MsgData = await session?.getMsgService().getMsgsIncludeSelf({ chatType: 2, guildId: '', peerUid: GroupCode }, msgId, 1, false)
let param = {
groupCode: GroupCode,
msgRandom: parseInt(MsgData.msgList[0].msgRandom),
msgSeq: parseInt(MsgData.msgList[0].msgSeq)
}
// GetMsgByShoretID(ShoretID) -> MsgService.getMsgs(Peer,MsgId,1,false) -> 组出参数
return session?.getGroupService().removeGroupEssence(param)
}
static async addGroupEssence(GroupCode: string, msgId: string) {
const session = wrapperApi.NodeIQQNTWrapperSession
// 代码没测过
// 需要 ob11msgid->msgId + (peer) -> msgSeq + msgRandom
let MsgData = await session?.getMsgService().getMsgsIncludeSelf({ chatType: 2, guildId: '', peerUid: GroupCode }, msgId, 1, false)
let param = {
groupCode: GroupCode,
msgRandom: parseInt(MsgData.msgList[0].msgRandom),
msgSeq: parseInt(MsgData.msgList[0].msgSeq)
}
// GetMsgByShoretID(ShoretID) -> MsgService.getMsgs(Peer,MsgId,1,false) -> 组出参数
return session?.getGroupService().addGroupEssence(param)
}
}

View File

@@ -1,25 +1,94 @@
import { callNTQQApi, GeneralCallResult, NTQQApiMethod } from '../ntcall'
import { ChatType, RawMessage, SendMessageElement } from '../types'
import { ChatType, RawMessage, SendMessageElement, Peer } from '../types'
import { dbUtil } from '../../common/db'
import { selfInfo } from '../../common/data'
import { ReceiveCmdS, registerReceiveHook } from '../hook'
import { log } from '../../common/utils/log'
import { sleep } from '../../common/utils/helper'
import { isQQ998 } from '../../common/utils'
import { wrapperApi } from '@/ntqqapi/wrapper'
export let sendMessagePool: Record<string, ((sendSuccessMsg: RawMessage) => void) | null> = {} // peerUid: callbackFunnc
export let sendMessagePool: Record<string, ((sendSuccessMsg: RawMessage) => void) | null> = {} // peerUid: callbackFunc
export interface Peer {
chatType: ChatType
peerUid: string // 如果是群聊uid为群号私聊uid就是加密的字符串
guildId?: ''
export let sentMessages: Record<string, RawMessage> = {} // msgId: RawMessage
async function sendWaiter(peer: Peer, waitComplete = true, timeout: number = 10000) {
// 等待上一个相同的peer发送完
const peerUid = peer.peerUid
let checkLastSendUsingTime = 0
const waitLastSend = async () => {
if (checkLastSendUsingTime > timeout) {
throw '发送超时'
}
let lastSending = sendMessagePool[peer.peerUid]
if (lastSending) {
// log("有正在发送的消息,等待中...")
await sleep(500)
checkLastSendUsingTime += 500
return await waitLastSend()
}
else {
return
}
}
await waitLastSend()
let sentMessage: RawMessage | null = null
sendMessagePool[peerUid] = async (rawMessage: RawMessage) => {
delete sendMessagePool[peerUid]
sentMessage = rawMessage
sentMessages[rawMessage.msgId] = rawMessage
}
let checkSendCompleteUsingTime = 0
const checkSendComplete = async (): Promise<RawMessage> => {
if (sentMessage) {
if (waitComplete) {
if (sentMessage.sendStatus == 2) {
delete sentMessages[sentMessage.msgId]
return sentMessage
}
}
else {
delete sentMessages[sentMessage.msgId]
return sentMessage
}
// log(`给${peerUid}发送消息成功`)
}
checkSendCompleteUsingTime += 500
if (checkSendCompleteUsingTime > timeout) {
throw '发送超时'
}
await sleep(500)
return await checkSendComplete()
}
return checkSendComplete()
}
export class NTQQMsgApi {
static enterOrExitAIO(peer: Peer, enter: boolean) {
return callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.ENTER_OR_EXIT_AIO,
args: [
{
"info_list": [
{
peer,
"option": enter ? 1 : 2
}
]
},
{
"send": true
},
],
})
}
static async setEmojiLike(peer: Peer, msgSeq: string, emojiId: string, set: boolean = true) {
// nt_qq//global//nt_data//Emoji//emoji-resource//sysface_res/apng/ 下可以看到所有QQ表情预览
// nt_qq\global\nt_data\Emoji\emoji-resource\face_config.json 里面有所有表情的id, 自带表情id是QSid, 标准emoji表情id是QCid
// 其实以官方文档为准是最好的https://bot.q.qq.com/wiki/develop/api-v2/openapi/emoji/model.html#EmojiType
emojiId = emojiId.toString()
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.EMOJI_LIKE,
args: [
@@ -34,6 +103,7 @@ export class NTQQMsgApi {
],
})
}
static async getMultiMsg(peer: Peer, rootMsgId: string, parentMsgId: string) {
return await callNTQQApi<GeneralCallResult & { msgList: RawMessage[] }>({
methodName: NTQQApiMethod.GET_MULTI_MSG,
@@ -48,6 +118,20 @@ export class NTQQMsgApi {
})
}
static async getMsgBoxInfo(peer: Peer) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.GET_MSG_BOX_INFO,
args: [
{
contacts: [
peer
],
},
null,
],
})
}
static async activateChat(peer: Peer) {
// await this.fetchRecentContact();
// await sleep(500);
@@ -56,6 +140,7 @@ export class NTQQMsgApi {
args: [{ peer, cnt: 20 }, null],
})
}
static async activateChatAndGetHistory(peer: Peer) {
// await this.fetchRecentContact();
// await sleep(500);
@@ -65,6 +150,7 @@ export class NTQQMsgApi {
args: [{ peer, cnt: 20 }, null],
})
}
static async getMsgHistory(peer: Peer, msgId: string, count: number) {
// 消息时间从旧到新
return await callNTQQApi<GeneralCallResult & { msgList: RawMessage[] }>({
@@ -80,6 +166,7 @@ export class NTQQMsgApi {
],
})
}
static async fetchRecentContact() {
await callNTQQApi({
methodName: NTQQApiMethod.RECENT_CONTACT,
@@ -115,52 +202,7 @@ export class NTQQMsgApi {
}
static async sendMsg(peer: Peer, msgElements: SendMessageElement[], waitComplete = true, timeout = 10000) {
const peerUid = peer.peerUid
// 等待上一个相同的peer发送完
let checkLastSendUsingTime = 0
const waitLastSend = async () => {
if (checkLastSendUsingTime > timeout) {
throw '发送超时'
}
let lastSending = sendMessagePool[peer.peerUid]
if (lastSending) {
// log("有正在发送的消息,等待中...")
await sleep(500)
checkLastSendUsingTime += 500
return await waitLastSend()
} else {
return
}
}
await waitLastSend()
let sentMessage: RawMessage = null
sendMessagePool[peerUid] = async (rawMessage: RawMessage) => {
delete sendMessagePool[peerUid]
sentMessage = rawMessage
}
let checkSendCompleteUsingTime = 0
const checkSendComplete = async (): Promise<RawMessage> => {
if (sentMessage) {
if (waitComplete) {
if ((await dbUtil.getMsgByLongId(sentMessage.msgId)).sendStatus == 2) {
return sentMessage
}
} else {
return sentMessage
}
// log(`给${peerUid}发送消息成功`)
}
checkSendCompleteUsingTime += 500
if (checkSendCompleteUsingTime > timeout) {
throw '发送超时'
}
await sleep(500)
return await checkSendComplete()
}
const waiter = sendWaiter(peer, waitComplete, timeout)
callNTQQApi({
methodName: NTQQApiMethod.SEND_MSG,
args: [
@@ -173,11 +215,12 @@ export class NTQQMsgApi {
null,
],
}).then()
return await checkSendComplete()
return await waiter
}
static async forwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) {
return await callNTQQApi<GeneralCallResult>({
const waiter = sendWaiter(destPeer, true, 10000)
callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.FORWARD_MSG,
args: [
{
@@ -189,7 +232,8 @@ export class NTQQMsgApi {
},
null,
],
})
}).then().catch(log)
return await waiter
}
static async multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) {
@@ -244,4 +288,8 @@ export class NTQQMsgApi {
})
})
}
static async getMsgsBySeqAndCount(peer: Peer, seq: string, count: number, desc: boolean, z: boolean) {
const session = wrapperApi.NodeIQQNTWrapperSession
return await session?.getMsgService().getMsgsBySeqAndCount(peer, seq, count, desc, z);
}
}

64
src/ntqqapi/api/rkey.ts Normal file
View File

@@ -0,0 +1,64 @@
//远端rkey获取
import { log } from '@/common/utils'
interface ServerRkeyData {
group_rkey: string
private_rkey: string
expired_time: number
}
class RkeyManager {
serverUrl: string = ''
private rkeyData: ServerRkeyData = {
group_rkey: '',
private_rkey: '',
expired_time: 0
}
constructor(serverUrl: string) {
this.serverUrl = serverUrl
}
async getRkey() {
if (this.isExpired()) {
try {
await this.refreshRkey()
} catch (e) {
log('获取rkey失败', e)
}
}
return this.rkeyData
}
isExpired(): boolean {
const now = new Date().getTime() / 1000
// console.log(`now: ${now}, expired_time: ${this.rkeyData.expired_time}`)
return now > this.rkeyData.expired_time
}
async refreshRkey(): Promise<any> {
//刷新rkey
this.rkeyData = await this.fetchServerRkey()
}
async fetchServerRkey() {
return new Promise<ServerRkeyData>((resolve, reject) => {
fetch(this.serverUrl)
.then(response => {
if (!response.ok) {
return reject(response.statusText) // 请求失败,返回错误信息
}
return response.json() // 解析 JSON 格式的响应体
})
.then(data => {
resolve(data)
})
.catch(error => {
reject(error)
})
})
}
}
export const rkeyManager = new RkeyManager('http://napcat-sign.wumiao.wang:2082/rkey')

View File

@@ -2,10 +2,22 @@ import { callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod } from '../
import { Group, SelfInfo, User } from '../types'
import { ReceiveCmdS } from '../hook'
import { selfInfo, uidMaps } from '../../common/data'
import { NTQQWindowApi, NTQQWindows } from './window'
import { isQQ998, log, sleep } from '../../common/utils'
import { cacheFunc, isQQ998, log, sleep } from '../../common/utils'
import { wrapperApi } from '@/ntqqapi/wrapper'
import { RequestUtil } from '@/common/utils/request'
import { NodeIKernelProfileService, UserDetailSource, ProfileBizType } from '../services'
import { NodeIKernelProfileListener } from '../listeners'
import { NTEventDispatch } from '@/common/utils/EventTask'
import { qqPkgInfo } from '@/common/utils/QQBasicInfo'
let userInfoCache: Record<string, User> = {} // uid: User
const userInfoCache: Record<string, User> = {} // uid: User
export interface ClientKeyData extends GeneralCallResult {
url: string
keyIndex: string
clientKey: string
expireTime: string
}
export class NTQQUserApi {
static async setQQAvatar(filePath: string) {
@@ -28,6 +40,7 @@ export class NTQQUserApi {
timeoutSecond: 2,
})
}
static async getUserInfo(uid: string) {
const result = await callNTQQApi<{ profiles: Map<string, User> }>({
methodName: NTQQApiMethod.USER_INFO,
@@ -36,9 +49,54 @@ export class NTQQUserApi {
})
return result.profiles.get(uid)
}
static async getUserDetailInfo(uid: string, getLevel = false) {
// this.getUserInfo(uid);
// 26702
static async fetchUserDetailInfo(uid: string) {
type EventService = NodeIKernelProfileService['fetchUserDetailInfo']
type EventListener = NodeIKernelProfileListener['onUserDetailInfoChanged']
const [_retData, profile] = await NTEventDispatch.CallNormalEvent
<EventService, EventListener>
(
'NodeIKernelProfileService/fetchUserDetailInfo',
'NodeIKernelProfileListener/onUserDetailInfoChanged',
1,
5000,
(profile) => {
if (profile.uid === uid) {
return true;
}
return false;
},
'BuddyProfileStore',
[
uid
],
UserDetailSource.KSERVER,
[
ProfileBizType.KALL
]
)
const RetUser: User = {
...profile.simpleInfo.coreInfo,
...profile.simpleInfo.status,
...profile.simpleInfo.vasInfo,
...profile.commonExt,
...profile.simpleInfo.baseInfo,
qqLevel: profile.commonExt.qqLevel,
pendantId: ''
}
return RetUser
}
static async getUserDetailInfo(uid: string, getLevel = false, withBizInfo = true) {
if (+qqPkgInfo.buildVersion >= 26702) {
return this.fetchUserDetailInfo(uid)
}
// this.getUserInfo(uid)
let methodName = !isQQ998 ? NTQQApiMethod.USER_DETAIL_INFO : NTQQApiMethod.USER_DETAIL_INFO_WITH_BIZ_INFO
if (!withBizInfo) {
methodName = NTQQApiMethod.USER_DETAIL_INFO
}
const fetchInfo = async () => {
const result = await callNTQQApi<{ info: User }>({
methodName,
@@ -67,7 +125,7 @@ export class NTQQUserApi {
await fetchInfo()
await sleep(1000)
}
let userInfo = await fetchInfo()
const userInfo = await fetchInfo()
userInfoCache[uid] = userInfo
return userInfo
}
@@ -84,64 +142,43 @@ export class NTQQUserApi {
],
})
}
static async getSkey(groupName: string, groupCode: string): Promise<{ data: string }> {
return await NTQQWindowApi.openWindow<{ data: string }>(
NTQQWindows.GroupHomeWorkWindow,
[
{
groupName,
groupCode,
source: 'funcbar',
},
],
ReceiveCmdS.SKEY_UPDATE,
1,
)
// return await callNTQQApi<string>({
// className: NTQQApiClass.GROUP_HOME_WORK,
// methodName: NTQQApiMethod.UPDATE_SKEY,
// args: [
// {
// domain: "qun.qq.com"
// }
// ]
// })
// return await callNTQQApi<GeneralCallResult>({
// methodName: NTQQApiMethod.GET_SKEY,
// args: [
// {
// "domains": [
// "qzone.qq.com",
// "qlive.qq.com",
// "qun.qq.com",
// "gamecenter.qq.com",
// "vip.qq.com",
// "qianbao.qq.com",
// "qidian.qq.com"
// ],
// "isForNewPCQQ": false
// },
// null
// ]
// })
static async getQzoneCookies() {
const requestUrl = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + selfInfo.uin + '&clientkey=' + (await this.getClientKey()).clientKey + '&u1=https%3A%2F%2Fuser.qzone.qq.com%2F' + selfInfo.uin + '%2Finfocenter&keyindex=19%27'
let cookies: { [key: string]: string } = {}
try {
cookies = await RequestUtil.HttpsGetCookies(requestUrl)
} catch (e: any) {
log('获取QZone Cookies失败', e)
cookies = {}
}
return cookies
}
static async getSkey(): Promise<string> {
const clientKeyData = await this.getClientKey()
if (clientKeyData.result !== 0) {
throw new Error('获取clientKey失败')
}
const url = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + selfInfo.uin
+ '&clientkey=' + clientKeyData.clientKey
+ '&u1=https%3A%2F%2Fh5.qzone.qq.com%2Fqqnt%2Fqzoneinpcqq%2Ffriend%3Frefresh%3D0%26clientuin%3D0%26darkMode%3D0&keyindex=' + clientKeyData.keyIndex
return (await RequestUtil.HttpsGetCookies(url))?.skey
}
static async getCookie(group: Group) {
let cookies = await this.getCookieWithoutSkey()
let skey = ''
for (let i = 0; i < 2; i++) {
skey = (await this.getSkey(group.groupName, group.groupCode)).data
skey = skey.trim()
if (skey) {
break
}
await sleep(1000)
@cacheFunc(60 * 30 * 1000)
static async getCookies(domain: string) {
if (domain.endsWith("qzone.qq.com")) {
let data = (await NTQQUserApi.getQzoneCookies())
const CookieValue = 'p_skey=' + data.p_skey + '; skey=' + data.skey + '; p_uin=o' + selfInfo.uin + '; uin=o' + selfInfo.uin
return { bkn: NTQQUserApi.genBkn(data.p_skey), cookies: CookieValue }
}
if (!skey) {
throw new Error('获取skey失败')
const skey = await this.getSkey()
const pskey = (await this.getPSkey([domain])).get(domain)
if (!pskey || !skey) {
throw new Error('获取Cookies失败')
}
const bkn = NTQQUserApi.genBkn(skey)
cookies = cookies.replace('skey=;', `skey=${skey};`)
const cookies = `p_skey=${pskey}; skey=${skey}; p_uin=o${selfInfo.uin}; uin=o${selfInfo.uin}`
return { cookies, bkn }
}
@@ -156,4 +193,18 @@ export class NTQQUserApi {
return (hash & 0x7fffffff).toString()
}
static async getPSkey(domains: string[]): Promise<Map<string, string>> {
const session = wrapperApi.NodeIQQNTWrapperSession
const res = await session?.getTipOffService().getPskey(domains, true)
if (res.result !== 0) {
throw new Error(`获取Pskey失败: ${res.errMsg}`)
}
return res.domainPskeyMap
}
static async getClientKey(): Promise<ClientKeyData> {
const session = wrapperApi.NodeIQQNTWrapperSession
return await session?.getTicketService().forceFetchClientKey('')
}
}

View File

@@ -1,76 +1,379 @@
import { groups } from '../../common/data'
import { log } from '../../common/utils'
import { WebGroupData, groups, selfInfo } from '@/common/data'
import { log } from '@/common/utils/log'
import { NTQQUserApi } from './user'
import { RequestUtil } from '@/common/utils/request'
export class WebApi {
private static bkn: string
private static skey: string
private static pskey: string
private static cookie: string
private defaultHeaders: Record<string, string> = {
'User-Agent': 'QQ/8.9.28.635 CFNetwork/1312 Darwin/21.0.0',
export enum WebHonorType {
ALL = 'all',
TALKACTIVE = 'talkative',
PERFROMER = 'performer',
LEGEND = 'legend',
STORONGE_NEWBI = 'strong_newbie',
EMOTION = 'emotion'
}
export interface WebApiGroupMember {
uin: number
role: number
g: number
join_time: number
last_speak_time: number
lv: {
point: number
level: number
}
card: string
tags: string
flag: number
nick: string
qage: number
rm: number
}
constructor() {}
interface WebApiGroupMemberRet {
ec: number
errcode: number
em: string
cache: number
adm_num: number
levelname: any
mems: WebApiGroupMember[]
count: number
svr_time: number
max_count: number
search_count: number
extmode: number
}
public async addGroupDigest(groupCode: string, msgSeq: string) {
const url = `https://qun.qq.com/cgi-bin/group_digest/cancel_digest?random=665&X-CROSS-ORIGIN=fetch&group_code=${groupCode}&msg_seq=${msgSeq}&msg_random=444021292`
const res = await this.request(url)
return await res.json()
export interface WebApiGroupNoticeFeed {
u: number//发送者
fid: string//fid
pubt: number//时间
msg: {
text: string
text_face: string
title: string,
pics?: {
id: string,
w: string,
h: string
}[]
}
public async getGroupDigest(groupCode: string) {
const url = `https://qun.qq.com/cgi-bin/group_digest/digest_list?random=665&X-CROSS-ORIGIN=fetch&group_code=${groupCode}&page_start=0&page_limit=20`
const res = await this.request(url)
log(res.headers)
return await res.json()
type: number
fn: number
cn: number
vn: number
settings: {
is_show_edit_card: number
remind_ts: number
tip_window_type: number
confirm_required: number
}
read_num: number
is_read: number
is_all_confirm: number
}
private genBkn(sKey: string) {
return NTQQUserApi.genBkn(sKey)
}
private async init() {
if (!WebApi.bkn) {
const group = groups[0]
WebApi.skey = (await NTQQUserApi.getSkey(group.groupName, group.groupCode)).data
WebApi.bkn = this.genBkn(WebApi.skey)
let cookie = await NTQQUserApi.getCookieWithoutSkey()
const pskeyRegex = /p_skey=([^;]+)/
const match = cookie.match(pskeyRegex)
const pskeyValue = match ? match[1] : null
WebApi.pskey = pskeyValue
if (cookie.indexOf('skey=;') !== -1) {
cookie = cookie.replace('skey=;', `skey=${WebApi.skey};`)
}
WebApi.cookie = cookie
// for(const kv of WebApi.cookie.split(";")){
// const [key, value] = kv.split("=");
// }
// log("set cookie", key, value)
// await session.defaultSession.cookies.set({
// url: 'https://qun.qq.com', // 你要请求的域名
// name: key.trim(),
// value: value.trim(),
// expirationDate: Date.now() / 1000 + 300000, // Cookie 过期时间例如设置为当前时间之后的300秒
// });
// }
}
export interface WebApiGroupNoticeRet {
ec: number
em: string
ltsm: number
srv_code: number
read_only: number
role: number
feeds: WebApiGroupNoticeFeed[]
group: {
group_id: number
class_ext: number
}
sta: number,
gln: number
tst: number,
ui: any
server_time: number
svrt: number
ad: number
}
private async request(url: string, method: 'GET' | 'POST' = 'GET', headers: Record<string, string> = {}) {
await this.init()
url += '&bkn=' + WebApi.bkn
let _headers: Record<string, string> = {
...this.defaultHeaders,
...headers,
Cookie: WebApi.cookie,
credentials: 'include',
}
log('request', url, _headers)
const options = {
method: method,
headers: _headers,
}
return fetch(url, options)
interface GroupEssenceMsg {
group_code: string
msg_seq: number
msg_random: number
sender_uin: string
sender_nick: string
sender_time: number
add_digest_uin: string
add_digest_nick: string
add_digest_time: number
msg_content: any[]
can_be_removed: true
}
export interface GroupEssenceMsgRet {
retcode: number
retmsg: string
data: {
msg_list: GroupEssenceMsg[]
is_end: boolean
group_role: number
config_page_url: string
}
}
export class WebApi {
static async getGroupEssenceMsg(GroupCode: string, page_start: string): Promise<GroupEssenceMsgRet | undefined> {
const { cookies: CookieValue, bkn: Bkn } = (await NTQQUserApi.getCookies('qun.qq.com'))
const url = 'https://qun.qq.com/cgi-bin/group_digest/digest_list?bkn=' + Bkn + '&group_code=' + GroupCode + '&page_start=' + page_start + '&page_limit=20'
let ret: GroupEssenceMsgRet
try {
ret = await RequestUtil.HttpGetJson<GroupEssenceMsgRet>(url, 'GET', '', { 'Cookie': CookieValue })
} catch {
return undefined
}
//console.log(url, CookieValue)
if (ret.retcode !== 0) {
return undefined
}
return ret
}
static async getGroupMembers(GroupCode: string, cached: boolean = true): Promise<WebApiGroupMember[]> {
log('webapi 获取群成员', GroupCode);
let MemberData: Array<WebApiGroupMember> = new Array<WebApiGroupMember>();
try {
let cachedData = WebGroupData.GroupData.get(GroupCode);
let cachedTime = WebGroupData.GroupTime.get(GroupCode);
if (!cachedTime || Date.now() - cachedTime > 1800 * 1000 || !cached) {
const _Pskey = (await NTQQUserApi.getPSkey(['qun.qq.com']))['qun.qq.com'];
const _Skey = await NTQQUserApi.getSkey();
const CookieValue = 'p_skey=' + _Pskey + '; skey=' + _Skey + '; p_uin=o' + selfInfo.uin;
if (!_Skey || !_Pskey) {
return MemberData;
}
const Bkn = WebApi.genBkn(_Skey);
const retList: Promise<WebApiGroupMemberRet>[] = [];
const fastRet = await RequestUtil.HttpGetJson<WebApiGroupMemberRet>('https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?st=0&end=40&sort=1&gc=' + GroupCode + '&bkn=' + Bkn, 'POST', '', { 'Cookie': CookieValue });
if (!fastRet?.count || fastRet?.errcode !== 0 || !fastRet?.mems) {
return [];
} else {
for (const key in fastRet.mems) {
MemberData.push(fastRet.mems[key]);
}
}
//初始化获取PageNum
const PageNum = Math.ceil(fastRet.count / 40);
//遍历批量请求
for (let i = 2; i <= PageNum; i++) {
const ret: Promise<WebApiGroupMemberRet> = RequestUtil.HttpGetJson<WebApiGroupMemberRet>('https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?st=' + (i - 1) * 40 + '&end=' + i * 40 + '&sort=1&gc=' + GroupCode + '&bkn=' + Bkn, 'POST', '', { 'Cookie': CookieValue });
retList.push(ret);
}
//批量等待
for (let i = 1; i <= PageNum; i++) {
const ret = await (retList[i]);
if (!ret?.count || ret?.errcode !== 0 || !ret?.mems) {
continue;
}
for (const key in ret.mems) {
MemberData.push(ret.mems[key]);
}
}
WebGroupData.GroupData.set(GroupCode, MemberData);
WebGroupData.GroupTime.set(GroupCode, Date.now());
} else {
MemberData = cachedData as Array<WebApiGroupMember>;
}
} catch {
return MemberData;
}
return MemberData;
}
// public static async addGroupDigest(groupCode: string, msgSeq: string) {
// const url = `https://qun.qq.com/cgi-bin/group_digest/cancel_digest?random=665&X-CROSS-ORIGIN=fetch&group_code=${groupCode}&msg_seq=${msgSeq}&msg_random=444021292`;
// const res = await this.request(url);
// return await res.json();
// }
// public async getGroupDigest(groupCode: string) {
// const url = `https://qun.qq.com/cgi-bin/group_digest/digest_list?random=665&X-CROSS-ORIGIN=fetch&group_code=${groupCode}&page_start=0&page_limit=20`;
// const res = await this.request(url);
// return await res.json();
// }
static async setGroupNotice(GroupCode: string, Content: string = '') {
//https://web.qun.qq.com/cgi-bin/announce/add_qun_notice?bkn=${bkn}
//qid=${群号}&bkn=${bkn}&text=${内容}&pinned=0&type=1&settings={"is_show_edit_card":1,"tip_window_type":1,"confirm_required":1}
const _Pskey = (await NTQQUserApi.getPSkey(['qun.qq.com']))['qun.qq.com'];
const _Skey = await NTQQUserApi.getSkey();
const CookieValue = 'p_skey=' + _Pskey + '; skey=' + _Skey + '; p_uin=o' + selfInfo.uin;
let ret: any = undefined;
//console.log(CookieValue);
if (!_Skey || !_Pskey) {
//获取Cookies失败
return undefined;
}
const Bkn = WebApi.genBkn(_Skey);
const data = 'qid=' + GroupCode + '&bkn=' + Bkn + '&text=' + Content + '&pinned=0&type=1&settings={"is_show_edit_card":1,"tip_window_type":1,"confirm_required":1}';
const url = 'https://web.qun.qq.com/cgi-bin/announce/add_qun_notice?bkn=' + Bkn;
try {
ret = await RequestUtil.HttpGetJson<any>(url, 'GET', '', { 'Cookie': CookieValue });
return ret;
} catch (e) {
return undefined;
}
return undefined;
}
static async getGrouptNotice(GroupCode: string): Promise<undefined | WebApiGroupNoticeRet> {
const _Pskey = (await NTQQUserApi.getPSkey(['qun.qq.com']))['qun.qq.com'];
const _Skey = await NTQQUserApi.getSkey();
const CookieValue = 'p_skey=' + _Pskey + '; skey=' + _Skey + '; p_uin=o' + selfInfo.uin;
let ret: WebApiGroupNoticeRet | undefined = undefined;
//console.log(CookieValue);
if (!_Skey || !_Pskey) {
//获取Cookies失败
return undefined;
}
const Bkn = WebApi.genBkn(_Skey);
const url = 'https://web.qun.qq.com/cgi-bin/announce/get_t_list?bkn=' + Bkn + '&qid=' + GroupCode + '&ft=23&ni=1&n=1&i=1&log_read=1&platform=1&s=-1&n=20';
try {
ret = await RequestUtil.HttpGetJson<WebApiGroupNoticeRet>(url, 'GET', '', { 'Cookie': CookieValue });
if (ret?.ec !== 0) {
return undefined;
}
return ret;
} catch (e) {
return undefined;
}
return undefined;
}
static genBkn(sKey: string) {
sKey = sKey || '';
let hash = 5381;
for (let i = 0; i < sKey.length; i++) {
const code = sKey.charCodeAt(i);
hash = hash + (hash << 5) + code;
}
return (hash & 0x7FFFFFFF).toString();
}
//实现未缓存 考虑2h缓存
static async getGroupHonorInfo(groupCode: string, getType: WebHonorType) {
async function getDataInternal(Internal_groupCode: string, Internal_type: number) {
let url = 'https://qun.qq.com/interactive/honorlist?gc=' + Internal_groupCode + '&type=' + Internal_type.toString();
let res = '';
let resJson;
try {
res = await RequestUtil.HttpGetText(url, 'GET', '', { 'Cookie': CookieValue });
const match = res.match(/window\.__INITIAL_STATE__=(.*?);/);
if (match) {
resJson = JSON.parse(match[1].trim());
}
if (Internal_type === 1) {
return resJson?.talkativeList;
} else {
return resJson?.actorList;
}
} catch (e) {
log('获取当前群荣耀失败', url, e);
}
return undefined;
}
let HonorInfo: any = { group_id: groupCode };
const CookieValue = (await NTQQUserApi.getCookies('qun.qq.com')).cookies;
if (getType === WebHonorType.TALKACTIVE || getType === WebHonorType.ALL) {
try {
let RetInternal = await getDataInternal(groupCode, 1);
if (!RetInternal) {
throw new Error('获取龙王信息失败');
}
HonorInfo.current_talkative = {
user_id: RetInternal[0]?.uin,
avatar: RetInternal[0]?.avatar,
nickname: RetInternal[0]?.name,
day_count: 0,
description: RetInternal[0]?.desc
}
HonorInfo.talkative_list = [];
for (const talkative_ele of RetInternal) {
HonorInfo.talkative_list.push({
user_id: talkative_ele?.uin,
avatar: talkative_ele?.avatar,
description: talkative_ele?.desc,
day_count: 0,
nickname: talkative_ele?.name
});
}
} catch (e) {
log(e);
}
}
if (getType === WebHonorType.PERFROMER || getType === WebHonorType.ALL) {
try {
let RetInternal = await getDataInternal(groupCode, 2);
if (!RetInternal) {
throw new Error('获取群聊之火失败');
}
HonorInfo.performer_list = [];
for (const performer_ele of RetInternal) {
HonorInfo.performer_list.push({
user_id: performer_ele?.uin,
nickname: performer_ele?.name,
avatar: performer_ele?.avatar,
description: performer_ele?.desc
});
}
} catch (e) {
log(e);
}
}
if (getType === WebHonorType.PERFROMER || getType === WebHonorType.ALL) {
try {
let RetInternal = await getDataInternal(groupCode, 3);
if (!RetInternal) {
throw new Error('获取群聊炽焰失败');
}
HonorInfo.legend_list = [];
for (const legend_ele of RetInternal) {
HonorInfo.legend_list.push({
user_id: legend_ele?.uin,
nickname: legend_ele?.name,
avatar: legend_ele?.avatar,
desc: legend_ele?.description
});
}
} catch (e) {
log('获取群聊炽焰失败', e);
}
}
if (getType === WebHonorType.EMOTION || getType === WebHonorType.ALL) {
try {
let RetInternal = await getDataInternal(groupCode, 6);
if (!RetInternal) {
throw new Error('获取快乐源泉失败');
}
HonorInfo.emotion_list = [];
for (const emotion_ele of RetInternal) {
HonorInfo.emotion_list.push({
user_id: emotion_ele?.uin,
nickname: emotion_ele?.name,
avatar: emotion_ele?.avatar,
desc: emotion_ele?.description
});
}
} catch (e) {
log('获取快乐源泉失败', e);
}
}
//冒尖小春笋好像已经被tx扬了
if (getType === WebHonorType.EMOTION || getType === WebHonorType.ALL) {
HonorInfo.strong_newbie_list = [];
}
return HonorInfo;
}
}

View File

@@ -27,7 +27,7 @@ export class NTQQWindowApi {
static async openWindow<R = GeneralCallResult>(
ntQQWindow: NTQQWindow,
args: any[],
cbCmd: ReceiveCmd = null,
cbCmd: ReceiveCmd | null = null,
autoCloseSeconds: number = 2,
) {
const result = await callNTQQApi<R>({

View File

@@ -2,7 +2,6 @@ import {
AtType,
ElementType,
FaceIndex,
FaceType,
PicType,
SendArkElement,
SendFaceElement,
@@ -22,8 +21,9 @@ import { log } from '../common/utils/log'
import { defaultVideoThumb, getVideoInfo } from '../common/utils/video'
import { encodeSilk } from '../common/utils/audio'
import { isNull } from '../common/utils'
import faceConfig from './face_config.json';
export const mFaceCache = new Map<string, string>(); // emojiId -> faceName
export const mFaceCache = new Map<string, string>() // emojiId -> faceName
export class SendMsgElementConstructor {
static poke(groupCode: string, uin: string) {
@@ -76,6 +76,10 @@ export class SendMsgElementConstructor {
if (fileSize === 0) {
throw '文件异常大小为0'
}
const maxMB = 30;
if (fileSize > 1024 * 1024 * 30){
throw `图片过大,最大支持${maxMB}MB当前文件大小${fileSize}B`
}
const imageSize = await NTQQFileApi.getImageSize(picPath)
const picElement = {
md5HexStr: md5,
@@ -119,18 +123,22 @@ export class SendMsgElementConstructor {
}
static async video(filePath: string, fileName: string = '', diyThumbPath: string = ''): Promise<SendVideoElement> {
try{
try {
await fs.stat(filePath)
}catch (e) {
} catch (e) {
throw `文件${filePath}异常,不存在`
}
log("复制视频到QQ目录", filePath)
log('复制视频到QQ目录', filePath)
let { fileName: _fileName, path, fileSize, md5 } = await NTQQFileApi.uploadFile(filePath, ElementType.VIDEO)
log("复制视频到QQ目录完成", path)
log('复制视频到QQ目录完成', path)
if (fileSize === 0) {
throw '文件异常大小为0'
}
const maxMB = 100;
if (fileSize > 1024 * 1024 * maxMB) {
throw `视频过大,最大支持${maxMB}MB当前文件大小${fileSize}B`
}
const pathLib = require('path')
let thumbDir = path.replace(`${pathLib.sep}Ori${pathLib.sep}`, `${pathLib.sep}Thumb${pathLib.sep}`)
thumbDir = pathLib.dirname(thumbDir)
@@ -265,13 +273,30 @@ export class SendMsgElementConstructor {
}
static face(faceId: number): SendFaceElement {
// 从face_config.json中获取表情名称
const sysFaces = faceConfig.sysface
const emojiFaces = faceConfig.emoji
const face = sysFaces.find((face) => face.QSid === faceId.toString())
faceId = parseInt(faceId.toString())
// let faceType = parseInt(faceId.toString().substring(0, 1));
let faceType = 1
if (faceId >= 222){
faceType = 2
}
if (face?.AniStickerType){
faceType = 3;
}
return {
elementType: ElementType.FACE,
elementId: '',
faceElement: {
faceIndex: faceId,
faceType: faceId < 222 ? FaceType.normal : FaceType.normal2,
faceType,
faceText: face?.QDes,
stickerId: face?.AniStickerId,
stickerType: face?.AniStickerType,
packId: face?.AniStickerPackId,
sourceType: 1,
},
}
}
@@ -298,13 +323,13 @@ export class SendMsgElementConstructor {
elementId: '',
faceElement: {
faceIndex: FaceIndex.dice,
faceType: FaceType.dice,
faceType: 3,
faceText: '[骰子]',
packId: '1',
stickerId: '33',
sourceType: 1,
stickerType: 2,
resultId: resultId.toString(),
resultId: resultId?.toString(),
surpriseId: '',
// "randomType": 1,
},
@@ -326,7 +351,7 @@ export class SendMsgElementConstructor {
stickerId: '34',
sourceType: 1,
stickerType: 2,
resultId: resultId.toString(),
resultId: resultId?.toString(),
surpriseId: '',
// "randomType": 1,
},

View File

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

3665
src/ntqqapi/face_config.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,27 +1,29 @@
import { BrowserWindow } from 'electron'
import type { BrowserWindow } from 'electron'
import { NTQQApiClass, NTQQApiMethod } from './ntcall'
import { NTQQMsgApi, sendMessagePool } from './api/msg'
import { ChatType, Group, GroupMember, GroupMemberRole, RawMessage, User } from './types'
import { CategoryFriend, ChatType, Group, GroupMember, GroupMemberRole, RawMessage, User } from './types'
import {
deleteGroup,
friends,
getFriend,
getGroupMember,
groups,
groups, rawFriends,
selfInfo,
tempGroupCodeMap,
uidMaps,
} from '../common/data'
} from '@/common/data'
import { OB11GroupDecreaseEvent } from '../onebot11/event/notice/OB11GroupDecreaseEvent'
import { v4 as uuidv4 } from 'uuid'
import { postOB11Event } from '../onebot11/server/postOB11Event'
import { getConfigUtil, HOOK_LOG } from '../common/config'
import { postOb11Event } from '../onebot11/server/post-ob11-event'
import { getConfigUtil, HOOK_LOG } from '@/common/config'
import fs from 'fs'
import { dbUtil } from '../common/db'
import { dbUtil } from '@/common/db'
import { NTQQGroupApi } from './api/group'
import { log } from '../common/utils/log'
import { isNumeric, sleep } from '../common/utils/helper'
import { log } from '@/common/utils'
import { isNumeric, sleep } from '@/common/utils'
import { OB11Constructor } from '../onebot11/constructor'
import { OB11GroupCardEvent } from '../onebot11/event/notice/OB11GroupCardEvent'
import { OB11GroupAdminNoticeEvent } from '../onebot11/event/notice/OB11GroupAdminNoticeEvent'
import { randomUUID } from 'node:crypto'
export let hookApiCallbacks: Record<string, (apiReturn: any) => void> = {}
@@ -81,7 +83,7 @@ export function hookNTQQApiReceive(window: BrowserWindow) {
let isLogger = false
try {
isLogger = args[0]?.eventName?.startsWith('ns-LoggerApi')
} catch (e) {}
} catch (e) { }
if (!isLogger) {
try {
HOOK_LOG && log(`received ntqq api message: ${channel}`, args)
@@ -100,7 +102,7 @@ export function hookNTQQApiReceive(window: BrowserWindow) {
try {
let _ = hook.hookFunc(receiveData.payload)
if (hook.hookFunc.constructor.name === 'AsyncFunction') {
;(_ as Promise<void>).then()
; (_ as Promise<void>).then()
}
} catch (e) {
log('hook error', e, receiveData.payload)
@@ -121,7 +123,7 @@ export function hookNTQQApiReceive(window: BrowserWindow) {
delete hookApiCallbacks[callbackId]
}
}
} catch (e) {
} catch (e: any) {
log('hookNTQQApiReceive error', e.stack.toString(), args)
}
originalSend.call(window.webContents, channel, ...args)
@@ -140,11 +142,11 @@ export function hookNTQQApiCall(window: BrowserWindow) {
let isLogger = false
try {
isLogger = args[3][0].eventName.startsWith('ns-LoggerApi')
} catch (e) {}
} catch (e) { }
if (!isLogger) {
try {
HOOK_LOG && log('call NTQQ api', thisArg, args)
} catch (e) {}
} catch (e) { }
try {
const _args: unknown[] = args[3][1]
const cmdName: NTQQApiMethod = _args[0] as NTQQApiMethod
@@ -155,7 +157,7 @@ export function hookNTQQApiCall(window: BrowserWindow) {
try {
let _ = hook.hookFunc(callParams)
if (hook.hookFunc.constructor.name === 'AsyncFunction') {
;(_ as Promise<void>).then()
(_ as Promise<void>).then()
}
} catch (e) {
log('hook call error', e, _args)
@@ -163,7 +165,7 @@ export function hookNTQQApiCall(window: BrowserWindow) {
}).then()
}
})
} catch (e) {}
} catch (e) { }
}
return target.apply(thisArg, args)
},
@@ -187,7 +189,7 @@ export function hookNTQQApiCall(window: BrowserWindow) {
let ret = target.apply(thisArg, args)
try {
HOOK_LOG && log('call NTQQ invoke api return', ret)
} catch (e) {}
} catch (e) { }
return ret
},
})
@@ -202,7 +204,7 @@ export function registerReceiveHook<PayloadType>(
method: ReceiveCmd | ReceiveCmd[],
hookFunc: (payload: PayloadType) => void,
): string {
const id = uuidv4()
const id = randomUUID()
if (!Array.isArray(method)) {
method = [method]
}
@@ -294,12 +296,12 @@ async function processGroupEvent(payload: { groupList: Group[] }) {
// 判断bot是否是管理员如果是管理员不需要从这里得知有人退群这里的退群无法得知是主动退群还是被踢
let bot = await getGroupMember(group.groupCode, selfInfo.uin)
if (bot.role == GroupMemberRole.admin || bot.role == GroupMemberRole.owner) {
if (bot?.role == GroupMemberRole.admin || bot?.role == GroupMemberRole.owner) {
continue
}
for (const member of oldMembers) {
if (!newMembersSet.has(member.uin) && member.uin != selfInfo.uin) {
postOB11Event(
postOb11Event(
new OB11GroupDecreaseEvent(
parseInt(group.groupCode),
parseInt(member.uin),
@@ -318,211 +320,235 @@ async function processGroupEvent(payload: { groupList: Group[] }) {
}
updateGroups(newGroupList, false).then()
} catch (e) {
} catch (e: any) {
updateGroups(payload.groupList).then()
log('更新群信息错误', e.stack.toString())
}
}
// 群列表变动
registerReceiveHook<{ groupList: Group[]; updateType: number }>(ReceiveCmdS.GROUPS, (payload) => {
// updateType 3是群列表变动2是群成员变动
// log("群列表变动", payload.updateType, payload.groupList)
if (payload.updateType != 2) {
updateGroups(payload.groupList).then()
} else {
if (process.platform == 'win32') {
processGroupEvent(payload).then()
}
}
})
registerReceiveHook<{ groupList: Group[]; updateType: number }>(ReceiveCmdS.GROUPS_STORE, (payload) => {
// updateType 3是群列表变动2是群成员变动
// log("群列表变动", payload.updateType, payload.groupList)
if (payload.updateType != 2) {
updateGroups(payload.groupList).then()
} else {
if (process.platform != 'win32') {
processGroupEvent(payload).then()
}
}
})
export async function startHook() {
registerReceiveHook<{
groupCode: string
dataSource: number
members: Set<GroupMember>
}>(ReceiveCmdS.GROUP_MEMBER_INFO_UPDATE, async (payload) => {
const groupCode = payload.groupCode
const members = Array.from(payload.members.values())
// log("群成员信息变动", groupCode, members)
for (const member of members) {
const existMember = await getGroupMember(groupCode, member.uin)
if (existMember) {
Object.assign(existMember, member)
// 群列表变动
registerReceiveHook<{ groupList: Group[]; updateType: number }>(ReceiveCmdS.GROUPS, (payload) => {
// updateType 3是群列表变动2是群成员变动
// log("群列表变动", payload.updateType, payload.groupList)
if (payload.updateType != 2) {
updateGroups(payload.groupList).then()
}
}
// const existGroup = groups.find(g => g.groupCode == groupCode);
// if (existGroup) {
// log("对比群成员", existGroup.members, members)
// for (const member of members) {
// const existMember = existGroup.members.find(m => m.uin == member.uin);
// if (existMember) {
// log("对比群名片", existMember.cardName, member.cardName)
// if (existMember.cardName != member.cardName) {
// postOB11Event(new OB11GroupCardEvent(parseInt(existGroup.groupCode), parseInt(member.uin), member.cardName, existMember.cardName));
// }
// Object.assign(existMember, member);
// }
// }
// }
})
// 好友列表变动
registerReceiveHook<{
data: { categoryId: number; categroyName: string; categroyMbCount: number; buddyList: User[] }[]
}>(ReceiveCmdS.FRIENDS, (payload) => {
for (const fData of payload.data) {
const _friends = fData.buddyList
for (let friend of _friends) {
NTQQMsgApi.activateChat({ peerUid: friend.uid, chatType: ChatType.friend }).then()
let existFriend = friends.find((f) => f.uin == friend.uin)
if (!existFriend) {
friends.push(friend)
} else {
Object.assign(existFriend, friend)
else {
if (process.platform == 'win32') {
processGroupEvent(payload).then()
}
}
}
})
registerReceiveHook<{ msgList: Array<RawMessage> }>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], (payload) => {
// 保存一下uid
for (const message of payload.msgList) {
const uid = message.senderUid
const uin = message.senderUin
if (uid && uin) {
if (message.chatType === ChatType.temp) {
dbUtil.getReceivedTempUinMap().then((receivedTempUinMap) => {
if (!receivedTempUinMap[uin]) {
receivedTempUinMap[uin] = uid
dbUtil.setReceivedTempUinMap(receivedTempUinMap)
}
})
})
registerReceiveHook<{ groupList: Group[]; updateType: number }>(ReceiveCmdS.GROUPS_STORE, (payload) => {
// updateType 3是群列表变动2是群成员变动
// log("群列表变动", payload.updateType, payload.groupList)
if (payload.updateType != 2) {
updateGroups(payload.groupList).then()
}
else {
if (process.platform != 'win32') {
processGroupEvent(payload).then()
}
uidMaps[uid] = uin
}
}
})
// 自动清理新消息文件
const { autoDeleteFile } = getConfigUtil().getConfig()
if (!autoDeleteFile) {
return
}
for (const message of payload.msgList) {
// log("收到新消息push到历史记录", message.msgId)
// dbUtil.addMsg(message).then()
// 清理文件
for (const msgElement of message.elements) {
setTimeout(() => {
const picPath = msgElement.picElement?.sourcePath
const picThumbPath = [...msgElement.picElement?.thumbPath.values()]
const pttPath = msgElement.pttElement?.filePath
const filePath = msgElement.fileElement?.filePath
const videoPath = msgElement.videoElement?.filePath
const videoThumbPath: string[] = [...msgElement.videoElement?.thumbPath.values()]
const pathList = [picPath, ...picThumbPath, pttPath, filePath, videoPath, ...videoThumbPath]
if (msgElement.picElement) {
pathList.push(...Object.values(msgElement.picElement.thumbPath))
registerReceiveHook<{
groupCode: string
dataSource: number
members: Set<GroupMember>
}>(ReceiveCmdS.GROUP_MEMBER_INFO_UPDATE, async (payload) => {
const groupCode = payload.groupCode
const members = Array.from(payload.members.values())
// log("群成员信息变动", groupCode, members)
for (const member of members) {
const existMember = await getGroupMember(groupCode, member.uin)
if (existMember) {
if (member.cardName != existMember.cardName) {
log('群成员名片变动', `${groupCode}: ${existMember.uin}`, existMember.cardName, '->', member.cardName)
postOb11Event(
new OB11GroupCardEvent(parseInt(groupCode), parseInt(member.uin), member.cardName, existMember.cardName),
)
} else if (member.role != existMember.role) {
log('有管理员变动通知')
const groupAdminNoticeEvent = new OB11GroupAdminNoticeEvent(
member.role == GroupMemberRole.admin ? 'set' : 'unset',
parseInt(groupCode),
parseInt(member.uin)
)
postOb11Event(groupAdminNoticeEvent, true)
}
const aioOpGrayTipElement = msgElement.grayTipElement?.aioOpGrayTipElement
if (aioOpGrayTipElement) {
tempGroupCodeMap[aioOpGrayTipElement.peerUid] = aioOpGrayTipElement.fromGrpCodeOfTmpChat
}
// log("需要清理的文件", pathList);
for (const path of pathList) {
if (path) {
fs.unlink(picPath, () => {
log('删除文件成功', path)
})
}
}
}, getConfigUtil().getConfig().autoDeleteFileSecond * 1000)
Object.assign(existMember, member)
}
}
}
})
// const existGroup = groups.find(g => g.groupCode == groupCode);
// if (existGroup) {
// log("对比群成员", existGroup.members, members)
// for (const member of members) {
// const existMember = existGroup.members.find(m => m.uin == member.uin);
// if (existMember) {
// log("对比群名片", existMember.cardName, member.cardName)
// if (existMember.cardName != member.cardName) {
// postOB11Event(new OB11GroupCardEvent(parseInt(existGroup.groupCode), parseInt(member.uin), member.cardName, existMember.cardName));
// }
// Object.assign(existMember, member);
// }
// }
// }
})
registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, ({ msgRecord }) => {
const message = msgRecord
const peerUid = message.peerUid
// log("收到自己发送成功的消息", Object.keys(sendMessagePool), message);
// log("收到自己发送成功的消息", message.msgId, message.msgSeq);
dbUtil.addMsg(message).then()
const sendCallback = sendMessagePool[peerUid]
if (sendCallback) {
try {
sendCallback(message)
} catch (e) {
log('receive self msg error', e.stack)
// 好友列表变动
registerReceiveHook<{
data: CategoryFriend[]
}>(ReceiveCmdS.FRIENDS, (payload) => {
rawFriends.length = 0;
rawFriends.push(...payload.data);
for (const fData of payload.data) {
const _friends = fData.buddyList
for (let friend of _friends) {
NTQQMsgApi.activateChat({ peerUid: friend.uid, chatType: ChatType.friend }).then()
let existFriend = friends.find((f) => f.uin == friend.uin)
if (!existFriend) {
friends.push(friend)
}
else {
Object.assign(existFriend, friend)
}
}
}
}
})
})
registerReceiveHook<{ info: { status: number } }>(ReceiveCmdS.SELF_STATUS, (info) => {
selfInfo.online = info.info.status !== 20
})
let activatedPeerUids: string[] = []
registerReceiveHook<{
changedRecentContactLists: {
listType: number
sortedContactList: string[]
changedList: {
id: string // peerUid
chatType: ChatType
}[]
}[]
}>(ReceiveCmdS.RECENT_CONTACT, async (payload) => {
for (const recentContact of payload.changedRecentContactLists) {
for (const changedContact of recentContact.changedList) {
if (activatedPeerUids.includes(changedContact.id)) continue
activatedPeerUids.push(changedContact.id)
const peer = { peerUid: changedContact.id, chatType: changedContact.chatType }
if (changedContact.chatType === ChatType.temp) {
log('收到临时会话消息', peer)
NTQQMsgApi.activateChatAndGetHistory(peer).then(() => {
NTQQMsgApi.getMsgHistory(peer, '', 20).then(({ msgList }) => {
let lastTempMsg = msgList.pop()
log('激活窗口之前的第一条临时会话消息:', lastTempMsg)
if (Date.now() / 1000 - parseInt(lastTempMsg.msgTime) < 5) {
OB11Constructor.message(lastTempMsg).then((r) => postOB11Event(r))
registerReceiveHook<{ msgList: Array<RawMessage> }>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], (payload) => {
// 保存一下uid
for (const message of payload.msgList) {
const uid = message.senderUid
const uin = message.senderUin
if (uid && uin) {
if (message.chatType === ChatType.temp) {
dbUtil.getReceivedTempUinMap().then((receivedTempUinMap) => {
if (!receivedTempUinMap[uin]) {
receivedTempUinMap[uin] = uid
dbUtil.setReceivedTempUinMap(receivedTempUinMap)
}
})
})
} else {
NTQQMsgApi.activateChat(peer).then()
}
uidMaps[uid] = uin
}
}
}
})
registerCallHook(NTQQApiMethod.DELETE_ACTIVE_CHAT, async (payload) => {
const peerUid = payload[0] as string
log('激活的聊天窗口被删除,准备重新激活', peerUid)
let chatType = ChatType.friend
if (isNumeric(peerUid)) {
chatType = ChatType.group
} else {
// 检查是否好友
if (!(await getFriend(peerUid))) {
chatType = ChatType.temp
// 自动清理新消息文件
const { autoDeleteFile } = getConfigUtil().getConfig()
if (!autoDeleteFile) {
return
}
for (const message of payload.msgList) {
// log("收到新消息push到历史记录", message.msgId)
// dbUtil.addMsg(message).then()
// 清理文件
for (const msgElement of message.elements) {
setTimeout(() => {
const picPath = msgElement.picElement?.sourcePath
const picThumbPath = [...msgElement.picElement?.thumbPath.values()]
const pttPath = msgElement.pttElement?.filePath
const filePath = msgElement.fileElement?.filePath
const videoPath = msgElement.videoElement?.filePath
const videoThumbPath: string[] = [...msgElement.videoElement.thumbPath?.values()!]
const pathList = [picPath, ...picThumbPath, pttPath, filePath, videoPath, ...videoThumbPath]
if (msgElement.picElement) {
pathList.push(...Object.values(msgElement.picElement.thumbPath))
}
const aioOpGrayTipElement = msgElement.grayTipElement?.aioOpGrayTipElement
if (aioOpGrayTipElement) {
tempGroupCodeMap[aioOpGrayTipElement.peerUid] = aioOpGrayTipElement.fromGrpCodeOfTmpChat
}
// log("需要清理的文件", pathList);
for (const path of pathList) {
if (path) {
fs.unlink(picPath, () => {
log('删除文件成功', path)
})
}
}
}, getConfigUtil().getConfig().autoDeleteFileSecond! * 1000)
}
}
}
const peer = { peerUid, chatType }
await sleep(1000)
NTQQMsgApi.activateChat(peer).then((r) => {
log('重新激活聊天窗口', peer, { result: r.result, errMsg: r.errMsg })
})
})
registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, ({ msgRecord }) => {
const message = msgRecord
const peerUid = message.peerUid
// log("收到自己发送成功的消息", Object.keys(sendMessagePool), message);
// log("收到自己发送成功的消息", message.msgId, message.msgSeq);
dbUtil.addMsg(message).then()
const sendCallback = sendMessagePool[peerUid]
if (sendCallback) {
try {
sendCallback(message)
} catch (e: any) {
log('receive self msg error', e.stack)
}
}
})
registerReceiveHook<{ info: { status: number } }>(ReceiveCmdS.SELF_STATUS, (info) => {
selfInfo.online = info.info.status !== 20
})
let activatedPeerUids: string[] = []
registerReceiveHook<{
changedRecentContactLists: {
listType: number
sortedContactList: string[]
changedList: {
id: string // peerUid
chatType: ChatType
}[]
}[]
}>(ReceiveCmdS.RECENT_CONTACT, async (payload) => {
for (const recentContact of payload.changedRecentContactLists) {
for (const changedContact of recentContact.changedList) {
if (activatedPeerUids.includes(changedContact.id)) continue
activatedPeerUids.push(changedContact.id)
const peer = { peerUid: changedContact.id, chatType: changedContact.chatType }
if (changedContact.chatType === ChatType.temp) {
log('收到临时会话消息', peer)
NTQQMsgApi.activateChatAndGetHistory(peer).then(() => {
NTQQMsgApi.getMsgHistory(peer, '', 20).then(({ msgList }) => {
let lastTempMsg = msgList.pop()
log('激活窗口之前的第一条临时会话消息:', lastTempMsg)
if (Date.now() / 1000 - parseInt(lastTempMsg?.msgTime!) < 5) {
OB11Constructor.message(lastTempMsg!).then((r) => postOb11Event(r))
}
})
})
}
else {
NTQQMsgApi.activateChat(peer).then()
}
}
}
})
registerCallHook(NTQQApiMethod.DELETE_ACTIVE_CHAT, async (payload) => {
const peerUid = payload[0] as string
log('激活的聊天窗口被删除,准备重新激活', peerUid)
let chatType = ChatType.friend
if (isNumeric(peerUid)) {
chatType = ChatType.group
}
else {
// 检查是否好友
if (!(await getFriend(peerUid))) {
chatType = ChatType.temp
}
}
const peer = { peerUid, chatType }
await sleep(1000)
NTQQMsgApi.activateChat(peer).then((r) => {
log('重新激活聊天窗口', peer, { result: r.result, errMsg: r.errMsg })
})
})
}

View File

@@ -0,0 +1,44 @@
import { User, UserDetailInfoListenerArg } from '@/ntqqapi/types'
interface IProfileListener {
onProfileSimpleChanged(...args: unknown[]): void
onUserDetailInfoChanged(arg: UserDetailInfoListenerArg): void
onProfileDetailInfoChanged(profile: User): void
onStatusUpdate(...args: unknown[]): void
onSelfStatusChanged(...args: unknown[]): void
onStrangerRemarkChanged(...args: unknown[]): void
}
export interface NodeIKernelProfileListener extends IProfileListener {
new(listener: IProfileListener): NodeIKernelProfileListener
}
export class ProfileListener implements IProfileListener {
onUserDetailInfoChanged(arg: UserDetailInfoListenerArg): void {
}
onProfileSimpleChanged(...args: unknown[]) {
}
onProfileDetailInfoChanged(profile: User) {
}
onStatusUpdate(...args: unknown[]) {
}
onSelfStatusChanged(...args: unknown[]) {
}
onStrangerRemarkChanged(...args: unknown[]) {
}
}

View File

@@ -0,0 +1 @@
export * from './NodeIKernelProfileListener'

View File

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

View File

@@ -1,11 +1,9 @@
import * as os from "os";
import fs from "fs";
import path from "node:path";
import {cpModule} from "../cpmodule";
import { qqPkgInfo } from '@/common/utils/QQBasicInfo'
interface MoeHook {
GetRkey: () => string, // Return '&rkey=xxx'
HookRkey: () => string
HookRkey: (version: string) => string
}
@@ -16,7 +14,8 @@ class HookApi {
cpModule('MoeHoo');
try {
this.moeHook = require('./MoeHoo.node');
console.log("hook rkey地址", this.moeHook!.HookRkey());
console.log("hook rkey qq version", this.moeHook!.HookRkey(qqPkgInfo.version));
console.log("hook rkey地址", this.moeHook!.HookRkey(qqPkgInfo.version));
} catch (e) {
console.log('加载 moehoo 失败', e);
}
@@ -31,4 +30,4 @@ class HookApi {
}
}
export const hookApi = new HookApi();
// export const hookApi = new HookApi();

View File

@@ -1,11 +1,8 @@
import { ipcMain } from 'electron'
import { hookApiCallbacks, ReceiveCmd, ReceiveCmdS, registerReceiveHook, removeReceiveHook } from './hook'
import { v4 as uuidv4 } from 'uuid'
import { log } from '../common/utils/log'
import { NTQQWindow, NTQQWindowApi, NTQQWindows } from './api/window'
import { WebApi } from './api/webapi'
import { HOOK_LOG } from '../common/config'
import { randomUUID } from 'node:crypto'
export enum NTQQApiClass {
NT_API = 'ns-ntApi',
@@ -21,19 +18,24 @@ export enum NTQQApiClass {
}
export enum NTQQApiMethod {
TEST = 'NodeIKernelTipOffService/getPskey',
RECENT_CONTACT = 'nodeIKernelRecentContactService/fetchAndSubscribeABatchOfRecentContact',
ACTIVE_CHAT_PREVIEW = 'nodeIKernelMsgService/getAioFirstViewLatestMsgsAndAddActiveChat', // 激活聊天窗口,有时候必须这样才能收到消息, 并返回最新预览消息
ACTIVE_CHAT_HISTORY = 'nodeIKernelMsgService/getMsgsIncludeSelfAndAddActiveChat', // 激活聊天窗口,有时候必须这样才能收到消息, 并返回历史消息
HISTORY_MSG = 'nodeIKernelMsgService/getMsgsIncludeSelf',
GET_MULTI_MSG = 'nodeIKernelMsgService/getMultiMsg',
DELETE_ACTIVE_CHAT = 'nodeIKernelMsgService/deleteActiveChatByUid',
ENTER_OR_EXIT_AIO = 'nodeIKernelMsgService/enterOrExitAio',
LIKE_FRIEND = 'nodeIKernelProfileLikeService/setBuddyProfileLike',
SELF_INFO = 'fetchAuthData',
FRIENDS = 'nodeIKernelBuddyService/getBuddyList',
GROUPS = 'nodeIKernelGroupService/getGroupList',
GROUP_MEMBER_SCENE = 'nodeIKernelGroupService/createMemberListScene',
GROUP_MEMBERS = 'nodeIKernelGroupService/getNextMemberList',
GROUP_MEMBERS_INFO = 'nodeIKernelGroupService/getMemberInfo',
USER_INFO = 'nodeIKernelProfileService/getUserSimpleInfo',
USER_DETAIL_INFO = 'nodeIKernelProfileService/getUserDetailInfo',
USER_DETAIL_INFO_WITH_BIZ_INFO = 'nodeIKernelProfileService/getUserDetailInfoWithBizInfo',
@@ -65,6 +67,10 @@ export enum NTQQApiMethod {
PUBLISH_GROUP_BULLETIN = 'nodeIKernelGroupService/publishGroupBulletinBulletin',
SET_GROUP_NAME = 'nodeIKernelGroupService/modifyGroupName',
SET_GROUP_TITLE = 'nodeIKernelGroupService/modifyMemberSpecialTitle',
ACTIVATE_MEMBER_LIST_CHANGE = 'nodeIKernelGroupListener/onMemberListChange',
ACTIVATE_MEMBER_INFO_CHANGE = 'nodeIKernelGroupListener/onMemberInfoChange',
GET_MSG_BOX_INFO = 'nodeIKernelMsgService/getABatchOfContactMsgBoxInfo',
GET_GROUP_ALL_INFO = 'nodeIKernelGroupService/getGroupAllInfo',
CACHE_SET_SILENCE = 'nodeIKernelStorageCleanService/setSilentScan',
CACHE_ADD_SCANNED_PATH = 'nodeIKernelStorageCleanService/addCacheScanedPaths',
@@ -81,7 +87,7 @@ export enum NTQQApiMethod {
OPEN_EXTRA_WINDOW = 'openExternalWindow',
SET_QQ_AVATAR = 'nodeIKernelProfileService/setHeader',
GET_SKEY = 'nodeIKernelTipOffService/getPskey',
GET_PSKEY = 'nodeIKernelTipOffService/getPskey',
UPDATE_SKEY = 'updatePskey',
FETCH_UNITED_COMMEND_CONFIG = 'nodeIKernelUnitedConfigService/fetchUnitedCommendConfig', // 发包需要调用的
@@ -99,7 +105,7 @@ interface NTQQApiParams {
channel?: NTQQApiChannel
classNameIsRegister?: boolean
args?: unknown[]
cbCmd?: ReceiveCmd | null
cbCmd?: ReceiveCmd | ReceiveCmd[] | null
cmdCB?: (payload: any) => boolean
afterFirstCmd?: boolean // 是否在methodName调用完之后再去hook cbCmd
timeoutSecond?: number
@@ -122,7 +128,7 @@ export function callNTQQApi<ReturnType>(params: NTQQApiParams) {
args = args ?? []
timeout = timeout ?? 5
afterFirstCmd = afterFirstCmd ?? true
const uuid = uuidv4()
const uuid = randomUUID()
HOOK_LOG && log('callNTQQApi', channel, className, methodName, args, uuid)
return new Promise((resolve: (data: ReturnType) => void, reject) => {
// log("callNTQQApiPromise", channel, className, methodName, args, uuid)
@@ -139,7 +145,8 @@ export function callNTQQApi<ReturnType>(params: NTQQApiParams) {
success = true
resolve(r)
}
} else {
}
else {
// 这里的callback比较特殊QQ后端先返回是否调用成功再返回一条结果数据
const secondCallback = () => {
const hookId = registerReceiveHook<ReturnType>(cbCmd, (payload) => {
@@ -150,7 +157,8 @@ export function callNTQQApi<ReturnType>(params: NTQQApiParams) {
success = true
resolve(payload)
}
} else {
}
else {
removeReceiveHook(hookId)
success = true
resolve(payload)
@@ -162,7 +170,8 @@ export function callNTQQApi<ReturnType>(params: NTQQApiParams) {
log(`${methodName} callback`, result)
if (result?.result == 0 || result === undefined) {
afterFirstCmd && secondCallback()
} else {
}
else {
success = true
reject(`ntqq api call failed, ${result.errMsg}`)
}
@@ -180,7 +189,8 @@ export function callNTQQApi<ReturnType>(params: NTQQApiParams) {
channel,
{
sender: {
send: (..._args: unknown[]) => {},
send: (..._args: unknown[]) => {
},
},
},
{ type: 'request', callbackId: uuid, eventName },

View File

@@ -0,0 +1,125 @@
import { GeneralCallResult } from './common'
export enum BuddyListReqType {
KNOMAL,
KLETTER
}
export interface NodeIKernelBuddyService {
// 26702 以上
getBuddyListV2(callFrom: string, reqType: BuddyListReqType): Promise<GeneralCallResult & {
data: Array<{
categoryId: number,
categorySortId: number,
categroyName: string,
categroyMbCount: number,
onlineCount: number,
buddyUids: Array<string>
}>
}>
//26702 以上
getBuddyListFromCache(callFrom: string): Promise<Array<
{
categoryId: number,//9999应该跳过 那是兜底数据吧
categorySortId: number,//排序方式
categroyName: string,//分类名
categroyMbCount: number,//不懂
onlineCount: number,//在线数目
buddyUids: Array<string>//Uids
}>>
addKernelBuddyListener(listener: any): number
getAllBuddyCount(): number
removeKernelBuddyListener(listener: unknown): void
getBuddyList(nocache: boolean): Promise<GeneralCallResult>
getBuddyNick(uid: number): string
getBuddyRemark(uid: number): string
setBuddyRemark(uid: number, remark: string): void
getAvatarUrl(uid: number): string
isBuddy(uid: string): boolean
getCategoryNameWithUid(uid: number): string
getTargetBuddySetting(uid: number): unknown
getTargetBuddySettingByType(uid: number, type: number): unknown
getBuddyReqUnreadCnt(): number
getBuddyReq(): unknown
delBuddyReq(uid: number): void
clearBuddyReqUnreadCnt(): void
reqToAddFriends(uid: number, msg: string): void
setSpacePermission(uid: number, permission: number): void
approvalFriendRequest(arg: {
friendUid: string
reqTime: string
accept: boolean
}): Promise<void>
delBuddy(uid: number): void
delBatchBuddy(uids: number[]): void
getSmartInfos(uid: number): unknown
setBuddyCategory(uid: number, category: number): void
setBatchBuddyCategory(uids: number[], category: number): void
addCategory(category: string): void
delCategory(category: string): void
renameCategory(oldCategory: string, newCategory: string): void
resortCategory(categorys: string[]): void
pullCategory(uid: number, category: string): void
setTop(uid: number, isTop: boolean): void
SetSpecialCare(uid: number, isSpecialCare: boolean): void
setMsgNotify(uid: number, isNotify: boolean): void
hasBuddyList(): boolean
setBlock(uid: number, isBlock: boolean): void
isBlocked(uid: number): boolean
modifyAddMeSetting(setting: unknown): void
getAddMeSetting(): unknown
getDoubtBuddyReq(): unknown
getDoubtBuddyUnreadNum(): number
approvalDoubtBuddyReq(uid: number, isAgree: boolean): void
delDoubtBuddyReq(uid: number): void
delAllDoubtBuddyReq(): void
reportDoubtBuddyReqUnread(): void
getBuddyRecommendContactArkJson(uid: string, phoneNumber: string): Promise<unknown>
isNull(): boolean
}

View File

@@ -0,0 +1,106 @@
import { AnyCnameRecord } from 'node:dns'
import { SimpleInfo } from '../types'
import { GeneralCallResult } from './common'
export enum UserDetailSource {
KDB,
KSERVER
}
export enum ProfileBizType {
KALL,
KBASEEXTEND,
KVAS,
KQZONE,
KOTHER
}
export interface NodeIKernelProfileService {
getUidByUin(callfrom: string, uin: Array<string>): Promise<Map<string,string>>//uin->uid
getUinByUid(callfrom: string, uid: Array<string>): Promise<Map<string,string>>
// {
// coreInfo: CoreInfo,
// baseInfo: BaseInfo,
// status: null,
// vasInfo: null,
// relationFlags: null,
// otherFlags: null,
// intimate: null
// }
getCoreAndBaseInfo(callfrom: string, uids: string[]): Promise<Map<string, SimpleInfo>>
fetchUserDetailInfo(trace: string, uids: string[], arg2: number, arg3: number[]): Promise<unknown>
addKernelProfileListener(listener: any): number
removeKernelProfileListener(listenerId: number): void
prepareRegionConfig(...args: unknown[]): unknown
getLocalStrangerRemark(): Promise<AnyCnameRecord>
enumCountryOptions(): Array<string>
enumProvinceOptions(Country: string): Array<string>
enumCityOptions(Country: string, Province: string): unknown
enumAreaOptions(...args: unknown[]): unknown
//SimpleInfo
// this.uid = ""
// this.uid = str
// this.uin = j2
// this.isBuddy = z
// this.coreInfo = coreInfo
// this.baseInfo = baseInfo
// this.status = statusInfo
// this.vasInfo = vasInfo
// this.relationFlags = relationFlag
// this.otherFlags = otherFlag
// this.intimate = intimate
modifySelfProfile(...args: unknown[]): Promise<unknown>
modifyDesktopMiniProfile(param: any): Promise<GeneralCallResult>
setNickName(NickName: string): Promise<unknown>
setLongNick(longNick: string): Promise<unknown>
setBirthday(...args: unknown[]): Promise<unknown>
setGander(...args: unknown[]): Promise<unknown>
setHeader(arg: string): Promise<unknown>
setRecommendImgFlag(...args: unknown[]): Promise<unknown>
getUserSimpleInfo(force: boolean, uids: string[],): Promise<unknown>
getUserDetailInfo(uid: string): Promise<unknown>
getUserDetailInfoWithBizInfo(uid: string, Biz: any[]): Promise<GeneralCallResult>
getUserDetailInfoByUin(uin: string): Promise<any>
getZplanAvatarInfos(args: string[]): Promise<unknown>
getStatus(uid: string): Promise<unknown>
startStatusPolling(isForceReset: boolean): Promise<unknown>
getSelfStatus(): Promise<unknown>
setdisableEmojiShortCuts(...args: unknown[]): unknown
getProfileQzonePicInfo(uid: string, type: number, force: boolean): Promise<unknown>
//profileService.getCoreInfo("UserRemarkServiceImpl::getStrangerRemarkByUid", arrayList)
getCoreInfo(name: string, arg: any[]): unknown
//m429253e12.getOtherFlag("FriendListInfoCache_getKernelDataAndPutCache", new ArrayList<>())
isNull(): boolean
}

View File

@@ -0,0 +1,16 @@
export enum GeneralCallResultStatus {
OK = 0
// ERROR = 1
}
export interface GeneralCallResult {
result: GeneralCallResultStatus
errMsg: string
}
export interface forceFetchClientKeyRetType extends GeneralCallResult {
url: string
keyIndex: string
clientKey: string
expireTime: string
}

View File

@@ -0,0 +1,2 @@
export * from './NodeIKernelBuddyService'
export * from './NodeIKernelProfileService'

View File

@@ -1,5 +1,4 @@
import { GroupMemberRole } from './group'
import exp from 'constants'
export enum ElementType {
TEXT = 1,
@@ -188,6 +187,8 @@ export const IMAGE_HTTP_HOST = 'https://gchat.qpic.cn'
export const IMAGE_HTTP_HOST_NT = 'https://multimedia.nt.qq.com.cn'
export interface PicElement {
picSubType: PicSubType
picType: PicType // 有这玩意儿吗
originImageUrl: string // http url, 没有hosthost是https://gchat.qpic.cn/, 带download参数的是https://multimedia.nt.qq.com.cn
originImageMd5?: string
sourcePath: string // 图片本地路径
@@ -201,6 +202,7 @@ export interface PicElement {
}
export enum GrayTipElementSubType {
RECALL = 1,
INVITE_NEW_MEMBER = 12,
MEMBER_NEW_TITLE = 17,
}
@@ -213,6 +215,8 @@ export interface GrayTipElement {
operatorNick: string
operatorRemark: string
operatorMemRemark?: string
origMsgSenderUid?: string
isSelfOperate?: boolean
wording: string // 自定义的撤回提示语
}
aioOpGrayTipElement: TipAioOpGrayTipElement
@@ -222,15 +226,11 @@ export interface GrayTipElement {
content: string
}
jsonGrayTipElement: {
busiId: number
jsonStr: string
}
}
export enum FaceType {
normal = 1, // 小黄脸
normal2 = 2, // 新小黄脸, 从faceIndex 222开始
dice = 3, // 骰子
}
export enum FaceIndex {
dice = 358,
@@ -239,7 +239,7 @@ export enum FaceIndex {
export interface FaceElement {
faceIndex: number
faceType: FaceType
faceType: number
faceText?: string
packId?: string
stickerId?: string
@@ -414,3 +414,9 @@ export interface RawMessage {
multiForwardMsgElement: MultiForwardMsgElement
}[]
}
export interface Peer {
chatType: ChatType
peerUid: string // 如果是群聊uid为群号私聊uid就是加密的字符串
guildId?: string
}

View File

@@ -1,12 +1,13 @@
export enum GroupNotifyTypes {
INVITE_ME = 1,
INVITED_JOIN = 4, // 有人接受了邀请入群
JOIN_REQUEST_BY_INVITED = 5, // 有人邀请了别人入群
JOIN_REQUEST = 7,
ADMIN_SET = 8,
KICK_MEMBER = 9,
MEMBER_EXIT = 11, // 主动退出
ADMIN_UNSET = 12, // 我被取消管理员
ADMIN_UNSET_OTHER = 13, // 其他人取消管理员
ADMIN_UNSET = 12, // 我被取消管理员
ADMIN_UNSET_OTHER = 13, // 其他人取消管理员
}
export interface GroupNotifies {

View File

@@ -10,6 +10,7 @@ export interface QQLevel {
moonNum: number
starNum: number
}
export interface User {
uid: string // 加密的字符串
uin: string // QQ号
@@ -72,4 +73,189 @@ export interface SelfInfo extends User {
online?: boolean
}
export interface Friend extends User {}
export interface Friend extends User {
}
export interface CategoryFriend {
categoryId: number
categroyName: string
categroyMbCount: number
buddyList: User[]
}
export interface CoreInfo {
uid: string
uin: string
nick: string
remark: string
}
export interface BaseInfo {
qid: string
longNick: string
birthday_year: number
birthday_month: number
birthday_day: number
age: number
sex: number
eMail: string
phoneNum: string
categoryId: number
richTime: number
richBuffer: string
}
interface MusicInfo {
buf: string
}
interface VideoBizInfo {
cid: string
tvUrl: string
synchType: string
}
interface VideoInfo {
name: string
}
interface ExtOnlineBusinessInfo {
buf: string
customStatus: any
videoBizInfo: VideoBizInfo
videoInfo: VideoInfo
}
interface ExtBuffer {
buf: string
}
interface UserStatus {
uid: string
uin: string
status: number
extStatus: number
batteryStatus: number
termType: number
netType: number
iconType: number
customStatus: any
setTime: string
specialFlag: number
abiFlag: number
eNetworkType: number
showName: string
termDesc: string
musicInfo: MusicInfo
extOnlineBusinessInfo: ExtOnlineBusinessInfo
extBuffer: ExtBuffer
}
interface PrivilegeIcon {
jumpUrl: string
openIconList: any[]
closeIconList: any[]
}
interface VasInfo {
vipFlag: boolean
yearVipFlag: boolean
svipFlag: boolean
vipLevel: number
bigClub: boolean
bigClubLevel: number
nameplateVipType: number
grayNameplateFlag: number
superVipTemplateId: number
diyFontId: number
pendantId: number
pendantDiyId: number
faceId: number
vipFont: number
vipFontType: number
magicFont: number
fontEffect: number
newLoverDiamondFlag: number
extendNameplateId: number
diyNameplateIDs: any[]
vipStartFlag: number
vipDataFlag: number
gameNameplateId: string
gameLastLoginTime: string
gameRank: number
gameIconShowFlag: boolean
gameCardId: string
vipNameColorId: string
privilegeIcon: PrivilegeIcon
}
export interface SimpleInfo {
uid?: string
uin?: string
coreInfo: CoreInfo
baseInfo: BaseInfo
status: UserStatus | null
vasInfo: VasInfo | null
relationFlags: RelationFlags | null
otherFlags: any | null
intimate: any | null
}
interface RelationFlags {
topTime: string
isBlock: boolean
isMsgDisturb: boolean
isSpecialCareOpen: boolean
isSpecialCareZone: boolean
ringId: string
isBlocked: boolean
recommendImgFlag: number
disableEmojiShortCuts: number
qidianMasterFlag: number
qidianCrewFlag: number
qidianCrewFlag2: number
isHideQQLevel: number
isHidePrivilegeIcon: number
}
export interface FriendV2 extends SimpleInfo {
categoryId?: number
categroyName?: string
}
interface CommonExt {
constellation: number
shengXiao: number
kBloodType: number
homeTown: string
makeFriendCareer: number
pos: string
college: string
country: string
province: string
city: string
postCode: string
address: string
regTime: number
interest: string
labels: any[]
qqLevel: QQLevel
}
interface Pic {
picId: string
picTime: number
picUrlMap: Record<string, string>
}
interface PhotoWall {
picList: Pic[]
}
export interface UserDetailInfoListenerArg {
uid: string
uin: string
simpleInfo: SimpleInfo
commonExt: CommonExt
photoWall: PhotoWall
}

68
src/ntqqapi/wrapper.ts Normal file
View File

@@ -0,0 +1,68 @@
import { NodeIKernelBuddyService } from './services/NodeIKernelBuddyService'
import os from 'node:os'
const Process = require('node:process')
export interface NodeIQQNTWrapperSession {
[key: string]: any
getBuddyService(): NodeIKernelBuddyService
}
export interface WrapperApi {
NodeIQQNTWrapperSession?: NodeIQQNTWrapperSession
}
export interface WrapperConstructor {
[key: string]: any
NodeIKernelBuddyListener?: any
NodeIKernelGroupListener?: any
NodeQQNTWrapperUtil?: any
NodeIKernelMsgListener?: any
NodeIQQNTWrapperEngine?: any
NodeIGlobalAdapter?: any
NodeIDependsAdapter?: any
NodeIDispatcherAdapter?: any
NodeIKernelSessionListener?: any
NodeIKernelLoginService?: any
NodeIKernelLoginListener?: any
NodeIKernelProfileService?: any
NodeIKernelProfileListener?: any
}
export const wrapperApi: WrapperApi = {}
export const wrapperConstructor: WrapperConstructor = {}
const constructor = [
'NodeIKernelBuddyListener',
'NodeIKernelGroupListener',
'NodeQQNTWrapperUtil',
'NodeIKernelMsgListener',
'NodeIQQNTWrapperEngine',
'NodeIGlobalAdapter',
'NodeIDependsAdapter',
'NodeIDispatcherAdapter',
'NodeIKernelSessionListener',
'NodeIKernelLoginService',
'NodeIKernelLoginListener',
'NodeIKernelProfileService',
'NodeIKernelProfileListener',
]
Process.dlopenOrig = Process.dlopen
Process.dlopen = function (module, filename, flags = os.constants.dlopen.RTLD_LAZY) {
const dlopenRet = this.dlopenOrig(module, filename, flags)
for (let export_name in module.exports) {
module.exports[export_name] = new Proxy(module.exports[export_name], {
construct: (target, args, _newTarget) => {
const ret = new target(...args)
if (export_name === 'NodeIQQNTWrapperSession') wrapperApi.NodeIQQNTWrapperSession = ret
return ret
}
})
if (constructor.includes(export_name)) {
wrapperConstructor[export_name] = module.exports[export_name]
}
}
return dlopenRet
}

View File

@@ -4,8 +4,8 @@ import { OB11Return } from '../types'
import { log } from '../../common/utils/log'
class BaseAction<PayloadType, ReturnDataType> {
actionName: ActionName
abstract class BaseAction<PayloadType, ReturnDataType> {
abstract actionName: ActionName
protected async check(payload: PayloadType): Promise<BaseCheckResult> {
return {
@@ -21,7 +21,7 @@ class BaseAction<PayloadType, ReturnDataType> {
try {
const resData = await this._handle(payload)
return OB11Response.ok(resData)
} catch (e) {
} catch (e: any) {
log('发生错误', e)
return OB11Response.error(e?.toString() || e?.stack?.toString() || '未知错误,可能操作超时', 200)
}
@@ -35,7 +35,7 @@ class BaseAction<PayloadType, ReturnDataType> {
try {
const resData = await this._handle(payload)
return OB11Response.ok(resData, echo)
} catch (e) {
} catch (e: any) {
log('发生错误', e)
return OB11Response.error(e.stack?.toString() || e.toString(), 1200, echo)
}

View File

@@ -1,12 +1,12 @@
import BaseAction from '../BaseAction'
import fs from 'fs/promises'
import { dbUtil } from '../../../common/db'
import { getConfigUtil } from '../../../common/config'
import { log, sleep, uri2local } from '../../../common/utils'
import { NTQQFileApi } from '../../../ntqqapi/api/file'
import { dbUtil } from '@/common/db'
import { getConfigUtil } from '@/common/config'
import { checkFileReceived, log, sleep, uri2local } from '@/common/utils'
import { NTQQFileApi } from '@/ntqqapi/api'
import { ActionName } from '../types'
import { FileElement, RawMessage, VideoElement } from '../../../ntqqapi/types'
import { FileCache } from '../../../common/types'
import { FileElement, RawMessage, VideoElement } from '@/ntqqapi/types'
import { FileCache } from '@/common/types'
export interface GetFilePayload {
file: string // 文件名或者fileUuid
@@ -20,14 +20,13 @@ export interface GetFileResponse {
base64?: string
}
export class GetFileBase extends BaseAction<GetFilePayload, GetFileResponse> {
private getElement(msg: RawMessage): { id: string; element: VideoElement | FileElement } {
let element = msg.elements.find((e) => e.fileElement)
export abstract class GetFileBase extends BaseAction<GetFilePayload, GetFileResponse> {
private getElement(msg: RawMessage, elementId: string): VideoElement | FileElement {
let element = msg.elements.find((e) => e.elementId === elementId)
if (!element) {
element = msg.elements.find((e) => e.videoElement)
return { id: element.elementId, element: element.videoElement }
throw new Error('element not found')
}
return { id: element.elementId, element: element.fileElement }
return element.fileElement
}
private async download(cache: FileCache, file: string) {
log('需要调用 NTQQ 下载文件api')
@@ -35,24 +34,25 @@ export class GetFileBase extends BaseAction<GetFilePayload, GetFileResponse> {
let msg = await dbUtil.getMsgByLongId(cache.msgId)
if (msg) {
log('找到了文件 msg', msg)
let element = this.getElement(msg)
let element = this.getElement(msg, cache.elementId)
log('找到了文件 element', element)
// 构建下载函数
await NTQQFileApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid, element.id, '', '', true)
await sleep(1000)
await NTQQFileApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid, cache.elementId, '', '', true)
// 等待文件下载完成
msg = await dbUtil.getMsgByLongId(cache.msgId)
log('下载完成后的msg', msg)
cache.filePath = this.getElement(msg).element.filePath
cache.filePath = this.getElement(msg!, cache.elementId).filePath
await checkFileReceived(cache.filePath, 10 * 1000)
dbUtil.addFileCache(file, cache).then()
}
}
}
protected async _handle(payload: GetFilePayload): Promise<GetFileResponse> {
const cache = await dbUtil.getFileCache(payload.file)
const { autoDeleteFile, enableLocalFile2Url, autoDeleteFileSecond } = getConfigUtil().getConfig()
let cache = await dbUtil.getFileCache(payload.file)
if (!cache) {
throw new Error('file not found')
}
const { autoDeleteFile, enableLocalFile2Url, autoDeleteFileSecond } = getConfigUtil().getConfig()
if (cache.downloadFunc) {
await cache.downloadFunc()
}

View File

@@ -1,5 +1,9 @@
import { GetFileBase, GetFilePayload, GetFileResponse } from './GetFile'
import { ActionName } from '../types'
import {decodeSilk} from "@/common/utils/audio";
import { getConfigUtil } from '@/common/config'
import path from 'node:path'
import fs from 'node:fs'
interface Payload extends GetFilePayload {
out_format: 'mp3' | 'amr' | 'wma' | 'm4a' | 'spx' | 'ogg' | 'wav' | 'flac'
@@ -9,7 +13,13 @@ export default class GetRecord extends GetFileBase {
actionName = ActionName.GetRecord
protected async _handle(payload: Payload): Promise<GetFileResponse> {
let res = super._handle(payload)
let res = await super._handle(payload)
res.file = await decodeSilk(res.file!, payload.out_format)
res.file_name = path.basename(res.file)
res.file_size = fs.statSync(res.file).size.toString()
if (getConfigUtil().getConfig().enableLocalFile2Url){
res.base64 = fs.readFileSync(res.file, 'base64')
}
return res
}
}

View File

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

View File

@@ -3,7 +3,7 @@ import { ActionName } from '../types'
import fs from 'fs'
import { join as joinPath } from 'node:path'
import { calculateFileMD5, httpDownload, TEMP_DIR } from '../../../common/utils'
import { v4 as uuid4 } from 'uuid'
import { randomUUID } from 'node:crypto'
interface Payload {
thread_count?: number
@@ -22,7 +22,7 @@ export default class GoCQHTTPDownloadFile extends BaseAction<Payload, FileRespon
protected async _handle(payload: Payload): Promise<FileResponse> {
const isRandomName = !payload.name
let name = payload.name || uuid4()
let name = payload.name || randomUUID()
const filePath = joinPath(TEMP_DIR, name)
if (payload.base64) {

View File

@@ -1,12 +1,13 @@
import BaseAction from '../BaseAction'
import { OB11ForwardMessage, OB11Message, OB11MessageData } from '../../types'
import { NTQQMsgApi, Peer } from '../../../ntqqapi/api'
import { NTQQMsgApi } from '@/ntqqapi/api'
import { dbUtil } from '../../../common/db'
import { OB11Constructor } from '../../constructor'
import { ActionName } from '../types'
interface Payload {
message_id: string // long msg id
message_id: string // long msg idgocq
id?: string // long msg id, onebot11
}
interface Response {
@@ -16,7 +17,11 @@ interface Response {
export class GoCQHTTGetForwardMsgAction extends BaseAction<Payload, any> {
actionName = ActionName.GoCQHTTP_GetForwardMsg
protected async _handle(payload: Payload): Promise<any> {
const rootMsg = await dbUtil.getMsgByLongId(payload.message_id)
const message_id = payload.id || payload.message_id
if (!message_id) {
throw Error('message_id不能为空')
}
const rootMsg = await dbUtil.getMsgByLongId(message_id)
if (!rootMsg) {
throw Error('msg not found')
}
@@ -32,12 +37,13 @@ export class GoCQHTTGetForwardMsgAction extends BaseAction<Payload, any> {
let messages = await Promise.all(
msgList.map(async (msg) => {
let resMsg = await OB11Constructor.message(msg)
resMsg.message_id = await dbUtil.addMsg(msg)
resMsg.message_id = (await dbUtil.addMsg(msg))!
return resMsg
}),
)
messages.map((msg) => {
;(<OB11ForwardMessage>msg).content = msg.message
messages.map(v => {
const msg = v as Partial<OB11ForwardMessage>
msg.content = msg.message
delete msg.message
})
return { messages }

View File

@@ -6,7 +6,6 @@ import { ChatType } from '../../../ntqqapi/types'
import { dbUtil } from '../../../common/db'
import { NTQQMsgApi } from '../../../ntqqapi/api/msg'
import { OB11Constructor } from '../../constructor'
import { log } from '../../../common/utils'
interface Payload {
group_id: number

View File

@@ -0,0 +1,17 @@
import BaseAction from '../BaseAction'
import { handleQuickOperation, QuickOperation, QuickOperationEvent } from '../quick-operation'
import { log } from '@/common/utils'
import { ActionName } from '../types'
interface Payload{
context: QuickOperationEvent,
operation: QuickOperation
}
export class GoCQHTTHandleQuickOperation extends BaseAction<Payload, null>{
actionName = ActionName.GoCQHTTP_HandleQuickOperation
protected async _handle(payload: Payload): Promise<null> {
handleQuickOperation(payload.context, payload.operation).then().catch(log);
return null
}
}

View File

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

View File

@@ -1,11 +1,12 @@
import BaseAction from '../BaseAction'
import { getGroup, getUidByUin } from '../../../common/data'
import { getGroup, getUidByUin } from '@/common/data'
import { ActionName } from '../types'
import { SendMsgElementConstructor } from '../../../ntqqapi/constructor'
import { ChatType, SendFileElement } from '../../../ntqqapi/types'
import { SendMsgElementConstructor } from '@/ntqqapi/constructor'
import { ChatType, SendFileElement } from '@/ntqqapi/types'
import fs from 'fs'
import { NTQQMsgApi, Peer } from '../../../ntqqapi/api/msg'
import { uri2local } from '../../../common/utils'
import { NTQQMsgApi } from '@/ntqqapi/api/msg'
import { uri2local } from '@/common/utils'
import { Peer } from '@/ntqqapi/types'
interface Payload {
user_id: number
@@ -20,9 +21,9 @@ class GoCQHTTPUploadFileBase extends BaseAction<Payload, null> {
getPeer(payload: Payload): Peer {
if (payload.user_id) {
return { chatType: ChatType.friend, peerUid: getUidByUin(payload.user_id.toString()) }
return { chatType: ChatType.friend, peerUid: getUidByUin(payload.user_id.toString())! }
}
return { chatType: ChatType.group, peerUid: payload.group_id.toString() }
return { chatType: ChatType.group, peerUid: payload.group_id?.toString()! }
}
protected async _handle(payload: Payload): Promise<null> {

View File

@@ -0,0 +1,24 @@
import { GroupEssenceMsgRet, WebApi } from '@/ntqqapi/api'
import BaseAction from '../BaseAction'
import { ActionName } from '../types'
interface PayloadType {
group_id: number
pages?: number
}
export class GetGroupEssence extends BaseAction<PayloadType, GroupEssenceMsgRet | void> {
actionName = ActionName.GoCQHTTP_GetEssenceMsg
protected async _handle(payload: PayloadType) {
throw '此 api 暂不支持'
const ret = await WebApi.getGroupEssenceMsg(payload.group_id.toString(), payload.pages?.toString() || '0')
if (!ret) {
throw new Error('获取失败')
}
// ret.map((item) => {
//
// })
return ret
}
}

View File

@@ -0,0 +1,23 @@
import { WebApi, WebHonorType } from '@/ntqqapi/api'
import { ActionName } from '../types'
import BaseAction from '../BaseAction'
interface Payload {
group_id: number
type?: WebHonorType
}
export class GetGroupHonorInfo extends BaseAction<Payload, Array<any>> {
actionName = ActionName.GetGroupHonorInfo
protected async _handle(payload: Payload) {
// console.log(await NTQQUserApi.getRobotUinRange())
if (!payload.group_id) {
throw '缺少参数group_id'
}
if (!payload.type) {
payload.type = WebHonorType.ALL
}
return await WebApi.getGroupHonorInfo(payload.group_id.toString(), payload.type)
}
}

View File

@@ -14,13 +14,10 @@ class GetGroupList extends BaseAction<Payload, OB11Group[]> {
actionName = ActionName.GetGroupList
protected async _handle(payload: Payload) {
if (
groups.length === 0
|| payload?.no_cache === true || payload?.no_cache === 'true'
) {
if (groups.length === 0 || payload?.no_cache === true || payload?.no_cache === 'true') {
try {
const groups = await NTQQGroupApi.getGroups(true)
log("强制刷新群列表, 数量:", groups.length)
log('强制刷新群列表, 数量:', groups.length)
return OB11Constructor.groups(groups)
} catch (e) {}
}

View File

@@ -7,7 +7,7 @@ import { NTQQGroupApi } from '../../../ntqqapi/api/group'
import { log } from '../../../common/utils'
export interface PayloadType {
group_id: number,
group_id: number
no_cache: boolean | string
}

View File

@@ -2,13 +2,11 @@ import SendMsg from '../msg/SendMsg'
import { ActionName, BaseCheckResult } from '../types'
import { OB11PostSendMsg } from '../../types'
import { log } from '../../../common/utils/log'
class SendGroupMsg extends SendMsg {
actionName = ActionName.SendGroupMsg
protected async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> {
delete payload.user_id
delete (payload as Partial<OB11PostSendMsg>).user_id
payload.message_type = 'group'
return super.check(payload)
}

View File

@@ -1,6 +1,6 @@
import GetMsg from './msg/GetMsg'
import GetLoginInfo from './system/GetLoginInfo'
import GetFriendList from './user/GetFriendList'
import { GetFriendList, GetFriendWithCategory} from './user/GetFriendList'
import GetGroupList from './group/GetGroupList'
import GetGroupInfo from './group/GetGroupInfo'
import GetGroupMemberList from './group/GetGroupMemberList'
@@ -46,7 +46,14 @@ import GetFile from './file/GetFile'
import { GoCQHTTGetForwardMsgAction } from './go-cqhttp/GetForwardMsg'
import { GetCookies } from './user/GetCookie'
import { SetMsgEmojiLike } from './msg/SetMsgEmojiLike'
import { ForwardFriendSingleMsg, ForwardSingleGroupMsg } from './msg/ForwardSingleMsg'
import { ForwardFriendSingleMsg, ForwardGroupSingleMsg } from './msg/ForwardSingleMsg'
import { GetGroupEssence } from './group/GetGroupEssence'
import { GetGroupHonorInfo } from './group/GetGroupHonorInfo'
import { GoCQHTTHandleQuickOperation } from './go-cqhttp/QuickOperation'
import GoCQHTTPSetEssenceMsg from './go-cqhttp/SetEssenceMsg'
import GoCQHTTPDelEssenceMsg from './go-cqhttp/DelEssenceMsg'
import GetEvent from './llonebot/GetEvent'
export const actionHandlers = [
new GetFile(),
@@ -55,6 +62,8 @@ export const actionHandlers = [
new SetConfigAction(),
new GetGroupAddRequest(),
new SetQQAvatar(),
new GetFriendWithCategory(),
new GetEvent(),
// onebot11
new SendLike(),
new GetMsg(),
@@ -87,8 +96,10 @@ export const actionHandlers = [
new GetCookies(),
new SetMsgEmojiLike(),
new ForwardFriendSingleMsg(),
new ForwardSingleGroupMsg(),
new ForwardGroupSingleMsg(),
//以下为go-cqhttp api
new GetGroupEssence(),
new GetGroupHonorInfo(),
new GoCQHTTPSendForwardMsg(),
new GoCQHTTPSendGroupForwardMsg(),
new GoCQHTTPSendPrivateForwardMsg(),
@@ -100,6 +111,9 @@ export const actionHandlers = [
new GoCQHTTPUploadPrivateFile(),
new GoCQHTTPGetGroupMsgHistory(),
new GoCQHTTGetForwardMsgAction(),
new GoCQHTTHandleQuickOperation(),
new GoCQHTTPSetEssenceMsg(),
new GoCQHTTPDelEssenceMsg()
]
function initActionMap() {

View File

@@ -0,0 +1,23 @@
import BaseAction from '../BaseAction'
import { ActionName } from '../types'
import { getHttpEvent } from '../../server/event-for-http'
import { PostEventType } from '../../server/post-ob11-event'
// import { log } from "../../../common/utils";
interface Payload {
key: string
timeout: number
}
export default class GetEvent extends BaseAction<Payload, PostEventType[]> {
actionName = ActionName.GetEvent
protected async _handle(payload: Payload): Promise<PostEventType[]> {
let key = ''
if (payload.key) {
key = payload.key;
}
let timeout = parseInt(payload.timeout?.toString()) || 0;
let evts = await getHttpEvent(key,timeout);
return evts;
}
}

View File

@@ -1,9 +1,10 @@
import BaseAction from '../BaseAction'
import { NTQQMsgApi, Peer } from '../../../ntqqapi/api'
import { ChatType, RawMessage } from '../../../ntqqapi/types'
import { dbUtil } from '../../../common/db'
import { getUidByUin } from '../../../common/data'
import { NTQQMsgApi } from '@/ntqqapi/api'
import { ChatType, RawMessage } from '@/ntqqapi/types'
import { dbUtil } from '@/common/db'
import { getUidByUin } from '@/common/data'
import { ActionName } from '../types'
import { Peer } from '@/ntqqapi/types'
interface Payload {
message_id: number
@@ -11,18 +12,22 @@ interface Payload {
user_id?: number
}
class ForwardSingleMsg extends BaseAction<Payload, null> {
interface Response {
message_id: number
}
abstract class ForwardSingleMsg extends BaseAction<Payload, Response> {
protected async getTargetPeer(payload: Payload): Promise<Peer> {
if (payload.user_id) {
return { chatType: ChatType.friend, peerUid: getUidByUin(payload.user_id.toString()) }
return { chatType: ChatType.friend, peerUid: getUidByUin(payload.user_id.toString())! }
}
return { chatType: ChatType.group, peerUid: payload.group_id.toString() }
}
protected async _handle(payload: Payload): Promise<null> {
const msg = await dbUtil.getMsgByShortId(payload.message_id)
protected async _handle(payload: Payload): Promise<Response> {
const msg = (await dbUtil.getMsgByShortId(payload.message_id))!
const peer = await this.getTargetPeer(payload)
await NTQQMsgApi.forwardMsg(
const sentMsg = await NTQQMsgApi.forwardMsg(
{
chatType: msg.chatType,
peerUid: msg.peerUid,
@@ -30,7 +35,8 @@ class ForwardSingleMsg extends BaseAction<Payload, null> {
peer,
[msg.msgId],
)
return null
const ob11MsgId = await dbUtil.addMsg(sentMsg)
return { message_id: ob11MsgId! }
}
}
@@ -38,6 +44,6 @@ export class ForwardFriendSingleMsg extends ForwardSingleMsg {
actionName = ActionName.ForwardFriendSingleMsg
}
export class ForwardSingleGroupMsg extends ForwardSingleMsg {
export class ForwardGroupSingleMsg extends ForwardSingleMsg {
actionName = ActionName.ForwardGroupSingleMsg
}

View File

@@ -14,14 +14,16 @@ import { friends, getFriend, getGroup, getGroupMember, getUidByUin, selfInfo } f
import {
OB11MessageCustomMusic,
OB11MessageData,
OB11MessageDataType, OB11MessageFile,
OB11MessageDataType,
OB11MessageFile,
OB11MessageJson,
OB11MessageMixType,
OB11MessageMusic,
OB11MessageNode, OB11MessageVideo,
OB11MessageNode,
OB11MessageVideo,
OB11PostSendMsg,
} from '../../types'
import { NTQQMsgApi, Peer } from '../../../ntqqapi/api/msg'
import { NTQQMsgApi } from '../../../ntqqapi/api/msg'
import { SendMsgElementConstructor } from '../../../ntqqapi/constructor'
import BaseAction from '../BaseAction'
import { ActionName, BaseCheckResult } from '../types'
@@ -32,9 +34,10 @@ import { ALLOW_SEND_TEMP_MSG, getConfigUtil } from '../../../common/config'
import { log } from '../../../common/utils/log'
import { sleep } from '../../../common/utils/helper'
import { uri2local } from '../../../common/utils'
import { crychic } from '../../../ntqqapi/external/crychic'
import { crychic } from '../../../ntqqapi/native/crychic'
import { NTQQGroupApi } from '../../../ntqqapi/api'
import { CustomMusicSignPostData, IdMusicSignPostData, MusicSign, MusicSignPostData } from '../../../common/utils/sign'
import { Peer } from '../../../ntqqapi/types/msg'
function checkSendMessage(sendMsgList: OB11MessageData[]) {
function checkUri(uri: string): boolean {
@@ -48,22 +51,28 @@ function checkSendMessage(sendMsgList: OB11MessageData[]) {
let data = msg['data']
if (type === 'text' && !data['text']) {
return 400
} else if (['image', 'voice', 'record'].includes(type)) {
}
else if (['image', 'voice', 'record'].includes(type)) {
if (!data['file']) {
return 400
} else {
}
else {
if (checkUri(data['file'])) {
return 200
} else {
}
else {
return 400
}
}
} else if (type === 'at' && !data['qq']) {
return 400
} else if (type === 'reply' && !data['id']) {
}
else if (type === 'at' && !data['qq']) {
return 400
}
} else {
else if (type === 'reply' && !data['id']) {
return 400
}
}
else {
return 400
}
}
@@ -85,10 +94,12 @@ export function convertMessage2List(message: OB11MessageMixType, autoEscape = fa
},
},
]
} else {
}
else {
message = decodeCQCode(message.toString())
}
} else if (!Array.isArray(message)) {
}
else if (!Array.isArray(message)) {
message = [message]
}
return message
@@ -106,172 +117,177 @@ export async function createSendElements(
continue
}
switch (sendMsg.type) {
case OB11MessageDataType.text:
{
const text = sendMsg.data?.text
if (text) {
sendElements.push(SendMsgElementConstructor.text(sendMsg.data!.text))
}
case OB11MessageDataType.text: {
const text = sendMsg.data?.text
if (text) {
sendElements.push(SendMsgElementConstructor.text(sendMsg.data!.text))
}
}
break
case OB11MessageDataType.at:
{
if (!target) {
continue
case OB11MessageDataType.at: {
if (!target) {
continue
}
let atQQ = sendMsg.data?.qq
if (atQQ) {
atQQ = atQQ.toString()
if (atQQ === 'all') {
// todo查询剩余的at全体次数
const groupCode = (target as Group)?.groupCode
let remainAtAllCount = 1
let isAdmin: boolean = true
if (groupCode) {
try {
remainAtAllCount = (await NTQQGroupApi.getGroupAtAllRemainCount(groupCode)).atInfo
.RemainAtAllCountForUin
log(`${groupCode}剩余at全体次数`, remainAtAllCount)
const self = await getGroupMember((target as Group)?.groupCode, selfInfo.uin)
isAdmin = self?.role === GroupMemberRole.admin || self?.role === GroupMemberRole.owner
} catch (e) {
}
}
if (isAdmin && remainAtAllCount > 0) {
sendElements.push(SendMsgElementConstructor.at(atQQ, atQQ, AtType.atAll, '全体成员'))
}
}
let atQQ = sendMsg.data?.qq
if (atQQ) {
atQQ = atQQ.toString()
if (atQQ === 'all') {
// todo查询剩余的at全体次数
const groupCode = (target as Group)?.groupCode
let remainAtAllCount = 1
let isAdmin: boolean = true
if (groupCode) {
try {
remainAtAllCount = (await NTQQGroupApi.getGroupAtAllRemainCount(groupCode)).atInfo
.RemainAtAllCountForUin
log(`${groupCode}剩余at全体次数`, remainAtAllCount)
const self = await getGroupMember((target as Group)?.groupCode, selfInfo.uin)
isAdmin = self.role === GroupMemberRole.admin || self.role === GroupMemberRole.owner
} catch (e) {}
}
if (isAdmin && remainAtAllCount > 0) {
sendElements.push(SendMsgElementConstructor.at(atQQ, atQQ, AtType.atAll, '全体成员'))
}
} else {
// const atMember = group?.members.find(m => m.uin == atQQ)
const atMember = await getGroupMember((target as Group)?.groupCode, atQQ)
if (atMember) {
sendElements.push(
SendMsgElementConstructor.at(atQQ, atMember.uid, AtType.atUser, atMember.cardName || atMember.nick),
)
}
else {
// const atMember = group?.members.find(m => m.uin == atQQ)
const atMember = await getGroupMember((target as Group)?.groupCode, atQQ)
if (atMember) {
sendElements.push(
SendMsgElementConstructor.at(atQQ, atMember.uid, AtType.atUser, atMember.cardName || atMember.nick),
)
}
}
}
}
break
case OB11MessageDataType.reply:
{
let replyMsgId = sendMsg.data.id
if (replyMsgId) {
const replyMsg = await dbUtil.getMsgByShortId(parseInt(replyMsgId))
if (replyMsg) {
case OB11MessageDataType.reply: {
let replyMsgId = sendMsg.data.id
if (replyMsgId) {
const replyMsg = await dbUtil.getMsgByShortId(parseInt(replyMsgId))
if (replyMsg) {
sendElements.push(
SendMsgElementConstructor.reply(
replyMsg.msgSeq,
replyMsg.msgId,
replyMsg.senderUin!,
replyMsg.senderUin!,
),
)
}
}
}
break
case OB11MessageDataType.face: {
const faceId = sendMsg.data?.id
if (faceId) {
sendElements.push(SendMsgElementConstructor.face(parseInt(faceId)))
}
}
break
case OB11MessageDataType.mface: {
sendElements.push(
SendMsgElementConstructor.mface(
sendMsg.data.emoji_package_id,
sendMsg.data.emoji_id,
sendMsg.data.key,
sendMsg.data.summary,
),
)
}
break
case OB11MessageDataType.image:
case OB11MessageDataType.file:
case OB11MessageDataType.video:
case OB11MessageDataType.voice: {
const data = (sendMsg as OB11MessageFile).data
let file = data.file
const payloadFileName = data?.name
if (file) {
const cache = await dbUtil.getFileCache(file)
if (cache) {
if (fs.existsSync(cache.filePath)) {
file = 'file://' + cache.filePath
}
else if (cache.downloadFunc) {
await cache.downloadFunc()
file = cache.filePath
}
else if (cache.url) {
file = cache.url
}
log('找到文件缓存', file)
}
const { path, isLocal, fileName, errMsg } = await uri2local(file)
if (errMsg) {
throw errMsg
}
if (path) {
if (!isLocal) {
// 只删除http和base64转过来的文件
deleteAfterSentFiles.push(path)
}
if (sendMsg.type === OB11MessageDataType.file) {
log('发送文件', path, payloadFileName || fileName)
sendElements.push(await SendMsgElementConstructor.file(path, payloadFileName || fileName))
}
else if (sendMsg.type === OB11MessageDataType.video) {
log('发送视频', path, payloadFileName || fileName)
let thumb = sendMsg.data?.thumb
if (thumb) {
let uri2LocalRes = await uri2local(thumb)
if (uri2LocalRes.success) {
thumb = uri2LocalRes.path
}
}
sendElements.push(await SendMsgElementConstructor.video(path, payloadFileName || fileName, thumb))
}
else if (sendMsg.type === OB11MessageDataType.voice) {
sendElements.push(await SendMsgElementConstructor.ptt(path))
}
else if (sendMsg.type === OB11MessageDataType.image) {
sendElements.push(
SendMsgElementConstructor.reply(
replyMsg.msgSeq,
replyMsg.msgId,
replyMsg.senderUin,
replyMsg.senderUin,
await SendMsgElementConstructor.pic(
path,
sendMsg.data.summary || '',
<PicSubType>parseInt(sendMsg.data?.subType?.toString()!) || 0,
),
)
}
}
}
}
break
case OB11MessageDataType.face:
{
const faceId = sendMsg.data?.id
if (faceId) {
sendElements.push(SendMsgElementConstructor.face(parseInt(faceId)))
case OB11MessageDataType.json: {
sendElements.push(SendMsgElementConstructor.ark(sendMsg.data.data))
}
break
case OB11MessageDataType.poke: {
let qq = sendMsg.data?.qq || sendMsg.data?.id
if (qq) {
if ('groupCode' in target!) {
crychic.sendGroupPoke(target.groupCode, qq.toString())
}
}
break
case OB11MessageDataType.mface: {
sendElements.push(
SendMsgElementConstructor.mface(sendMsg.data.emoji_package_id, sendMsg.data.emoji_id, sendMsg.data.key, sendMsg.data.summary),
)
}break;
case OB11MessageDataType.image:
case OB11MessageDataType.file:
case OB11MessageDataType.video:
case OB11MessageDataType.voice:
{
const data = (sendMsg as OB11MessageFile).data
let file = data.file
const payloadFileName = data?.name
if (file) {
const cache = await dbUtil.getFileCache(file)
if (cache) {
if (fs.existsSync(cache.filePath)) {
file = 'file://' + cache.filePath
} else if (cache.downloadFunc) {
await cache.downloadFunc()
file = cache.filePath
} else if (cache.url) {
file = cache.url
}
log('找到文件缓存', file)
}
const { path, isLocal, fileName, errMsg } = await uri2local(file)
if (errMsg) {
throw errMsg
}
if (path) {
if (!isLocal) {
// 只删除http和base64转过来的文件
deleteAfterSentFiles.push(path)
}
if (sendMsg.type === OB11MessageDataType.file) {
log('发送文件', path, payloadFileName || fileName)
sendElements.push(await SendMsgElementConstructor.file(path, payloadFileName || fileName))
} else if (sendMsg.type === OB11MessageDataType.video) {
log('发送视频', path, payloadFileName || fileName)
let thumb = sendMsg.data?.thumb
if (thumb) {
let uri2LocalRes = await uri2local(thumb)
if (uri2LocalRes.success) {
thumb = uri2LocalRes.path
}
}
sendElements.push(await SendMsgElementConstructor.video(path, payloadFileName || fileName, thumb))
} else if (sendMsg.type === OB11MessageDataType.voice) {
sendElements.push(await SendMsgElementConstructor.ptt(path))
} else if (sendMsg.type === OB11MessageDataType.image) {
sendElements.push(
await SendMsgElementConstructor.pic(
path,
sendMsg.data.summary || '',
<PicSubType>parseInt(sendMsg.data?.subType?.toString()) || 0,
),
)
}
else {
if (!qq) {
qq = parseInt(target?.uin!)
}
crychic.sendFriendPoke(qq.toString())
}
sendElements.push(SendMsgElementConstructor.poke('', '')!)
}
}
break
case OB11MessageDataType.json:
{
sendElements.push(SendMsgElementConstructor.ark(sendMsg.data.data))
}
case OB11MessageDataType.dice: {
const resultId = sendMsg.data?.result
sendElements.push(SendMsgElementConstructor.dice(resultId))
}
break
case OB11MessageDataType.poke:
{
let qq = sendMsg.data?.qq || sendMsg.data?.id
if (qq) {
if ('groupCode' in target) {
crychic.sendGroupPoke(target.groupCode, qq.toString())
} else {
if (!qq) {
qq = parseInt(target.uin)
}
crychic.sendFriendPoke(qq.toString())
}
sendElements.push(SendMsgElementConstructor.poke('', ''))
}
}
break
case OB11MessageDataType.dice:
{
const resultId = sendMsg.data?.result
sendElements.push(SendMsgElementConstructor.dice(resultId))
}
break
case OB11MessageDataType.RPS:
{
const resultId = sendMsg.data?.result
sendElements.push(SendMsgElementConstructor.rps(resultId))
}
case OB11MessageDataType.RPS: {
const resultId = sendMsg.data?.result
sendElements.push(SendMsgElementConstructor.rps(resultId))
}
break
}
}
@@ -291,10 +307,34 @@ export async function sendMsg(
if (!sendElements.length) {
throw '消息体无法解析,请检查是否发送了不支持的消息类型'
}
const returnMsg = await NTQQMsgApi.sendMsg(peer, sendElements, waitComplete, 20000)
// 计算发送的文件大小
let totalSize = 0
for (const fileElement of sendElements) {
try {
if (fileElement.elementType === ElementType.PTT) {
totalSize += fs.statSync(fileElement.pttElement.filePath).size
}
if (fileElement.elementType === ElementType.FILE) {
totalSize += fs.statSync(fileElement.fileElement.filePath).size
}
if (fileElement.elementType === ElementType.VIDEO) {
totalSize += fs.statSync(fileElement.videoElement.filePath).size
}
if (fileElement.elementType === ElementType.PIC) {
totalSize += fs.statSync(fileElement.picElement.sourcePath).size
}
} catch (e) {
log('文件大小计算失败', e, fileElement)
}
}
log('发送消息总大小', totalSize, 'bytes')
let timeout = ((totalSize / 1024 / 100) * 1000) + 5000 // 100kb/s
log('设置消息超时时间', timeout)
const returnMsg = await NTQQMsgApi.sendMsg(peer, sendElements, waitComplete, timeout)
log('消息发送结果', returnMsg)
returnMsg.msgShortId = await dbUtil.addMsg(returnMsg)
deleteAfterSentFiles.map((f) => fs.unlink(f, () => {}))
deleteAfterSentFiles.map((f) => fs.unlink(f, () => {
}))
return returnMsg
}
@@ -303,14 +343,14 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
protected async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> {
const messages = convertMessage2List(payload.message)
const fmNum = this.getSpecialMsgNum(payload, OB11MessageDataType.node)
const fmNum = this.getSpecialMsgNum(messages, OB11MessageDataType.node)
if (fmNum && fmNum != messages.length) {
return {
valid: false,
message: '转发消息不能和普通消息混在一起发送,转发需要保证message只有type为node的元素',
}
}
const musicNum = this.getSpecialMsgNum(payload, OB11MessageDataType.music)
const musicNum = this.getSpecialMsgNum(messages, OB11MessageDataType.music)
if (musicNum && messages.length > 1) {
return {
valid: false,
@@ -347,10 +387,10 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
let group: Group | undefined = undefined
let friend: Friend | undefined = undefined
const genGroupPeer = async () => {
group = await getGroup(payload.group_id.toString())
group = await getGroup(payload.group_id?.toString()!)
peer.chatType = ChatType.group
// peer.name = group.name
peer.peerUid = group.groupCode
peer.peerUid = group?.groupCode!
}
const genFriendPeer = () => {
@@ -358,7 +398,8 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
if (friend) {
// peer.name = friend.nickName
peer.peerUid = friend.uid
} else {
}
else {
peer.chatType = ChatType.temp
const tempUserUid = getUidByUin(payload.user_id.toString())
if (!tempUserUid) {
@@ -371,25 +412,29 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
}
if (payload?.group_id && payload.message_type === 'group') {
await genGroupPeer()
} else if (payload?.user_id) {
}
else if (payload?.user_id) {
genFriendPeer()
} else if (payload.group_id) {
}
else if (payload.group_id) {
await genGroupPeer()
} else {
}
else {
throw '发送消息参数错误, 请指定group_id或user_id'
}
const messages = convertMessage2List(
payload.message,
payload.auto_escape === true || payload.auto_escape === 'true',
)
if (this.getSpecialMsgNum(payload, OB11MessageDataType.node)) {
if (this.getSpecialMsgNum(messages, OB11MessageDataType.node)) {
try {
const returnMsg = await this.handleForwardNode(peer, messages as OB11MessageNode[], group)
return { message_id: returnMsg.msgShortId }
} catch (e) {
return { message_id: returnMsg?.msgShortId! }
} catch (e: any) {
throw '发送转发消息失败 ' + e.toString()
}
} else if (this.getSpecialMsgNum(payload, OB11MessageDataType.music)) {
}
else if (this.getSpecialMsgNum(messages, OB11MessageDataType.music)) {
const music = messages[0] as OB11MessageMusic
if (music) {
const { musicSignUrl } = getConfigUtil().getConfig()
@@ -402,24 +447,24 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
}
const postData: MusicSignPostData = { ...music.data }
if (type === 'custom' && music.data.content) {
;(postData as CustomMusicSignPostData).singer = music.data.content
delete (postData as OB11MessageCustomMusic['data']).content
const data = postData as CustomMusicSignPostData
data.singer = music.data.content
delete (data as OB11MessageCustomMusic['data']).content
}
if (type === 'custom'){
if (type === 'custom') {
const customMusicData = music.data as CustomMusicSignPostData
if (!customMusicData.url){
throw ('自定义音卡缺少参数url');
if (!customMusicData.url) {
throw '自定义音卡缺少参数url'
}
if (!customMusicData.audio){
throw('自定义音卡缺少参数audio');
if (!customMusicData.audio) {
throw '自定义音卡缺少参数audio'
}
if (!customMusicData.title){
throw('自定义音卡缺少参数title');
if (!customMusicData.title) {
throw '自定义音卡缺少参数title'
}
}
if (type === 'qq' || type === '163') {
const idMusicData = music.data as IdMusicSignPostData;
const idMusicData = music.data as IdMusicSignPostData
if (!idMusicData.id) {
throw '音乐卡片缺少id参数'
}
@@ -427,6 +472,9 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
let jsonContent: string
try {
jsonContent = await new MusicSign(musicSignUrl).sign(postData)
if (!jsonContent) {
throw '音乐消息生成失败,提交内容有误或者签名服务器签名失败'
}
} catch (e) {
throw `签名音乐消息失败:${e}`
}
@@ -444,18 +492,19 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
}
}
const returnMsg = await sendMsg(peer, sendElements, deleteAfterSentFiles)
deleteAfterSentFiles.map((f) => fs.unlink(f, () => {}))
return { message_id: returnMsg.msgShortId }
deleteAfterSentFiles.map((f) => fs.unlink(f, () => {
}))
return { message_id: returnMsg.msgShortId! }
}
private getSpecialMsgNum(payload: OB11PostSendMsg, msgType: OB11MessageDataType): number {
if (Array.isArray(payload.message)) {
return payload.message.filter((msg) => msg.type == msgType).length
private getSpecialMsgNum(message: OB11MessageData[], msgType: OB11MessageDataType): number {
if (Array.isArray(message)) {
return message.filter((msg) => msg.type == msgType).length
}
return 0
}
private async cloneMsg(msg: RawMessage): Promise<RawMessage> {
private async cloneMsg(msg: RawMessage): Promise<RawMessage | undefined> {
log('克隆的目标消息', msg)
let sendElements: SendMessageElement[] = []
for (const ele of msg.elements) {
@@ -501,16 +550,18 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
if (nodeId) {
let nodeMsg = await dbUtil.getMsgByShortId(parseInt(nodeId))
if (!needClone) {
nodeMsgIds.push(nodeMsg.msgId)
} else {
if (nodeMsg.peerUid !== selfInfo.uid) {
const cloneMsg = await this.cloneMsg(nodeMsg)
nodeMsgIds.push(nodeMsg?.msgId!)
}
else {
if (nodeMsg?.peerUid !== selfInfo.uid) {
const cloneMsg = await this.cloneMsg(nodeMsg!)
if (cloneMsg) {
nodeMsgIds.push(cloneMsg.msgId)
}
}
}
} else {
}
else {
// 自定义的消息
// 提取消息段发给自己生成消息id
try {
@@ -532,7 +583,8 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
}
sendElementsSplit[splitIndex] = [ele]
splitIndex++
} else {
}
else {
sendElementsSplit[splitIndex].push(ele)
}
log(sendElementsSplit)
@@ -544,7 +596,8 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
await sleep(500)
log('转发节点生成成功', nodeMsg.msgId)
}
deleteAfterSentFiles.map((f) => fs.unlink(f, () => {}))
deleteAfterSentFiles.map((f) => fs.unlink(f, () => {
}))
} catch (e) {
log('生成转发消息节点失败', e)
}
@@ -553,7 +606,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
// 检查srcPeer是否一致不一致则需要克隆成自己的消息, 让所有srcPeer都变成自己的使其保持一致才能够转发
let nodeMsgArray: Array<RawMessage> = []
let srcPeer: Peer = null
let srcPeer: Peer | null = null
let needSendSelf = false
for (const [index, msgId] of nodeMsgIds.entries()) {
const nodeMsg = await dbUtil.getMsgByLongId(msgId)
@@ -561,7 +614,8 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
nodeMsgArray.push(nodeMsg)
if (!srcPeer) {
srcPeer = { chatType: nodeMsg.chatType, peerUid: nodeMsg.peerUid }
} else if (srcPeer.peerUid !== nodeMsg.peerUid) {
}
else if (srcPeer.peerUid !== nodeMsg.peerUid) {
needSendSelf = true
srcPeer = selfPeer
}
@@ -595,7 +649,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
}
try {
log('开发转发', nodeMsgIds)
return await NTQQMsgApi.multiForwardMsg(srcPeer, destPeer, nodeMsgIds)
return await NTQQMsgApi.multiForwardMsg(srcPeer!, destPeer, nodeMsgIds)
} catch (e) {
log('forward failed', e)
return null

View File

@@ -0,0 +1,149 @@
// handle quick action, create at 2024-5-18 10:54:39 by linyuchen
import { OB11Message, OB11MessageAt, OB11MessageData, OB11MessageDataType } from '../types'
import { OB11FriendRequestEvent } from '../event/request/OB11FriendRequest'
import { OB11GroupRequestEvent } from '../event/request/OB11GroupRequest'
import { dbUtil } from '@/common/db'
import { NTQQFriendApi, NTQQGroupApi, NTQQMsgApi } from '@/ntqqapi/api'
import { ChatType, Group, GroupRequestOperateTypes, Peer } from '@/ntqqapi/types'
import { getGroup, getUidByUin } from '@/common/data'
import { convertMessage2List, createSendElements, sendMsg } from './msg/SendMsg'
import { isNull, log } from '@/common/utils'
import { getConfigUtil } from '@/common/config'
interface QuickOperationPrivateMessage {
reply?: string
auto_escape?: boolean
}
interface QuickOperationGroupMessage extends QuickOperationPrivateMessage {
// 回复群消息
at_sender?: boolean
delete?: boolean
kick?: boolean
ban?: boolean
ban_duration?: number
//
}
interface QuickOperationFriendRequest {
approve?: boolean
remark?: string
}
interface QuickOperationGroupRequest {
approve?: boolean
reason?: string
}
export type QuickOperation = QuickOperationPrivateMessage &
QuickOperationGroupMessage &
QuickOperationFriendRequest &
QuickOperationGroupRequest
export type QuickOperationEvent = OB11Message | OB11FriendRequestEvent | OB11GroupRequestEvent;
export async function handleQuickOperation(context: QuickOperationEvent, quickAction: QuickOperation) {
if (context.post_type === 'message') {
handleMsg(context as OB11Message, quickAction).then().catch(log)
}
if (context.post_type === 'request') {
const friendRequest = context as OB11FriendRequestEvent
const groupRequest = context as OB11GroupRequestEvent
if ((friendRequest).request_type === 'friend') {
handleFriendRequest(friendRequest, quickAction).then().catch(log)
}
else if (groupRequest.request_type === 'group') {
handleGroupRequest(groupRequest, quickAction).then().catch(log)
}
}
}
async function handleMsg(msg: OB11Message, quickAction: QuickOperationPrivateMessage | QuickOperationGroupMessage) {
msg = msg as OB11Message
const rawMessage = await dbUtil.getMsgByShortId(msg.message_id)
const reply = quickAction.reply
const ob11Config = getConfigUtil().getConfig().ob11
let peer: Peer = {
chatType: ChatType.friend,
peerUid: msg.user_id.toString(),
}
if (msg.message_type == 'private') {
peer.peerUid = getUidByUin(msg.user_id.toString())!
if (msg.sub_type === 'group') {
peer.chatType = ChatType.temp
}
}
else {
peer.chatType = ChatType.group
peer.peerUid = msg.group_id?.toString()!
}
if (reply) {
let group: Group | null = null
let replyMessage: OB11MessageData[] = []
if (ob11Config.enableQOAutoQuote) {
replyMessage.push({
type: OB11MessageDataType.reply,
data: {
id: msg.message_id.toString(),
},
})
}
if (msg.message_type == 'group') {
group = (await getGroup(msg.group_id?.toString()!))!
if ((quickAction as QuickOperationGroupMessage).at_sender) {
replyMessage.push({
type: 'at',
data: {
qq: msg.user_id.toString(),
},
} as OB11MessageAt)
}
}
replyMessage = replyMessage.concat(convertMessage2List(reply, quickAction.auto_escape))
const { sendElements, deleteAfterSentFiles } = await createSendElements(replyMessage, group!)
log(`发送消息给`, peer, sendElements)
sendMsg(peer, sendElements, deleteAfterSentFiles, false).then().catch(log)
}
if (msg.message_type === 'group') {
const groupMsgQuickAction = quickAction as QuickOperationGroupMessage
// handle group msg
if (groupMsgQuickAction.delete) {
NTQQMsgApi.recallMsg(peer, [rawMessage?.msgId!]).then().catch(log)
}
if (groupMsgQuickAction.kick) {
NTQQGroupApi.kickMember(peer.peerUid, [rawMessage?.senderUid!]).then().catch(log)
}
if (groupMsgQuickAction.ban) {
NTQQGroupApi.banMember(peer.peerUid, [
{
uid: rawMessage?.senderUid!,
timeStamp: groupMsgQuickAction.ban_duration || 60 * 30,
},
]).then().catch(log)
}
}
}
async function handleFriendRequest(request: OB11FriendRequestEvent,
quickAction: QuickOperationFriendRequest) {
if (!isNull(quickAction.approve)) {
// todo: set remark
NTQQFriendApi.handleFriendRequest(request.flag, quickAction.approve).then().catch(log)
}
}
async function handleGroupRequest(request: OB11GroupRequestEvent,
quickAction: QuickOperationGroupRequest) {
if (!isNull(quickAction.approve)) {
NTQQGroupApi.handleGroupRequest(
request.flag,
quickAction.approve ? GroupRequestOperateTypes.approve : GroupRequestOperateTypes.reject,
quickAction.reason,
).then().catch(log)
}
}

View File

@@ -1,9 +1,8 @@
import BaseAction from '../BaseAction'
import { ActionName } from '../types'
import fs from 'fs'
import Path from 'path'
import fs from 'node:fs'
import Path from 'node:path'
import { ChatType, ChatCacheListItemBasic, CacheFileType } from '../../../ntqqapi/types'
import { dbUtil } from '../../../common/db'
import { NTQQFileApi, NTQQFileCacheApi } from '../../../ntqqapi/api/file'
export default class CleanCache extends BaseAction<void, void> {
@@ -12,14 +11,16 @@ export default class CleanCache extends BaseAction<void, void> {
protected _handle(): Promise<void> {
return new Promise<void>(async (res, rej) => {
try {
// dbUtil.clearCache();
// dbUtil.clearCache()
const cacheFilePaths: string[] = []
await NTQQFileCacheApi.setCacheSilentScan(false)
cacheFilePaths.push(await NTQQFileCacheApi.getHotUpdateCachePath())
cacheFilePaths.push(await NTQQFileCacheApi.getDesktopTmpPath())
;(await NTQQFileCacheApi.getCacheSessionPathList()).forEach((e) => cacheFilePaths.push(e.value))
const list = await NTQQFileCacheApi.getCacheSessionPathList()
list.forEach((e) => cacheFilePaths.push(e.value))
// await NTQQApi.addCacheScannedPaths(); // XXX: 调用就崩溃,原因目前还未知
const cacheScanResult = await NTQQFileCacheApi.scanCache()

View File

@@ -8,7 +8,7 @@ export default class GetStatus extends BaseAction<any, OB11Status> {
protected async _handle(payload: any): Promise<OB11Status> {
return {
online: selfInfo.online,
online: selfInfo.online!,
good: true,
}
}

View File

@@ -21,6 +21,8 @@ export enum ActionName {
SetConfig = 'set_config',
Debug = 'llonebot_debug',
GetFile = 'get_file',
GetFriendsWithCategory = 'get_friends_with_category',
GetEvent = 'get_event',
// onebot 11
SendLike = 'send_like',
GetLoginInfo = 'get_login_info',
@@ -66,4 +68,9 @@ export enum ActionName {
GoCQHTTP_DownloadFile = 'download_file',
GoCQHTTP_GetGroupMsgHistory = 'get_group_msg_history',
GoCQHTTP_GetForwardMsg = 'get_forward_msg',
GoCQHTTP_GetEssenceMsg = "get_essence_msg_list",
GoCQHTTP_HandleQuickOperation = ".handle_quick_operation",
GetGroupHonorInfo = "get_group_honor_info",
GoCQHTTP_SetEssenceMsg = 'set_essence_msg',
GoCQHTTP_DelEssenceMsg = 'delete_essence_msg',
}

View File

@@ -1,12 +1,16 @@
import BaseAction from '../BaseAction'
import { NTQQUserApi } from '../../../ntqqapi/api'
import { groups } from '../../../common/data'
import { ActionName } from '../types'
export class GetCookies extends BaseAction<null, { cookies: string; bkn: string }> {
actionName = ActionName.GetCookies
protected async _handle() {
return NTQQUserApi.getCookie(groups[0])
}
}
import BaseAction from '../BaseAction'
import { NTQQUserApi } from '@/ntqqapi/api'
import { ActionName } from '../types'
interface Payload {
domain: string
}
export class GetCookies extends BaseAction<Payload, { cookies: string; bkn: string }> {
actionName = ActionName.GetCookies
protected async _handle(payload: Payload) {
const domain = payload.domain || 'qun.qq.com'
return NTQQUserApi.getCookies(domain);
}
}

View File

@@ -1,19 +1,23 @@
import { OB11User } from '../../types'
import { OB11Constructor } from '../../constructor'
import { friends } from '../../../common/data'
import { friends, rawFriends } from '@/common/data'
import BaseAction from '../BaseAction'
import { ActionName } from '../types'
import { NTQQFriendApi } from '../../../ntqqapi/api'
import { log } from '../../../common/utils'
import { NTQQFriendApi } from '@/ntqqapi/api'
import { CategoryFriend } from '@/ntqqapi/types'
import { qqPkgInfo } from '@/common/utils/QQBasicInfo'
interface Payload{
interface Payload {
no_cache: boolean | string
}
class GetFriendList extends BaseAction<Payload, OB11User[]> {
export class GetFriendList extends BaseAction<Payload, OB11User[]> {
actionName = ActionName.GetFriendList
protected async _handle(payload: Payload) {
if (+qqPkgInfo.buildVersion >= 26702) {
return OB11Constructor.friendsV2(await NTQQFriendApi.getBuddyV2(payload?.no_cache === true || payload?.no_cache === 'true'))
}
if (friends.length === 0 || payload?.no_cache === true || payload?.no_cache === 'true') {
const _friends = await NTQQFriendApi.getFriends(true)
// log('强制刷新好友列表,结果: ', _friends)
@@ -26,4 +30,11 @@ class GetFriendList extends BaseAction<Payload, OB11User[]> {
}
}
export default GetFriendList
export class GetFriendWithCategory extends BaseAction<void, Array<CategoryFriend>> {
actionName = ActionName.GetFriendsWithCategory;
protected async _handle(payload: void) {
return rawFriends;
}
}

View File

@@ -19,7 +19,7 @@ export default class SendLike extends BaseAction<Payload, null> {
const friend = await getFriend(qq)
let uid: string
if (!friend) {
uid = getUidByUin(qq)
uid = getUidByUin(qq)!
} else {
uid = friend.uid
}

View File

@@ -1,4 +1,4 @@
import fastXmlParser, { XMLParser } from 'fast-xml-parser'
import fastXmlParser from 'fast-xml-parser'
import {
OB11Group,
OB11GroupMember,
@@ -15,17 +15,18 @@ import {
FaceIndex,
GrayTipElementSubType,
Group,
Peer,
GroupMember,
IMAGE_HTTP_HOST,
IMAGE_HTTP_HOST_NT,
PicType,
RawMessage,
SelfInfo,
Sex,
TipGroupElementType,
User,
VideoElement,
FriendV2
} from '../ntqqapi/types'
import { deleteGroup, getFriend, getGroupMember, groups, selfInfo, tempGroupCodeMap } from '../common/data'
import { deleteGroup, getFriend, getGroupMember, selfInfo, tempGroupCodeMap, uidMaps } from '../common/data'
import { EventType } from './event/OB11BaseEvent'
import { encodeCQCode } from './cqcode'
import { dbUtil } from '../common/db'
@@ -35,9 +36,10 @@ import { OB11GroupUploadNoticeEvent } from './event/notice/OB11GroupUploadNotice
import { OB11GroupNoticeEvent } from './event/notice/OB11GroupNoticeEvent'
import { NTQQUserApi } from '../ntqqapi/api/user'
import { NTQQFileApi } from '../ntqqapi/api/file'
import { NTQQMsgApi } from '../ntqqapi/api/msg'
import { calcQQLevel } from '../common/utils/qqlevel'
import { log } from '../common/utils/log'
import { sleep } from '../common/utils/helper'
import { isNull, sleep } from '../common/utils/helper'
import { getConfigUtil } from '../common/config'
import { OB11GroupTitleEvent } from './event/notice/OB11GroupTitleEvent'
import { OB11GroupCardEvent } from './event/notice/OB11GroupCardEvent'
@@ -46,6 +48,11 @@ import { NTQQGroupApi } from '../ntqqapi/api'
import { OB11GroupMsgEmojiLikeEvent } from './event/notice/OB11MsgEmojiLikeEvent'
import { mFaceCache } from '../ntqqapi/constructor'
import { OB11FriendAddNoticeEvent } from './event/notice/OB11FriendAddNoticeEvent'
import { OB11FriendRecallNoticeEvent } from './event/notice/OB11FriendRecallNoticeEvent'
import { OB11GroupRecallNoticeEvent } from './event/notice/OB11GroupRecallNoticeEvent'
import { OB11FriendPokeEvent, OB11GroupPokeEvent } from './event/notice/OB11PokeEvent'
import { OB11BaseNoticeEvent } from './event/notice/OB11BaseNoticeEvent'
import { OB11GroupEssenceEvent } from './event/notice/OB11GroupEssenceEvent'
let lastRKeyUpdateTime = 0
@@ -54,18 +61,20 @@ export class OB11Constructor {
let config = getConfigUtil().getConfig()
const {
enableLocalFile2Url,
debug,
ob11: { messagePostFormat },
} = config
const message_type = msg.chatType == ChatType.group ? 'group' : 'private'
const resMsg: OB11Message = {
self_id: parseInt(selfInfo.uin),
user_id: parseInt(msg.senderUin),
user_id: parseInt(msg.senderUin!),
time: parseInt(msg.msgTime) || Date.now(),
message_id: msg.msgShortId,
real_id: msg.msgShortId,
message_id: msg.msgShortId!,
real_id: msg.msgShortId!,
message_seq: msg.msgShortId!,
message_type: msg.chatType == ChatType.group ? 'group' : 'private',
sender: {
user_id: parseInt(msg.senderUin),
user_id: parseInt(msg.senderUin!),
nickname: msg.sendNickName,
card: msg.sendMemberName || '',
},
@@ -76,21 +85,26 @@ export class OB11Constructor {
message_format: messagePostFormat === 'string' ? 'string' : 'array',
post_type: selfInfo.uin == msg.senderUin ? EventType.MESSAGE_SENT : EventType.MESSAGE,
}
if (debug) {
resMsg.raw = msg
}
if (msg.chatType == ChatType.group) {
resMsg.sub_type = 'normal' // 这里go-cqhttp是group而onebot11标准是normal, 蛋疼
resMsg.sub_type = 'normal'
resMsg.group_id = parseInt(msg.peerUin)
const member = await getGroupMember(msg.peerUin, msg.senderUin)
const member = await getGroupMember(msg.peerUin, msg.senderUin!)
if (member) {
resMsg.sender.role = OB11Constructor.groupMemberRole(member.role)
resMsg.sender.nickname = member.nick
}
} else if (msg.chatType == ChatType.friend) {
}
else if (msg.chatType == ChatType.friend) {
resMsg.sub_type = 'friend'
const friend = await getFriend(msg.senderUin)
const friend = await getFriend(msg.senderUin!)
if (friend) {
resMsg.sender.nickname = friend.nick
}
} else if (msg.chatType == ChatType.temp) {
}
else if (msg.chatType == ChatType.temp) {
resMsg.sub_type = 'group'
const tempGroupCode = tempGroupCodeMap[msg.peerUin]
if (tempGroupCode) {
@@ -99,64 +113,84 @@ export class OB11Constructor {
}
for (let element of msg.elements) {
let message_data: OB11MessageData | any = {
data: {},
type: 'unknown',
let message_data: OB11MessageData = {
data: {} as any,
type: 'unknown' as any,
}
if (element.textElement && element.textElement?.atType !== AtType.notAt) {
message_data['type'] = OB11MessageDataType.at
let qq: string
let name: string | undefined
if (element.textElement.atType == AtType.atAll) {
// message_data["data"]["mention"] = "all"
message_data['data']['qq'] = 'all'
} else {
let atUid = element.textElement.atNtUid
qq = 'all'
}
else {
const { atNtUid, content } = element.textElement
let atQQ = element.textElement.atUid
if (!atQQ || atQQ === '0') {
const atMember = await getGroupMember(msg.peerUin, atUid)
const atMember = await getGroupMember(msg.peerUin, atNtUid)
if (atMember) {
atQQ = atMember.uin
}
}
if (atQQ) {
// message_data["data"]["mention"] = atQQ
message_data['data']['qq'] = atQQ
qq = atQQ
name = content.replace('@', '')
}
}
} else if (element.textElement) {
message_data['type'] = 'text'
message_data = {
type: OB11MessageDataType.at,
data: {
qq: qq!,
name
}
}
}
else if (element.textElement) {
message_data['type'] = OB11MessageDataType.text
let text = element.textElement.content
if (!text.trim()) {
continue
}
message_data['data']['text'] = text
} else if (element.replyElement) {
message_data['type'] = 'reply'
}
else if (element.replyElement) {
message_data['type'] = OB11MessageDataType.reply
// log("收到回复消息", element.replyElement.replayMsgSeq)
try {
const replyMsg = await dbUtil.getMsgBySeqId(element.replyElement.replayMsgSeq)
// log("找到回复消息", replyMsg.msgShortId, replyMsg.msgId)
if (replyMsg) {
message_data['data']['id'] = replyMsg.msgShortId.toString()
} else {
message_data['data']['id'] = replyMsg.msgShortId?.toString()
}
else {
continue
}
} catch (e) {
} catch (e: any) {
log('获取不到引用的消息', e.stack, element.replyElement.replayMsgSeq)
}
} else if (element.picElement) {
message_data['type'] = 'image'
}
else if (element.picElement) {
message_data['type'] = OB11MessageDataType.image
// message_data["data"]["file"] = element.picElement.sourcePath
message_data['data']['file'] = element.picElement.fileName
let fileName = element.picElement.fileName
const sourcePath = element.picElement.sourcePath
const isGif = element.picElement.picType === PicType.gif
if (isGif && !fileName.endsWith('.gif')) {
fileName += '.gif'
}
message_data['data']['file'] = fileName
message_data['data']['subType'] = element.picElement.picSubType
// message_data["data"]["path"] = element.picElement.sourcePath
// let currentRKey = "CAQSKAB6JWENi5LMk0kc62l8Pm3Jn1dsLZHyRLAnNmHGoZ3y_gDZPqZt-64"
message_data['data']['url'] = await NTQQFileApi.getImageUrl(msg)
message_data['data']['url'] = await NTQQFileApi.getImageUrl(element.picElement, msg.chatType)
// message_data["data"]["file_id"] = element.picElement.fileUuid
message_data['data']['file_size'] = element.picElement.fileSize
dbUtil
.addFileCache(element.picElement.fileName, {
fileName: element.picElement.fileName,
filePath: element.picElement.sourcePath,
.addFileCache(fileName, {
fileName,
elementId: element.elementId,
filePath: sourcePath,
fileSize: element.picElement.fileSize.toString(),
url: message_data['data']['url'],
downloadFunc: async () => {
@@ -169,10 +203,9 @@ export class OB11Constructor {
element.picElement.sourcePath,
)
},
})
.then()
// 不在自动下载图片
} else if (element.videoElement || element.fileElement) {
}).then()
}
else if (element.videoElement || element.fileElement) {
const videoOrFileElement = element.videoElement || element.fileElement
const ob11MessageDataType = element.videoElement ? OB11MessageDataType.video : OB11MessageDataType.file
message_data['type'] = ob11MessageDataType
@@ -180,12 +213,20 @@ export class OB11Constructor {
message_data['data']['path'] = videoOrFileElement.filePath
message_data['data']['file_id'] = videoOrFileElement.fileUuid
message_data['data']['file_size'] = videoOrFileElement.fileSize
if (element.videoElement) {
message_data['data']['url'] = await NTQQFileApi.getVideoUrl({
chatType: msg.chatType,
peerUid: msg.peerUid,
}, msg.msgId, element.elementId,
)
}
dbUtil
.addFileCache(videoOrFileElement.fileUuid, {
.addFileCache(videoOrFileElement.fileUuid!, {
msgId: msg.msgId,
elementId: element.elementId,
fileName: videoOrFileElement.fileName,
filePath: videoOrFileElement.filePath,
fileSize: videoOrFileElement.fileSize,
fileSize: videoOrFileElement.fileSize!,
downloadFunc: async () => {
await NTQQFileApi.downloadMedia(
msg.msgId,
@@ -193,7 +234,7 @@ export class OB11Constructor {
msg.peerUid,
element.elementId,
ob11MessageDataType == OB11MessageDataType.video
? (videoOrFileElement as VideoElement).thumbPath.get(0)
? (videoOrFileElement as VideoElement).thumbPath?.get(0)
: null,
videoOrFileElement.filePath,
)
@@ -201,7 +242,8 @@ export class OB11Constructor {
})
.then()
// 怎么拿到url呢
} else if (element.pttElement) {
}
else if (element.pttElement) {
message_data['type'] = OB11MessageDataType.voice
message_data['data']['file'] = element.pttElement.fileName
message_data['data']['path'] = element.pttElement.filePath
@@ -209,6 +251,7 @@ export class OB11Constructor {
message_data['data']['file_size'] = element.pttElement.fileSize
dbUtil
.addFileCache(element.pttElement.fileName, {
elementId: element.elementId,
fileName: element.pttElement.fileName,
filePath: element.pttElement.filePath,
fileSize: element.pttElement.fileSize,
@@ -217,26 +260,31 @@ export class OB11Constructor {
// log("收到语音消息", msg)
// window.LLAPI.Ptt2Text(message.raw.msgId, message.peer, messages).then(text => {
// console.log("语音转文字结果", text);
// console.log("语音转文字结果", text)
// }).catch(err => {
// console.log("语音转文字失败", err);
// console.log("语音转文字失败", err)
// })
} else if (element.arkElement) {
}
else if (element.arkElement) {
message_data['type'] = OB11MessageDataType.json
message_data['data']['data'] = element.arkElement.bytesData
} else if (element.faceElement) {
}
else if (element.faceElement) {
const faceId = element.faceElement.faceIndex
if (faceId === FaceIndex.dice) {
message_data['type'] = OB11MessageDataType.dice
message_data['data']['result'] = element.faceElement.resultId
} else if (faceId === FaceIndex.RPS) {
}
else if (faceId === FaceIndex.RPS) {
message_data['type'] = OB11MessageDataType.RPS
message_data['data']['result'] = element.faceElement.resultId
} else {
}
else {
message_data['type'] = OB11MessageDataType.face
message_data['data']['id'] = element.faceElement.faceIndex.toString()
}
} else if (element.marketFaceElement) {
}
else if (element.marketFaceElement) {
message_data['type'] = OB11MessageDataType.mface
message_data['data']['summary'] = element.marketFaceElement.faceName
const md5 = element.marketFaceElement.emojiId
@@ -249,19 +297,22 @@ export class OB11Constructor {
message_data['data']['emoji_id'] = element.marketFaceElement.emojiId
message_data['data']['emoji_package_id'] = String(element.marketFaceElement.emojiPackageId)
message_data['data']['key'] = element.marketFaceElement.key
mFaceCache.set(md5, element.marketFaceElement.faceName);
} else if (element.markdownElement) {
mFaceCache.set(md5, element.marketFaceElement.faceName!)
}
else if (element.markdownElement) {
message_data['type'] = OB11MessageDataType.markdown
message_data['data']['data'] = element.markdownElement.content
} else if (element.multiForwardMsgElement) {
}
else if (element.multiForwardMsgElement) {
message_data['type'] = OB11MessageDataType.forward
message_data['data']['id'] = msg.msgId
}
if (message_data.type !== 'unknown' && message_data.data) {
if ((message_data.type as string) !== 'unknown' && message_data.data) {
const cqCode = encodeCQCode(message_data)
if (messagePostFormat === 'string') {
;(resMsg.message as string) += cqCode
} else (resMsg.message as OB11MessageData[]).push(message_data)
(resMsg.message as string) += cqCode
}
else (resMsg.message as OB11MessageData[]).push(message_data)
resMsg.raw_message += cqCode
}
@@ -270,7 +321,36 @@ export class OB11Constructor {
return resMsg
}
static async GroupEvent(msg: RawMessage): Promise<OB11GroupNoticeEvent> {
static async PrivateEvent(msg: RawMessage): Promise<OB11BaseNoticeEvent | void> {
if (msg.chatType !== ChatType.friend) {
return
}
for (const element of msg.elements) {
if (element.grayTipElement) {
if (element.grayTipElement.subElementType == GrayTipElementSubType.MEMBER_NEW_TITLE) {
const json = JSON.parse(element.grayTipElement.jsonGrayTipElement.jsonStr)
if (element.grayTipElement.jsonGrayTipElement.busiId == 1061) {
//判断业务类型
//Poke事件
const pokedetail: any[] = json.items
//筛选item带有uid的元素
const poke_uid = pokedetail.filter(item => item.uid)
if (poke_uid.length == 2) {
return new OB11FriendPokeEvent(parseInt((uidMaps[poke_uid[0].uid])!), parseInt((uidMaps[poke_uid[1].uid])), pokedetail)
}
}
//下面得改 上面也是错的grayTipElement.subElementType == GrayTipElementSubType.MEMBER_NEW_TITLE
}
}
}
// 好友增加事件
if (msg.msgType === 5 && msg.subMsgType === 12) {
const event = new OB11FriendAddNoticeEvent(parseInt(msg.peerUin))
return event
}
}
static async GroupEvent(msg: RawMessage): Promise<OB11GroupNoticeEvent | void> {
if (msg.chatType !== ChatType.group) {
return
}
@@ -280,14 +360,14 @@ export class OB11Constructor {
const event = new OB11GroupCardEvent(
parseInt(msg.peerUid),
parseInt(msg.senderUin),
msg.sendMemberName,
msg.sendMemberName!,
member.cardName,
)
member.cardName = msg.sendMemberName
member.cardName = msg.sendMemberName!
return event
}
}
// log("group msg", msg);
// log("group msg", msg)
for (let element of msg.elements) {
const grayTipElement = element.grayTipElement
const groupElement = grayTipElement?.groupElement
@@ -310,25 +390,27 @@ export class OB11Constructor {
// log("构造群增加事件", event)
return event
}
} else if (groupElement.type === TipGroupElementType.ban) {
}
else if (groupElement.type === TipGroupElementType.ban) {
log('收到群群员禁言提示', groupElement)
const memberUid = groupElement.shutUp.member.uid
const adminUid = groupElement.shutUp.admin.uid
const memberUid = groupElement.shutUp?.member.uid
const adminUid = groupElement.shutUp?.admin.uid
let memberUin: string = ''
let duration = parseInt(groupElement.shutUp.duration)
let duration = parseInt(groupElement.shutUp?.duration!)
let sub_type: 'ban' | 'lift_ban' = duration > 0 ? 'ban' : 'lift_ban'
if (memberUid) {
memberUin =
(await getGroupMember(msg.peerUid, memberUid))?.uin ||
(await NTQQUserApi.getUserDetailInfo(memberUid))?.uin
} else {
}
else {
memberUin = '0' // 0表示全员禁言
if (duration > 0) {
duration = -1
}
}
const adminUin =
(await getGroupMember(msg.peerUid, adminUid))?.uin || (await NTQQUserApi.getUserDetailInfo(adminUid))?.uin
(await getGroupMember(msg.peerUid, adminUid!))?.uin || (await NTQQUserApi.getUserDetailInfo(adminUid!))?.uin
if (memberUin && adminUin) {
return new OB11GroupBanEvent(
parseInt(msg.peerUid),
@@ -338,7 +420,8 @@ export class OB11Constructor {
sub_type,
)
}
} else if (groupElement.type == TipGroupElementType.kicked) {
}
else if (groupElement.type == TipGroupElementType.kicked) {
log(`收到我被踢出或退群提示, 群${msg.peerUid}`, groupElement)
deleteGroup(msg.peerUid)
NTQQGroupApi.quitGroup(msg.peerUid).then()
@@ -358,9 +441,10 @@ export class OB11Constructor {
return new OB11GroupDecreaseEvent(parseInt(msg.peerUid), parseInt(selfInfo.uin), 0, 'leave')
}
}
} else if (element.fileElement) {
return new OB11GroupUploadNoticeEvent(parseInt(msg.peerUid), parseInt(msg.senderUin), {
id: element.fileElement.fileUuid,
}
else if (element.fileElement) {
return new OB11GroupUploadNoticeEvent(parseInt(msg.peerUid), parseInt(msg.senderUin!), {
id: element.fileElement.fileUuid!,
name: element.fileElement.fileName,
size: parseInt(element.fileElement.fileSize),
busid: element.fileElement.fileBizId || 0,
@@ -392,13 +476,13 @@ export class OB11Constructor {
if (!msg) {
return
}
return new OB11GroupMsgEmojiLikeEvent(parseInt(msg.peerUid), parseInt(senderUin), msg.msgShortId, [
return new OB11GroupMsgEmojiLikeEvent(parseInt(msg.peerUid), parseInt(senderUin), msg.msgShortId!, [
{
emoji_id: emojiId,
count: 1,
},
])
} catch (e) {
} catch (e: any) {
log('解析表情回应消息失败', e.stack)
}
}
@@ -411,8 +495,8 @@ export class OB11Constructor {
if (xmlElement?.content) {
const regex = /jp="(\d+)"/g
let matches = []
let match = null
const matches: string[] = []
let match: RegExpExecArray | null = null
while ((match = regex.exec(xmlElement.content)) !== null) {
matches.push(match[1])
@@ -423,7 +507,8 @@ export class OB11Constructor {
return new OB11GroupIncreaseEvent(parseInt(msg.peerUid), parseInt(invitee), parseInt(inviter), 'invite')
}
}
} else if (grayTipElement.subElementType == GrayTipElementSubType.MEMBER_NEW_TITLE) {
}
else if (grayTipElement.subElementType == GrayTipElementSubType.MEMBER_NEW_TITLE) {
const json = JSON.parse(grayTipElement.jsonGrayTipElement.jsonStr)
/*
{
@@ -449,34 +534,85 @@ export class OB11Constructor {
}
* */
const memberUin = json.items[1].param[0]
const title = json.items[3].txt
log('收到群成员新头衔消息', json)
getGroupMember(msg.peerUid, memberUin).then((member) => {
member.memberSpecialTitle = title
})
return new OB11GroupTitleEvent(parseInt(msg.peerUid), parseInt(memberUin), title)
if (grayTipElement.jsonGrayTipElement.busiId == 1061) {
//判断业务类型
//Poke事件
const pokedetail: any[] = json.items
//筛选item带有uid的元素
const poke_uid = pokedetail.filter(item => item.uid)
if (poke_uid.length == 2) {
return new OB11GroupPokeEvent(parseInt(msg.peerUid), parseInt((uidMaps[poke_uid[0].uid])!), parseInt((uidMaps[poke_uid[1].uid])), pokedetail)
}
}
if (grayTipElement.jsonGrayTipElement.busiId == 2401) {
log('收到群精华消息', json)
const searchParams = new URL(json.items[0].jp).searchParams
const msgSeq = searchParams.get('msgSeq')!
const Group = searchParams.get('groupCode')
const Businessid = searchParams.get('businessid')
const Peer: Peer = {
guildId: '',
chatType: ChatType.group,
peerUid: Group!
}
let msgList = (await NTQQMsgApi.getMsgsBySeqAndCount(Peer, msgSeq.toString(), 1, true, true)).msgList
const origMsg = await dbUtil.getMsgByLongId(msgList[0].msgId)
const postMsg = await dbUtil.getMsgBySeqId(origMsg?.msgSeq!) ?? origMsg
// 如果 senderUin 为 0可能是 历史消息 或 自身消息
if (msgList[0].senderUin === '0') {
msgList[0].senderUin = postMsg?.senderUin ?? selfInfo.uin
}
return new OB11GroupEssenceEvent(parseInt(msg.peerUid), postMsg?.msgShortId!, parseInt(msgList[0].senderUin))
// 获取MsgSeq+Peer可获取具体消息
}
if (grayTipElement.jsonGrayTipElement.busiId == 2407) {
const memberUin = json.items[1].param[0]
const title = json.items[3].txt
log('收到群成员新头衔消息', json)
getGroupMember(msg.peerUid, memberUin).then(member => {
if (!isNull(member)) {
member.memberSpecialTitle = title
}
})
return new OB11GroupTitleEvent(parseInt(msg.peerUid), parseInt(memberUin), title)
}
}
}
}
}
static async FriendAddEvent(msg: RawMessage): Promise<OB11FriendAddNoticeEvent | undefined> {
if (msg.chatType !== ChatType.friend) {
return;
static async RecallEvent(
msg: RawMessage,
): Promise<OB11FriendRecallNoticeEvent | OB11GroupRecallNoticeEvent | undefined> {
let msgElement = msg.elements.find(
(element) => element.grayTipElement?.subElementType === GrayTipElementSubType.RECALL,
)
if (!msgElement) {
return
}
if (msg.msgType === 5 && msg.subMsgType === 12) {
const event = new OB11FriendAddNoticeEvent(parseInt(msg.peerUin));
return event;
const isGroup = msg.chatType === ChatType.group
const revokeElement = msgElement.grayTipElement.revokeElement
if (isGroup) {
const operator = await getGroupMember(msg.peerUid, revokeElement.operatorUid)
const sender = await getGroupMember(msg.peerUid, revokeElement.origMsgSenderUid!)
return new OB11GroupRecallNoticeEvent(
parseInt(msg.peerUid),
parseInt(sender?.uin!),
parseInt(operator?.uin!),
msg.msgShortId!,
)
}
else {
return new OB11FriendRecallNoticeEvent(parseInt(msg.senderUin!), msg.msgShortId!)
}
return;
}
static friend(friend: User): OB11User {
return {
user_id: parseInt(friend.uin),
nickname: friend.nick,
remark: friend.remark,
sex: OB11Constructor.sex(friend.sex),
sex: OB11Constructor.sex(friend.sex!),
level: (friend.qqLevel && calcQQLevel(friend.qqLevel)) || 0,
}
}
@@ -492,6 +628,25 @@ export class OB11Constructor {
return friends.map(OB11Constructor.friend)
}
static friendsV2(friends: FriendV2[]): OB11User[] {
const data: OB11User[] = []
for (const friend of friends) {
const sexValue = this.sex(friend.baseInfo.sex!)
data.push({
...friend.baseInfo,
...friend.coreInfo,
user_id: parseInt(friend.coreInfo.uin),
nickname: friend.coreInfo.nick,
remark: friend.coreInfo.nick,
sex: sexValue,
level: 0,
categroyName: friend.categroyName,
categoryId: friend.categoryId
})
}
return data
}
static groupMemberRole(role: number): OB11GroupMemberRole | undefined {
return {
4: OB11GroupMemberRole.owner,
@@ -515,7 +670,7 @@ export class OB11Constructor {
user_id: parseInt(member.uin),
nickname: member.nick,
card: member.cardName,
sex: OB11Constructor.sex(member.sex),
sex: OB11Constructor.sex(member.sex!),
age: 0,
area: '',
level: 0,
@@ -537,7 +692,7 @@ export class OB11Constructor {
...user,
user_id: parseInt(user.uin),
nickname: user.nick,
sex: OB11Constructor.sex(user.sex),
sex: OB11Constructor.sex(user.sex!),
age: 0,
qid: user.qid,
login_days: 0,

View File

@@ -60,7 +60,15 @@ export function encodeCQCode(data: OB11MessageData) {
let result = '[CQ:' + data.type
for (const name in data.data) {
const value = data.data[name]
result += `,${name}=${CQCodeEscape(value)}`
if (value === undefined) {
continue
}
try {
const text = value.toString()
result += `,${name}=${CQCodeEscape(text)}`
} catch (error) {
// If it can't be converted, skip this name-value pair
}
}
result += ']'
return result

View File

@@ -11,5 +11,5 @@ export enum EventType {
export abstract class OB11BaseEvent {
time = Math.floor(Date.now() / 1000)
self_id = parseInt(selfInfo.uin)
post_type: EventType
abstract post_type: EventType
}

View File

@@ -2,5 +2,5 @@ import { EventType, OB11BaseEvent } from '../OB11BaseEvent'
export abstract class OB11BaseMetaEvent extends OB11BaseEvent {
post_type = EventType.META
meta_event_type: string
abstract meta_event_type: string
}

View File

@@ -1,11 +1,11 @@
import { OB11BaseNoticeEvent } from './OB11BaseNoticeEvent';
import { OB11BaseNoticeEvent } from './OB11BaseNoticeEvent'
export class OB11FriendAddNoticeEvent extends OB11BaseNoticeEvent {
notice_type = 'friend_add';
user_id: number;
notice_type = 'friend_add'
user_id: number
public constructor(userId: number) {
super();
this.user_id = userId;
super()
this.user_id = userId
}
}

View File

@@ -2,5 +2,14 @@ import { OB11GroupNoticeEvent } from './OB11GroupNoticeEvent'
export class OB11GroupAdminNoticeEvent extends OB11GroupNoticeEvent {
notice_type = 'group_admin'
sub_type: 'set' | 'unset' // "set" | "unset"
sub_type: 'set' | 'unset'
group_id: number
user_id: number
constructor(subType: 'set' | 'unset', groupId: number, userId: number) {
super()
this.sub_type = subType
this.group_id = groupId
this.user_id = userId
}
}

View File

@@ -5,6 +5,8 @@ export class OB11GroupBanEvent extends OB11GroupNoticeEvent {
operator_id: number
duration: number
sub_type: 'ban' | 'lift_ban'
group_id: number
user_id: number
constructor(groupId: number, userId: number, operatorId: number, duration: number, sub_type: 'ban' | 'lift_ban') {
super()

View File

@@ -4,6 +4,8 @@ export class OB11GroupCardEvent extends OB11GroupNoticeEvent {
notice_type = 'group_card'
card_new: string
card_old: string
group_id: number
user_id: number
constructor(groupId: number, userId: number, cardNew: string, cardOld: string) {
super()

View File

@@ -6,6 +6,8 @@ export class OB11GroupDecreaseEvent extends OB11GroupNoticeEvent {
notice_type = 'group_decrease'
sub_type: GroupDecreaseSubType = 'leave' // TODO: 实现其他几种子类型的识别 ("leave" | "kick" | "kick_me")
operator_id: number
group_id: number
user_id: number
constructor(groupId: number, userId: number, operatorId: number, subType: GroupDecreaseSubType = 'leave') {
super()

View File

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

View File

@@ -1,10 +1,14 @@
import { OB11GroupNoticeEvent } from './OB11GroupNoticeEvent'
type GroupIncreaseSubType = 'approve' | 'invite'
export class OB11GroupIncreaseEvent extends OB11GroupNoticeEvent {
notice_type = 'group_increase'
operator_id: number
sub_type: GroupIncreaseSubType
group_id: number
user_id: number
constructor(groupId: number, userId: number, operatorId: number, subType: GroupIncreaseSubType = 'approve') {
super()
this.group_id = groupId

View File

@@ -1,6 +1,7 @@
import { OB11BaseNoticeEvent } from './OB11BaseNoticeEvent'
export abstract class OB11GroupNoticeEvent extends OB11BaseNoticeEvent {
group_id: number
user_id: number
abstract group_id: number
abstract user_id: number
abstract notice_type: string
}

View File

@@ -4,6 +4,8 @@ export class OB11GroupRecallNoticeEvent extends OB11GroupNoticeEvent {
notice_type = 'group_recall'
operator_id: number
message_id: number
group_id: number
user_id: number
constructor(groupId: number, userId: number, operatorId: number, messageId: number) {
super()

View File

@@ -4,6 +4,8 @@ export class OB11GroupTitleEvent extends OB11GroupNoticeEvent {
notice_type = 'notify'
sub_type = 'title'
title: string
group_id: number
user_id: number
constructor(groupId: number, userId: number, title: string) {
super()

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