Compare commits

...

287 Commits

Author SHA1 Message Date
linyuchen
112ef202d1 fix: ws max payload 2024-04-01 22:05:40 +08:00
linyuchen
267052afbb fix: egg 2024-03-31 22:01:20 +08:00
linyuchen
0c59371ed1 fix: report first temp msg 2024-03-31 21:45:17 +08:00
linyuchen
655225e027 feat: icon 2024-03-30 19:53:55 +08:00
linyuchen
bc49bf520c fix: reverse ws restart 2024-03-30 19:11:20 +08:00
linyuchen
dd03e384ce fix: group title
fix: http quick action handle friend request
2024-03-30 14:35:43 +08:00
linyuchen
ecd64529a4 chore: ver 3.20.5 2024-03-30 13:50:17 +08:00
linyuchen
016482c9e5 fix: friend request flag invalid 2024-03-30 13:48:06 +08:00
linyuchen
23be081d29 fix: some png can not send 2024-03-30 12:02:38 +08:00
linyuchen
33688e9e5c fix: image url can not access when appid=1406 2024-03-30 11:52:00 +08:00
linyuchen
de8c2e1168 chore: ver 3.20.3 2024-03-29 21:42:07 +08:00
linyuchen
2a1fc07b94 fix: image rkey expired 2024-03-29 21:26:29 +08:00
linyuchen
c1b6daaf32 refactor: emmm 2024-03-29 01:30:05 +08:00
linyuchen
02c973fe5e refactor: optimize save image rkey 2024-03-29 01:26:38 +08:00
linyuchen
d6b44053de chore: ver 3.20.2 2024-03-29 00:32:14 +08:00
linyuchen
1d69764952 refactor: optimize save config 2024-03-29 00:30:47 +08:00
linyuchen
d9377e4684 fix: kick group member event sub_type 2024-03-29 00:19:03 +08:00
linyuchen
f30dd81455 Merge branch 'main' into dev
# Conflicts:
#	src/onebot11/constructor.ts
2024-03-28 23:37:27 +08:00
linyuchen
0116f8d384 fix: user info level 2024-03-28 23:35:52 +08:00
linyuchen
88d68f4360 Merge pull request #166 from CHH2000day/dev
修复rkey缺失导致的某些图片无法获取
2024-03-28 23:03:00 +08:00
Ayatsuki Renge
ea0f5a9f80 fix:invalid image url due to missing rkey
ref:2c8094c8c8
2024-03-28 22:47:49 +08:00
linyuchen
4591c1b659 fix: some audio can't play 2024-03-27 23:17:56 +08:00
linyuchen
97a424f62e Merge pull request #163 from idanran/main
fix: audio
2024-03-27 22:44:07 +08:00
idanran
410ef5a050 fix: audio 2024-03-27 14:35:19 +00:00
linyuchen
128091dff9 chore: ver 3.20.0 2024-03-27 21:29:56 +08:00
linyuchen
c7b6fd89fd fix: bot join group event 2024-03-27 21:27:34 +08:00
linyuchen
b55f35549d feat: report forward msg,get_forward_msg 2024-03-27 20:07:56 +08:00
linyuchen
ca0a6cfb22 chore: ver 3.19.4 2024-03-25 19:04:30 +08:00
linyuchen
3303b30c4c Merge branch 'main' into dev 2024-03-25 19:01:34 +08:00
linyuchen
429d8deb5c feat: gocq api router add send_forward_msg 2024-03-25 19:01:28 +08:00
linyuchen
48f12fc30b fix: pic subType 2024-03-25 18:52:15 +08:00
linyuchen
41f0e8f574 Merge pull request #159 from MisakaTAT/main
feat: added an gocqhttp extended api send_forward_msg
2024-03-25 18:11:05 +08:00
MisakaTAT
cd50df3a56 feat: added an gocqhttp extended api send_forward_msg 2024-03-25 17:51:04 +08:00
linyuchen
4461c7ed47 fix: group card event old_card 2024-03-25 15:07:35 +08:00
linyuchen
e5f4992eb3 feat: market face 2024-03-24 21:32:25 +08:00
linyuchen
468f1710b9 fix: group member role not sync 2024-03-24 20:21:18 +08:00
linyuchen
626d445dc3 chore: ver 3.19.2 2024-03-24 12:10:23 +08:00
linyuchen
b413a224be fix: send forward 2024-03-24 12:02:56 +08:00
linyuchen
6542f2e63b fix: get group list
fix: 兼容 cc
2024-03-24 11:57:02 +08:00
linyuchen
94c928905e fix: get self uin on old QQ 2024-03-24 00:48:30 +08:00
linyuchen
c14f8b21c2 fix: send private msg 2024-03-24 00:10:13 +08:00
linyuchen
6d5ccc6664 fix: add field busid of upload group file event
fix: operator_id typo of group_increase event
2024-03-23 23:24:17 +08:00
linyuchen
79090d764f Merge pull request #156 from idanran/main
fix: audio
2024-03-23 22:43:18 +08:00
idanran
6ab0cd7f4b fix: audio 2024-03-23 14:41:08 +00:00
linyuchen
bb3bce203d fix: audio sample rate 2024-03-23 22:25:48 +08:00
linyuchen
36f7f1b026 refactor: audio.ts 2024-03-23 21:14:24 +08:00
linyuchen
5a0dbdb5ce refactor: remove guess silk duration 2024-03-23 21:12:43 +08:00
linyuchen
48d62be2d6 Merge branch 'main' into dev 2024-03-23 21:10:09 +08:00
linyuchen
b314e2f3a0 refactor: log dir 2024-03-23 21:08:34 +08:00
linyuchen
63b9204a4b Merge pull request #154 from idanran/main
fix: audio encoding exception in some cases
2024-03-23 20:51:16 +08:00
idanran
bf701c2110 fix: audio encoding exception in some cases 2024-03-23 11:57:13 +00:00
linyuchen
95b4b11f02 chore: ver 3.19.0 2024-03-23 19:27:30 +08:00
linyuchen
1735babb7d feat: http post quick operation 2024-03-23 19:16:07 +08:00
linyuchen
89c3f07cba refactor: parse video|file element 2024-03-23 12:03:22 +08:00
linyuchen
5cf45a452b merge main 2024-03-23 00:08:46 +08:00
linyuchen
23d5fa7218 Merge branch 'main' into dev 2024-03-23 00:00:49 +08:00
linyuchen
983d2462d4 refactor: action folder
feat: group card event
feat: group title event
2024-03-23 00:00:43 +08:00
Misa Liu
3c68bc77ce chore: Refactoring GitHub issue template 2024-03-22 17:43:40 +08:00
Misa Liu
501211fb57 fix(renderer): Fix typo & format error 2024-03-22 16:56:17 +08:00
linyuchen
0cd41a8a52 feat: ask save config dialog 2024-03-21 21:53:17 +08:00
linyuchen
d339a778df fix: get_group_msg_history return type 2024-03-21 19:54:59 +08:00
linyuchen
dc843f77a3 chore: ver 3.18.1 2024-03-21 18:15:08 +08:00
linyuchen
b103f2015c chore: ver 3.18.1 2024-03-21 18:14:57 +08:00
linyuchen
baf35d5496 fix: get_group_msg_history on qq version < 22106 2024-03-21 18:10:01 +08:00
linyuchen
5c34afc228 fix: audio duration 2024-03-21 13:34:49 +08:00
linyuchen
a8a6290b70 chore: ver 3.18.0 2024-03-21 13:21:08 +08:00
linyuchen
9d50c6d4fd fix: audio duration 2024-03-21 13:18:59 +08:00
linyuchen
175a8ceb3d Merge branch 'main' into dev
# Conflicts:
#	src/common/utils/file.ts
2024-03-21 13:05:15 +08:00
linyuchen
31601981f2 Merge remote-tracking branch 'origin/main' 2024-03-21 13:03:40 +08:00
linyuchen
6a8c5ec24a fix: auto create temp dir 2024-03-21 13:03:20 +08:00
linyuchen
ebca6a07c5 fix: auto create temp dir 2024-03-21 13:02:15 +08:00
linyuchen
4f9345e4e5 fix: send forward msg message param 2024-03-21 12:23:16 +08:00
linyuchen
ac17dbefe0 feat: http post secret 2024-03-21 12:21:52 +08:00
linyuchen
c9486b4f55 Merge pull request #145 from idanran/main
fix: unable to send voice in some cases
2024-03-21 10:16:27 +08:00
idanran
35951fd61a fix: unable to send voice in some cases 2024-03-20 17:32:21 +00:00
linyuchen
fdc23d7721 fix: silk duration 2024-03-20 22:47:24 +08:00
linyuchen
560428a5f9 fix: url boolean param 2024-03-20 21:00:24 +08:00
linyuchen
e276d0e4f8 Merge branch 'dev' 2024-03-20 18:53:08 +08:00
linyuchen
965aa48729 fix: check new version 2024-03-20 18:52:44 +08:00
linyuchen
51e332ec38 Merge branch 'dev' of github.com:linyuchen/LiteLoaderQQNT-OneBotApi into dev 2024-03-20 18:25:50 +08:00
linyuchen
1307679dae fix: stranger info 2024-03-20 18:25:09 +08:00
linyuchen
7966bf75c3 Merge pull request #143 from LLOneBot/fix-update
Fix: update
2024-03-20 12:24:17 +08:00
手瓜一十雪
d5a3687f2b fix: checkVersion 2024-03-20 12:03:25 +08:00
linyuchen
7cafbdfae5 fix: set p_skey cookie 2024-03-20 11:36:15 +08:00
linyuchen
103bf94170 test pskey 2024-03-20 11:06:00 +08:00
linyuchen
235328e4fe feat: get pskey & skey 2024-03-19 23:45:56 +08:00
linyuchen
c371f1c5a3 Merge branch 'dev'
# Conflicts:
#	manifest.json
2024-03-19 20:47:25 +08:00
linyuchen
d0377bd2d3 chore: ver 3.17.0 2024-03-19 20:38:59 +08:00
linyuchen
aae10181b5 chore: ver 3.17.0 2024-03-19 20:35:57 +08:00
linyuchen
a298377717 feat: send video not need ffmpeg 2024-03-19 20:35:30 +08:00
linyuchen
8afe0af940 Merge branch 'no-ffprobe' into dev 2024-03-19 20:12:38 +08:00
linyuchen
352793d05f feat: send json 2024-03-19 20:10:10 +08:00
linyuchen
3a443f4ebf feat: default video thumb 2024-03-19 18:58:44 +08:00
linyuchen
917b55c1c3 Merge branch 'dev' of github.com:linyuchen/LiteLoaderQQNT-OneBotApi into dev 2024-03-19 18:40:43 +08:00
linyuchen
01d77827a8 feat: api /get_file 2024-03-19 18:39:22 +08:00
linyuchen
ead79a39f7 Merge pull request #141 from MisaLiu/fix_select
使用自制的 `<setting-select>` 组件来避免下拉框滚动不跟随问题
2024-03-19 17:38:43 +08:00
HIMlaoS_Misa
ebc245b9f3 fix: Uncomment code 2024-03-19 17:36:11 +08:00
HIMlaoS_Misa
47d6dc09db fix: Missing brace 2024-03-19 17:32:14 +08:00
HIMlaoS_Misa
165fcb13cb fix: Remove unused config code 2024-03-19 17:31:18 +08:00
HIMlaoS_Misa
c2405abdd3 Merge branch 'dev' into fix_select 2024-03-19 17:29:01 +08:00
Misa Liu
56492b21dd fix: Use custom setting-select component 2024-03-19 17:25:17 +08:00
linyuchen
37c4f02118 refactor: upgrade 2024-03-19 16:32:12 +08:00
linyuchen
92a2d8b5e2 test no ffmpeg 2024-03-19 14:57:38 +08:00
linyuchen
3a964af0b0 refactor: http download function 2024-03-19 14:36:58 +08:00
linyuchen
fa5540da5c Merge pull request #137 from LLOneBot/feat-update
feat: upgrade
2024-03-19 12:35:55 +08:00
linyuchen
eccf588569 feat: api /get_group_msg_history 2024-03-19 12:33:08 +08:00
手瓜一十雪
aad165ce5e chore: remove test version 2024-03-19 12:03:17 +08:00
手瓜一十雪
10c48a5b86 feat:check update 2024-03-19 12:01:57 +08:00
手瓜一十雪
63c2b95cbb feat:download update 2024-03-19 11:28:37 +08:00
手瓜一十雪
1d130d4580 Merge branch 'dev' of https://github.com/LLOneBot/LLOneBot into feat-update 2024-03-19 11:23:41 +08:00
手瓜一十雪
2dd5d81ffe feat: update text 2024-03-19 11:18:41 +08:00
手瓜一十雪
affefca19f fix:checkVersion error 2024-03-19 11:02:41 +08:00
手瓜一十雪
7381fb3e11 feat:update renderer 2024-03-19 10:47:58 +08:00
linyuchen
9679f29f48 Merge branch 'main' into dev 2024-03-19 00:56:33 +08:00
linyuchen
dda5ea3972 feat: stranger info add sex & qq_level 2024-03-19 00:45:59 +08:00
linyuchen
b12d205059 feat: stranger info add sex & qq_level 2024-03-19 00:37:20 +08:00
linyuchen
6ea6b33e9a refactor: file utils 2024-03-19 00:33:51 +08:00
手瓜一十雪
b5655a1a5f fix: updater real download url 2024-03-18 19:37:57 +08:00
linyuchen
dc559ce36c Merge branch 'dev' of github.com:linyuchen/LiteLoaderQQNT-OneBotApi into dev 2024-03-18 11:46:38 +08:00
linyuchen
9ed67628bc Merge pull request #133 from HollisMeynell/main
add: support for file download
2024-03-18 11:44:21 +08:00
spring
5aecf45959 add: support for file download 2024-03-18 11:32:56 +08:00
linyuchen
911841401a refactor: utils structure 2024-03-18 11:10:53 +08:00
linyuchen
4c6bd3df0b refactor: utils structure 2024-03-18 11:07:08 +08:00
linyuchen
c5932bcd98 refactor: utils update 2024-03-18 10:56:50 +08:00
linyuchen
3abc9f2ae0 chore: ver 3.16.1 2024-03-18 09:53:41 +08:00
linyuchen
e716c28e9a Merge branch 'dev'
# Conflicts:
#	src/main/main.ts
#	src/version.ts
2024-03-18 09:53:13 +08:00
linyuchen
9209ae766c feat: 接收戳一戳开关
feat: message_sent 事件添加 target_id 字段
feat: 回复临时消息
2024-03-18 09:49:47 +08:00
linyuchen
af8cf1882c refactor: member qq_level 2024-03-18 06:59:11 +08:00
linyuchen
d93193c7fd fix merge conflict 2024-03-17 15:46:04 +08:00
linyuchen
8a2245e2ec Merge branch 'dev'
# Conflicts:
#	src/main/main.ts
#	src/renderer/index.ts
2024-03-17 15:41:32 +08:00
linyuchen
1e144a1377 feat: cc poke 2024-03-17 15:33:56 +08:00
手瓜一十雪
8a44086419 fix: checkVersion 2024-03-17 15:22:10 +08:00
手瓜一十雪
133719f96a fix: VersionCheck 2024-03-17 14:50:36 +08:00
手瓜一十雪
75c92a68bd feat:checkVersion 2024-03-17 14:48:19 +08:00
手瓜一十雪
90820cf74d fix: doc url 2024-03-17 14:22:02 +08:00
手瓜一十雪
f149594e23 feat: update 2024-03-17 13:34:39 +08:00
linyuchen
60e0c9e4ba Merge branch 'main' into dev 2024-03-17 11:36:11 +08:00
linyuchen
1a6739ffab refactor: ntqqapi types 2024-03-17 11:35:38 +08:00
linyuchen
d8e31985af refactor: class NTQQApi 2024-03-17 09:07:33 +08:00
linyuchen
c313fcd491 refactor: output log ignore ntqq logger event 2024-03-16 12:34:00 +08:00
linyuchen
f42727c8ad feat: invite join group event 2024-03-16 11:20:00 +08:00
手瓜一十雪
17d9c48e68 docs: ffmpeg config 2024-03-16 10:50:13 +08:00
linyuchen
54179cb686 fix: can't get qq of the at member 2024-03-16 03:28:43 +08:00
linyuchen
c9a5ee69cf Merge pull request #130 from super1207/main 2024-03-15 21:17:25 +08:00
super1207
e348103e84 新增设置头像的api,set_qq_avatar 2024-03-15 20:45:22 +08:00
linyuchen
fccb0852aa feat: 新增主动获取被过滤的加群通知 2024-03-15 18:54:56 +08:00
linyuchen
b3ea8fbc0c Merge branch 'config-api' into dev
# Conflicts:
#	src/onebot11/action/index.ts
2024-03-15 17:42:15 +08:00
linyuchen
ee483dd0cc Merge branch 'dev' 2024-03-15 17:28:56 +08:00
linyuchen
ed681b8adf feat: 群文件上传事件
feat: 群文件上传接口
2024-03-15 17:28:32 +08:00
linyuchen
dcd4533eb3 Merge branch 'dev' 2024-03-15 14:37:31 +08:00
linyuchen
178c32053b fix: 转发消息id时顺序不对
fix: 以文件名发送文件失败
2024-03-15 14:37:05 +08:00
linyuchen
49ba276f5d fix: 收到的文件没有删干净 2024-03-15 11:41:37 +08:00
linyuchen
2bfe9e236b fix: 独立窗口下撤回消息重复上报 2024-03-15 11:41:11 +08:00
linyuchen
05fd258afd feat: config api 2024-03-13 21:51:17 +08:00
linyuchen
3b3098e017 docs: update plugin description 2024-03-13 11:30:13 +08:00
linyuchen
ddf9eed3a5 docs: update readme 2024-03-13 10:00:14 +08:00
linyuchen
712f0a8256 refactor: auto delete db memory cache 2024-03-13 09:11:58 +08:00
linyuchen
a93220f9d2 Merge branch 'main' into dev
# Conflicts:
#	manifest.json
2024-03-13 08:44:13 +08:00
linyuchen
253cee7458 chore: ver 3.14.1 2024-03-13 04:23:18 +08:00
linyuchen
82f9a4c63f Merge branch 'short-video' 2024-03-13 04:22:25 +08:00
linyuchen
de6c8a5558 feat: send video by videoElement 2024-03-13 04:22:11 +08:00
linyuchen
c75337b8cb fix: 开启独立窗口后重复上报 2024-03-13 01:40:44 +08:00
linyuchen
2b796e33fe test: try to send video element 2024-03-13 01:20:50 +08:00
linyuchen
175307d980 docs: update readme 2024-03-12 09:18:39 +08:00
linyuchen
993f8a9e8f docs: update readme 2024-03-12 09:07:47 +08:00
linyuchen
0130b8f6f7 fix: plugin icon 2024-03-12 08:35:41 +08:00
linyuchen
ba482d492f fix: plugin icon 2024-03-12 08:32:51 +08:00
linyuchen
6e71cd6064 feat: group whole ban event 2024-03-11 11:55:10 +08:00
linyuchen
83dc1abd4a refactor: optimize json parser 2024-03-11 11:44:17 +08:00
linyuchen
4cabb9696e refactor: custom json parser 2024-03-11 10:46:58 +08:00
linyuchen
75883e9cae refactor: refactor new group member event
feat: group ban event
2024-03-11 09:55:50 +08:00
linyuchen
eeadaa12e9 fix: group notify db cache 2024-03-10 22:44:33 +08:00
linyuchen
192736c8be docs: add friend link 2024-03-10 10:05:13 +08:00
linyuchen
586fbb6518 refactor: host input component add attribute parameter 2024-03-10 10:04:09 +08:00
linyuchen
0a42e2df5b Merge remote-tracking branch 'origin/main' 2024-03-10 00:04:37 +08:00
linyuchen
97a637f0c6 chore: ver 3.13.10 2024-03-10 00:04:09 +08:00
linyuchen
3f10b7a002 fix: message real_id use int32 2024-03-10 00:03:37 +08:00
linyuchen
f638e48260 fix: 消息重复入库导致message_id每次都是+2 2024-03-10 00:03:18 +08:00
linyuchen
354ee389bc feat: clean cache api 2024-03-09 22:58:21 +08:00
linyuchen
7188946d7a refactor: try to refactor forward msg 2024-03-09 22:30:16 +08:00
linyuchen
53055e9eab Merge pull request #124 from super1207/main
fix cqcode encode
2024-03-09 22:24:24 +08:00
super1207
7bdb84b11b fix cqcode format 2024-03-09 15:04:09 +08:00
linyuchen
c906bcf7ea docs: update todo list 2024-03-09 12:53:16 +08:00
linyuchen
cdc82562a3 docs: update readme 2024-03-09 12:49:35 +08:00
linyuchen
c34ce8ce0c docs: update readme 2024-03-09 12:40:52 +08:00
linyuchen
d1c94754ee fix: send forward msg too fast 2024-03-09 09:59:39 +08:00
linyuchen
2626555c51 fix: check pic and ptt size 2024-03-09 02:43:58 +08:00
linyuchen
5ff6ceec6d chore: ver 3.13.8 2024-03-09 02:38:05 +08:00
linyuchen
17af156451 fix: update msg seqId 2024-03-09 02:34:21 +08:00
linyuchen
c3c9e74832 fix: image cache url 2024-03-08 23:52:35 +08:00
linyuchen
0480208738 feat: file cache db 2024-03-08 23:27:20 +08:00
linyuchen
62eefbdb69 fix: sent pic message url 2024-03-08 22:16:05 +08:00
linyuchen
566537cbe3 fix: 构建转发消息的文件没有自动删除 2024-03-07 20:19:00 +08:00
linyuchen
ed831ae4cd fix: 网络下载文件大小异常提示 2024-03-07 19:07:00 +08:00
linyuchen
501031b39b fix: 网络视频后缀识别
fix: 网络文件下载失败的错误提示
2024-03-07 18:43:47 +08:00
linyuchen
7bfb3f2003 fix: 私聊带@报错,现已过滤私聊的at消息 2024-03-07 17:37:57 +08:00
linyuchen
ba8ed36c6a fix: can not send reply msg
fix: send like return error
2024-03-07 15:34:09 +08:00
linyuchen
55d046b4f9 fix: reply msg id 2024-03-07 14:18:24 +08:00
linyuchen
ac417cedd3 fix: reply msg id 2024-03-07 10:43:17 +08:00
linyuchen
ade509f26d perf: 优化数据库缓存 2024-03-07 09:00:22 +08:00
linyuchen
a66c1a9779 Merge remote-tracking branch 'origin/main' 2024-03-06 23:33:35 +08:00
linyuchen
239cf18887 fix: init db failed 2024-03-06 23:33:19 +08:00
linyuchen
936554cca2 chore: ver 3.13.2 2024-03-06 22:57:36 +08:00
linyuchen
e1d47f55bf fix: hotfix db initialize failed 2024-03-06 22:55:33 +08:00
linyuchen
cc8e8f108b docs: update readme, file msg add new field name 2024-03-06 22:02:11 +08:00
linyuchen
e6d36dc6c3 fix: send video filename
fix: send msg don't return message_id on Linux
2024-03-06 21:14:13 +08:00
linyuchen
aedc8cfc91 Merge branch 'main' into dev 2024-03-06 20:30:59 +08:00
linyuchen
00c80bf181 feat: send file api add name field 2024-03-06 20:30:26 +08:00
linyuchen
72890a8b59 Merge pull request #111 from lgc2333/patch-1
fix typo
2024-03-06 19:48:53 +08:00
student_2333
3b2577bcad fix typo 2024-03-06 19:46:15 +08:00
linyuchen
063b2460f8 feat: some link on setting ui 2024-03-06 11:44:02 +08:00
linyuchen
9427377f30 Merge remote-tracking branch 'origin/main' 2024-03-06 11:24:58 +08:00
linyuchen
ecff16050a feat: history msg db cache
test: try send music card
2024-03-06 11:24:37 +08:00
手瓜一十雪
873cc6d6a5 docs: update readme 2024-03-05 22:26:57 +08:00
手瓜一十雪
eeb429048b docs: update readme 2024-03-05 22:26:09 +08:00
linyuchen
f240f28ea6 docs: update readme 2024-03-05 11:53:42 +08:00
linyuchen
f993846230 fix: send silk
fix: try get group qq from temp msg
2024-03-05 11:22:12 +08:00
linyuchen
276767e8bb ui: report self message tips 2024-03-05 09:39:55 +08:00
linyuchen
38368afa10 ui: report self message tips 2024-03-05 09:38:43 +08:00
linyuchen
b23170e24f fix: send ws heart packet 2024-03-05 09:28:50 +08:00
linyuchen
3c73826788 refactor: 'heartInterval', 'token', 'ffmpeg' not need auto save 2024-03-05 09:03:38 +08:00
linyuchen
3fb4b6a8da Merge branch 'dev' of github.com:linyuchen/LiteLoaderQQNT-OneBotApi into dev 2024-03-05 08:33:51 +08:00
linyuchen
48768c18a9 fix: same url same path 2024-03-05 08:33:23 +08:00
linyuchen
2fe0df5ab9 Merge pull request #106 from MisaLiu/feat_setting_ui
重写设置页面
2024-03-05 07:22:50 +08:00
Misa Liu
3cea991839 chore: Edit config(s) 2024-03-05 00:25:28 +08:00
Misa Liu
f02ad6f788 feat: Finishing code 2024-03-05 00:25:28 +08:00
Misa Liu
beb372d102 feat: Add style to link(s) 2024-03-05 00:25:27 +08:00
Misa Liu
1cc726bcdc feat: Made open config path button work 2024-03-05 00:25:27 +08:00
Misa Liu
9ff851ebb4 feat: Made ffmpeg select button work 2024-03-05 00:25:27 +08:00
Misa Liu
7d36e49bb2 feat: Made add button of reverse list work 2024-03-05 00:25:26 +08:00
Misa Liu
aec06d37b6 feat: Made reverse host list editable 2024-03-05 00:25:26 +08:00
Misa Liu
68dc2222d4 feat: Made delete work in host list 2024-03-05 00:25:26 +08:00
Misa Liu
0f51db62c9 feat: Generate reverse host list 2024-03-05 00:25:26 +08:00
Misa Liu
67cb8b2f0e fix: Add missing setting option 2024-03-05 00:25:25 +08:00
Misa Liu
9e6ec92628 feat: Made save button work 2024-03-05 00:25:25 +08:00
Misa Liu
afacc79b56 feat: Made select works 2024-03-05 00:25:25 +08:00
Misa Liu
cbb732c778 feat: Made inputs work 2024-03-05 00:25:25 +08:00
Misa Liu
66fbce9e4c fix: Add missing setting option 2024-03-05 00:25:24 +08:00
Misa Liu
a5877fec17 feat: Write config 2024-03-05 00:25:24 +08:00
Misa Liu
9acb0665d8 fix: Missing setting option 2024-03-05 00:25:24 +08:00
Misa Liu
f4fbe198e9 feat: Made switch works (UI) 2024-03-05 00:25:23 +08:00
Misa Liu
b668f948df feat: Init new setting page 2024-03-05 00:25:17 +08:00
Misa Liu
1fc7356628 feat: Create setting components 2024-03-05 00:24:34 +08:00
linyuchen
b672a47d4e Update README.md 2024-03-04 23:56:00 +08:00
linyuchen
cf423972ab chore: ver 3.11.1 2024-03-04 22:59:50 +08:00
linyuchen
91baad9488 chore: ver 3.11.1 2024-03-04 22:59:37 +08:00
linyuchen
8417450c3c fix: group member role change not sync group member list 2024-03-04 22:58:25 +08:00
linyuchen
15fe2837dc fix: gif not work 2024-03-04 21:52:52 +08:00
linyuchen
4a09a51722 fix: http file ext
fix: vite build ws
2024-03-04 21:12:44 +08:00
linyuchen
c22965275c feat: more group member info field 2024-03-03 21:26:27 +08:00
linyuchen
9aeb328952 refactor: remove sample rate from silk encode 2024-03-03 17:05:22 +08:00
linyuchen
d1e4135442 style: format 2024-03-03 00:22:07 +08:00
linyuchen
f7b9d599c3 chore: use electron-vite build 2024-03-02 10:45:42 +08:00
手瓜一十雪
e9e8288f34 Merge pull request #97 from MliKiowa/main
chore: webpack to vite
2024-03-02 00:15:27 +08:00
手瓜一十雪
55a35bbfe1 fix:version 2024-03-02 00:10:05 +08:00
手瓜一十雪
71ab1e6ff0 chore:vite 2024-03-02 00:08:11 +08:00
linyuchen
906fa4c382 Create LICENSE 2024-03-01 23:11:52 +08:00
linyuchen
ebff21affd docs: update support api 2024-03-01 23:05:01 +08:00
linyuchen
912834572b docs: install by termux 2024-03-01 22:40:15 +08:00
linyuchen
96d4f79b83 fix: GitHub action npm install 2024-03-01 22:11:12 +08:00
linyuchen
4aadd81e60 feat: auto delete file when call get_file 2024-03-01 22:00:38 +08:00
linyuchen
57ef8ed3e4 chore: ver 3.11.0 2024-03-01 21:49:32 +08:00
linyuchen
4249f4e088 refactor: remove mention field from at message 2024-03-01 21:44:41 +08:00
linyuchen
3d0b90db35 fix: support get_status online
feat: seconds of auto delete file
refactor: file report
2024-03-01 21:43:05 +08:00
linyuchen
fdaf0e5269 fix: forward recall msg 2024-03-01 03:29:27 +08:00
linyuchen
f23abb1d9c chore: write version that has modified 2024-03-01 03:20:35 +08:00
linyuchen
72aeefd501 chore: auto gen version 2024-03-01 02:54:15 +08:00
linyuchen
f4a53c5aec fix: forward msg by msg id
fix: send wav voice msg
2024-03-01 02:53:33 +08:00
linyuchen
f0790b03bb Merge remote-tracking branch 'origin/main'
# Conflicts:
#	.github/ISSUE_TEMPLATE/bug_report.md
2024-02-29 18:23:38 +08:00
Misa Liu
66ca936148 style: Finishing code 2024-02-28 00:16:04 +08:00
Misa Liu
5088112864 feat: Now clean_cache API can delete cache files 2024-02-28 00:16:04 +08:00
Misa Liu
91075e192b feat: Add getFileCacheInfo to NTQQApi 2024-02-28 00:16:03 +08:00
Misa Liu
11108bc13f fix: Fix type 2024-02-28 00:16:03 +08:00
Misa Liu
3ec1134204 feat: Add clean_cache API to OneBot adapter 2024-02-28 00:16:02 +08:00
Misa Liu
de41dab846 fix: Fix a typo 2024-02-28 00:16:02 +08:00
Misa Liu
ededfe0f8c fix: Delete specific IPC channel for cache related APIs 2024-02-28 00:16:02 +08:00
Misa Liu
6548876c74 fix: Use a specific IPC channel for cache related API 2024-02-28 00:16:01 +08:00
Misa Liu
839fd7f1ab feat: Add addCacheScannedPaths to NTQQApi 2024-02-28 00:16:01 +08:00
Misa Liu
2f9cd8ba19 feat: Add getDesktopTmpPath to NTQQApi 2024-02-28 00:16:00 +08:00
Misa Liu
85d648622d feat: Add getHotUpdateCachePath to NTQQApi 2024-02-28 00:16:00 +08:00
Misa Liu
f08c816286 feat: Add clearCache to NTQQApi 2024-02-28 00:16:00 +08:00
Misa Liu
6c267044f0 fix: Use a specific IPC channel for cache related API 2024-02-28 00:15:59 +08:00
Misa Liu
b548fd3f0e feat: Add getCacheSessingPathList to NTQQApi 2024-02-28 00:15:59 +08:00
Misa Liu
f110c2d3df style: Fix typo 2024-02-28 00:15:58 +08:00
Misa Liu
f521873ba7 feat: Add scanCache to NTQQApi 2024-02-28 00:15:58 +08:00
Misa Liu
afe0ff89a7 feat: Add chat cache scan & clear to NTQQApi 2024-02-28 00:15:58 +08:00
linyuchen
a98ce843ef chore:Optimized GitHub issue template 2024-02-07 18:13:36 +08:00
152 changed files with 11595 additions and 6602 deletions

View File

@@ -1,20 +0,0 @@
---
name: Bug反馈
about: 报个Bug
title: ''
labels: bug
assignees: ''
---
QQ版本
LLOneBot版本
调用LLOneBot的方式或者应用端(如postman直接调用或NoneBot2、Koishi)
BUG描述
复现步骤:
LLOneBot日志:

81
.github/ISSUE_TEMPLATE/bug_report.yaml vendored Normal file
View File

@@ -0,0 +1,81 @@
name: Bug 反馈
description: 报告可能的 LLOneBot 异常行为
title: '[BUG] '
labels: bug
body:
- type: markdown
attributes:
value: |
欢迎来到 LLOneBot 的 Issue Tracker请填写以下表格来提交 Bug。
在提交新的 Bug 反馈前,请确保您:
* 已经搜索了现有的 issues并且没有找到可以解决您问题的方法
* 不与现有的某一 issue 重复
- type: input
id: system-version
attributes:
label: 系统版本
description: 运行 QQNT 的系统版本
placeholder: Windows 10 Pro Workstation 22H2
validations:
required: true
- type: input
id: qqnt-version
attributes:
label: QQNT 版本
description: 可在 QQNT 的「关于」或是在 LiteLoaderQQNT 的设置页中找到
placeholder: 9.9.7-21804
validations:
required: true
- type: input
id: llonebot-version
attributes:
label: LLOneBot 版本
description: 可在 LiteLoaderQQNT 的设置页或是 QQNT 的设置页侧栏中找到
placeholder: 3.18.0
validations:
required: true
- type: input
id: onebot-client-version
attributes:
label: OneBot 客户端
description: 连接至 LLOneBot 的客户端版本信息
placeholder: Overflow 2.16.0-2cf7991-SNAPSHOT
validations:
required: true
- type: textarea
id: what-happened
attributes:
label: 发生了什么?
description: 填写你认为的 LLOneBot 的不正常行为
validations:
required: true
- type: textarea
id: how-reproduce
attributes:
label: 如何复现
description: 填写应当如何操作才能触发这个不正常行为
placeholder: |
1. xxx
2. xxx
3. xxx
validations:
required: true
- type: textarea
id: what-expected
attributes:
label: 期望的结果?
description: 填写你认为 LLOneBot 应当执行的正常行为
validations:
required: true
- type: textarea
id: llonebot-log
attributes:
label: LLOneBot 运行日志
description: 在 LLOneBot 的设置页中打开「写入日志」然后粘贴相关日志内容到此处
render: shell
- type: textarea
id: onebot-client-log
attributes:
label: OneBot 客户端运行日志
description: 粘贴 OneBot 客户端的相关日志内容到此处
render: shell

View File

@@ -17,7 +17,9 @@ jobs:
node-version: 18
- name: install dependenies
run: export ELECTRON_SKIP_BINARY_DOWNLOAD=1 && npm install
run: |
export ELECTRON_SKIP_BINARY_DOWNLOAD=1
npm install
- name: build
run: npm run build

2
.gitignore vendored
View File

@@ -1,4 +1,6 @@
node_modules/
package-lock.json
dist/
out/
.idea/
.DS_Store

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 LLOneBot
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

170
README.md
View File

@@ -1,167 +1,25 @@
# LLOneBot API
LiteLoaderQQNTOneBot11协议插件
LiteLoaderQQNT插件使你的NTQQ支持OneBot11协议进行QQ机器人开发
TG群<https://t.me/+nLZEnpne-pQ1OWFl>
*注意:本文档对应的是 LiteLoader 1.0.0及以上版本如果你使用的是旧版本请切换到本项目v1分支查看文档*
*V3之后不再需要LLAPI*
## 安装方法
### Linux 容器化快速安装
执行以下任意脚本按照提示设置NoVnc密码即可运行脚本问题与异常参考 [llonebot-docker](https://github.com/MliKiowa/llonebot-docker) 项目。
<https://llonebot.github.io/zh-CN/guide/getting-started>
```bash
curl https://cdn.jsdelivr.net/gh/MliKiowa/llonebot-docker/fastboot.sh -o fastboot.sh & chmod +x fastboot.sh & sudo sh fastboot.sh
```
```bash
wget -O fastboot.sh https://cdn.jsdelivr.net/gh/MliKiowa/llonebot-docker/fastboot.sh & chmod +x fastboot.sh & sudo sh fastboot.sh
```
## 设置界面
### 通用手动安装方法
<img src="./doc/image/setting.png" width="500px" alt="图片名称"/>
1.安装[LiteLoaderQQNT](https://liteloaderqqnt.github.io/guide/install.html)
2.安装本项目插件[OneBotApi](https://github.com/linyuchen/LiteLoaderQQNT-OneBotApi/releases/), 注意本插件2.0以下的版本不支持LiteLoader 1.0.0及以上版本
*关于插件的安装方法: 下载后解压复制到插件目录*
*插件目录:`LiteLoaderQQNT/plugins`*
安装后的目录结构如下
```
├── plugins
│ ├── LLOneBot
│ │ └── main.js
│ │ └── preload.js
│ │ └── renderer.js
│ │ └── manifest.json
│ │ └── node_modules/...
```
## 支持的API
目前支持的协议
- [x] http调用api
- [x] http事件上报
- [x] 正向websocket
- [x] 反向websocket
主要功能:
- [x] 发送好友消息
- [x] 发送群消息
- [x] 获取好友列表
- [x] 获取群列表
- [x] 获取群成员列表
- [x] 撤回消息
- [x] 处理添加好友请求
- [x] 处理加群请求
- [x] 退群
- [x] 上报好友消息
- [x] 上报添加好友请求
- [x] 上报群消息
- [x] 上报好友、群消息撤回
- [x] 上报加群请求
- [x] 上报群员人数变动(尚不支持识别群员人数变动原因)
- [x] 设置群管理员
- [x] 群禁言/全体禁言
- [x] 群踢人
- [x] 群改群成员名片
- [x] 修改群名
消息格式支持:
- [x] cq码
- [x] 文字
- [x] 表情
- [x] 图片
- [x] 引用消息
- [x] @群成员
- [x] 语音(支持mp3、wav等多种音频格式直接发送)
- [x] json消息(只上报)
- [x] 转发消息记录(目前只能发不能收)
- [x] 视频(上报时暂时只有个空的file)
- [x] 文件(上报时暂时只有个空的file), type为file, data为{file: uri}, 发送时uri支持http://, file://, base64://
```
{
"type": "file",
"data": {
"file": "file:///D:/1.txt"
}
}
```
- [ ] 发送音乐卡片
- [ ] 红包(没有计划支持)
- [ ] xml (没有计划支持)
## 示例
## HTTP 调用示例
![](doc/image/example.jpg)
## 一些坑
## 支持的 api 和功能详情
<details>
<summary>下载了插件但是没有看到在NTQQ中生效</summary>
<br/>
检查是否下载的是插件release的版本如果是源码的话需要自行编译。依然不生效请查阅<a href="https://liteloaderqqnt.github.io/guide/plugins.html">LiteLoaderQQNT的文档</a>
</details>
<br/>
<https://llonebot.github.io/zh-CN/develop/api>
<details>
<summary>调用接口报404</summary>
<br/>
目前没有支持全部的onebot规范接口请检查是否调用了不支持的接口
-
</details>
<br/>
<details>
<summary>发送不了图片和语音</summary>
<br/>
检查当前操作用户是否有LiteLoaderQQNT/data/LLOneBot的写入权限如Windows把QQ上安装到C盘有可能会出现无权限导致发送失败
</details>
<br/>
<details>
<summary>QQ变得很卡</summary>
<br/>
这是你的群特别多导致的,因为启动后会批量获取群成员列表,获取完之后就正常了
</details>
<br/>
## 支持的onebot v11 api:
- [x] get_login_info
- [x] send_msg
- [x] send_group_msg
- [x] send_private_msg
- [x] delete_msg
- [x] get_group_list
- [x] get_group_info
- [x] get_group_member_list
- [x] get_group_member_info
- [x] get_friend_list
- [x] set_friend_add_request
- [x] get_msg
- [x] send_like
- [x] set_group_add_request
- [x] set_group_leave
- [x] set_group_kick
- [x] set_group_ban
- [x] set_group_whole_ban
- [x] set_group_kick
- [x] set_group_admin
- [x] set_group_card
- [x] set_group_name
- [x] get_version_info
- [x] get_status
- [x] can_send_image
- [x] can_send_record
### 支持的go-cqhtp api:
- [x] send_private_forward_msg
- [x] send_group_forward_msg
- [x] get_stranger_info
## TODO
- [x] 重构摆脱LLAPI目前调用LLAPI只能在renderer进程调用需重构成在main进程调用
@@ -171,14 +29,24 @@ wget -O fastboot.sh https://cdn.jsdelivr.net/gh/MliKiowa/llonebot-docker/fastboo
- [x] 群管理功能,禁言、踢人,改群名片等
- [x] 视频消息
- [x] 文件消息
- [ ] 音乐卡片
- [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)
## 鸣谢
* [LiteLoaderQQNT](https://liteloaderqqnt.github.io/guide/install.html)
* [LLAPI](https://github.com/Night-stars-1/LiteLoaderQQNT-Plugin-LLAPI)
* chronocat
* [chronocat](https://github.com/chrononeko/chronocat/)
* [koishi-plugin-adapter-onebot](https://github.com/koishijs/koishi-plugin-adapter-onebot)
## 友链
* [Lagrange.Core](https://github.com/LagrangeDev/Lagrange.Core) 一款用C#实现的NTQQ纯协议跨平台QQ机器人框架

BIN
doc/image/setting.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

73
electron.vite.config.ts Normal file
View File

@@ -0,0 +1,73 @@
import cp from 'vite-plugin-cp';
import "./scripts/gen-version"
const external = ["silk-wasm", "ws",
"level", "classic-level", "abstract-level", "level-supports", "level-transcoder",
"module-error", "catering", "node-gyp-build"];
function genCpModule(module: string) {
return {src: `./node_modules/${module}`, dest: `dist/node_modules/${module}`, flatten: false}
}
let config = {
main: {
build: {
outDir: "dist/main",
emptyOutDir: true,
lib: {
formats: ["cjs"],
entry: {"main": "src/main/main.ts"},
},
rollupOptions: {
external,
input: "src/main/main.ts",
}
},
resolve: {
alias: {
'./lib-cov/fluent-ffmpeg': './lib/fluent-ffmpeg'
},
},
plugins: [cp({
targets: [
...external.map(genCpModule),
{src: './manifest.json', dest: 'dist'}, {src: './icon.jpg', dest: 'dist'},
{src: './src/ntqqapi/external/ccpoke/poke-win32-x64.node', dest: 'dist/main/ccpoke/'},
]
})]
},
preload: {
// vite config options
build: {
outDir: "dist/preload",
emptyOutDir: true,
lib: {
formats: ["cjs"],
entry: {"preload": "src/preload.ts"},
},
rollupOptions: {
// external: externalAll,
input: "src/preload.ts",
}
},
resolve: {}
},
renderer: {
// vite config options
build: {
outDir: "dist/renderer",
emptyOutDir: true,
lib: {
formats: ["es"],
entry: {"renderer": "src/renderer/index.ts"},
},
rollupOptions: {
// external: externalAll,
input: "src/renderer/index.ts",
}
},
resolve: {}
}
}
export default config;

BIN
icon.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

@@ -1,11 +1,11 @@
{
"manifest_version": 4,
"type": "extension",
"name": "LLOneBot",
"name": "LLOneBot v3.20.7",
"slug": "LLOneBot",
"description": "LiteLoaderQQNT的OneBotApi",
"version": "3.10.1",
"thumbnail": "./icon.png",
"description": "使你的NTQQ支持OneBot11协议进行QQ机器人开发, 不支持商店在线更新",
"version": "3.20.7",
"icon": "./icon.jpg",
"authors": [
{
"name": "linyuchen",
@@ -26,8 +26,8 @@
"darwin"
],
"injects": {
"renderer": "./renderer.js",
"main": "./main.js",
"preload": "./preload.js"
"renderer": "./renderer/index.js",
"main": "./main/main.cjs",
"preload": "./preload/preload.cjs"
}
}
}

7466
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,44 +1,45 @@
{
"name": "llonebot",
"version": "1.0.0",
"type": "module",
"description": "NTQQLiteLoaderOneBotApi",
"main": "dist/main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "npm run build-main && npm run build-preload && npm run build-renderer",
"build-main": "webpack --config webpack.main.config.js",
"build-preload": "webpack --config webpack.preload.config.js",
"build-renderer": "webpack --config webpack.renderer.config.js",
"build-mac": "npm run build && cp manifest.json dist/ && npm run deploy-mac",
"deploy-mac": "cp -r dist/* ~/Library/Containers/com.tencent.qq/Data/LiteLoaderQQNT/plugins/LLOnebot/",
"build-win": "npm run build && cp manifest.json dist/ && npm run deploy-win",
"deploy-win": "cmd /c \"copy dist\\* %USERPROFILE%\\documents\\LiteLoaderQQNT\\plugins\\LLOnebot\\\""
"build": "electron-vite build",
"build-mac": "npm run build && npm run deploy-mac",
"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\\\""
},
"author": "",
"license": "ISC",
"dependencies": {
"compressing": "^1.10.0",
"express": "^4.18.2",
"file-type": "^19.0.0",
"fluent-ffmpeg": "^2.1.2",
"json-bigint": "^1.0.0",
"music-metadata": "^8.1.4",
"silk-wasm": "^3.2.3",
"level": "^8.0.1",
"silk-wasm": "^3.3.4",
"utf-8-validate": "^6.0.3",
"uuid": "^9.0.1",
"ws": "^8.16.0"
},
"devDependencies": {
"@babel/preset-env": "^7.23.2",
"@types/express": "^4.17.20",
"@types/fluent-ffmpeg": "^2.1.24",
"@types/node": "^20.11.19",
"@types/node": "^20.11.24",
"@types/uuid": "^9.0.8",
"@types/ws": "^8.5.10",
"babel-loader": "^9.1.3",
"copy-webpack-plugin": "^12.0.2",
"@typescript-eslint/eslint-plugin": "^6.4.0",
"electron": "^29.0.1",
"ts-loader": "^9.5.0",
"typescript": "^5.2.2",
"webpack": "^5.89.0",
"webpack-cli": "^5.1.4"
"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",
"vite-plugin-cp": "^4.0.8"
}
}

22
scripts/gen-version.ts Normal file
View File

@@ -0,0 +1,22 @@
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)
}

17
scripts/test/test_db.ts Normal file
View File

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

View File

@@ -1,7 +1,7 @@
import {Peer} from "../ntqqapi/ntcall";
export const CHANNEL_GET_CONFIG = "llonebot_get_config"
export const CHANNEL_SET_CONFIG = "llonebot_set_config"
export const CHANNEL_LOG = "llonebot_log"
export const CHANNEL_ERROR = "llonebot_error"
export const CHANNEL_SELECT_FILE = "llonebot_select_ffmpeg"
export const CHANNEL_GET_CONFIG = 'llonebot_get_config'
export const CHANNEL_SET_CONFIG = 'llonebot_set_config'
export const CHANNEL_LOG = 'llonebot_log'
export const CHANNEL_ERROR = 'llonebot_error'
export const CHANNEL_UPDATE = 'llonebot_update'
export const CHANNEL_CHECK_VERSION = 'llonebot_check_version'
export const CHANNEL_SELECT_FILE = 'llonebot_select_ffmpeg'

View File

@@ -1,8 +1,15 @@
import fs from "fs";
import {Config, OB11Config} from "./types";
import {mergeNewProperties} from "./utils";
import fsPromise from "fs/promises";
import {Config, OB11Config} from './types';
export const HOOK_LOG= false;
import {mergeNewProperties} from "./utils/helper";
import path from "node:path";
import {selfInfo} from "./data";
import {DATA_DIR} from "./utils";
export const HOOK_LOG = false;
export const ALLOW_SEND_TEMP_MSG = false;
export class ConfigUtil {
private readonly configPath: string;
@@ -12,7 +19,7 @@ export class ConfigUtil {
this.configPath = configPath;
}
getConfig(cache=true) {
getConfig(cache = true) {
if (this.config && cache) {
return this.config;
}
@@ -24,6 +31,7 @@ export class ConfigUtil {
let ob11Default: OB11Config = {
httpPort: 3000,
httpHosts: [],
httpSecret: "",
wsPort: 3001,
wsHosts: [],
enableHttp: true,
@@ -41,6 +49,8 @@ export class ConfigUtil {
log: false,
reportSelfMessage: false,
autoDeleteFile: false,
autoDeleteFileSecond: 60,
enablePoke: false
};
if (!fs.existsSync(this.configPath)) {
@@ -81,3 +91,8 @@ export class ConfigUtil {
}
}
}
export function getConfigUtil() {
const configFilePath = path.join(DATA_DIR, `config_${selfInfo.uin}.json`)
return new ConfigUtil(configFilePath)
}

View File

@@ -1,99 +1,104 @@
import {NTQQApi} from '../ntqqapi/ntcall';
import {Friend, FriendRequest, Group, GroupMember, GroupNotify, RawMessage, SelfInfo} from "../ntqqapi/types";
import {LLOneBotError} from "./types";
import {
type Friend,
type FriendRequest,
type Group,
type GroupMember,
type SelfInfo
} 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";
export const selfInfo: SelfInfo = {
uid: '',
uin: '',
nick: '',
online: true
}
export let groups: Group[] = []
export let friends: Friend[] = []
export let msgHistory: Record<string, RawMessage> = {} // msgId: RawMessage
export const version = "3.10.1"
export let groupNotifies: Map<string, GroupNotify> = new Map<string, GroupNotify>();
export let friendRequests: Map<number, FriendRequest> = new Map<number, FriendRequest>();
export let llonebotError: LLOneBotError = {
ffmpegError: "",
otherError: ""
}
let globalMsgId = Math.floor(Date.now() / 1000);
export function addHistoryMsg(msg: RawMessage): boolean {
let existMsg = msgHistory[msg.msgId]
if (existMsg) {
Object.assign(existMsg, msg)
msg.msgShortId = existMsg.msgShortId;
return false
}
msg.msgShortId = ++globalMsgId
msgHistory[msg.msgId] = msg
return true
}
export function getHistoryMsgByShortId(shortId: number | string) {
// log("getHistoryMsgByShortId", shortId, Object.values(msgHistory).map(m=>m.msgShortId))
return Object.values(msgHistory).find(msg => msg.msgShortId.toString() == shortId.toString())
export let friendRequests: Map<number, FriendRequest> = new Map<number, FriendRequest>()
export const llonebotError: LLOneBotError = {
ffmpegError: '',
httpServerError: '',
wsServerError: '',
otherError: 'LLOnebot未能正常启动请检查日志查看错误'
}
export async function getFriend(qq: string): Promise<Friend | undefined> {
let friend = friends.find(friend => friend.uin === qq)
// if (!friend){
// friends = (await NTQQApi.getFriends(true))
// friend = friends.find(friend => friend.uin === qq)
export async function getFriend(uinOrUid: string): Promise<Friend | undefined> {
let filterKey = isNumeric(uinOrUid) ? "uin" : "uid"
let filterValue = uinOrUid
let friend = friends.find(friend => friend[filterKey] === filterValue.toString())
// if (!friend) {
// try {
// friends = (await NTQQApi.getFriends(true))
// friend = friends.find(friend => friend[filterKey] === filterValue.toString())
// } catch (e) {
// // log("刷新好友列表失败", e.stack.toString())
// }
// }
return friend
}
export async function getGroup(qq: string): Promise<Group | undefined> {
let group = groups.find(group => group.groupCode === qq)
// if (!group){
// groups = await NTQQApi.getGroups(true);
// group = groups.find(group => group.groupCode === qq)
// }
let group = groups.find(group => group.groupCode === qq.toString())
if (!group) {
try {
const _groups = await NTQQGroupApi.getGroups(true);
group = _groups.find(group => group.groupCode === qq.toString())
if (group) {
groups.push(group)
}
} catch (e) {
}
}
return group
}
export async function getGroupMember(groupQQ: string | number, memberQQ: string | number, memberUid: string = null) {
groupQQ = groupQQ.toString();
if (memberQQ){
memberQQ = memberQQ.toString();
}
export async function getGroupMember(groupQQ: string | number, memberUinOrUid: string | number) {
groupQQ = groupQQ.toString()
memberUinOrUid = memberUinOrUid.toString()
const group = await getGroup(groupQQ)
if (group) {
let filterFunc: (member: GroupMember) => boolean
if (memberQQ) {
filterFunc = member => member.uin === memberQQ
} else if (memberUid) {
filterFunc = member => member.uid === memberUid
}
const filterKey = isNumeric(memberUinOrUid) ? "uin" : "uid"
const filterValue = memberUinOrUid
let filterFunc: (member: GroupMember) => boolean = member => member[filterKey] === filterValue
let member = group.members?.find(filterFunc)
if (!member) {
const _members = await NTQQApi.getGroupMembers(groupQQ)
if (_members.length) {
group.members = _members
try {
const _members = await NTQQGroupApi.getGroupMembers(groupQQ)
if (_members.length > 0) {
group.members = _members
}
} catch (e) {
// log("刷新群成员列表失败", e.stack.toString())
}
member = group.members?.find(filterFunc)
}
return member
}
return null
}
export let selfInfo: SelfInfo = {
uid: "",
uin: "",
nick: "",
export async function refreshGroupMembers(groupQQ: string) {
const group = groups.find(group => group.groupCode === groupQQ)
if (group) {
group.members = await NTQQGroupApi.getGroupMembers(groupQQ)
}
}
export function getHistoryMsgBySeq(seq: string) {
return Object.values(msgHistory).find(msg => msg.msgSeq === seq)
}
export let uidMaps: Record<string, string> = {} // 一串加密的字符串(uid) -> qq号
export const uidMaps: Record<string, string> = {} // 一串加密的字符串(uid) -> qq号
export function getUidByUin(uin: string) {
for (const key in uidMaps) {
if (uidMaps[key] === uin) {
return key;
for (const uid in uidMaps) {
if (uidMaps[uid] === uin) {
return uid
}
}
}
export let tempGroupCodeMap: Record<string, string> = {} // peerUid => 群号

277
src/common/db.ts Normal file
View File

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

View File

@@ -1,8 +1,8 @@
import express, {Express, Request, Response} from "express";
import {getConfigUtil, log} from "../utils";
import http from "http";
const JSONbig = require('json-bigint')({storeAsString: true});
import {log} from "../utils/log";
import {getConfigUtil} from "../config";
import {llonebotError} from "../data";
type RegisterHandler = (res: Response, payload: any) => Promise<any>
@@ -13,20 +13,17 @@ export abstract class HttpServerBase {
constructor() {
this.expressAPP = express();
this.expressAPP.use(express.urlencoded({extended: true, limit: "500mb"}));
this.expressAPP.use(express.urlencoded({extended: true, limit: "5000mb"}));
this.expressAPP.use((req, res, next) => {
let data = '';
req.on('data', chunk => {
data += chunk.toString();
});
req.on('end', () => {
if (data) {
try {
// log("receive raw", data)
req.body = JSONbig.parse(data);
} catch (e) {
return next(e);
}
// 兼容处理没有带content-type的请求
// log("req.headers['content-type']", req.headers['content-type'])
req.headers['content-type'] = 'application/json';
const originalJson = express.json({limit: "5000mb"});
// 调用原始的express.json()处理器
originalJson(req, res, (err) => {
if (err) {
log("Error parsing JSON:", err);
return res.status(400).send("Invalid JSON");
}
next();
});
@@ -56,20 +53,27 @@ export abstract class HttpServerBase {
};
start(port: number) {
this.expressAPP.get('/', (req: Request, res: Response) => {
res.send(`${this.name}已启动`);
})
this.listen(port);
try {
this.expressAPP.get('/', (req: Request, res: Response) => {
res.send(`${this.name}已启动`);
})
this.listen(port);
llonebotError.httpServerError = ""
} catch (e) {
log("HTTP服务启动失败", e.toString())
llonebotError.httpServerError = "HTTP服务启动失败, " + e.toString()
}
}
stop() {
if (this.server){
llonebotError.httpServerError = ""
if (this.server) {
this.server.close()
this.server = null;
}
}
restart(port: number){
restart(port: number) {
this.stop()
this.start(port)
}
@@ -81,20 +85,20 @@ export abstract class HttpServerBase {
url = "/" + url
}
if (!this.expressAPP[method]){
if (!this.expressAPP[method]) {
const err = `${this.name} register router failed${method} not exist`;
log(err);
throw err;
}
this.expressAPP[method](url, this.authorize, async (req: Request, res: Response) => {
let payload = req.body;
if (method == "get"){
if (method == "get") {
payload = req.query
}
log("收到http请求", url, payload);
try{
try {
res.send(await handler(res, payload))
}catch (e) {
} catch (e) {
this.handleFailed(res, payload, e.stack.toString())
}
});

View File

@@ -1,7 +1,9 @@
import {Server, WebSocket} from "ws";
import {getConfigUtil, log} from "../utils";
import {WebSocket, WebSocketServer} from "ws";
import urlParse from "url";
import {IncomingMessage} from "node:http";
import {log} from "../utils/log";
import {getConfigUtil} from "../config";
import {llonebotError} from "../data";
class WebsocketClientBase {
private wsClient: WebSocket
@@ -15,37 +17,46 @@ class WebsocketClientBase {
}
}
onMessage(msg: string){
onMessage(msg: string) {
}
}
export class WebsocketServerBase {
private ws: Server = null;
private ws: WebSocketServer = null;
constructor() {
console.log(`llonebot websocket service started`)
}
start(port: number) {
this.ws = new Server({port});
this.ws.on("connection", (wsClient, req)=>{
try {
this.ws = new WebSocketServer({port,
maxPayload: 1024 * 1024 * 1024
});
llonebotError.wsServerError = ''
}catch (e) {
llonebotError.wsServerError = "正向ws服务启动失败, " + e.toString()
}
this.ws.on("connection", (wsClient, req) => {
const url = req.url.split("?").shift()
this.authorize(wsClient, req);
this.onConnect(wsClient, url, req);
wsClient.on("message", async (msg)=>{
wsClient.on("message", async (msg) => {
this.onMessage(wsClient, url, msg.toString())
})
})
}
stop() {
llonebotError.wsServerError = ''
this.ws.close((err) => {
log("ws server close failed!", err)
});
this.ws = null;
}
restart(port: number){
restart(port: number) {
this.stop();
this.start(port);
}
@@ -85,7 +96,7 @@ export class WebsocketServerBase {
}
onMessage(wsClient: WebSocket, url: string, msg: string) {
onMessage(wsClient: WebSocket, url: string, msg: string) {
}

View File

@@ -1,28 +1,47 @@
export interface OB11Config {
httpPort: number
httpHosts: string[]
wsPort: number
wsHosts: string[]
enableHttp?: boolean
enableHttpPost?: boolean
enableWs?: boolean
enableWsReverse?: boolean
messagePostFormat?: 'array' | 'string'
httpPort: number
httpHosts: string[]
httpSecret?: string
wsPort: number
wsHosts: string[]
enableHttp?: boolean
enableHttpPost?: boolean
enableWs?: boolean
enableWsReverse?: boolean
messagePostFormat?: 'array' | 'string'
}
export interface CheckVersion {
result: boolean,
version: string
}
export interface Config {
ob11: OB11Config
token?: string
heartInterval?: number // ms
enableLocalFile2Url?: boolean // 开启后本地文件路径图片会转成http链接, 语音会转成base64
debug?: boolean
reportSelfMessage?: boolean
log?: boolean
autoDeleteFile?: boolean
ffmpeg?: string // ffmpeg路径
imageRKey?: string;
ob11: OB11Config
token?: string
heartInterval?: number // ms
enableLocalFile2Url?: boolean // 开启后本地文件路径图片会转成http链接, 语音会转成base64
debug?: boolean
reportSelfMessage?: boolean
log?: boolean
autoDeleteFile?: boolean
autoDeleteFileSecond?: number
ffmpeg?: string // ffmpeg路径
enablePoke?: boolean
}
export type LLOneBotError = {
ffmpegError?: string
otherError?: string
}
export interface LLOneBotError {
httpServerError?: string
wsServerError?: string
ffmpegError?: string
otherError?: string
}
export interface FileCache {
fileName: string
filePath: string
fileSize: string
fileUuid?: string
url?: string
msgId?: string
downloadFunc?: () => Promise<void>
}

View File

@@ -1,273 +0,0 @@
import * as path from "path";
import {selfInfo} from "./data";
import {ConfigUtil} from "./config";
import util from "util";
import {encode, getDuration} from "silk-wasm";
import fs from 'fs';
import {v4 as uuidv4} from "uuid";
import {exec} from "node:child_process";
import ffmpeg from "fluent-ffmpeg"
export const CONFIG_DIR = global.LiteLoader.plugins["LLOneBot"].path.data;
export function getConfigUtil() {
const configFilePath = path.join(CONFIG_DIR, `config_${selfInfo.uin}.json`)
return new ConfigUtil(configFilePath)
}
function truncateString(obj: any, maxLength = 500) {
if (obj !== null && typeof obj === 'object') {
Object.keys(obj).forEach(key => {
if (typeof obj[key] === 'string') {
// 如果是字符串且超过指定长度,则截断
if (obj[key].length > maxLength) {
obj[key] = obj[key].substring(0, maxLength) + '...';
}
} else if (typeof obj[key] === 'object') {
// 如果是对象或数组,则递归调用
truncateString(obj[key], maxLength);
}
});
}
return obj;
}
export function log(...msg: any[]) {
if (!getConfigUtil().getConfig().log) {
return
}
let currentDateTime = new Date().toLocaleString();
const date = new Date();
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const currentDate = `${year}-${month}-${day}`;
const userInfo = selfInfo.uin ? `${selfInfo.nick}(${selfInfo.uin})` : ""
let logMsg = "";
for (let msgItem of msg) {
// 判断是否是对象
if (typeof msgItem === "object") {
let obj = JSON.parse(JSON.stringify(msgItem));
logMsg += JSON.stringify(truncateString(obj)) + " ";
continue;
}
logMsg += msgItem + " ";
}
logMsg = `${currentDateTime} ${userInfo}: ${logMsg}\n\n`
// sendLog(...msg);
// console.log(msg)
fs.appendFile(path.join(CONFIG_DIR, `llonebot-${currentDate}.log`), logMsg, (err: any) => {
})
}
export function isGIF(path: string) {
const buffer = Buffer.alloc(4);
const fd = fs.openSync(path, 'r');
fs.readSync(fd, buffer, 0, 4, 0);
fs.closeSync(fd);
return buffer.toString() === 'GIF8'
}
export function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 定义一个异步函数来检查文件是否存在
export function checkFileReceived(path: string, timeout: number = 3000): Promise<void> {
return new Promise((resolve, reject) => {
const startTime = Date.now();
function check() {
if (fs.existsSync(path)) {
resolve();
} else if (Date.now() - startTime > timeout) {
reject(new Error(`文件不存在: ${path}`));
} else {
setTimeout(check, 100);
}
}
check();
});
}
export async function file2base64(path: string) {
const readFile = util.promisify(fs.readFile);
let result = {
err: "",
data: ""
}
try {
// 读取文件内容
// if (!fs.existsSync(path)){
// path = path.replace("\\Ori\\", "\\Thumb\\");
// }
try {
await checkFileReceived(path, 5000);
} catch (e: any) {
result.err = e.toString();
return result;
}
const data = await readFile(path);
// 转换为Base64编码
result.data = data.toString('base64');
} catch (err) {
result.err = err.toString();
}
return result;
}
// 在保证老对象已有的属性不变化的情况下将新对象的属性复制到老对象
export function mergeNewProperties(newObj: any, oldObj: any) {
Object.keys(newObj).forEach(key => {
// 如果老对象不存在当前属性,则直接复制
if (!oldObj.hasOwnProperty(key)) {
oldObj[key] = newObj[key];
} else {
// 如果老对象和新对象的当前属性都是对象,则递归合并
if (typeof oldObj[key] === 'object' && typeof newObj[key] === 'object') {
mergeNewProperties(newObj[key], oldObj[key]);
} else if (typeof oldObj[key] === 'object' || typeof newObj[key] === 'object') {
// 属性冲突,有一方不是对象,直接覆盖
oldObj[key] = newObj[key];
}
}
});
}
export function checkFFMPEG(newPath: string=null): Promise<boolean> {
return new Promise((resolve, reject) => {
const ffmpegPath = newPath || 'ffmpeg'
exec(ffmpegPath + ' -version', (error, stdout, stderr) => {
if (error) {
log('ffmpeg is not installed or not found in PATH:', error);
resolve(false)
}
log('ffmpeg is installed. Version info:', stdout);
resolve(true);
});
});
}
export async function encodeSilk(filePath: string) {
const fsp = require("fs").promises
function getFileHeader(filePath: string) {
// 定义要读取的字节数
const bytesToRead = 7;
try {
const buffer = fs.readFileSync(filePath, {
encoding: null,
flag: "r",
});
const fileHeader = buffer.toString("hex", 0, bytesToRead);
return fileHeader;
} catch (err) {
console.error("读取文件错误:", err);
return;
}
}
function isWavFile(filePath: string) {
return new Promise((resolve, reject) => {
fs.open(filePath, 'r', (err, fd) => {
if (err) {
reject(err);
return;
}
// 读取前12个字节
const buffer = Buffer.alloc(12);
fs.read(fd, buffer, 0, 12, 0, (err, bytesRead, buffer) => {
if (err) {
reject(err);
return;
}
fs.close(fd, (err) => {
if (err) {
reject(err);
return;
}
// 检查RIFF头和WAVE格式标识
const isRIFF = buffer.toString('utf8', 0, 4) === 'RIFF';
const isWAVE = buffer.toString('utf8', 8, 12) === 'WAVE';
resolve(isRIFF && isWAVE);
});
});
});
});
}
async function getAudioSampleRate(filePath: string) {
try {
const mm = await import('music-metadata');
const metadata = await mm.parseFile(filePath);
log(`${filePath}采样率`, metadata.format.sampleRate);
return metadata.format.sampleRate;
} catch (error) {
log(`${filePath}采样率获取失败`, error.stack);
// console.error(error);
}
}
try {
const fileName = path.basename(filePath);
const pttPath = path.join(CONFIG_DIR, uuidv4());
if (getFileHeader(filePath) !== "02232153494c4b") {
log(`语音文件${filePath}需要转换`)
const isWav = await isWavFile(filePath);
if (!isWav) {
log(`语音文件${filePath}正在转换成wav`)
// let voiceData = await fsp.readFile(filePath)
const wavPath = pttPath + ".wav"
await new Promise((resolve, reject) => {
const ffmpegPath = getConfigUtil().getConfig().ffmpeg;
if (ffmpegPath){
ffmpeg.setFfmpegPath(ffmpegPath);
}
ffmpeg(filePath).toFormat("wav").on('end', function () {
log('wav转换完成');
})
.on('error', function (err) {
log(`wav转换出错: `, err.message,);
reject(err);
})
.save(wavPath)
.on("end", ()=>{
filePath = wavPath
resolve(wavPath);
});
})
const sampleRate = await getAudioSampleRate(filePath) || 44100;
const pcm = fs.readFileSync(filePath);
const silk = await encode(pcm, sampleRate);
fs.writeFileSync(pttPath, silk.data);
fs.unlink(wavPath, (err) => {});
log(`语音文件${filePath}转换成功!`, pttPath)
return {
converted: true,
path: pttPath,
duration: silk.duration,
};
} else {
const pcm = fs.readFileSync(filePath);
const duration = getDuration(pcm);
return {
converted: false,
path: filePath,
duration: duration,
};
}
}
} catch (error) {
log("convert silk failed", error.stack);
return {};
}
}
export function isNull(value: any) {
return value === undefined || value === null;
}

137
src/common/utils/audio.ts Normal file
View File

@@ -0,0 +1,137 @@
import fs from "fs";
import {encode, getDuration, getWavFileInfo, isWav} from "silk-wasm";
import fsPromise from "fs/promises";
import {log} from "./log";
import path from "node:path";
import {DATA_DIR, TEMP_DIR} from "./index";
import {v4 as uuidv4} from "uuid";
import {getConfigUtil} from "../config";
import ffmpeg from "fluent-ffmpeg";
export async function encodeSilk(filePath: string) {
function getFileHeader(filePath: string) {
// 定义要读取的字节数
const bytesToRead = 7;
try {
const buffer = fs.readFileSync(filePath, {
encoding: null,
flag: "r",
});
const fileHeader = buffer.toString("hex", 0, bytesToRead);
return fileHeader;
} catch (err) {
console.error("读取文件错误:", err);
return;
}
}
async function isWavFile(filePath: string) {
return isWav(fs.readFileSync(filePath));
}
async function guessDuration(pttPath: string) {
const pttFileInfo = await fsPromise.stat(pttPath)
let duration = pttFileInfo.size / 1024 / 3 // 3kb/s
duration = Math.floor(duration)
duration = Math.max(1, duration)
log(`通过文件大小估算语音的时长:`, duration)
return duration
}
// function verifyDuration(oriDuration: number, guessDuration: number) {
// // 单位都是秒
// if (oriDuration - guessDuration > 10) {
// return guessDuration
// }
// oriDuration = Math.max(1, oriDuration)
// return oriDuration
// }
// async function getAudioSampleRate(filePath: string) {
// try {
// const mm = await import('music-metadata');
// const metadata = await mm.parseFile(filePath);
// log(`${filePath}采样率`, metadata.format.sampleRate);
// return metadata.format.sampleRate;
// } catch (error) {
// log(`${filePath}采样率获取失败`, error.stack);
// // console.error(error);
// }
// }
try {
const pttPath = path.join(TEMP_DIR, uuidv4());
if (getFileHeader(filePath) !== "02232153494c4b") {
log(`语音文件${filePath}需要转换成silk`)
const _isWav = await isWavFile(filePath);
const wavPath = pttPath + ".wav"
const convert = async () => {
return await new Promise((resolve, reject) => {
const ffmpegPath = getConfigUtil().getConfig().ffmpeg;
if (ffmpegPath) {
ffmpeg.setFfmpegPath(ffmpegPath);
}
ffmpeg(filePath).toFormat("wav")
.audioChannels(1)
.audioFrequency(24000)
.on('end', function () {
log('wav转换完成');
})
.on('error', function (err) {
log(`wav转换出错: `, err.message,);
reject(err);
})
.save(wavPath)
.on("end", () => {
filePath = wavPath
resolve(wavPath);
});
})
}
let wav: Buffer
if (!_isWav) {
log(`语音文件${filePath}正在转换成wav`)
await convert()
} else {
wav = fs.readFileSync(filePath)
const allowSampleRate = [8000, 12000, 16000, 24000, 32000, 44100, 48000]
const {fmt} = getWavFileInfo(wav)
// log(`wav文件信息`, fmt)
if (!allowSampleRate.includes(fmt.sampleRate)) {
wav = undefined
await convert()
}
}
wav ||= fs.readFileSync(filePath);
const silk = await encode(wav, 0);
fs.writeFileSync(pttPath, silk.data);
fs.unlink(wavPath, (err) => {
});
// const gDuration = await guessDuration(pttPath)
log(`语音文件${filePath}转换成功!`, pttPath, `时长:`, silk.duration)
return {
converted: true,
path: pttPath,
duration: silk.duration / 1000
};
} else {
const silk = fs.readFileSync(filePath);
let duration = 0;
try {
duration = getDuration(silk) / 1000
} catch (e) {
log("获取语音文件时长失败, 使用文件大小推测时长", filePath, e.stack)
duration = await guessDuration(filePath);
}
return {
converted: false,
path: filePath,
duration: duration,
};
}
} catch (error) {
log("convert silk failed", error.stack);
return {};
}
}

258
src/common/utils/file.ts Normal file
View File

@@ -0,0 +1,258 @@
import fs from "fs";
import fsPromise from "fs/promises";
import crypto from "crypto";
import util from "util";
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";
export function isGIF(path: string) {
const buffer = Buffer.alloc(4);
const fd = fs.openSync(path, 'r');
fs.readSync(fd, buffer, 0, 4, 0);
fs.closeSync(fd);
return buffer.toString() === 'GIF8'
}
// 定义一个异步函数来检查文件是否存在
export function checkFileReceived(path: string, timeout: number = 3000): Promise<void> {
return new Promise((resolve, reject) => {
const startTime = Date.now();
function check() {
if (fs.existsSync(path)) {
resolve();
} else if (Date.now() - startTime > timeout) {
reject(new Error(`文件不存在: ${path}`));
} else {
setTimeout(check, 100);
}
}
check();
});
}
export async function file2base64(path: string) {
const readFile = util.promisify(fs.readFile);
let result = {
err: "",
data: ""
}
try {
// 读取文件内容
// if (!fs.existsSync(path)){
// path = path.replace("\\Ori\\", "\\Thumb\\");
// }
try {
await checkFileReceived(path, 5000);
} catch (e: any) {
result.err = e.toString();
return result;
}
const data = await readFile(path);
// 转换为Base64编码
result.data = data.toString('base64');
} catch (err) {
result.err = err.toString();
}
return result;
}
export function calculateFileMD5(filePath: string): Promise<string> {
return new Promise((resolve, reject) => {
// 创建一个流式读取器
const stream = fs.createReadStream(filePath);
const hash = crypto.createHash('md5');
stream.on('data', (data: Buffer) => {
// 当读取到数据时,更新哈希对象的状态
hash.update(data);
});
stream.on('end', () => {
// 文件读取完成,计算哈希
const md5 = hash.digest('hex');
resolve(md5);
});
stream.on('error', (err: Error) => {
// 处理可能的读取错误
reject(err);
});
});
}
export interface HttpDownloadOptions {
url: string;
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": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36"
};
if (typeof options === "string") {
url = options;
} else {
url = options.url;
if (options.headers) {
if (typeof options.headers === "string") {
headers = JSON.parse(options.headers);
} else {
headers = options.headers;
}
}
}
const fetchRes = await net.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);
}
type Uri2LocalRes = {
success: boolean,
errMsg: string,
fileName: string,
ext: string,
path: string,
isLocal: boolean
}
export async function uri2local(uri: string, fileName: string = null): Promise<Uri2LocalRes> {
let res = {
success: false,
errMsg: "",
fileName: "",
ext: "",
path: "",
isLocal: false
}
if (!fileName) {
fileName = uuidv4();
}
let filePath = path.join(TEMP_DIR, fileName)
let url = null;
try {
url = new URL(uri);
} catch (e) {
res.errMsg = `uri ${uri} 解析失败,` + e.toString() + ` 可能${uri}不存在`
return res
}
// log("uri protocol", url.protocol, uri);
if (url.protocol == "base64:") {
// base64转成文件
let base64Data = uri.split("base64://")[1]
try {
const buffer = Buffer.from(base64Data, 'base64');
fs.writeFileSync(filePath, buffer);
} catch (e: any) {
res.errMsg = `base64文件下载失败,` + e.toString()
return res
}
} else if (url.protocol == "http:" || url.protocol == "https:") {
// 下载文件
let buffer: Buffer = null;
try {
buffer = await httpDownload(uri);
} catch (e) {
res.errMsg = `${url}下载失败,` + e.toString()
return res
}
try {
const pathInfo = path.parse(decodeURIComponent(url.pathname))
if (pathInfo.name) {
fileName = pathInfo.name
if (pathInfo.ext) {
fileName += pathInfo.ext
// res.ext = pathInfo.ext
}
}
res.fileName = fileName
filePath = path.join(TEMP_DIR, uuidv4() + fileName)
fs.writeFileSync(filePath, buffer);
} catch (e: any) {
res.errMsg = `${url}下载失败,` + e.toString()
return res
}
} else {
let pathname: string;
if (url.protocol === "file:") {
// await fs.copyFile(url.pathname, filePath);
pathname = decodeURIComponent(url.pathname)
if (process.platform === "win32") {
filePath = pathname.slice(1)
} else {
filePath = pathname
}
} else {
const cache = await dbUtil.getFileCache(uri);
if (cache) {
filePath = cache.filePath
} else {
filePath = uri;
}
}
res.isLocal = true
}
// else{
// res.errMsg = `不支持的file协议,` + url.protocol
// return res
// }
// if (isGIF(filePath) && !res.isLocal) {
// await fs.rename(filePath, filePath + ".gif");
// filePath += ".gif";
// }
if (!res.isLocal && !res.ext) {
try {
let ext: string = (await fileType.fileTypeFromFile(filePath)).ext
if (ext) {
log("获取文件类型", ext, filePath)
fs.renameSync(filePath, filePath + `.${ext}`)
filePath += `.${ext}`
res.fileName += `.${ext}`
res.ext = ext
}
} catch (e) {
// log("获取文件类型失败", filePath,e.stack)
}
}
res.success = true
res.path = filePath
return res
}
export async function copyFolder(sourcePath: string, destPath: string) {
try {
const entries = await fsPromise.readdir(sourcePath, {withFileTypes: true});
await fsPromise.mkdir(destPath, {recursive: true});
for (let entry of entries) {
const srcPath = path.join(sourcePath, entry.name);
const dstPath = path.join(destPath, entry.name);
if (entry.isDirectory()) {
await copyFolder(srcPath, dstPath);
} else {
try {
await fsPromise.copyFile(srcPath, dstPath);
} catch (error) {
console.error(`无法复制文件 '${srcPath}' 到 '${dstPath}': ${error}`);
// 这里可以决定是否要继续复制其他文件
}
}
}
} catch (error) {
console.error('复制文件夹时出错:', error);
}
}

View File

@@ -0,0 +1,68 @@
export function truncateString(obj: any, maxLength = 500) {
if (obj !== null && typeof obj === 'object') {
Object.keys(obj).forEach(key => {
if (typeof obj[key] === 'string') {
// 如果是字符串且超过指定长度,则截断
if (obj[key].length > maxLength) {
obj[key] = obj[key].substring(0, maxLength) + '...';
}
} else if (typeof obj[key] === 'object') {
// 如果是对象或数组,则递归调用
truncateString(obj[key], maxLength);
}
});
}
return obj;
}
export function isNumeric(str: string) {
return /^\d+$/.test(str);
}
export function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 在保证老对象已有的属性不变化的情况下将新对象的属性复制到老对象
export function mergeNewProperties(newObj: any, oldObj: any) {
Object.keys(newObj).forEach(key => {
// 如果老对象不存在当前属性,则直接复制
if (!oldObj.hasOwnProperty(key)) {
oldObj[key] = newObj[key];
} else {
// 如果老对象和新对象的当前属性都是对象,则递归合并
if (typeof oldObj[key] === 'object' && typeof newObj[key] === 'object') {
mergeNewProperties(newObj[key], oldObj[key]);
} else if (typeof oldObj[key] === 'object' || typeof newObj[key] === 'object') {
// 属性冲突,有一方不是对象,直接覆盖
oldObj[key] = newObj[key];
}
}
});
}
export function isNull(value: any) {
return value === undefined || value === null;
}
/**
* 将字符串按最大长度分割并添加换行符
* @param str 原始字符串
* @param maxLength 每行的最大字符数
* @returns 处理后的字符串,超过长度的地方将会换行
*/
export function wrapText(str: string, maxLength: number): string {
// 初始化一个空字符串用于存放结果
let result: string = '';
// 循环遍历字符串每次步进maxLength个字符
for (let i = 0; i < str.length; i += maxLength) {
// 从i开始截取长度为maxLength的字符串段并添加到结果字符串
// 如果不是第一段,先添加一个换行符
if (i > 0) result += '\n';
result += str.substring(i, i + maxLength);
}
return result;
}

18
src/common/utils/index.ts Normal file
View File

@@ -0,0 +1,18 @@
import path from "node:path";
import fs from "fs";
export * from './file'
export * from './helper'
export * from './log'
export * from './qqlevel'
export * from './qqpkg'
export * from './upgrade'
export const DATA_DIR = global.LiteLoader.plugins["LLOneBot"].path.data;
export const TEMP_DIR = path.join(DATA_DIR, "temp");
export const PLUGIN_DIR = global.LiteLoader.plugins["LLOneBot"].path.plugin;
if (!fs.existsSync(TEMP_DIR)) {
fs.mkdirSync(TEMP_DIR, {recursive: true});
}
export {getVideoInfo} from "./video";
export {checkFfmpeg} from "./video";
export {encodeSilk} from "./audio";

37
src/common/utils/log.ts Normal file
View File

@@ -0,0 +1,37 @@
import {selfInfo} from "../data";
import fs from "fs";
import path from "node:path";
import {DATA_DIR, truncateString} from "./index";
import {getConfigUtil} from "../config";
const date = new Date();
const logFileName = `llonebot-${date.toLocaleString("zh-CN")}.log`.replace(/\//g, "-").replace(/:/g, "-");
const logDir = path.join(DATA_DIR, "logs");
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, {recursive: true});
}
export function log(...msg: any[]) {
if (!getConfigUtil().getConfig().log) {
return //console.log(...msg);
}
const userInfo = selfInfo.uin ? `${selfInfo.nick}(${selfInfo.uin})` : ""
let logMsg = "";
for (let msgItem of msg) {
// 判断是否是对象
if (typeof msgItem === "object") {
let obj = JSON.parse(JSON.stringify(msgItem));
logMsg += JSON.stringify(truncateString(obj)) + " ";
continue;
}
logMsg += msgItem + " ";
}
let currentDateTime = new Date().toLocaleString();
logMsg = `${currentDateTime} ${userInfo}: ${logMsg}\n\n`
// sendLog(...msg);
// console.log(msg)
fs.appendFile(path.join(logDir, logFileName), logMsg, (err: any) => {
})
}

View File

@@ -0,0 +1,7 @@
// QQ等级换算
import {QQLevel} from "../../ntqqapi/types";
export function calcQQLevel(level: QQLevel) {
const {crownNum, sunNum, moonNum, starNum} = level
return crownNum * 64 + sunNum * 16 + moonNum * 4 + starNum
}

12
src/common/utils/qqpkg.ts Normal file
View File

@@ -0,0 +1,12 @@
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"

View File

@@ -0,0 +1,98 @@
import { version } from "../../version";
import * as path from "node:path";
import * as fs from "node:fs";
import { copyFolder, httpDownload, log, PLUGIN_DIR, TEMP_DIR } from ".";
import compressing from "compressing";
const downloadMirrorHosts = ["https://mirror.ghproxy.com/"];
const checkVersionMirrorHosts = ["https://521github.com"];
export async function checkNewVersion() {
const latestVersionText = await getRemoteVersion();
const latestVersion = latestVersionText.split(".");
log("llonebot last version", latestVersion);
const currentVersion: string[] = version.split(".");
log("llonebot current version", currentVersion);
for (let k of [0, 1, 2]) {
if (parseInt(latestVersion[k]) > parseInt(currentVersion[k])) {
log("")
return { result: true, version: latestVersionText };
}
else if (parseInt(latestVersion[k]) < parseInt(currentVersion[k])) {
break;
}
}
return { result: false, version: version };
}
export async function upgradeLLOneBot() {
const latestVersion = await getRemoteVersion();
if (latestVersion && latestVersion != "") {
const downloadUrl = "https://github.com/LLOneBot/LLOneBot/releases/download/v" + latestVersion + "/LLOneBot.zip";
const filePath = path.join(TEMP_DIR, "./update-" + latestVersion + ".zip");
let downloadSuccess = false;
// 多镜像下载
for (const mirrorGithub of downloadMirrorHosts) {
try {
const buffer = await httpDownload(mirrorGithub + downloadUrl);
fs.writeFileSync(filePath, buffer)
downloadSuccess = true;
break;
} catch (e) {
log("llonebot upgrade error", e);
}
}
if (!downloadSuccess) {
log("llonebot upgrade error", "download failed");
return false;
}
const temp_ver_dir = path.join(TEMP_DIR, "LLOneBot" + latestVersion);
let uncompressedPromise = async function () {
return new Promise<boolean>((resolve, reject) => {
compressing.zip.uncompress(filePath, temp_ver_dir).then(() => {
resolve(true);
}).catch((reason: any) => {
log("llonebot upgrade failed, ", reason);
if (reason?.errno == -4082) {
resolve(true);
}
resolve(false);
});
});
}
const uncompressedResult = await uncompressedPromise();
// 复制文件
await copyFolder(temp_ver_dir, PLUGIN_DIR);
return uncompressedResult;
}
return false;
}
export async function getRemoteVersion() {
let Version = "";
for (let i = 0; i < checkVersionMirrorHosts.length; i++) {
let mirrorGithub = checkVersionMirrorHosts[i];
let tVersion = await getRemoteVersionByMirror(mirrorGithub);
if (tVersion && tVersion != "") {
Version = tVersion;
break;
}
}
return Version;
}
export async function getRemoteVersionByMirror(mirrorGithub: string) {
let releasePage = "error";
try {
releasePage = (await httpDownload(mirrorGithub + "/LLOneBot/LLOneBot/releases")).toString();
// log("releasePage", releasePage);
if (releasePage === "error") return "";
return releasePage.match(new RegExp('(?<=(tag/v)).*?(?=("))'))[0];
} catch {
}
return "";
}

88
src/common/utils/video.ts Normal file

File diff suppressed because one or more lines are too long

14
src/global.d.ts vendored
View File

@@ -1,10 +1,8 @@
import {LLOneBot} from "./preload";
import { type LLOneBot } from './preload'
declare global {
interface Window {
llonebot: LLOneBot;
LiteLoader: any;
}
}
interface Window {
llonebot: LLOneBot
LiteLoader: any
}
}

View File

@@ -1,34 +1,35 @@
// 运行在 Electron 主进程 下的插件入口
import {BrowserWindow, dialog, ipcMain} from 'electron';
import fs from 'fs';
import * as fs from 'node:fs';
import {Config} from "../common/types";
import {
CHANNEL_CHECK_VERSION,
CHANNEL_ERROR,
CHANNEL_GET_CONFIG,
CHANNEL_LOG,
CHANNEL_SELECT_FILE,
CHANNEL_SET_CONFIG,
CHANNEL_UPDATE,
} from "../common/channels";
import {ob11WebsocketServer} from "../onebot11/server/ws/WebsocketServer";
import {checkFFMPEG, CONFIG_DIR, getConfigUtil, log} from "../common/utils";
import {DATA_DIR} from "../common/utils";
import {
addHistoryMsg,
friendRequests,
getFriend,
getGroup,
getGroupMember,
groupNotifies,
getGroupMember, groups,
llonebotError,
msgHistory,
selfInfo
refreshGroupMembers,
selfInfo,
uidMaps
} from "../common/data";
import {hookNTQQApiCall, hookNTQQApiReceive, ReceiveCmd, registerReceiveHook} from "../ntqqapi/hook";
import {hookNTQQApiCall, hookNTQQApiReceive, ReceiveCmdS, registerReceiveHook} from "../ntqqapi/hook";
import {OB11Constructor} from "../onebot11/constructor";
import {NTQQApi} from "../ntqqapi/ntcall";
import {
ChatType,
FriendRequestNotify,
GroupMember,
GroupMemberRole,
GroupNotifies,
GroupNotifyTypes,
RawMessage
@@ -39,19 +40,34 @@ import {OB11GroupRecallNoticeEvent} from "../onebot11/event/notice/OB11GroupReca
import {postOB11Event} from "../onebot11/server/postOB11Event";
import {ob11ReverseWebsockets} from "../onebot11/server/ws/ReverseWebsocket";
import {OB11GroupAdminNoticeEvent} from "../onebot11/event/notice/OB11GroupAdminNoticeEvent";
import {OB11GroupDecreaseEvent} from "../onebot11/event/notice/OB11GroupDecreaseEvent";
import {OB11GroupRequestEvent} from "../onebot11/event/request/OB11GroupRequest";
import {OB11FriendRequestEvent} from "../onebot11/event/request/OB11FriendRequest";
import * as path from "node:path";
import {dbUtil} from "../common/db";
import {setConfig} from "./setConfig";
import {NTQQUserApi} from "../ntqqapi/api/user";
import {NTQQGroupApi} from "../ntqqapi/api/group";
import {registerPokeHandler} from "../ntqqapi/external/ccpoke";
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;
let mainWindow: BrowserWindow | null = null;
// 加载插件时触发
function onLoad() {
log("llonebot main onLoad");
ipcMain.handle(CHANNEL_CHECK_VERSION, async (event, arg) => {
return checkNewVersion();
});
ipcMain.handle(CHANNEL_UPDATE, async (event, arg) => {
return upgradeLLOneBot();
});
ipcMain.handle(CHANNEL_SELECT_FILE, async (event, arg) => {
const selectPath = new Promise<string>((resolve, reject) => {
dialog
@@ -82,114 +98,122 @@ function onLoad() {
return ""
}
})
if (!fs.existsSync(CONFIG_DIR)) {
fs.mkdirSync(CONFIG_DIR, {recursive: true});
if (!fs.existsSync(DATA_DIR)) {
fs.mkdirSync(DATA_DIR, {recursive: true});
}
ipcMain.handle(CHANNEL_ERROR, (event, arg) => {
return llonebotError;
ipcMain.handle(CHANNEL_ERROR, async (event, arg) => {
const ffmpegOk = await checkFfmpeg(getConfigUtil().getConfig().ffmpeg)
llonebotError.ffmpegError = ffmpegOk ? "" : "没有找到ffmpeg,音频只能发送wav和silk,视频尺寸可能异常"
let {httpServerError, wsServerError, otherError, ffmpegError} = llonebotError;
let error = `${otherError}\n${httpServerError}\n${wsServerError}\n${ffmpegError}`
error = error.replace("\n\n", "\n")
error = error.trim();
log("查询llonebot错误信息", error);
return error;
})
ipcMain.handle(CHANNEL_GET_CONFIG, async (event, arg) => {
const config = getConfigUtil().getConfig()
return config;
})
ipcMain.on(CHANNEL_SET_CONFIG, (event, arg: Config) => {
let oldConfig = getConfigUtil().getConfig();
getConfigUtil().setConfig(arg)
if (arg.ob11.httpPort != oldConfig.ob11.httpPort && arg.ob11.enableHttp) {
ob11HTTPServer.restart(arg.ob11.httpPort);
ipcMain.on(CHANNEL_SET_CONFIG, (event, ask: boolean, config: Config) => {
if (!ask) {
setConfig(config).then().catch(e => {
log("保存设置失败", e.stack)
});
return
}
// 判断是否启用或关闭HTTP服务
if (!arg.ob11.enableHttp) {
ob11HTTPServer.stop();
} else {
ob11HTTPServer.start(arg.ob11.httpPort);
}
// 正向ws端口变化重启服务
if (arg.ob11.wsPort != oldConfig.ob11.wsPort) {
ob11WebsocketServer.restart(arg.ob11.wsPort);
}
// 判断是否启用或关闭正向ws
if (arg.ob11.enableWs != oldConfig.ob11.enableWs) {
if (arg.ob11.enableWs) {
ob11WebsocketServer.start(arg.ob11.wsPort);
dialog.showMessageBox(mainWindow, {
type: 'question',
buttons: ['确认', '取消'],
defaultId: 0, // 默认选中的按钮0 代表第一个按钮,即 "确认"
title: '确认保存',
message: '是否保存?',
detail: 'LLOneBot配置已更改是否保存'
}).then(result => {
if (result.response === 0) {
setConfig(config).then().catch(e => {
log("保存设置失败", e.stack)
});
} else {
ob11WebsocketServer.stop();
}
}
// 判断是否启用或关闭反向ws
if (arg.ob11.enableWsReverse != oldConfig.ob11.enableWsReverse) {
if (arg.ob11.enableWsReverse) {
ob11ReverseWebsockets.start();
} else {
ob11ReverseWebsockets.stop();
}
}
if (arg.ob11.enableWsReverse) {
// 判断反向ws地址有变化
if (arg.ob11.wsHosts.length != oldConfig.ob11.wsHosts.length) {
ob11ReverseWebsockets.restart();
} else {
for (const newHost of arg.ob11.wsHosts) {
if (!oldConfig.ob11.wsHosts.includes(newHost)) {
ob11ReverseWebsockets.restart();
break;
}
}
}
}
// 检查ffmpeg
if (arg.ffmpeg) {
checkFFMPEG(arg.ffmpeg).then(success => {
llonebotError.ffmpegError = ''
})
}
}).catch(err => {
log("保存设置询问弹窗错误", err);
});
})
ipcMain.on(CHANNEL_LOG, (event, arg) => {
log(arg);
})
function postReceiveMsg(msgList: RawMessage[]) {
async function postReceiveMsg(msgList: RawMessage[]) {
const {debug, reportSelfMessage} = getConfigUtil().getConfig();
for (let message of msgList) {
// log("收到新消息", message)
message.msgShortId = msgHistory[message.msgId]?.msgShortId
if (!message.msgShortId) {
addHistoryMsg(message);
}
// log("收到新消息", message.msgId, message.msgSeq)
// if (message.senderUin !== selfInfo.uin){
message.msgShortId = await dbUtil.addMsg(message);
// }
OB11Constructor.message(message).then((msg) => {
if (debug) {
msg.raw = message;
} else {
if (msg.message.length === 0) {
return
}
}
const isSelfMsg = msg.user_id.toString() == selfInfo.uin
if (isSelfMsg && !reportSelfMessage) {
return
}
if (isSelfMsg) {
msg.target_id = parseInt(message.peerUin);
}
postOB11Event(msg);
// log("post msg", msg)
}).catch(e => log("constructMessage error: ", e.toString()));
}).catch(e => log("constructMessage error: ", e.stack.toString()));
OB11Constructor.GroupEvent(message).then(groupEvent => {
if (groupEvent) {
// log("post group event", groupEvent);
postOB11Event(groupEvent);
}
})
}
}
async function startReceiveHook() {
registerReceiveHook<{ msgList: Array<RawMessage> }>(ReceiveCmd.NEW_MSG, (payload) => {
if (getConfigUtil().getConfig().enablePoke) {
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);
})
}
registerReceiveHook<{
msgList: Array<RawMessage>
}>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], async (payload) => {
try {
postReceiveMsg(payload.msgList);
await postReceiveMsg(payload.msgList);
} catch (e) {
log("report message error: ", e.toString());
log("report message error: ", e.stack.toString());
}
})
registerReceiveHook<{ msgList: Array<RawMessage> }>(ReceiveCmd.UPDATE_MSG, async (payload) => {
registerReceiveHook<{ msgList: Array<RawMessage> }>([ReceiveCmdS.UPDATE_MSG], async (payload) => {
for (const message of payload.msgList) {
// log("message update", message.sendStatus, message)
if (message.recallTime != "0") {
// log("message update", message.sendStatus, message.msgId, message.msgSeq)
if (message.recallTime != "0") { //todo: 这个判断方法不太好,应该使用灰色消息元素来判断
// 撤回消息上报
const oriMessage = msgHistory[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);
@@ -197,7 +221,7 @@ function onLoad() {
let operatorId = message.senderUin
for (const element of message.elements) {
const operatorUid = element.grayTipElement?.revokeElement.operatorUid
const operator = await getGroupMember(message.peerUin, null, operatorUid)
const operator = await getGroupMember(message.peerUin, operatorUid)
operatorId = operator.uin
}
const groupRecallEvent = new OB11GroupRecallNoticeEvent(
@@ -206,36 +230,36 @@ function onLoad() {
parseInt(operatorId),
oriMessage.msgShortId
)
postOB11Event(groupRecallEvent);
}
// 不让入库覆盖原来消息,不然就获取不到撤回的消息内容了
continue
}
addHistoryMsg(message)
dbUtil.updateMsg(message).then();
}
})
registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmd.SELF_SEND_MSG, (payload) => {
registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, async (payload) => {
const {reportSelfMessage} = getConfigUtil().getConfig();
if (!reportSelfMessage) {
return
}
// log("reportSelfMessage", payload)
try {
postReceiveMsg([payload.msgRecord]);
await postReceiveMsg([payload.msgRecord]);
} catch (e) {
log("report self message error: ", e.toString());
log("report self message error: ", e.stack.toString());
}
})
registerReceiveHook<{
"doubt": boolean,
"oldestUnreadSeq": string,
"unreadCount": number
}>(ReceiveCmd.UNREAD_GROUP_NOTIFY, async (payload) => {
}>(ReceiveCmdS.UNREAD_GROUP_NOTIFY, async (payload) => {
if (payload.unreadCount) {
// log("开始获取群通知详情")
let notify: GroupNotifies;
try {
notify = await NTQQApi.getGroupNotifies();
notify = await NTQQGroupApi.getGroupNotifies();
} catch (e) {
// log("获取群通知详情失败", e);
return
@@ -243,29 +267,29 @@ function onLoad() {
const notifies = notify.notifies.slice(0, payload.unreadCount)
// log("获取群通知详情完成", notifies, payload);
try {
for (const notify of notifies) {
for (const notify of notifies) {
try {
notify.time = Date.now();
const notifyTime = parseInt(notify.seq) / 1000
// const notifyTime = parseInt(notify.seq) / 1000
// log(`加群通知时间${notifyTime}`, `LLOneBot启动时间${startTime}`);
if (notifyTime < startTime) {
continue;
}
let existNotify = groupNotifies[notify.seq];
if (existNotify){
if (Date.now() - existNotify.time < 3000){
continue
}
// if (notifyTime < startTime) {
// continue;
// }
let existNotify = await dbUtil.getGroupNotify(notify.seq);
if (existNotify) {
continue
}
log("收到群通知", notify);
groupNotifies[notify.seq] = notify;
const member1 = await getGroupMember(notify.group.groupCode, null, notify.user1.uid);
let member2: GroupMember;
if (notify.user2.uid) {
member2 = await getGroupMember(notify.group.groupCode, null, notify.user2.uid);
}
await dbUtil.addGroupNotify(notify);
// let member2: GroupMember;
// if (notify.user2.uid) {
// member2 = await getGroupMember(notify.group.groupCode, null, notify.user2.uid);
// }
if ([GroupNotifyTypes.ADMIN_SET, GroupNotifyTypes.ADMIN_UNSET].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("开始获取变动的管理员")
@@ -273,21 +297,35 @@ function onLoad() {
log("变动管理员获取成功")
groupAdminNoticeEvent.user_id = parseInt(member1.uin);
groupAdminNoticeEvent.sub_type = notify.type == GroupNotifyTypes.ADMIN_UNSET ? "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) {
log("有成员退出通知");
let groupDecreaseEvent = new OB11GroupDecreaseEvent(parseInt(notify.group.groupCode), parseInt(member1.uin))
// postEvent(groupDecreaseEvent, true);
} else if (notify.type == GroupNotifyTypes.MEMBER_EXIT || notify.type == GroupNotifyTypes.KICK_MEMBER) {
log("有成员退出通知", notify);
try {
const member1 = await NTQQUserApi.getUserDetailInfo(notify.user1.uid);
let operatorId = member1.uin;
let subType: GroupDecreaseSubType = "leave";
if (notify.user2.uid) {
// 是被踢的
const member2 = await getGroupMember(notify.group.groupCode, notify.user2.uid);
operatorId = member2.uin;
subType = "kick";
}
let groupDecreaseEvent = new OB11GroupDecreaseEvent(parseInt(notify.group.groupCode), parseInt(member1.uin), parseInt(operatorId), subType)
postOB11Event(groupDecreaseEvent, true);
} catch (e) {
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 NTQQApi.getUserDetailInfo(notify.user1.uid)).uin;
requestQQ = (await NTQQUserApi.getUserDetailInfo(notify.user1.uid)).uin;
} catch (e) {
log("获取加群人QQ号失败", e)
}
@@ -296,36 +334,43 @@ function onLoad() {
groupRequestEvent.comment = notify.postscript;
groupRequestEvent.flag = notify.seq;
postOB11Event(groupRequestEvent);
}
else if(notify.type == GroupNotifyTypes.INVITE_ME){
} else if (notify.type == GroupNotifyTypes.INVITE_ME) {
log("收到邀请我加群通知")
let groupInviteEvent = new OB11GroupRequestEvent();
groupInviteEvent.group_id = parseInt(notify.group.groupCode);
let user_id = (await NTQQApi.getUserDetailInfo(notify.user2.uid))?.uin
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) {
log("解析群通知失败", e.stack.toString());
}
} catch (e) {
log("解析群通知失败", e.stack);
}
} else if (payload.doubt) {
// 可能有群管理员变动
}
})
registerReceiveHook<FriendRequestNotify>(ReceiveCmd.FRIEND_REQUEST, async (payload) => {
registerReceiveHook<FriendRequestNotify>(ReceiveCmdS.FRIEND_REQUEST, async (payload) => {
for (const req of payload.data.buddyReqs) {
if (req.isUnread && !friendRequests[req.sourceId] && (parseInt(req.reqTime) > startTime / 1000)) {
friendRequests[req.sourceId] = req;
let flag = req.friendUid + req.reqTime;
if (req.isUnread && (parseInt(req.reqTime) > startTime / 1000)) {
friendRequests[flag] = req;
log("有新的好友请求", req);
let friendRequestEvent = new OB11FriendRequestEvent();
try {
let requester = await NTQQApi.getUserDetailInfo(req.friendUid)
let requester = await NTQQUserApi.getUserDetailInfo(req.friendUid)
friendRequestEvent.user_id = parseInt(requester.uin);
} catch (e) {
log("获取加好友者QQ号失败", e);
}
friendRequestEvent.flag = req.sourceId.toString();
friendRequestEvent.flag = flag;
friendRequestEvent.comment = req.extWords;
postOB11Event(friendRequestEvent);
}
@@ -336,22 +381,19 @@ function onLoad() {
let startTime = 0;
async function start() {
log("llonebot pid", process.pid)
llonebotError.otherError = "";
startTime = Date.now();
startReceiveHook().then();
NTQQApi.getGroups(true).then()
const config = getConfigUtil().getConfig()
// 检查ffmpeg
checkFFMPEG(config.ffmpeg).then(exist => {
if (!exist) {
llonebotError.ffmpegError = `没有找到ffmpeg,音频只能发送wav和silk`
dbUtil.getReceivedTempUinMap().then(m => {
for (const [key, value] of Object.entries(m)) {
uidMaps[value] = key;
}
})
startReceiveHook().then();
NTQQGroupApi.getGroups(true).then()
const config = getConfigUtil().getConfig()
if (config.ob11.enableHttp) {
try {
ob11HTTPServer.start(config.ob11.httpPort)
} catch (e) {
log("http server start failed", e);
}
ob11HTTPServer.start(config.ob11.httpPort)
}
if (config.ob11.enableWs) {
ob11WebsocketServer.start(config.ob11.wsPort);
@@ -366,29 +408,39 @@ function onLoad() {
let getSelfNickCount = 0;
const init = async () => {
try {
const _ = await NTQQApi.getSelfInfo();
log("start get self info")
const _ = await NTQQUserApi.getSelfInfo();
log("get self info api result:", _);
Object.assign(selfInfo, _);
selfInfo.nick = selfInfo.uin;
log("get self simple info", _);
} catch (e) {
log("retry get self info");
log("retry get self info", e);
}
if (!selfInfo.uin) {
selfInfo.uin = globalThis.authData?.uin;
selfInfo.uid = globalThis.authData?.uid;
selfInfo.nick = selfInfo.uin;
}
log("self info", selfInfo, globalThis.authData);
if (selfInfo.uin) {
try {
const userInfo = (await NTQQApi.getUserInfo(selfInfo.uid));
log("self info", userInfo);
if (userInfo) {
selfInfo.nick = userInfo.nick;
} else {
async function getUserNick() {
try {
getSelfNickCount++;
if (getSelfNickCount < 10) {
return setTimeout(init, 1000);
const userInfo = (await NTQQUserApi.getUserDetailInfo(selfInfo.uid));
log("self info", userInfo);
if (userInfo) {
selfInfo.nick = userInfo.nick;
return
}
} catch (e) {
log("get self nickname failed", e.stack);
}
if (getSelfNickCount < 10) {
return setTimeout(getUserNick, 1000);
}
} catch (e) {
log("get self nickname failed", e.toString());
return setTimeout(init, 1000);
}
getUserNick().then()
start().then();
} else {
setTimeout(init, 1000)
@@ -400,6 +452,11 @@ function onLoad() {
// 创建窗口时触发
function onBrowserWindowCreated(window: BrowserWindow) {
if (selfInfo.uid) {
return
}
mainWindow = window;
log("window create", window.webContents.getURL().toString())
try {
hookNTQQApiCall(window);
hookNTQQApiReceive(window);
@@ -414,6 +471,7 @@ try {
console.log(e.toString())
}
// 这两个函数都是可选的
export {
onBrowserWindowCreated

60
src/main/setConfig.ts Normal file
View File

@@ -0,0 +1,60 @@
import {Config} from "../common/types";
import {ob11HTTPServer} from "../onebot11/server/http";
import {ob11WebsocketServer} from "../onebot11/server/ws/WebsocketServer";
import {ob11ReverseWebsockets} from "../onebot11/server/ws/ReverseWebsocket";
import {llonebotError} from "../common/data";
import {getConfigUtil} from "../common/config";
import {checkFfmpeg, log} from "../common/utils";
export async function setConfig(config: Config) {
let oldConfig = {...(getConfigUtil().getConfig())};
getConfigUtil().setConfig(config)
if (config.ob11.httpPort != oldConfig.ob11.httpPort && config.ob11.enableHttp) {
ob11HTTPServer.restart(config.ob11.httpPort);
}
// 判断是否启用或关闭HTTP服务
if (!config.ob11.enableHttp) {
ob11HTTPServer.stop();
} else {
ob11HTTPServer.start(config.ob11.httpPort);
}
// 正向ws端口变化重启服务
if (config.ob11.wsPort != oldConfig.ob11.wsPort) {
ob11WebsocketServer.restart(config.ob11.wsPort);
llonebotError.wsServerError = ''
}
// 判断是否启用或关闭正向ws
if (config.ob11.enableWs != oldConfig.ob11.enableWs) {
if (config.ob11.enableWs) {
ob11WebsocketServer.start(config.ob11.wsPort);
} else {
ob11WebsocketServer.stop();
}
}
// 判断是否启用或关闭反向ws
if (config.ob11.enableWsReverse != oldConfig.ob11.enableWsReverse) {
if (config.ob11.enableWsReverse) {
ob11ReverseWebsockets.start();
} else {
ob11ReverseWebsockets.stop();
}
}
if (config.ob11.enableWsReverse) {
// 判断反向ws地址有变化
if (config.ob11.wsHosts.length != oldConfig.ob11.wsHosts.length) {
log("反向ws地址有变化, 重启反向ws服务")
ob11ReverseWebsockets.restart();
} else {
for (const newHost of config.ob11.wsHosts) {
if (!oldConfig.ob11.wsHosts.includes(newHost)) {
log("反向ws地址有变化, 重启反向ws服务")
ob11ReverseWebsockets.restart();
break;
}
}
}
}
log("old config", oldConfig)
log("配置已更新", config)
checkFfmpeg(config.ffmpeg).then()
}

236
src/ntqqapi/api/file.ts Normal file
View File

@@ -0,0 +1,236 @@
import {callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod} from "../ntcall";
import {
CacheFileList,
CacheFileListItem,
CacheFileType,
CacheScanResult,
ChatCacheList,
ChatCacheListItemBasic,
ChatType,
ElementType
} from "../types";
import path from "path";
import fs from "fs";
import {ReceiveCmdS} from "../hook";
import {log} from "../../common/utils/log";
export class NTQQFileApi {
static async getFileType(filePath: string) {
return await callNTQQApi<{ ext: string }>({
className: NTQQApiClass.FS_API, methodName: NTQQApiMethod.FILE_TYPE, args: [filePath]
})
}
static async getFileMd5(filePath: string) {
return await callNTQQApi<string>({
className: NTQQApiClass.FS_API,
methodName: NTQQApiMethod.FILE_MD5,
args: [filePath]
})
}
static async copyFile(filePath: string, destPath: string) {
return await callNTQQApi<string>({
className: NTQQApiClass.FS_API,
methodName: NTQQApiMethod.FILE_COPY,
args: [{
fromPath: filePath,
toPath: destPath
}]
})
}
static async getFileSize(filePath: string) {
return await callNTQQApi<number>({
className: NTQQApiClass.FS_API, methodName: NTQQApiMethod.FILE_SIZE, args: [filePath]
})
}
// 上传文件到QQ的文件夹
static async uploadFile(filePath: string, elementType: ElementType = ElementType.PIC, elementSubType: number = 0) {
const md5 = await NTQQFileApi.getFileMd5(filePath);
let ext = (await NTQQFileApi.getFileType(filePath))?.ext
if (ext) {
ext = "." + ext
} else {
ext = ""
}
let fileName = `${path.basename(filePath)}`;
if (fileName.indexOf(".") === -1) {
fileName += ext;
}
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: ""
}
}]
})
log("media path", mediaPath)
await NTQQFileApi.copyFile(filePath, mediaPath);
const fileSize = await NTQQFileApi.getFileSize(filePath);
return {
md5,
fileName,
path: mediaPath,
fileSize,
ext
}
}
static async downloadMedia(msgId: string, chatType: ChatType, peerUid: string, elementId: string, thumbPath: string, sourcePath: string, isFile: boolean = false) {
// 用于下载收到的消息中的图片等
if (sourcePath && fs.existsSync(sourcePath)) {
return sourcePath
}
const apiParams = [
{
getReq: {
fileModelId: "0",
downloadSourceType: 0,
triggerType: 1,
msgId: msgId,
chatType: chatType,
peerUid: peerUid,
elementId: elementId,
thumbSize: 0,
downloadType: 1,
filePath: thumbPath,
},
},
null,
]
// log("需要下载media", sourcePath);
await callNTQQApi({
methodName: NTQQApiMethod.DOWNLOAD_MEDIA,
args: apiParams,
cbCmd: ReceiveCmdS.MEDIA_DOWNLOAD_COMPLETE,
cmdCB: (payload: { notifyInfo: { filePath: string, msgId: string } }) => {
log("media 下载完成判断", payload.notifyInfo.msgId, msgId);
return payload.notifyInfo.msgId == msgId;
}
})
return sourcePath
}
static async getImageSize(filePath: string) {
return await callNTQQApi<{ width: number, height: number }>({
className: NTQQApiClass.FS_API, methodName: NTQQApiMethod.IMAGE_SIZE, args: [filePath]
})
}
}
export class NTQQFileCacheApi {
static async setCacheSilentScan(isSilent: boolean = true) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.CACHE_SET_SILENCE,
args: [{
isSilent
}, null]
});
}
static getCacheSessionPathList() {
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: 目前还不知道真正的返回值是什么
methodName: NTQQApiMethod.CACHE_CLEAR,
args: [{
keys: cacheKeys
}, null]
});
}
static addCacheScannedPaths(pathMap: object = {}) {
return callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.CACHE_ADD_SCANNED_PATH,
args: [{
pathMap: {...pathMap},
}, null]
});
}
static scanCache() {
callNTQQApi<GeneralCallResult>({
methodName: ReceiveCmdS.CACHE_SCAN_FINISH,
classNameIsRegister: true,
}).then();
return callNTQQApi<CacheScanResult>({
methodName: NTQQApiMethod.CACHE_SCAN,
args: [null, null],
timeoutSecond: 300,
});
}
static getHotUpdateCachePath() {
return callNTQQApi<string>({
className: NTQQApiClass.HOTUPDATE_API,
methodName: NTQQApiMethod.CACHE_PATH_HOT_UPDATE
});
}
static getDesktopTmpPath() {
return callNTQQApi<string>({
className: NTQQApiClass.BUSINESS_API,
methodName: NTQQApiMethod.CACHE_PATH_DESKTOP_TEMP
});
}
static getChatCacheList(type: ChatType, pageSize: number = 1000, pageIndex: number = 0) {
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));
});
}
static getFileCacheInfo(fileType: CacheFileType, pageSize: number = 1000, lastRecord?: CacheFileListItem) {
const _lastRecord = lastRecord ? lastRecord : {fileType: fileType};
return callNTQQApi<CacheFileList>({
methodName: NTQQApiMethod.CACHE_FILE_GET,
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]
});
}
}

61
src/ntqqapi/api/friend.ts Normal file
View File

@@ -0,0 +1,61 @@
import {Friend, FriendRequest} from "../types";
import {ReceiveCmdS} from "../hook";
import {callNTQQApi, GeneralCallResult, NTQQApiMethod} from "../ntcall";
import {friendRequests} from "../../common/data";
export class NTQQFriendApi{
static async getFriends(forced = false) {
const data = await callNTQQApi<{
data: {
categoryId: number,
categroyName: string,
categroyMbCount: number,
buddyList: Friend[]
}[]
}>(
{
methodName: NTQQApiMethod.FRIENDS,
args: [{force_update: forced}, undefined],
cbCmd: ReceiveCmdS.FRIENDS
})
let _friends: Friend[] = [];
for (const fData of data.data) {
_friends.push(...fData.buddyList)
}
return _friends
}
static async likeFriend(uid: string, count = 1) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.LIKE_FRIEND,
args: [{
doLikeUserInfo: {
friendUid: uid,
sourceId: 71,
doLikeCount: count,
doLikeTollCount: 0
}
}, null]
})
}
static async handleFriendRequest(flag: string, accept: boolean,) {
const request: FriendRequest = friendRequests[flag]
if (!request) {
throw `flat: ${flag}, 对应的好友请求不存在`
}
const result = await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.HANDLE_FRIEND_REQUEST,
args: [
{
"approvalInfo": {
"friendUid": request.friendUid,
"reqTime": request.reqTime,
accept
}
}
]
})
delete friendRequests[flag];
return result;
}
}

205
src/ntqqapi/api/group.ts Normal file
View File

@@ -0,0 +1,205 @@
import {ReceiveCmdS} from "../hook";
import {Group, GroupMember, GroupMemberRole, GroupNotifies, GroupNotify, GroupRequestOperateTypes} from "../types";
import {callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod} from "../ntcall";
import {uidMaps} from "../../common/data";
import {dbUtil} from "../../common/db";
import {log} from "../../common/utils/log";
import {NTQQWindowApi, NTQQWindows} from "./window";
export class NTQQGroupApi{
static async getGroups(forced = false) {
let cbCmd = ReceiveCmdS.GROUPS
if (process.platform != "win32") {
cbCmd = ReceiveCmdS.GROUPS_STORE
}
const result = await callNTQQApi<{
updateType: number,
groupList: Group[]
}>({methodName: NTQQApiMethod.GROUPS, args: [{force_update: forced}, undefined], cbCmd})
return result.groupList
}
static async getGroupMembers(groupQQ: string, num = 3000): Promise<GroupMember[]> {
const sceneId = await callNTQQApi({
methodName: NTQQApiMethod.GROUP_MEMBER_SCENE,
args: [{
groupCode: groupQQ,
scene: "groupMemberList_MainWindow"
}]
})
// log("get group member sceneId", sceneId);
try {
const result = await callNTQQApi<{
result: { infos: any }
}>({
methodName: NTQQApiMethod.GROUP_MEMBERS,
args: [{
sceneId: sceneId,
num: num
},
null
]
})
// log("members info", typeof result.result.infos, Object.keys(result.result.infos))
const values = result.result.infos.values()
const members: GroupMember[] = Array.from(values)
for (const member of members) {
uidMaps[member.uid] = member.uin;
}
// log(uidMaps);
// log("members info", values);
log(`get group ${groupQQ} members success`)
return members
} catch (e) {
log(`get group ${groupQQ} members failed`, e)
return []
}
}
static async getGroupNotifies() {
// 获取管理员变更
// 加群通知,退出通知,需要管理员权限
callNTQQApi<GeneralCallResult>({
methodName: ReceiveCmdS.GROUP_NOTIFY,
classNameIsRegister: true,
}).then()
return await callNTQQApi<GroupNotifies>({
methodName: NTQQApiMethod.GET_GROUP_NOTICE,
cbCmd: ReceiveCmdS.GROUP_NOTIFY,
afterFirstCmd: false,
args: [
{"doubt": false, "startSeq": "", "number": 14},
null
]
});
}
static async getGroupIgnoreNotifies() {
await NTQQGroupApi.getGroupNotifies();
return await NTQQWindowApi.openWindow(NTQQWindows.GroupNotifyFilterWindow,[], ReceiveCmdS.GROUP_NOTIFY);
}
static async handleGroupRequest(seq: string, operateType: GroupRequestOperateTypes, reason?: string) {
const notify: GroupNotify = await dbUtil.getGroupNotify(seq)
if (!notify) {
throw `${seq}对应的加群通知不存在`
}
// delete groupNotifies[seq];
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.HANDLE_GROUP_REQUEST,
args: [
{
"doubt": false,
"operateMsg": {
"operateType": operateType, // 2 拒绝
"targetMsg": {
"seq": seq, // 通知序列号
"type": notify.type,
"groupCode": notify.group.groupCode,
"postscript": reason
}
}
},
null
]
});
}
static async quitGroup(groupQQ: string) {
await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.QUIT_GROUP,
args: [
{"groupCode": groupQQ},
null
]
})
}
static async kickMember(groupQQ: string, kickUids: string[], refuseForever: boolean = false, kickReason: string = '') {
return await callNTQQApi<GeneralCallResult>(
{
methodName: NTQQApiMethod.KICK_MEMBER,
args: [
{
groupCode: groupQQ,
kickUids,
refuseForever,
kickReason,
}
]
}
)
}
static async banMember(groupQQ: string, memList: Array<{ uid: string, timeStamp: number }>) {
// timeStamp为秒数, 0为解除禁言
return await callNTQQApi<GeneralCallResult>(
{
methodName: NTQQApiMethod.MUTE_MEMBER,
args: [
{
groupCode: groupQQ,
memList,
}
]
}
)
}
static async banGroup(groupQQ: string, shutUp: boolean) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.MUTE_GROUP,
args: [
{
groupCode: groupQQ,
shutUp
}, null
]
})
}
static async setMemberCard(groupQQ: string, memberUid: string, cardName: string) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_MEMBER_CARD,
args: [
{
groupCode: groupQQ,
uid: memberUid,
cardName
}, null
]
})
}
static async setMemberRole(groupQQ: string, memberUid: string, role: GroupMemberRole) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_MEMBER_ROLE,
args: [
{
groupCode: groupQQ,
uid: memberUid,
role
}, null
]
})
}
static async setGroupName(groupQQ: string, groupName: string) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_GROUP_NAME,
args: [
{
groupCode: groupQQ,
groupName
}, null
]
})
}
// 头衔不可用
static async setGroupTitle(groupQQ: string, uid: string, title: string) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_GROUP_TITLE,
args: [
{
groupCode: groupQQ,
uid,
title
}, null
]
})
}
static publishGroupBulletin(groupQQ: string, title: string, content: string) {
}
}

7
src/ntqqapi/api/index.ts Normal file
View File

@@ -0,0 +1,7 @@
export * from "./file";
export * from "./friend";
export * from "./group";
export * from "./msg";
export * from "./user";
export * from "./webapi";
export * from "./window";

221
src/ntqqapi/api/msg.ts Normal file
View File

@@ -0,0 +1,221 @@
import {callNTQQApi, GeneralCallResult, NTQQApiMethod} from "../ntcall";
import {ChatType, RawMessage, SendMessageElement} 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";
export let sendMessagePool: Record<string, ((sendSuccessMsg: RawMessage) => void) | null> = {}// peerUid: callbackFunnc
export interface Peer {
chatType: ChatType
peerUid: string // 如果是群聊uid为群号私聊uid就是加密的字符串
guildId?: ""
}
export class NTQQMsgApi {
static async getMultiMsg(peer: Peer, rootMsgId: string, parentMsgId: string) {
return await callNTQQApi<GeneralCallResult & {msgList: RawMessage[]}>({
methodName: NTQQApiMethod.GET_MULTI_MSG,
args: [{
peer,
rootMsgId,
parentMsgId
}, null]
})
}
static async activateChat(peer: Peer) {
// await this.fetchRecentContact();
// await sleep(500);
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.ACTIVE_CHAT_PREVIEW,
args: [{peer, cnt: 20}, null]
})
}
static async activateChatAndGetHistory(peer: Peer) {
// await this.fetchRecentContact();
// await sleep(500);
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.ACTIVE_CHAT_HISTORY,
// 参数似乎不是这样
args: [{peer, cnt: 20}, null]
})
}
static async getMsgHistory(peer: Peer, msgId: string, count: number) {
// 消息时间从旧到新
return await callNTQQApi<GeneralCallResult & {msgList: RawMessage[]}>({
methodName: isQQ998 ? NTQQApiMethod.ACTIVE_CHAT_HISTORY : NTQQApiMethod.HISTORY_MSG,
args: [{
peer,
msgId,
cnt: count,
queryOrder: true,
}, null]
})
}
static async fetchRecentContact(){
await callNTQQApi({
methodName: NTQQApiMethod.RECENT_CONTACT,
args: [
{
fetchParam: {
anchorPointContact: {
contactId: '',
sortField: '',
pos: 0,
},
relativeMoveCount: 0,
listType: 2, // 1普通消息2群助手内的消息
count: 200,
fetchOld: true,
},
}
]
})
}
static async recallMsg(peer: Peer, msgIds: string[]) {
return await callNTQQApi({
methodName: NTQQApiMethod.RECALL_MSG,
args: [{
peer,
msgIds
}, null]
})
}
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()
}
callNTQQApi({
methodName: NTQQApiMethod.SEND_MSG,
args: [{
msgId: "0",
peer, msgElements,
msgAttributeInfos: new Map(),
}, null]
}).then()
return await checkSendComplete()
}
static async forwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.FORWARD_MSG,
args: [
{
msgIds: msgIds,
srcContact: srcPeer,
dstContacts: [
destPeer
],
commentElements: [],
msgAttributeInfos: new Map()
},
null,
]
})
}
static async multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) {
const msgInfos = msgIds.map(id => {
return {msgId: id, senderShowName: selfInfo.nick}
})
const apiArgs = [
{
msgInfos,
srcContact: srcPeer,
dstContact: destPeer,
commentElements: [],
msgAttributeInfos: new Map()
},
null,
]
return await new Promise<RawMessage>((resolve, reject) => {
let complete = false
setTimeout(() => {
if (!complete) {
reject("转发消息超时");
}
}, 5000)
registerReceiveHook(ReceiveCmdS.SELF_SEND_MSG, async (payload: { msgRecord: RawMessage }) => {
const msg = payload.msgRecord
// 需要判断它是转发的消息,并且识别到是当前转发的这一条
const arkElement = msg.elements.find(ele => ele.arkElement)
if (!arkElement) {
// log("收到的不是转发消息")
return
}
const forwardData: any = JSON.parse(arkElement.arkElement.bytesData)
if (forwardData.app != 'com.tencent.multimsg') {
return
}
if (msg.peerUid == destPeer.peerUid && msg.senderUid == selfInfo.uid) {
complete = true
await dbUtil.addMsg(msg)
resolve(msg)
log('转发消息成功:', payload)
}
})
callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.MULTI_FORWARD_MSG,
args: apiArgs
}).then(result => {
log("转发消息结果:", result, apiArgs)
if (result.result !== 0) {
complete = true;
reject("转发消息失败," + JSON.stringify(result));
}
})
})
}
}

117
src/ntqqapi/api/user.ts Normal file
View File

@@ -0,0 +1,117 @@
import {callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod} from "../ntcall";
import {SelfInfo, User} from "../types";
import {ReceiveCmdS} from "../hook";
import {uidMaps} from "../../common/data";
import {NTQQWindowApi, NTQQWindows} from "./window";
import {isQQ998, sleep} from "../../common/utils";
let userInfoCache: Record<string, User> = {}; // uid: User
export class NTQQUserApi{
static async setQQAvatar(filePath: string) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_QQ_AVATAR,
args: [{
path:filePath
}, null],
timeoutSecond: 10 // 10秒不一定够
});
}
static async getSelfInfo() {
return await callNTQQApi<SelfInfo>({
className: NTQQApiClass.GLOBAL_DATA,
methodName: NTQQApiMethod.SELF_INFO, timeoutSecond: 2
})
}
static async getUserInfo(uid: string) {
const result = await callNTQQApi<{ profiles: Map<string, User> }>({
methodName: NTQQApiMethod.USER_INFO,
args: [{force: true, uids: [uid]}, undefined],
cbCmd: ReceiveCmdS.USER_INFO
})
return result.profiles.get(uid)
}
static async getUserDetailInfo(uid: string, getLevel=false) {
// this.getUserInfo(uid);
let methodName = !isQQ998 ? NTQQApiMethod.USER_DETAIL_INFO : NTQQApiMethod.USER_DETAIL_INFO_WITH_BIZ_INFO
const fetchInfo = async ()=>{
const result = await callNTQQApi<{ info: User }>({
methodName,
cbCmd: ReceiveCmdS.USER_DETAIL_INFO,
afterFirstCmd: false,
cmdCB: (payload) => {
const success = payload.info.uid == uid
// log("get user detail info", success, uid, payload)
return success
},
args: [
{
uid
},
null
]
})
const info = result.info
if (info?.uin) {
uidMaps[info.uid] = info.uin
}
return info
}
// 首次请求两次才能拿到的等级信息
if (!userInfoCache[uid] && getLevel) {
await fetchInfo()
await sleep(1000);
}
let userInfo = await fetchInfo()
userInfoCache[uid] = userInfo
return userInfo
}
static async getPSkey() {
return await callNTQQApi<string>({
className: NTQQApiClass.GROUP_HOME_WORK,
methodName: NTQQApiMethod.UPDATE_SKEY,
args: [
{
domain: "qun.qq.com"
}
]
})
}
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
// ]
// })
}
}

86
src/ntqqapi/api/webapi.ts Normal file
View File

@@ -0,0 +1,86 @@
import {groups} from "../../common/data";
import {log} from "../../common/utils";
import {NTQQUserApi} from "./user";
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"
}
constructor() {
}
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()
}
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()
}
private 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();
}
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.getPSkey();
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秒
// });
// }
}
}
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)
}
}

49
src/ntqqapi/api/window.ts Normal file
View File

@@ -0,0 +1,49 @@
import {callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod} from "../ntcall";
import {ReceiveCmd} from "../hook";
import {BrowserWindow} from "electron";
export interface NTQQWindow{
windowName: string,
windowUrlHash: string,
}
export class NTQQWindows{
static GroupHomeWorkWindow: NTQQWindow = {
windowName: "GroupHomeWorkWindow",
windowUrlHash: "#/group-home-work"
}
static GroupNotifyFilterWindow: NTQQWindow = {
windowName: "GroupNotifyFilterWindow",
windowUrlHash: "#/group-notify-filter"
}
static GroupEssenceWindow: NTQQWindow = {
windowName: "GroupEssenceWindow",
windowUrlHash: "#/group-essence"
}
}
export class NTQQWindowApi{
// 打开窗口并获取对应的下发事件
static async openWindow<R=GeneralCallResult>(ntQQWindow: NTQQWindow, args: any[], cbCmd: ReceiveCmd=null, autoCloseSeconds: number=2){
const result = await callNTQQApi<R>({
className: NTQQApiClass.WINDOW_API,
methodName: NTQQApiMethod.OPEN_EXTRA_WINDOW,
cbCmd,
afterFirstCmd: false,
args: [
ntQQWindow.windowName,
...args
]
})
setTimeout(() => {
for (const w of BrowserWindow.getAllWindows()) {
// log("close window", w.webContents.getURL())
if (w.webContents.getURL().indexOf(ntQQWindow.windowUrlHash) != -1) {
w.close();
}
}
}, autoCloseSeconds * 1000);
return result;
}
}

View File

@@ -1,16 +1,23 @@
import {
AtType,
ElementType,
PicType,
SendArkElement,
SendFaceElement,
SendFileElement,
SendPicElement,
SendPttElement,
SendReplyElement,
SendTextElement
SendTextElement,
SendVideoElement
} from "./types";
import {NTQQApi} from "./ntcall";
import {encodeSilk} from "../common/utils";
import fs from "fs";
import {promises as fs} from "node:fs";
import ffmpeg from "fluent-ffmpeg"
import {NTQQFileApi} from "./api/file";
import {calculateFileMD5, isGIF} from "../common/utils/file";
import {log} from "../common/utils/log";
import {defaultVideoThumb, getVideoInfo} from "../common/utils/video";
import {encodeSilk} from "../common/utils/audio";
export class SendMsgElementConstructor {
@@ -55,66 +62,146 @@ export class SendMsgElementConstructor {
}
}
static async pic(picPath: string): Promise<SendPicElement> {
const {md5, fileName, path, fileSize} = await NTQQApi.uploadFile(picPath, ElementType.PIC);
const imageSize = await NTQQApi.getImageSize(picPath);
static async pic(picPath: string, summary: string = "", subType: 0|1=0): Promise<SendPicElement> {
const {md5, fileName, path, fileSize} = await NTQQFileApi.uploadFile(picPath, ElementType.PIC, subType);
if (fileSize === 0) {
throw "文件异常大小为0";
}
const imageSize = await NTQQFileApi.getImageSize(picPath);
const picElement = {
md5HexStr: md5,
fileSize: fileSize,
fileSize: fileSize.toString(),
picWidth: imageSize.width,
picHeight: imageSize.height,
fileName: fileName,
sourcePath: path,
original: true,
picType: 1001,
picSubType: 0,
picType: isGIF(picPath) ? PicType.gif : PicType.jpg,
picSubType: subType,
fileUuid: "",
fileSubId: "",
thumbFileSize: 0,
summary: "",
summary
};
log("图片信息", picElement)
return {
elementType: ElementType.PIC,
elementId: "",
picElement
picElement,
};
}
static async file(filePath: string, isVideo: boolean = false): Promise<SendFileElement> {
let picHeight = 0;
let picWidth = 0;
if (isVideo) {
picHeight = 1024;
picWidth = 768;
static async file(filePath: string, fileName: string = ""): Promise<SendFileElement> {
const {md5, fileName: _fileName, path, fileSize} = await NTQQFileApi.uploadFile(filePath, ElementType.FILE);
if (fileSize === 0) {
throw "文件异常大小为0";
}
const {md5, fileName, path, fileSize} = await NTQQApi.uploadFile(filePath, ElementType.FILE);
let element: SendFileElement = {
elementType: ElementType.FILE,
elementId: "",
fileElement: {
fileName,
fileName: fileName || _fileName,
"filePath": path,
"fileSize": (fileSize).toString(),
picHeight,
picWidth
}
}
return element;
}
static video(filePath: string): Promise<SendFileElement> {
return SendMsgElementConstructor.file(filePath, true);
static async video(filePath: string, fileName: string = "", diyThumbPath: string = ""): Promise<SendVideoElement> {
let {fileName: _fileName, path, fileSize, md5} = await NTQQFileApi.uploadFile(filePath, ElementType.VIDEO);
if (fileSize === 0) {
throw "文件异常大小为0";
}
const pathLib = require("path");
let thumb = path.replace(`${pathLib.sep}Ori${pathLib.sep}`, `${pathLib.sep}Thumb${pathLib.sep}`)
thumb = pathLib.dirname(thumb)
// log("thumb 目录", thumb)
let videoInfo = {
width: 1920, height: 1080,
time: 15,
format: "mp4",
size: fileSize,
filePath
};
try {
videoInfo = await getVideoInfo(path);
log("视频信息", videoInfo)
} catch (e) {
log("获取视频信息失败", e)
}
const createThumb = new Promise<string>((resolve, reject) => {
const thumbFileName = `${md5}_0.png`
const thumbPath = pathLib.join(thumb, thumbFileName)
ffmpeg(filePath)
.on("end", () => {
})
.on("error", (err) => {
log("获取视频封面失败,使用默认封面", err)
if (diyThumbPath) {
fs.copyFile(diyThumbPath, thumbPath).then(() => {
resolve(thumbPath);
}).catch(reject)
} else {
fs.writeFile(thumbPath, defaultVideoThumb).then(() => {
resolve(thumbPath);
}).catch(reject)
}
})
.screenshots({
timestamps: [0],
filename: thumbFileName,
folder: thumb,
size: videoInfo.width + "x" + videoInfo.height
}).on("end", () => {
resolve(thumbPath);
});
})
let thumbPath = new Map()
const _thumbPath = await createThumb;
const thumbSize = (await fs.stat(_thumbPath)).size;
// log("生成缩略图", _thumbPath)
thumbPath.set(0, _thumbPath)
const thumbMd5 = await calculateFileMD5(_thumbPath);
let element: SendVideoElement = {
elementType: ElementType.VIDEO,
elementId: "",
videoElement: {
fileName: fileName || _fileName,
filePath: path,
videoMd5: md5,
thumbMd5,
fileTime: videoInfo.time,
thumbPath: thumbPath,
thumbSize,
thumbWidth: videoInfo.width,
thumbHeight: videoInfo.height,
fileSize: "" + fileSize,
// fileUuid: "",
// transferStatus: 0,
// progress: 0,
// invalidState: 0,
// fileSubId: "",
// fileBizId: null,
// originVideoMd5: "",
// fileFormat: 2,
// import_rich_media_context: null,
// sourceVideoCodecFormat: 2
}
}
return element;
}
static async ptt(pttPath: string): Promise<SendPttElement> {
const {converted, path: silkPath, duration} = await encodeSilk(pttPath);
// log("生成语音", silkPath, duration);
const {md5, fileName, path, fileSize} = await NTQQApi.uploadFile(silkPath, ElementType.PTT);
const {md5, fileName, path, fileSize} = await NTQQFileApi.uploadFile(silkPath, ElementType.PTT);
if (fileSize === 0) {
throw "文件异常大小为0";
}
if (converted) {
fs.unlink(silkPath, () => {
});
fs.unlink(silkPath).then();
}
return {
elementType: ElementType.PTT,
@@ -125,7 +212,7 @@ export class SendMsgElementConstructor {
md5HexStr: md5,
fileSize: fileSize,
// duration: Math.max(1, Math.round(fileSize / 1024 / 3)), // 一秒钟大概是3kb大小, 小于1秒的按1秒算
duration: duration / 1000,
duration: duration,
formatType: 1,
voiceType: 1,
voiceChangeType: 0,
@@ -150,4 +237,16 @@ export class SendMsgElementConstructor {
}
}
}
static ark(data: any): SendArkElement {
return {
elementType: ElementType.ARK,
elementId: "",
arkElement: {
bytesData: data,
linkInfo: null,
subElementType: null
}
}
}
}

28
src/ntqqapi/external/ccpoke/index.ts vendored Normal file
View File

@@ -0,0 +1,28 @@
import {log} from "../../../common/utils/log";
let pokeEngine: any = null
type PokeHandler = (id: string, isGroup: boolean)=>void
let pokeRecords: Record<string, number> = {}
export function registerPokeHandler(handler: PokeHandler){
if(!pokeEngine){
try {
pokeEngine = require("./ccpoke/poke-win32-x64.node")
pokeEngine.performHooks();
}catch (e) {
log("戳一戳引擎加载失败", e)
return
}
}
pokeEngine.setHandlerForPokeHook((id: string, isGroup: boolean)=>{
let existTime = pokeRecords[id]
if (existTime){
if (Date.now() - existTime < 1500){
return
}
}
pokeRecords[id] = Date.now()
handler(id, isGroup);
})
}

Binary file not shown.

View File

@@ -1,32 +1,46 @@
import {BrowserWindow} from 'electron';
import {getConfigUtil, log, sleep} from "../common/utils";
import {NTQQApi, NTQQApiClass, sendMessagePool} from "./ntcall";
import {Group, RawMessage, User} from "./types";
import {addHistoryMsg, friends, groups, msgHistory} from "../common/data";
import {NTQQApiClass} from "./ntcall";
import {NTQQMsgApi, sendMessagePool} from "./api/msg"
import {ChatType, Group, GroupMember, GroupMemberRole, RawMessage, User} from "./types";
import {friends, getGroupMember, groups, selfInfo, tempGroupCodeMap, uidMaps} from "../common/data";
import {OB11GroupDecreaseEvent} from "../onebot11/event/notice/OB11GroupDecreaseEvent";
import {OB11GroupIncreaseEvent} from "../onebot11/event/notice/OB11GroupIncreaseEvent";
import {v4 as uuidv4} from "uuid"
import {postOB11Event} from "../onebot11/server/postOB11Event";
import {HOOK_LOG} from "../common/config";
import {getConfigUtil, HOOK_LOG} from "../common/config";
import fs from "fs";
import {dbUtil} from "../common/db";
import {NTQQGroupApi} from "./api/group";
import {log} from "../common/utils/log";
import {sleep} from "../common/utils/helper";
import {OB11Constructor} from "../onebot11/constructor";
export let hookApiCallbacks: Record<string, (apiReturn: any) => void> = {}
export enum ReceiveCmd {
UPDATE_MSG = "nodeIKernelMsgListener/onMsgInfoListUpdate",
NEW_MSG = "nodeIKernelMsgListener/onRecvMsg",
SELF_SEND_MSG = "nodeIKernelMsgListener/onAddSendMsg",
USER_INFO = "nodeIKernelProfileListener/onProfileSimpleChanged",
USER_DETAIL_INFO = "nodeIKernelProfileListener/onProfileDetailInfoChanged",
GROUPS = "nodeIKernelGroupListener/onGroupListUpdate",
GROUPS_UNIX = "onGroupListUpdate",
FRIENDS = "onBuddyListChange",
MEDIA_DOWNLOAD_COMPLETE = "nodeIKernelMsgListener/onRichMediaDownloadComplete",
UNREAD_GROUP_NOTIFY = "nodeIKernelGroupListener/onGroupNotifiesUnreadCountUpdated",
GROUP_NOTIFY = "nodeIKernelGroupListener/onGroupSingleScreenNotifies",
FRIEND_REQUEST = "nodeIKernelBuddyListener/onBuddyReqChange"
export let ReceiveCmdS = {
RECENT_CONTACT: "nodeIKernelRecentContactListener/onRecentContactListChangedVer2",
UPDATE_MSG: "nodeIKernelMsgListener/onMsgInfoListUpdate",
UPDATE_ACTIVE_MSG: "nodeIKernelMsgListener/onActiveMsgInfoUpdate",
NEW_MSG: `nodeIKernelMsgListener/onRecvMsg`,
NEW_ACTIVE_MSG: `nodeIKernelMsgListener/onRecvActiveMsg`,
SELF_SEND_MSG: "nodeIKernelMsgListener/onAddSendMsg",
USER_INFO: "nodeIKernelProfileListener/onProfileSimpleChanged",
USER_DETAIL_INFO: "nodeIKernelProfileListener/onProfileDetailInfoChanged",
GROUPS: "nodeIKernelGroupListener/onGroupListUpdate",
GROUPS_STORE: "onGroupListUpdate",
GROUP_MEMBER_INFO_UPDATE: "nodeIKernelGroupListener/onMemberInfoChange",
FRIENDS: "onBuddyListChange",
MEDIA_DOWNLOAD_COMPLETE: "nodeIKernelMsgListener/onRichMediaDownloadComplete",
UNREAD_GROUP_NOTIFY: "nodeIKernelGroupListener/onGroupNotifiesUnreadCountUpdated",
GROUP_NOTIFY: "nodeIKernelGroupListener/onGroupSingleScreenNotifies",
FRIEND_REQUEST: "nodeIKernelBuddyListener/onBuddyReqChange",
SELF_STATUS: 'nodeIKernelProfileListener/onSelfStatusChanged',
CACHE_SCAN_FINISH: "nodeIKernelStorageCleanListener/onFinishScan",
MEDIA_UPLOAD_COMPLETE: "nodeIKernelMsgListener/onRichMediaUploadComplete",
SKEY_UPDATE: "onSkeyUpdate"
}
export type ReceiveCmd = typeof ReceiveCmdS[keyof typeof ReceiveCmdS]
interface NTQQApiReturnData<PayloadType = unknown> extends Array<any> {
0: {
"type": "request",
@@ -42,7 +56,7 @@ interface NTQQApiReturnData<PayloadType = unknown> extends Array<any> {
}
let receiveHooks: Array<{
method: ReceiveCmd,
method: ReceiveCmd[],
hookFunc: ((payload: any) => void | Promise<void>)
id: string
}> = []
@@ -50,39 +64,56 @@ let receiveHooks: Array<{
export function hookNTQQApiReceive(window: BrowserWindow) {
const originalSend = window.webContents.send;
const patchSend = (channel: string, ...args: NTQQApiReturnData) => {
HOOK_LOG && log(`received ntqq api message: ${channel}`, JSON.stringify(args))
if (args?.[1] instanceof Array) {
for (let receiveData of args?.[1]) {
const ntQQApiMethodName = receiveData.cmdName;
// log(`received ntqq api message: ${channel} ${ntQQApiMethodName}`, JSON.stringify(receiveData))
for (let hook of receiveHooks) {
if (hook.method === ntQQApiMethodName) {
new Promise((resolve, reject) => {
try {
let _ = hook.hookFunc(receiveData.payload)
if (hook.hookFunc.constructor.name === "AsyncFunction") {
(_ as Promise<void>).then()
// console.log("hookNTQQApiReceive", channel, args)
let isLogger = false
try {
isLogger = args[0]?.eventName?.startsWith("ns-LoggerApi")
} catch (e) {
}
if (!isLogger) {
try {
HOOK_LOG && log(`received ntqq api message: ${channel}`, args)
} catch (e) {
log("hook log error", e, args)
}
}
try {
if (args?.[1] instanceof Array) {
for (let receiveData of args?.[1]) {
const ntQQApiMethodName = receiveData.cmdName;
// log(`received ntqq api message: ${channel} ${ntQQApiMethodName}`, JSON.stringify(receiveData))
for (let hook of receiveHooks) {
if (hook.method.includes(ntQQApiMethodName)) {
new Promise((resolve, reject) => {
try {
let _ = hook.hookFunc(receiveData.payload)
if (hook.hookFunc.constructor.name === "AsyncFunction") {
(_ as Promise<void>).then()
}
} catch (e) {
log("hook error", e, receiveData.payload)
}
} catch (e) {
log("hook error", e, receiveData.payload)
}
}).then()
}).then()
}
}
}
}
}
if (args[0]?.callbackId) {
// log("hookApiCallback", hookApiCallbacks, args)
const callbackId = args[0].callbackId;
if (hookApiCallbacks[callbackId]) {
// log("callback found")
new Promise((resolve, reject) => {
hookApiCallbacks[callbackId](args[1]);
}).then()
delete hookApiCallbacks[callbackId];
if (args[0]?.callbackId) {
// log("hookApiCallback", hookApiCallbacks, args)
const callbackId = args[0].callbackId;
if (hookApiCallbacks[callbackId]) {
// log("callback found")
new Promise((resolve, reject) => {
hookApiCallbacks[callbackId](args[1]);
}).then()
delete hookApiCallbacks[callbackId];
}
}
} catch (e) {
log("hookNTQQApiReceive error", e.stack.toString(), args)
}
return originalSend.call(window.webContents, channel, ...args);
originalSend.call(window.webContents, channel, ...args);
}
window.webContents.send = patchSend;
}
@@ -94,7 +125,20 @@ export function hookNTQQApiCall(window: BrowserWindow) {
const proxyIpcMsg = new Proxy(ipc_message_proxy, {
apply(target, thisArg, args) {
HOOK_LOG && log("call NTQQ api", thisArg, args);
// console.log(thisArg, args);
let isLogger = false
try {
isLogger = args[3][0].eventName.startsWith("ns-LoggerApi")
} catch (e) {
}
if (!isLogger) {
try {
HOOK_LOG && log("call NTQQ api", thisArg, args);
} catch (e) {
}
}
return target.apply(thisArg, args);
},
});
@@ -103,10 +147,38 @@ export function hookNTQQApiCall(window: BrowserWindow) {
} else {
webContents._events["-ipc-message"] = proxyIpcMsg;
}
const ipc_invoke_proxy = webContents._events["-ipc-invoke"]?.[0] || webContents._events["-ipc-invoke"];
const proxyIpcInvoke = new Proxy(ipc_invoke_proxy, {
apply(target, thisArg, args) {
// console.log(args);
HOOK_LOG && log("call NTQQ invoke api", thisArg, args)
args[0]["_replyChannel"]["sendReply"] = new Proxy(args[0]["_replyChannel"]["sendReply"], {
apply(sendtarget, sendthisArg, sendargs) {
sendtarget.apply(sendthisArg, sendargs);
}
});
let ret = target.apply(thisArg, args);
try {
HOOK_LOG && log("call NTQQ invoke api return", ret)
} catch (e) {
}
return ret;
}
});
if (webContents._events["-ipc-invoke"]?.[0]) {
webContents._events["-ipc-invoke"][0] = proxyIpcInvoke;
} else {
webContents._events["-ipc-invoke"] = proxyIpcInvoke;
}
}
export function registerReceiveHook<PayloadType>(method: ReceiveCmd, hookFunc: (payload: PayloadType) => void): string {
export function registerReceiveHook<PayloadType>(method: ReceiveCmd | ReceiveCmd[], hookFunc: (payload: PayloadType) => void): string {
const id = uuidv4()
if (!Array.isArray(method)) {
method = [method]
}
receiveHooks.push({
method,
hookFunc,
@@ -120,8 +192,21 @@ export function removeReceiveHook(id: string) {
receiveHooks.splice(index, 1);
}
let activatedGroups: string[] = [];
async function updateGroups(_groups: Group[], needUpdate: boolean = true) {
for (let group of _groups) {
log("update group", group)
// if (!activatedGroups.includes(group.groupCode)) {
NTQQMsgApi.activateChat({peerUid: group.groupCode, chatType: ChatType.group}).then((r) => {
// activatedGroups.push(group.groupCode);
// log(`激活群聊天窗口${group.groupName}(${group.groupCode})`, r)
// if (r.result !== 0) {
// setTimeout(() => NTQQMsgApi.activateGroupChat(group.groupCode).then(r => log(`再次激活群聊天窗口${group.groupName}(${group.groupCode})`, r)), 500);
// }else {
// }
}).catch(log)
// }
let existGroup = groups.find(g => g.groupCode == group.groupCode);
if (existGroup) {
Object.assign(existGroup, group);
@@ -131,7 +216,7 @@ async function updateGroups(_groups: Group[], needUpdate: boolean = true) {
}
if (needUpdate) {
const members = await NTQQApi.getGroupMembers(group.groupCode);
const members = await NTQQGroupApi.getGroupMembers(group.groupCode);
if (members) {
existGroup.members = members;
@@ -140,17 +225,18 @@ async function updateGroups(_groups: Group[], needUpdate: boolean = true) {
}
}
async function processGroupEvent(payload) {
async function processGroupEvent(payload: { groupList: Group[] }) {
try {
const newGroupList = payload.groupList;
for (const group of newGroupList) {
let existGroup = groups.find(g => g.groupCode == group.groupCode);
if (existGroup) {
if (existGroup.memberCount > group.memberCount) {
log(`群(${group.groupCode})成员数量减少${existGroup.memberCount} -> ${group.memberCount}`);
const oldMembers = existGroup.members;
await sleep(200); // 如果请求QQ API的速度过快通常无法正确拉取到最新的群信息因此这里人为引入一个延时
const newMembers = await NTQQApi.getGroupMembers(group.groupCode);
const newMembers = await NTQQGroupApi.getGroupMembers(group.groupCode);
group.members = newMembers;
const newMembersSet = new Set<string>(); // 建立索引降低时间复杂度
@@ -159,42 +245,33 @@ async function processGroupEvent(payload) {
newMembersSet.add(member.uin);
}
for (const member of oldMembers) {
if (!newMembersSet.has(member.uin)) {
postOB11Event(new OB11GroupDecreaseEvent(group.groupCode, parseInt(member.uin)));
break;
}
// 判断bot是否是管理员如果是管理员不需要从这里得知有人退群这里的退群无法得知是主动退群还是被踢
let bot = await getGroupMember(group.groupCode, selfInfo.uin)
if (bot.role == GroupMemberRole.admin || bot.role == GroupMemberRole.owner) {
continue
}
} else if (existGroup.memberCount < group.memberCount) {
const oldMembers = existGroup.members;
const oldMembersSet = new Set<string>();
for (const member of oldMembers) {
oldMembersSet.add(member.uin);
}
await sleep(200);
const newMembers = await NTQQApi.getGroupMembers(group.groupCode);
group.members = newMembers;
for (const member of newMembers) {
if (!oldMembersSet.has(member.uin)) {
postOB11Event(new OB11GroupIncreaseEvent(group.groupCode, parseInt(member.uin)));
if (!newMembersSet.has(member.uin) && member.uin != selfInfo.uin) {
postOB11Event(new OB11GroupDecreaseEvent(parseInt(group.groupCode), parseInt(member.uin), parseInt(member.uin), "leave"));
break;
}
}
}
}
}
updateGroups(newGroupList, false).then();
} catch (e) {
updateGroups(payload.groupList).then();
console.log(e);
log("更新群信息错误", e.stack.toString());
}
}
registerReceiveHook<{ groupList: Group[], updateType: number }>(ReceiveCmd.GROUPS, (payload) => {
// 群列表变动
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 {
@@ -203,7 +280,9 @@ registerReceiveHook<{ groupList: Group[], updateType: number }>(ReceiveCmd.GROUP
}
}
})
registerReceiveHook<{ groupList: Group[], updateType: number }>(ReceiveCmd.GROUPS_UNIX, (payload) => {
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 {
@@ -213,12 +292,44 @@ registerReceiveHook<{ groupList: Group[], updateType: number }>(ReceiveCmd.GROUP
}
})
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);
}
}
// 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[] }[]
}>(ReceiveCmd.FRIENDS, payload => {
}>(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)
@@ -229,23 +340,52 @@ registerReceiveHook<{
}
})
registerReceiveHook<{ msgList: Array<RawMessage> }>(ReceiveCmd.NEW_MSG, (payload) => {
const {autoDeleteFile} = getConfigUtil().getConfig();
registerReceiveHook<{ msgList: Array<RawMessage> }>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], (payload) => {
// 保存一下uid
for (const message of payload.msgList) {
// log("收到新消息push到历史记录", message)
addHistoryMsg(message)
// 清理文件
if (!autoDeleteFile) {
continue
const uid = message.senderUid;
const uin = message.senderUin;
if (uid && uin) {
if (message.chatType === ChatType.temp) {
dbUtil.getReceivedTempUinMap().then(receivedTempUinMap => {
if (!receivedTempUinMap[uin]) {
receivedTempUinMap[uin] = uid;
dbUtil.setReceivedTempUinMap(receivedTempUinMap)
}
})
}
uidMaps[uid] = uin;
}
}
// 自动清理新消息文件
const {autoDeleteFile} = getConfigUtil().getConfig();
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 pttPath = msgElement.pttElement?.filePath;
const pathList = [picPath, pttPath];
if (msgElement.picElement){
pathList.push(...Object.values(msgElement.picElement.thumbPath));
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) {
@@ -254,20 +394,18 @@ registerReceiveHook<{ msgList: Array<RawMessage> }>(ReceiveCmd.NEW_MSG, (payload
});
}
}
}, 60 * 1000)
}, getConfigUtil().getConfig().autoDeleteFileSecond * 1000)
}
}
const msgIds = Object.keys(msgHistory);
if (msgIds.length > 30000) {
delete msgHistory[msgIds.sort()[0]]
}
})
registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmd.SELF_SEND_MSG, ({msgRecord}) => {
registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, ({msgRecord}) => {
const message = msgRecord;
const peerUid = message.peerUid;
// log("收到自己发送成功的消息", Object.keys(sendMessagePool), message);
const sendCallback = sendMessagePool[peerUid];
// log("收到自己发送成功的消息", message.msgId, message.msgSeq);
dbUtil.addMsg(message).then()
const sendCallback = sendMessagePool[peerUid]
if (sendCallback) {
try {
sendCallback(message);
@@ -277,3 +415,42 @@ registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmd.SELF_SEND_MSG, ({msgRe
}
})
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()
}
}
}
})

View File

@@ -1,46 +1,32 @@
import {ipcMain} from "electron";
import {hookApiCallbacks, ReceiveCmd, registerReceiveHook, removeReceiveHook} from "./hook";
import {log, sleep} from "../common/utils";
import {
ChatType,
ElementType,
Friend,
FriendRequest,
Group,
GroupMember,
GroupMemberRole,
GroupNotifies,
GroupNotify,
GroupRequestOperateTypes,
RawMessage,
SelfInfo,
SendMessageElement,
User
} from "./types";
import * as fs from "fs";
import {addHistoryMsg, friendRequests, groupNotifies, msgHistory, selfInfo} from "../common/data";
import {hookApiCallbacks, ReceiveCmd, ReceiveCmdS, registerReceiveHook, removeReceiveHook} from "./hook";
import {v4 as uuidv4} from "uuid"
import path from "path";
interface IPCReceiveEvent {
eventName: string
callbackId: string
}
export type IPCReceiveDetail = [
{
cmdName: NTQQApiMethod
payload: unknown
},
]
import {log} from "../common/utils/log";
import {NTQQWindow, NTQQWindowApi, NTQQWindows} from "./api/window";
import {WebApi} from "./api/webapi";
import {HOOK_LOG} from "../common/config";
export enum NTQQApiClass {
NT_API = "ns-ntApi",
FS_API = "ns-FsApi",
GLOBAL_DATA = "ns-GlobalDataApi"
OS_API = "ns-OsApi",
WINDOW_API = "ns-WindowApi",
HOTUPDATE_API = "ns-HotUpdateApi",
BUSINESS_API = "ns-BusinessApi",
GLOBAL_DATA = "ns-GlobalDataApi",
SKEY_API = "ns-SkeyApi",
GROUP_HOME_WORK = "ns-GroupHomeWork",
GROUP_ESSENCE = "ns-GroupEssence",
}
export enum NTQQApiMethod {
RECENT_CONTACT = "nodeIKernelRecentContactService/fetchAndSubscribeABatchOfRecentContact",
ACTIVE_CHAT_PREVIEW = "nodeIKernelMsgService/getAioFirstViewLatestMsgsAndAddActiveChat", // 激活聊天窗口,有时候必须这样才能收到消息, 并返回最新预览消息
ACTIVE_CHAT_HISTORY = "nodeIKernelMsgService/getMsgsIncludeSelfAndAddActiveChat", // 激活聊天窗口,有时候必须这样才能收到消息, 并返回历史消息
HISTORY_MSG = "nodeIKernelMsgService/getMsgsIncludeSelf",
GET_MULTI_MSG = "nodeIKernelMsgService/getMultiMsg",
LIKE_FRIEND = "nodeIKernelProfileLikeService/setBuddyProfileLike",
SELF_INFO = "fetchAuthData",
FRIENDS = "nodeIKernelBuddyService/getBuddyList",
@@ -49,6 +35,7 @@ export enum NTQQApiMethod {
GROUP_MEMBERS = "nodeIKernelGroupService/getNextMemberList",
USER_INFO = "nodeIKernelProfileService/getUserSimpleInfo",
USER_DETAIL_INFO = "nodeIKernelProfileService/getUserDetailInfo",
USER_DETAIL_INFO_WITH_BIZ_INFO = "nodeIKernelProfileService/getUserDetailInfoWithBizInfo",
FILE_TYPE = "getFileType",
FILE_MD5 = "getFileMd5",
FILE_COPY = "copyFile",
@@ -58,6 +45,7 @@ export enum NTQQApiMethod {
RECALL_MSG = "nodeIKernelMsgService/recallMsg",
SEND_MSG = "nodeIKernelMsgService/sendMsg",
DOWNLOAD_MEDIA = "nodeIKernelMsgService/downloadRichMedia",
FORWARD_MSG = "nodeIKernelMsgService/forwardMsgWithComment",
MULTI_FORWARD_MSG = "nodeIKernelMsgService/multiForwardMsgWithComment", // 合并转发
GET_GROUP_NOTICE = "nodeIKernelGroupService/getSingleScreenNotifies",
HANDLE_GROUP_REQUEST = "nodeIKernelGroupService/operateSysNotify",
@@ -71,6 +59,25 @@ export enum NTQQApiMethod {
SET_MEMBER_ROLE = "nodeIKernelGroupService/modifyMemberRole",
PUBLISH_GROUP_BULLETIN = "nodeIKernelGroupService/publishGroupBulletinBulletin",
SET_GROUP_NAME = "nodeIKernelGroupService/modifyGroupName",
SET_GROUP_TITLE = "nodeIKernelGroupService/modifyMemberSpecialTitle",
CACHE_SET_SILENCE = 'nodeIKernelStorageCleanService/setSilentScan',
CACHE_ADD_SCANNED_PATH = 'nodeIKernelStorageCleanService/addCacheScanedPaths',
CACHE_PATH_HOT_UPDATE = 'getHotUpdateCachePath',
CACHE_PATH_DESKTOP_TEMP = 'getDesktopTmpPath',
CACHE_PATH_SESSION = 'getCleanableAppSessionPathList',
CACHE_SCAN = 'nodeIKernelStorageCleanService/scanCache',
CACHE_CLEAR = 'nodeIKernelStorageCleanService/clearCacheDataByKeys',
CACHE_CHAT_GET = 'nodeIKernelStorageCleanService/getChatCacheInfo',
CACHE_FILE_GET = 'nodeIKernelStorageCleanService/getFileCacheInfo',
CACHE_CHAT_CLEAR = 'nodeIKernelStorageCleanService/clearChatCacheInfo',
OPEN_EXTRA_WINDOW = 'openExternalWindow',
SET_QQ_AVATAR = 'nodeIKernelProfileService/setHeader',
GET_SKEY = "nodeIKernelTipOffService/getPskey",
UPDATE_SKEY = "updatePskey"
}
enum NTQQApiChannel {
@@ -79,12 +86,6 @@ enum NTQQApiChannel {
IPC_UP_1 = "IPC_UP_1",
}
export interface Peer {
chatType: ChatType
peerUid: string // 如果是群聊uid为群号私聊uid就是加密的字符串
guildId?: ""
}
interface NTQQApiParams {
methodName: NTQQApiMethod | string,
className?: NTQQApiClass,
@@ -97,7 +98,7 @@ interface NTQQApiParams {
timeoutSecond?: number,
}
function callNTQQApi<ReturnType>(params: NTQQApiParams) {
export function callNTQQApi<ReturnType>(params: NTQQApiParams) {
let {
className, methodName, channel, args,
cbCmd, timeoutSecond: timeout,
@@ -109,7 +110,7 @@ function callNTQQApi<ReturnType>(params: NTQQApiParams) {
timeout = timeout ?? 5;
afterFirstCmd = afterFirstCmd ?? true;
const uuid = uuidv4();
// log("callNTQQApi", channel, className, methodName, args, uuid)
HOOK_LOG && log("callNTQQApi", channel, className, methodName, args, uuid)
return new Promise((resolve: (data: ReturnType) => void, reject) => {
// log("callNTQQApiPromise", channel, className, methodName, args, uuid)
const _timeout = timeout * 1000
@@ -120,7 +121,7 @@ function callNTQQApi<ReturnType>(params: NTQQApiParams) {
}
const apiArgs = [methodName, ...args]
if (!cbCmd) {
// QQ后端会返回结果并且可以根据uuid识别
// QQ后端会返回结果并且可以根据uuid识别
hookApiCallbacks[uuid] = (r: ReturnType) => {
success = true
resolve(r)
@@ -164,7 +165,12 @@ function callNTQQApi<ReturnType>(params: NTQQApiParams) {
ipcMain.emit(
channel,
{},
{
sender: {
send: (..._args: unknown[]) => {
},
},
},
{type: 'request', callbackId: uuid, eventName},
apiArgs
)
@@ -172,516 +178,20 @@ function callNTQQApi<ReturnType>(params: NTQQApiParams) {
}
export let sendMessagePool: Record<string, ((sendSuccessMsg: RawMessage) => void) | null> = {}// peerUid: callbackFunnc
interface GeneralCallResult {
export interface GeneralCallResult {
result: number, // 0: success
errMsg: string
}
export class NTQQApi {
// static likeFriend = defineNTQQApi<void>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.LIKE_FRIEND)
static likeFriend(uid: string, count = 1) {
return callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.LIKE_FRIEND,
args: [{
doLikeUserInfo: {
friendUid: uid,
sourceId: 71,
doLikeCount: count,
doLikeTollCount: 0
}
}, null]
})
}
static getSelfInfo() {
return callNTQQApi<SelfInfo>({
className: NTQQApiClass.GLOBAL_DATA,
methodName: NTQQApiMethod.SELF_INFO, timeoutSecond: 2
})
}
static async getUserInfo(uid: string) {
const result = await callNTQQApi<{ profiles: Map<string, User> }>({
methodName: NTQQApiMethod.USER_INFO,
args: [{force: true, uids: [uid]}, undefined],
cbCmd: ReceiveCmd.USER_INFO
})
return result.profiles.get(uid)
}
static async getUserDetailInfo(uid: string) {
const result = await callNTQQApi<{ info: User }>({
methodName: NTQQApiMethod.USER_DETAIL_INFO,
cbCmd: ReceiveCmd.USER_DETAIL_INFO,
afterFirstCmd: false,
cmdCB: (payload) => {
const success = payload.info.uid == uid
// log("get user detail info", success, uid, payload)
return success
},
args: [
{
uid
},
null
]
})
return result.info
}
static async getFriends(forced = false) {
const data = await callNTQQApi<{
data: {
categoryId: number,
categroyName: string,
categroyMbCount: number,
buddyList: Friend[]
}[]
}>(
{
methodName: NTQQApiMethod.FRIENDS,
args: [{force_update: forced}, undefined],
cbCmd: ReceiveCmd.FRIENDS
})
let _friends: Friend[] = [];
for (const fData of data.data) {
_friends.push(...fData.buddyList)
}
return _friends
}
static async getGroups(forced = false) {
let cbCmd = ReceiveCmd.GROUPS
if (process.platform != "win32") {
cbCmd = ReceiveCmd.GROUPS_UNIX
}
const result = await callNTQQApi<{
updateType: number,
groupList: Group[]
}>({methodName: NTQQApiMethod.GROUPS, args: [{force_update: forced}, undefined], cbCmd})
return result.groupList
}
static async getGroupMembers(groupQQ: string, num = 3000) {
const sceneId = await callNTQQApi({
methodName: NTQQApiMethod.GROUP_MEMBER_SCENE,
args: [{
groupCode: groupQQ,
scene: "groupMemberList_MainWindow"
}]
})
// log("get group member sceneId", sceneId);
try {
const result = await callNTQQApi<{
result: { infos: any }
}>({
methodName: NTQQApiMethod.GROUP_MEMBERS,
args: [{
sceneId: sceneId,
num: num
},
null
]
})
// log("members info", typeof result.result.infos, Object.keys(result.result.infos))
let values = result.result.infos.values()
let members = Array.from(values) as GroupMember[]
for (const member of members) {
// uidMaps[member.uid] = member.uin;
}
// log(uidMaps);
// log("members info", values);
log(`get group ${groupQQ} members success`)
return members
} catch (e) {
log(`get group ${groupQQ} members failed`, e)
return []
}
}
static getFileType(filePath: string) {
return callNTQQApi<{ ext: string }>({
className: NTQQApiClass.FS_API, methodName: NTQQApiMethod.FILE_TYPE, args: [filePath]
})
}
static getFileMd5(filePath: string) {
return callNTQQApi<string>({
className: NTQQApiClass.FS_API,
methodName: NTQQApiMethod.FILE_MD5,
args: [filePath]
})
}
static copyFile(filePath: string, destPath: string) {
return callNTQQApi<string>({
className: NTQQApiClass.FS_API, methodName: NTQQApiMethod.FILE_COPY, args: [{
fromPath: filePath,
toPath: destPath
}]
})
}
static getImageSize(filePath: string) {
return callNTQQApi<{ width: number, height: number }>({
className: NTQQApiClass.FS_API, methodName: NTQQApiMethod.IMAGE_SIZE, args: [filePath]
})
}
static getFileSize(filePath: string) {
return callNTQQApi<number>({
className: NTQQApiClass.FS_API, methodName: NTQQApiMethod.FILE_SIZE, args: [filePath]
})
}
// 上传文件到QQ的文件夹
static async uploadFile(filePath: string, elementType: ElementType = ElementType.PIC) {
const md5 = await NTQQApi.getFileMd5(filePath);
let ext = (await NTQQApi.getFileType(filePath))?.ext
if (ext) {
ext = "." + ext
} else {
ext = ""
}
let fileName = `${path.basename(filePath)}`;
if (fileName.indexOf(".") === -1) {
fileName += ext;
}
const mediaPath = await callNTQQApi<string>({
methodName: NTQQApiMethod.MEDIA_FILE_PATH,
args: [{
path_info: {
md5HexStr: md5,
fileName: fileName,
elementType: elementType,
elementSubType: 0,
thumbSize: 0,
needCreate: true,
downloadType: 1,
file_uuid: ""
}
}]
})
log("media path", mediaPath)
await NTQQApi.copyFile(filePath, mediaPath);
const fileSize = await NTQQApi.getFileSize(filePath);
return {
md5,
fileName,
path: mediaPath,
fileSize
}
}
static async downloadMedia(msgId: string, chatType: ChatType, peerUid: string, elementId: string, thumbPath: string, sourcePath: string) {
// 用于下载收到的消息中的图片等
if (fs.existsSync(sourcePath)) {
return sourcePath
}
const apiParams = [
{
getReq: {
msgId: msgId,
chatType: chatType,
peerUid: peerUid,
elementId: elementId,
thumbSize: 0,
downloadType: 1,
filePath: thumbPath,
},
},
undefined,
]
// log("需要下载media", sourcePath);
await callNTQQApi({
methodName: NTQQApiMethod.DOWNLOAD_MEDIA,
args: apiParams,
cbCmd: ReceiveCmd.MEDIA_DOWNLOAD_COMPLETE,
cmdCB: (payload: { notifyInfo: { filePath: string } }) => {
// log("media 下载完成判断", payload.notifyInfo.filePath, sourcePath);
return payload.notifyInfo.filePath == sourcePath;
}
})
return sourcePath
}
static recallMsg(peer: Peer, msgIds: string[]) {
return callNTQQApi({
methodName: NTQQApiMethod.RECALL_MSG, args: [{
peer,
msgIds
}, null]
})
}
static async sendMsg(peer: Peer, msgElements: SendMessageElement[], waitComplete = false, 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 && msgHistory[sentMessage.msgId]?.sendStatus == 2) {
// log(`给${peerUid}发送消息成功`)
return sentMessage;
} else {
checkSendCompleteUsingTime += 500;
if (checkSendCompleteUsingTime > timeout) {
throw ("发送超时")
}
await sleep(500);
return await checkSendComplete()
}
}
callNTQQApi({
methodName: NTQQApiMethod.SEND_MSG,
args: [{
msgId: "0",
peer, msgElements,
msgAttributeInfos: new Map(),
}, null]
}).then()
return checkSendComplete();
}
static multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) {
let msgInfos = msgIds.map(id => {
return {msgId: id, senderShowName: selfInfo.nick}
})
const apiArgs = [
{
msgInfos,
srcContact: srcPeer,
dstContact: destPeer,
commentElements: [],
msgAttributeInfos: new Map()
},
null,
]
return new Promise<RawMessage>((resolve, reject) => {
let complete = false
setTimeout(() => {
if (!complete) {
reject("转发消息超时");
}
}, 5000)
registerReceiveHook(ReceiveCmd.SELF_SEND_MSG, (payload: { msgRecord: RawMessage }) => {
const msg = payload.msgRecord;
// 需要判断它是转发的消息,并且识别到是当前转发的这一条
const arkElement = msg.elements.find(ele => ele.arkElement)
if (!arkElement) {
// log("收到的不是转发消息")
return
}
const forwardData: any = JSON.parse(arkElement.arkElement.bytesData);
if (forwardData.app != "com.tencent.multimsg") {
return
}
if (msg.peerUid == destPeer.peerUid && msg.senderUid == selfInfo.uid) {
complete = true;
addHistoryMsg(msg)
resolve(msg);
log("转发消息成功:", payload)
}
})
callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.MULTI_FORWARD_MSG,
args: apiArgs
}).then(result => {
log("转发消息结果:", result, apiArgs)
if (result.result !== 0) {
complete = true;
reject("转发消息失败," + JSON.stringify(result));
}
})
})
}
static async getGroupNotifies() {
// 获取管理员变更
// 加群通知,退出通知,需要管理员权限
callNTQQApi<GeneralCallResult>({
methodName: ReceiveCmd.GROUP_NOTIFY,
classNameIsRegister: true,
}).then()
return await callNTQQApi<GroupNotifies>({
methodName: NTQQApiMethod.GET_GROUP_NOTICE,
cbCmd: ReceiveCmd.GROUP_NOTIFY,
afterFirstCmd: false,
args: [
{"doubt": false, "startSeq": "", "number": 14},
null
]
});
}
static async handleGroupRequest(seq: string, operateType: GroupRequestOperateTypes, reason?: string) {
const notify: GroupNotify = groupNotifies[seq];
if (!notify) {
throw `${seq}对应的加群通知不存在`
}
delete groupNotifies[seq];
static async call(className: NTQQApiClass, cmdName: string, args: any[],) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.HANDLE_GROUP_REQUEST,
className,
methodName: cmdName,
args: [
{
"doubt": false,
"operateMsg": {
"operateType": operateType, // 2 拒绝
"targetMsg": {
"seq": seq, // 通知序列号
"type": notify.type,
"groupCode": notify.group.groupCode,
"postscript": reason
}
}
},
null
]
});
}
static async quitGroup(groupQQ: string) {
await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.QUIT_GROUP,
args: [
{"groupCode": groupQQ},
null
...args,
]
})
}
static async handleFriendRequest(sourceId: number, accept: boolean,) {
const request: FriendRequest = friendRequests[sourceId]
if (!request) {
throw `sourceId ${sourceId}, 对应的好友请求不存在`
}
const result = await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.HANDLE_FRIEND_REQUEST,
args: [
{
"approvalInfo": {
"friendUid": request.friendUid,
"reqTime": request.reqTime,
accept
}
}
]
})
delete friendRequests[sourceId];
return result;
}
static kickMember(groupQQ: string, kickUids: string[], refuseForever: boolean = false, kickReason: string = "") {
return callNTQQApi<GeneralCallResult>(
{
methodName: NTQQApiMethod.KICK_MEMBER,
args: [
{
groupCode: groupQQ,
kickUids,
refuseForever,
kickReason,
}
]
}
)
}
static banMember(groupQQ: string, memList: { uid: string, timeStamp: number }[]) {
// timeStamp为秒数, 0为解除禁言
return callNTQQApi<GeneralCallResult>(
{
methodName: NTQQApiMethod.MUTE_MEMBER,
args: [
{
groupCode: groupQQ,
memList,
}
]
}
)
}
static banGroup(groupQQ: string, shutUp: boolean) {
return callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.MUTE_GROUP,
args: [
{
groupCode: groupQQ,
shutUp
}, null
]
})
}
static setMemberCard(groupQQ: string, memberUid: string, cardName: string) {
return callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_MEMBER_CARD,
args: [
{
groupCode: groupQQ,
uid: memberUid,
cardName
}, null
]
})
}
static setMemberRole(groupQQ: string, memberUid: string, role: GroupMemberRole) {
return callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_MEMBER_ROLE,
args: [
{
groupCode: groupQQ,
uid: memberUid,
role
}, null
]
})
}
static setGroupName(groupQQ: string, groupName: string) {
return callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_GROUP_NAME,
args: [
{
groupCode: groupQQ,
groupName
}, null
]
})
}
static publishGroupBulletin(groupQQ: string, title: string, content: string) {
}
}

View File

@@ -1,357 +0,0 @@
export interface User {
uid: string; // 加密的字符串
uin: string; // QQ号
nick: string;
avatarUrl?: string;
longNick?: string; // 签名
remark?: string
}
export interface SelfInfo extends User {
}
export interface Friend extends User {
}
export interface Group {
groupCode: string,
maxMember: number,
memberCount: number,
groupName: string,
groupStatus: 0,
memberRole: 2,
isTop: boolean,
toppedTimestamp: "0",
privilegeFlag: number, //65760
isConf: boolean,
hasModifyConfGroupFace: boolean,
hasModifyConfGroupName: boolean,
remarkName: string,
hasMemo: boolean,
groupShutupExpireTime: string, //"0",
personShutupExpireTime: string, //"0",
discussToGroupUin: string, //"0",
discussToGroupMaxMsgSeq: number,
discussToGroupTime: number,
groupFlagExt: number, //1073938496,
authGroupType: number, //0,
groupCreditLevel: number, //0,
groupFlagExt3: number, //0,
groupOwnerId: {
"memberUin": string, //"0",
"memberUid": string, //"u_fbf8N7aeuZEnUiJAbQ9R8Q"
},
members: GroupMember[] // 原始数据是没有这个的,为了方便自己加了这个字段
}
export enum GroupMemberRole {
normal = 2,
admin = 3,
owner = 4
}
export interface GroupMember {
avatarPath: string;
cardName: string;
cardType: number;
isDelete: boolean;
nick: string;
qid: string;
remark: string;
role: GroupMemberRole; // 群主:4, 管理员:3群员:2
shutUpTime: number; // 禁言时间,单位是什么暂时不清楚
uid: string; // 加密的字符串
uin: string; // QQ号
}
export enum ElementType {
TEXT = 1,
PIC = 2,
FILE = 3,
PTT = 4,
FACE = 6,
REPLY = 7,
}
export interface SendTextElement {
elementType: ElementType.TEXT,
elementId: "",
textElement: {
content: string,
atType: number,
atUid: string,
atTinyId: string,
atNtUid: string,
}
}
export interface SendPttElement {
elementType: ElementType.PTT,
elementId: "",
pttElement: {
fileName: string,
filePath: string,
md5HexStr: string,
fileSize: number,
duration: number,
formatType: number,
voiceType: number,
voiceChangeType: number,
canConvert2Text: boolean,
waveAmplitudes: number[],
fileSubId: "",
playState: number,
autoConvertText: number,
}
}
export interface SendPicElement {
elementType: ElementType.PIC,
elementId: "",
picElement: {
md5HexStr: string,
fileSize: number,
picWidth: number,
picHeight: number,
fileName: string,
sourcePath: string,
original: boolean,
picType: number,
picSubType: number,
fileUuid: string,
fileSubId: string,
thumbFileSize: number,
summary: string,
}
}
export interface SendReplyElement {
elementType: ElementType.REPLY,
elementId: "",
replyElement: {
replayMsgSeq: string,
replayMsgId: string,
senderUin: string,
senderUinStr: string,
}
}
export interface SendFaceElement {
elementType: ElementType.FACE,
elementId: "",
faceElement: FaceElement
}
export interface FileElement {
"fileMd5"?: "",
"fileName": string,
"filePath": string,
"fileSize": string,
"picHeight"?: number,
"picWidth"?: number,
"picThumbPath"?: {},
"file10MMd5"?: "",
"fileSha"?: "",
"fileSha3"?: "",
"fileUuid"?: "",
"fileSubId"?: "",
"thumbFileSize"?: number
}
export interface SendFileElement {
"elementType": ElementType.FILE,
"elementId": "",
"fileElement": FileElement
}
export type SendMessageElement = SendTextElement | SendPttElement |
SendPicElement | SendReplyElement | SendFaceElement | SendFileElement
export enum AtType {
notAt = 0,
atAll = 1,
atUser = 2
}
export enum ChatType {
friend = 1,
group = 2,
temp = 100
}
export interface PttElement {
canConvert2Text: boolean;
duration: number; // 秒数
fileBizId: null;
fileId: number; // 0
fileName: string; // "e4d09c784d5a2abcb2f9980bdc7acfe6.amr"
filePath: string; // "/Users//Library/Containers/com.tencent.qq/Data/Library/Application Support/QQ/nt_qq_a6b15c9820595d25a56c1633ce19ad40/nt_data/Ptt/2023-11/Ori/e4d09c784d5a2abcb2f9980bdc7acfe6.amr"
fileSize: string; // "4261"
fileSubId: string; // "0"
fileUuid: string; // "90j3z7rmRphDPrdVgP9udFBaYar#oK0TWZIV"
formatType: string; // 1
invalidState: number; // 0
md5HexStr: string; // "e4d09c784d5a2abcb2f9980bdc7acfe6"
playState: number; // 0
progress: number; // 0
text: string; // ""
transferStatus: number; // 0
translateStatus: number; // 0
voiceChangeType: number; // 0
voiceType: number; // 0
waveAmplitudes: number[];
}
export interface ArkElement {
bytesData: string;
}
export const IMAGE_HTTP_HOST = "https://gchat.qpic.cn"
export interface PicElement {
originImageUrl: string; // http url, 没有hosthost是https://gchat.qpic.cn/
sourcePath: string; // 图片本地路径
thumbPath: Map<number, string>;
picWidth: number;
picHeight: number;
fileSize: number;
fileName: string;
fileUuid: string;
}
export interface GrayTipElement {
revokeElement: {
operatorRole: string;
operatorUid: string;
operatorNick: string;
operatorRemark: string;
operatorMemRemark?: string;
wording: string; // 自定义的撤回提示语
}
}
export interface FaceElement {
faceIndex: number,
faceType: 1
}
export interface VideoElement {
"filePath": string,
"fileName": string,
"videoMd5": string,
"thumbMd5": string
"fileTime": 87, // second
"thumbSize": 314235, // byte
"fileFormat": 2, // 2表示mp4
"fileSize": string, // byte
"thumbWidth": number,
"thumbHeight": number,
"busiType": 0, // 未知
"subBusiType": 0, // 未知
"thumbPath": {},
"transferStatus": 0, // 未知
"progress": 0, // 下载进度?
"invalidState": 0, // 未知
"fileUuid": string, // 可以用于下载链接?
"fileSubId": "",
"fileBizId": null,
"originVideoMd5": "",
"import_rich_media_context": null,
"sourceVideoCodecFormat": 0
}
export interface RawMessage {
msgId: string;
msgShortId?: number; // 自己维护的消息id
msgTime: string; // 时间戳,秒
msgSeq: string;
senderUid: string;
senderUin?: string; // 发送者QQ号
peerUid: string; // 群号 或者 QQ uid
peerUin: string; // 群号 或者 发送者QQ号
sendNickName: string;
sendMemberName?: string; // 发送者群名片
chatType: ChatType;
sendStatus?: number; // 消息状态别人发的2是已撤回自己发的2是已发送
recallTime: string; // 撤回时间, "0"是没有撤回
elements: {
elementId: string,
replyElement: {
senderUid: string; // 原消息发送者QQ号
sourceMsgIsIncPic: boolean; // 原消息是否有图片
sourceMsgText: string;
replayMsgSeq: string; // 源消息的msgSeq可以通过这个找到源消息的msgId
};
textElement: {
atType: AtType;
atUid: string; // QQ号
content: string;
atNtUid: string; // uid号
};
picElement: PicElement;
pttElement: PttElement;
arkElement: ArkElement;
grayTipElement: GrayTipElement;
faceElement: FaceElement;
videoElement: VideoElement;
fileElement: FileElement;
}[];
}
export enum GroupNotifyTypes {
INVITE_ME = 1,
INVITED_JOIN = 4, // 有人接受了邀请入群
JOIN_REQUEST = 7,
ADMIN_SET = 8,
ADMIN_UNSET = 12,
MEMBER_EXIT = 11, // 主动退出?
}
export interface GroupNotifies {
doubt: boolean,
nextStartSeq: string,
notifies: GroupNotify[],
}
export interface GroupNotify {
time: number; // 自己添加的字段,时间戳,毫秒, 用于判断收到短时间内收到重复的notify
seq: string, // 转成数字再除以1000应该就是时间戳
type: GroupNotifyTypes,
status: 0, // 未知
group: { groupCode: string, groupName: string },
user1: { uid: string, nickName: string }, // 被设置管理员的人
user2: { uid: string, nickName: string }, // 操作者
actionUser: { uid: string, nickName: string }, //未知
actionTime: string,
invitationExt: {
srcType: number, // 0?未知
groupCode: string, waitStatus: number
},
postscript: string, // 加群用户填写的验证信息
repeatSeqs: [],
warningTips: string
}
export enum GroupRequestOperateTypes {
approve = 1,
reject = 2
}
export interface FriendRequest {
friendUid: string,
reqTime: string, // 时间戳,秒
extWords: string, // 申请人填写的验证消息
isUnread: boolean,
friendNick: string,
sourceId: number,
groupCode: string
}
export interface FriendRequestNotify {
data: {
unreadNums: number,
buddyReqs: FriendRequest[]
}
}

View File

@@ -0,0 +1,65 @@
import {ChatType} from "./msg";
export interface CacheScanResult {
result: number,
size: [ // 单位为字节
string, // 系统总存储空间
string, // 系统可用存储空间
string, // 系统已用存储空间
string, // QQ总大小
string, // 「聊天与文件」大小
string, // 未知
string, // 「缓存数据」大小
string, // 「其他数据」大小
string, // 未知
]
}
export interface ChatCacheList {
pageCount: number,
infos: ChatCacheListItem[]
}
export interface ChatCacheListItem {
chatType: ChatType,
basicChatCacheInfo: ChatCacheListItemBasic,
guildChatCacheInfo: unknown[] // TODO: 没用过频道所以不知道这里边的详细内容
}
export interface ChatCacheListItemBasic {
chatSize: string,
chatTime: string,
uid: string,
uin: string,
remarkName: string,
nickName: string,
chatType?: ChatType,
isChecked?: boolean
}
export enum CacheFileType {
IMAGE = 0,
VIDEO = 1,
AUDIO = 2,
DOCUMENT = 3,
OTHER = 4,
}
export interface CacheFileList {
infos: CacheFileListItem[],
}
export interface CacheFileListItem {
fileSize: string,
fileTime: string,
fileKey: string,
elementId: string,
elementIdStr: string,
fileType: CacheFileType,
path: string,
fileName: string,
senderId: string,
previewPath: string,
senderName: string,
isChecked?: boolean,
}

View File

@@ -0,0 +1,56 @@
import {QQLevel, Sex} from "./user";
export interface Group {
groupCode: string,
maxMember: number,
memberCount: number,
groupName: string,
groupStatus: 0,
memberRole: 2,
isTop: boolean,
toppedTimestamp: "0",
privilegeFlag: number, //65760
isConf: boolean,
hasModifyConfGroupFace: boolean,
hasModifyConfGroupName: boolean,
remarkName: string,
hasMemo: boolean,
groupShutupExpireTime: string, //"0",
personShutupExpireTime: string, //"0",
discussToGroupUin: string, //"0",
discussToGroupMaxMsgSeq: number,
discussToGroupTime: number,
groupFlagExt: number, //1073938496,
authGroupType: number, //0,
groupCreditLevel: number, //0,
groupFlagExt3: number, //0,
groupOwnerId: {
"memberUin": string, //"0",
"memberUid": string, //"u_fbf8N7aeuZEnUiJAbQ9R8Q"
},
members: GroupMember[] // 原始数据是没有这个的,为了方便自己加了这个字段
}
export enum GroupMemberRole {
normal = 2,
admin = 3,
owner = 4
}
export interface GroupMember {
memberSpecialTitle: string;
avatarPath: string;
cardName: string;
cardType: number;
isDelete: boolean;
nick: string;
qid: string;
remark: string;
role: GroupMemberRole; // 群主:4, 管理员:3群员:2
shutUpTime: number; // 禁言时间,单位是什么暂时不清楚
uid: string; // 加密的字符串
uin: string; // QQ号
isRobot: boolean;
sex?: Sex
qqLevel?: QQLevel
}

View File

@@ -0,0 +1,7 @@
export * from './user';
export * from './group';
export * from './msg';
export * from './notify';
export * from './cache';

410
src/ntqqapi/types/msg.ts Normal file
View File

@@ -0,0 +1,410 @@
import {GroupMemberRole} from "./group";
import exp from "constants";
export enum ElementType {
TEXT = 1,
PIC = 2,
FILE = 3,
PTT = 4,
VIDEO = 5,
FACE = 6,
REPLY = 7,
ARK = 10,
}
export interface SendTextElement {
elementType: ElementType.TEXT,
elementId: "",
textElement: {
content: string,
atType: number,
atUid: string,
atTinyId: string,
atNtUid: string,
}
}
export interface SendPttElement {
elementType: ElementType.PTT,
elementId: "",
pttElement: {
fileName: string,
filePath: string,
md5HexStr: string,
fileSize: number,
duration: number, // 单位是秒
formatType: number,
voiceType: number,
voiceChangeType: number,
canConvert2Text: boolean,
waveAmplitudes: number[],
fileSubId: "",
playState: number,
autoConvertText: number,
}
}
export enum PicType {
gif = 2000,
jpg = 1000
}
export enum PicSubType {
normal = 0, // 普通图片,大图
face = 1 // 表情包小图
}
export interface SendPicElement {
elementType: ElementType.PIC,
elementId: "",
picElement: {
md5HexStr: string,
fileSize: number | string,
picWidth: number,
picHeight: number,
fileName: string,
sourcePath: string,
original: boolean,
picType: PicType,
picSubType: PicSubType,
fileUuid: string,
fileSubId: string,
thumbFileSize: number,
summary: string,
},
}
export interface SendReplyElement {
elementType: ElementType.REPLY,
elementId: "",
replyElement: {
replayMsgSeq: string,
replayMsgId: string,
senderUin: string,
senderUinStr: string,
}
}
export interface SendFaceElement {
elementType: ElementType.FACE,
elementId: "",
faceElement: FaceElement
}
export interface FileElement {
"fileMd5"?: "",
"fileName": string,
"filePath": string,
"fileSize": string,
"picHeight"?: number,
"picWidth"?: number,
"picThumbPath"?: {},
"file10MMd5"?: "",
"fileSha"?: "",
"fileSha3"?: "",
"fileUuid"?: "",
"fileSubId"?: "",
"thumbFileSize"?: number,
fileBizId?: number
}
export interface SendFileElement {
elementType: ElementType.FILE
elementId: "",
fileElement: FileElement
}
export interface SendVideoElement {
elementType: ElementType.VIDEO
elementId: "",
videoElement: VideoElement
}
export interface SendArkElement {
elementType: ElementType.ARK,
elementId: "",
arkElement: ArkElement
}
export type SendMessageElement = SendTextElement | SendPttElement |
SendPicElement | SendReplyElement | SendFaceElement | SendFileElement | SendVideoElement | SendArkElement
export enum AtType {
notAt = 0,
atAll = 1,
atUser = 2
}
export enum ChatType {
friend = 1,
group = 2,
temp = 100
}
export interface PttElement {
canConvert2Text: boolean;
duration: number; // 秒数
fileBizId: null;
fileId: number; // 0
fileName: string; // "e4d09c784d5a2abcb2f9980bdc7acfe6.amr"
filePath: string; // "/Users//Library/Containers/com.tencent.qq/Data/Library/Application Support/QQ/nt_qq_a6b15c9820595d25a56c1633ce19ad40/nt_data/Ptt/2023-11/Ori/e4d09c784d5a2abcb2f9980bdc7acfe6.amr"
fileSize: string; // "4261"
fileSubId: string; // "0"
fileUuid: string; // "90j3z7rmRphDPrdVgP9udFBaYar#oK0TWZIV"
formatType: string; // 1
invalidState: number; // 0
md5HexStr: string; // "e4d09c784d5a2abcb2f9980bdc7acfe6"
playState: number; // 0
progress: number; // 0
text: string; // ""
transferStatus: number; // 0
translateStatus: number; // 0
voiceChangeType: number; // 0
voiceType: number; // 0
waveAmplitudes: number[];
}
export interface ArkElement {
bytesData: string;
linkInfo: null,
subElementType: null
}
export const IMAGE_HTTP_HOST = "https://gchat.qpic.cn"
export const IMAGE_HTTP_HOST_NT = "https://multimedia.nt.qq.com.cn"
export interface PicElement {
originImageUrl: string; // http url, 没有hosthost是https://gchat.qpic.cn/, 带download参数的是https://multimedia.nt.qq.com.cn
originImageMd5?: string;
sourcePath: string; // 图片本地路径
thumbPath: Map<number, string>;
picWidth: number;
picHeight: number;
fileSize: number;
fileName: string;
fileUuid: string;
md5HexStr?: string;
}
export enum GrayTipElementSubType {
INVITE_NEW_MEMBER = 12,
MEMBER_NEW_TITLE = 17
}
export interface GrayTipElement {
subElementType: GrayTipElementSubType;
revokeElement: {
operatorRole: string;
operatorUid: string;
operatorNick: string;
operatorRemark: string;
operatorMemRemark?: string;
wording: string; // 自定义的撤回提示语
}
aioOpGrayTipElement: TipAioOpGrayTipElement,
groupElement: TipGroupElement,
xmlElement: {
content: string;
},
jsonGrayTipElement: {
jsonStr: string;
}
}
export interface FaceElement {
faceIndex: number,
faceType: 1
}
export interface MarketFaceElement {
"itemType": 6,
"faceInfo": 1,
"emojiPackageId": 203875,
"subType": 3,
"mediaType": 0,
"imageWidth": 200,
"imageHeight": 200,
"faceName": string,
"emojiId": "094d53bd1c9ac5d35d04b08e8a6c992c",
"key": "a8b1dd0aebc8d910",
"param": null,
"mobileParam": null,
"sourceType": null,
"startTime": null,
"endTime": null,
"emojiType": 1,
"hasIpProduct": null,
"voiceItemHeightArr": null,
"sourceName": null,
"sourceJumpUrl": null,
"sourceTypeName": null,
"backColor": null,
"volumeColor": null,
"staticFacePath": "E:\\SystemDocuments\\QQ\\721011692\\nt_qq\\nt_data\\Emoji\\marketface\\203875\\094d53bd1c9ac5d35d04b08e8a6c992c_aio.png",
"dynamicFacePath": "E:\\SystemDocuments\\QQ\\721011692\\nt_qq\\nt_data\\Emoji\\marketface\\203875\\094d53bd1c9ac5d35d04b08e8a6c992c",
"supportSize": [
{
"width": 300,
"height": 300
},
{
"width": 200,
"height": 200
}
],
"apngSupportSize": null
}
export interface VideoElement {
"filePath": string,
"fileName": string,
"videoMd5"?: string,
"thumbMd5"?: string
"fileTime"?: number, // second
"thumbSize"?: number, // byte
"fileFormat"?: number, // 2表示mp4
"fileSize"?: string, // byte
"thumbWidth"?: number,
"thumbHeight"?: number,
"busiType"?: 0, // 未知
"subBusiType"?: 0, // 未知
"thumbPath"?: Map<number, any>,
"transferStatus"?: 0, // 未知
"progress"?: 0, // 下载进度?
"invalidState"?: 0, // 未知
"fileUuid"?: string, // 可以用于下载链接?
"fileSubId"?: "",
"fileBizId"?: null,
"originVideoMd5"?: "",
"import_rich_media_context"?: null,
"sourceVideoCodecFormat"?: number
}
export interface MarkdownElement {
content: string,
}
export interface InlineKeyboardElementRowButton{
"id": "",
"label": string,
"visitedLabel": string,
"style": 1, // 未知
"type": 2, // 未知
"clickLimit": 0, // 未知
"unsupportTips": "请升级新版手机QQ",
"data": string,
"atBotShowChannelList": false,
"permissionType": 2,
"specifyRoleIds": [],
"specifyTinyids": [],
"isReply": false,
"anchor": 0,
"enter": false,
"subscribeDataTemplateIds": []
}
export interface InlineKeyboardElement {
rows: [{
buttons: InlineKeyboardElementRowButton[]
}]
}
export interface TipAioOpGrayTipElement { // 这是什么提示来着?
operateType: number,
peerUid: string,
fromGrpCodeOfTmpChat: string,
}
export enum TipGroupElementType {
memberIncrease = 1,
kicked = 3, // 被移出群
ban = 8
}
export interface TipGroupElement {
"type": TipGroupElementType, // 1是表示有人加入群, 自己加入群也会收到这个
"role": 0, // 暂时不知
"groupName": string, // 暂时获取不到
"memberUid": string,
"memberNick": string,
"memberRemark": string,
"adminUid": string,
"adminNick": string,
"adminRemark": string,
"createGroup": null,
"memberAdd"?: {
"showType": 1,
"otherAdd": null,
"otherAddByOtherQRCode": null,
"otherAddByYourQRCode": null,
"youAddByOtherQRCode": null,
"otherInviteOther": null,
"otherInviteYou": null,
"youInviteOther": null
},
"shutUp"?: {
"curTime": string,
"duration": string, // 禁言时间,秒
"admin": {
"uid": string,
"card": string,
"name": string,
"role": GroupMemberRole
},
"member": {
"uid": string
"card": string,
"name": string,
"role": GroupMemberRole
}
}
}
export interface MultiForwardMsgElement{
xmlContent: string, // xml格式的消息内容
resId: string,
fileName: string,
}
export interface RawMessage {
msgId: string;
msgShortId?: number; // 自己维护的消息id
msgTime: string; // 时间戳,秒
msgSeq: string;
senderUid: string;
senderUin?: string; // 发送者QQ号
peerUid: string; // 群号 或者 QQ uid
peerUin: string; // 群号 或者 发送者QQ号
sendNickName: string;
sendMemberName?: string; // 发送者群名片
chatType: ChatType;
sendStatus?: number; // 消息状态别人发的2是已撤回自己发的2是已发送
recallTime: string; // 撤回时间, "0"是没有撤回
elements: {
elementId: string,
elementType: ElementType;
replyElement: {
senderUid: string; // 原消息发送者QQ号
sourceMsgIsIncPic: boolean; // 原消息是否有图片
sourceMsgText: string;
replayMsgSeq: string; // 源消息的msgSeq可以通过这个找到源消息的msgId
};
textElement: {
atType: AtType;
atUid: string; // QQ号
content: string;
atNtUid: string; // uid号
};
picElement: PicElement;
pttElement: PttElement;
arkElement: ArkElement;
grayTipElement: GrayTipElement;
faceElement: FaceElement;
videoElement: VideoElement;
fileElement: FileElement;
marketFaceElement: MarketFaceElement;
inlineKeyboardElement: InlineKeyboardElement;
markdownElement: MarkdownElement;
multiForwardMsgElement: MultiForwardMsgElement;
}[];
}

View File

@@ -0,0 +1,65 @@
export enum GroupNotifyTypes {
INVITE_ME = 1,
INVITED_JOIN = 4, // 有人接受了邀请入群
JOIN_REQUEST = 7,
ADMIN_SET = 8,
KICK_MEMBER = 9,
MEMBER_EXIT = 11, // 主动退出
ADMIN_UNSET = 12,
}
export interface GroupNotifies {
doubt: boolean,
nextStartSeq: string,
notifies: GroupNotify[],
}
export enum GroupNotifyStatus {
IGNORE = 0,
WAIT_HANDLE = 1,
APPROVE = 2,
REJECT = 3
}
export interface GroupNotify {
time: number; // 自己添加的字段,时间戳,毫秒, 用于判断收到短时间内收到重复的notify
seq: string, // 唯一标识符转成数字再除以1000应该就是时间戳
type: GroupNotifyTypes,
status: GroupNotifyStatus, // 0是已忽略1是未处理2是已同意
group: { groupCode: string, groupName: string },
user1: { uid: string, nickName: string }, // 被设置管理员的人
user2: { uid: string, nickName: string }, // 操作者
actionUser: { uid: string, nickName: string }, //未知
actionTime: string,
invitationExt: {
srcType: number, // 0?未知
groupCode: string, waitStatus: number
},
postscript: string, // 加群用户填写的验证信息
repeatSeqs: [],
warningTips: string
}
export enum GroupRequestOperateTypes {
approve = 1,
reject = 2
}
export interface FriendRequest {
friendUid: string,
reqTime: string, // 时间戳,秒
extWords: string, // 申请人填写的验证消息
isUnread: boolean,
friendNick: string,
sourceId: number,
groupCode: string
}
export interface FriendRequestNotify {
data: {
unreadNums: number,
buddyReqs: FriendRequest[]
}
}

75
src/ntqqapi/types/user.ts Normal file
View File

@@ -0,0 +1,75 @@
export enum Sex {
male = 0,
female = 2,
unknown = 255,
}
export interface QQLevel {
"crownNum": number,
"sunNum": number,
"moonNum": number,
"starNum": number
}
export interface User {
uid: string; // 加密的字符串
uin: string; // QQ号
nick: string;
avatarUrl?: string;
longNick?: string; // 签名
remark?: string;
sex?: Sex;
qqLevel?: QQLevel,
qid?: string
"birthday_year"?: number,
"birthday_month"?: number,
"birthday_day"?: number,
"topTime"?: string,
"constellation"?: number,
"shengXiao"?: number,
"kBloodType"?: number,
"homeTown"?: string, //"0-0-0",
"makeFriendCareer"?: number,
"pos"?: string,
"eMail"?: string
"phoneNum"?: string,
"college"?: string,
"country"?: string,
"province"?: string,
"city"?: string,
"postCode"?: string,
"address"?: string,
"isBlock"?: boolean,
"isSpecialCareOpen"?: boolean,
"isSpecialCareZone"?: boolean,
"ringId"?: string,
"regTime"?: number,
interest?: string,
"labels"?: string[],
"isHideQQLevel"?: number,
"privilegeIcon"?: {
"jumpUrl": string,
"openIconList": unknown[],
"closeIconList": unknown[]
},
"photoWall"?: {
"picList": unknown[]
},
"vipFlag"?: boolean,
"yearVipFlag"?: boolean,
"svipFlag"?: boolean,
"vipLevel"?: number,
"status"?: number,
"qidianMasterFlag"?: number,
"qidianCrewFlag"?: number,
"qidianCrewFlag2"?: number,
"extStatus"?: number,
"recommendImgFlag"?: number,
"disableEmojiShortCuts"?: number,
"pendantId"?: string,
}
export interface SelfInfo extends User {
online?: boolean;
}
export interface Friend extends User {}

View File

@@ -1,9 +1,12 @@
import {ActionName, BaseCheckResult} from "./types"
import {OB11Response} from "./utils"
import {OB11Response} from "./OB11Response"
import {OB11Return} from "../types";
import {log} from "../../common/utils/log";
class BaseAction<PayloadType, ReturnDataType> {
actionName: ActionName
protected async check(payload: PayloadType): Promise<BaseCheckResult> {
return {
valid: true,
@@ -19,7 +22,8 @@ class BaseAction<PayloadType, ReturnDataType> {
const resData = await this._handle(payload);
return OB11Response.ok(resData);
} catch (e) {
return OB11Response.error(e.toString(), 200);
log("发生错误", e)
return OB11Response.error(e?.toString() || e?.stack?.toString() || "未知错误,可能操作超时", 200);
}
}
@@ -32,7 +36,8 @@ class BaseAction<PayloadType, ReturnDataType> {
const resData = await this._handle(payload)
return OB11Response.ok(resData, echo);
} catch (e) {
return OB11Response.error(e.toString(), 1200, echo)
log("发生错误", e)
return OB11Response.error(e.stack?.toString() || e.toString(), 1200, echo)
}
}

View File

@@ -1,10 +0,0 @@
import {ActionName} from "./types";
import CanSendRecord from "./CanSendRecord";
interface ReturnType{
yes: boolean
}
export default class CanSendImage extends CanSendRecord{
actionName = ActionName.CanSendImage
}

View File

@@ -1,28 +0,0 @@
import BaseAction from "./BaseAction";
import {NTQQApi} from "../../ntqqapi/ntcall";
import {friends} from "../../common/data";
import {ActionName} from "./types";
import {log} from "../../common/utils";
interface Payload{
method: string,
args: any[],
}
export default class Debug extends BaseAction<Payload, any>{
actionName = ActionName.Debug
protected async _handle(payload: Payload): Promise<any> {
log("debug call ntqq api", payload);
const method = NTQQApi[payload.method]
if (!method){
throw `${method} 不存在`
}
const result = method(...payload.args);
if (method.constructor.name === "AsyncFunction"){
return await result
}
return result
// const info = await NTQQApi.getUserDetailInfo(friends[0].uid);
// return info
}
}

View File

@@ -1,22 +0,0 @@
import {ActionName} from "./types";
import BaseAction from "./BaseAction";
import {NTQQApi} from "../../ntqqapi/ntcall";
import {getHistoryMsgByShortId} from "../../common/data";
interface Payload {
message_id: number
}
class DeleteMsg extends BaseAction<Payload, void> {
actionName = ActionName.DeleteMsg
protected async _handle(payload:Payload){
let msg = getHistoryMsgByShortId(payload.message_id)
await NTQQApi.recallMsg({
chatType: msg.chatType,
peerUid: msg.peerUid
}, [msg.msgId])
}
}
export default DeleteMsg

View File

@@ -1,16 +0,0 @@
import {OB11User} from '../types';
import {OB11Constructor} from "../constructor";
import {friends} from "../../common/data";
import BaseAction from "./BaseAction";
import {ActionName} from "./types";
class GetFriendList extends BaseAction<null, OB11User[]> {
actionName = ActionName.GetFriendList
protected async _handle(payload: null){
return OB11Constructor.friends(friends);
}
}
export default GetFriendList

View File

@@ -1,16 +0,0 @@
import {OB11Group} from '../types';
import {OB11Constructor} from "../constructor";
import {groups} from "../../common/data";
import BaseAction from "./BaseAction";
import {ActionName} from "./types";
class GetGroupList extends BaseAction<null, OB11Group[]> {
actionName = ActionName.GetGroupList
protected async _handle(payload: null){
return OB11Constructor.groups(groups);
}
}
export default GetGroupList

View File

@@ -1,27 +0,0 @@
import {OB11GroupMember} from '../types';
import {getGroupMember} from "../../common/data";
import {OB11Constructor} from "../constructor";
import BaseAction from "./BaseAction";
import {ActionName} from "./types";
export interface PayloadType {
group_id: number
user_id: number
}
class GetGroupMemberInfo extends BaseAction<PayloadType, OB11GroupMember> {
actionName = ActionName.GetGroupMemberInfo
protected async _handle(payload: PayloadType){
const member = await getGroupMember(payload.group_id.toString(), payload.user_id.toString())
if (member) {
return OB11Constructor.groupMember(payload.group_id.toString(), member)
}
else {
throw(`群成员${payload.user_id}不存在`)
}
}
}
export default GetGroupMemberInfo

View File

@@ -1,16 +0,0 @@
import {OB11User} from '../types';
import {OB11Constructor} from "../constructor";
import {selfInfo} from "../../common/data";
import BaseAction from "./BaseAction";
import {ActionName} from "./types";
class GetLoginInfo extends BaseAction<null, OB11User> {
actionName = ActionName.GetLoginInfo
protected async _handle(payload: null){
return OB11Constructor.selfInfo(selfInfo);
}
}
export default GetLoginInfo

View File

@@ -1,32 +0,0 @@
import {getHistoryMsgByShortId} from "../../common/data";
import {OB11Message} from '../types';
import {OB11Constructor} from "../constructor";
import BaseAction from "./BaseAction";
import {ActionName} from "./types";
export interface PayloadType {
message_id: number
}
export type ReturnDataType = OB11Message
class GetMsg extends BaseAction<PayloadType, OB11Message> {
actionName = ActionName.GetMsg
protected async _handle(payload: PayloadType){
// log("history msg ids", Object.keys(msgHistory));
if (!payload.message_id){
throw("参数message_id不能为空")
}
const msg = getHistoryMsgByShortId(payload.message_id)
if (msg) {
const msgData = await OB11Constructor.message(msg);
return msgData
} else {
throw("消息不存在")
}
}
}
export default GetMsg

View File

@@ -1,5 +1,6 @@
import {OB11Return} from '../types';
import {isNull} from '../../common/utils';
import {isNull} from "../../common/utils/helper";
export class OB11Response {
static res<T>(data: T, status: string, retcode: number, message: string = ""): OB11Return<T> {

View File

@@ -1,9 +0,0 @@
import SendMsg from "./SendMsg";
import {ActionName} from "./types";
class SendGroupMsg extends SendMsg{
actionName = ActionName.SendGroupMsg
}
export default SendGroupMsg

View File

@@ -1,31 +0,0 @@
import BaseAction from "./BaseAction";
import {getFriend} from "../../common/data";
import {NTQQApi} from "../../ntqqapi/ntcall";
import {ActionName} from "./types";
import { log } from "../../common/utils";
interface Payload {
user_id: number,
times: number
}
export default class SendLike extends BaseAction<Payload, null> {
actionName = ActionName.SendLike
protected async _handle(payload: Payload): Promise<null> {
const qq = payload.user_id.toString();
const friend = await getFriend(qq)
if (!friend) {
throw (`点赞失败,${qq}不是好友`)
}
try {
let result = await NTQQApi.likeFriend(friend.uid, parseInt(payload.times.toString()) || 1);
if (result.result !== 0){
throw result.errMsg
}
} catch (e) {
throw `点赞失败 ${e}`
}
return null
}
}

View File

@@ -1,277 +0,0 @@
import {AtType, ChatType, Group, SendMessageElement} from "../../ntqqapi/types";
import {addHistoryMsg, friends, getGroup, getHistoryMsgByShortId, getUidByUin, selfInfo,} from "../../common/data";
import {OB11MessageData, OB11MessageDataType, OB11MessageMixType, OB11MessageNode, OB11PostSendMsg} from '../types';
import {NTQQApi, Peer} from "../../ntqqapi/ntcall";
import {SendMsgElementConstructor} from "../../ntqqapi/constructor";
import {uri2local} from "../utils";
import BaseAction from "./BaseAction";
import {ActionName, BaseCheckResult} from "./types";
import * as fs from "fs";
import {log} from "../../common/utils";
import {v4 as uuidv4} from "uuid"
import {decodeCQCode} from "../cqcode";
import {Send} from "express";
function checkSendMessage(sendMsgList: OB11MessageData[]) {
function checkUri(uri: string): boolean {
const pattern = /^(file:\/\/|http:\/\/|https:\/\/|base64:\/\/)/;
return pattern.test(uri);
}
for (let msg of sendMsgList) {
if (msg["type"] && msg["data"]) {
let type = msg["type"];
let data = msg["data"];
if (type === "text" && !data["text"]) {
return 400;
} else if (["image", "voice", "record"].includes(type)) {
if (!data["file"]) {
return 400;
} else {
if (checkUri(data["file"])) {
return 200;
} else {
return 400;
}
}
} else if (type === "at" && !data["qq"]) {
return 400;
} else if (type === "reply" && !data["id"]) {
return 400;
}
} else {
return 400
}
}
return 200;
}
export interface ReturnDataType {
message_id: number
}
export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
actionName = ActionName.SendMsg
protected async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> {
const messages = this.convertMessage2List(payload.message);
const fmNum = this.forwardMsgNum(payload)
if (fmNum && fmNum != messages.length) {
return {
valid: false,
message: "转发消息不能和普通消息混在一起发送,转发需要保证message只有type为node的元素"
}
}
return {
valid: true,
}
}
protected async _handle(payload: OB11PostSendMsg) {
const peer: Peer = {
chatType: ChatType.friend,
peerUid: ""
}
let group: Group | undefined = undefined;
if (payload?.group_id) {
group = await getGroup(payload.group_id.toString())
if (!group) {
throw (`${payload.group_id}不存在`)
}
peer.chatType = ChatType.group
// peer.name = group.name
peer.peerUid = group.groupCode
} else if (payload?.user_id) {
const friend = friends.find(f => f.uin == payload.user_id.toString())
if (friend) {
// peer.name = friend.nickName
peer.peerUid = friend.uid
} else {
peer.chatType = ChatType.temp
const tempUserUid = getUidByUin(payload.user_id.toString())
if (!tempUserUid) {
throw (`找不到私聊对象${payload.user_id}`)
}
// peer.name = tempUser.nickName
peer.peerUid = tempUserUid;
}
}
const messages = this.convertMessage2List(payload.message);
if (this.forwardMsgNum(payload)) {
try {
const returnMsg = await this.handleForwardNode(peer, messages as OB11MessageNode[], group)
return {message_id: returnMsg.msgShortId}
} catch (e) {
throw ("发送转发消息失败 " + e.toString())
}
}
// log("send msg:", peer, sendElements)
const {sendElements, deleteAfterSentFiles} = await this.createSendElements(messages, group)
try {
const returnMsg = await this.send(peer, sendElements, deleteAfterSentFiles)
return {message_id: returnMsg.msgShortId}
} catch (e) {
throw (e.toString())
}
}
protected convertMessage2List(message: OB11MessageMixType) {
if (typeof message === "string") {
// message = [{
// type: OB11MessageDataType.text,
// data: {
// text: message
// }
// }] as OB11MessageData[]
message = decodeCQCode(message.toString())
} else if (!Array.isArray(message)) {
message = [message]
}
return message;
}
private forwardMsgNum(payload: OB11PostSendMsg): number {
if (Array.isArray(payload.message)) {
return payload.message.filter(msg => msg.type == OB11MessageDataType.node).length
}
return 0
}
// 返回一个合并转发的消息id
private async handleForwardNode(destPeer: Peer, messageNodes: OB11MessageNode[], group: Group | undefined) {
const selfPeer: Peer = {
chatType: ChatType.friend,
peerUid: selfInfo.uid
}
let nodeIds: string[] = []
for (const messageNode of messageNodes) {
// 一个node表示一个人的消息
let nodeId = messageNode.data.id;
// 有nodeId表示一个子转发消息卡片
if (nodeId) {
let nodeMsg = getHistoryMsgByShortId(nodeId);
if (nodeMsg) {
nodeIds.push(nodeMsg.msgId);
}
} else {
// 自定义的消息
// 提取消息段发给自己生成消息id
try {
const {
sendElements,
deleteAfterSentFiles
} = await this.createSendElements(this.convertMessage2List(messageNode.data.content), group);
log("开始生成转发节点", sendElements);
const nodeMsg = await this.send(selfPeer, sendElements, deleteAfterSentFiles, true);
nodeIds.push(nodeMsg.msgId)
log("转发节点生成成功", nodeMsg.msgId);
} catch (e) {
log("生效转发消息节点失败", e)
}
}
}
// 开发转发
try {
return await NTQQApi.multiForwardMsg(selfPeer, destPeer, nodeIds)
} catch (e) {
log("forward failed", e)
return null;
}
}
private async createSendElements(messageData: OB11MessageData[], group: Group | undefined, ignoreTypes: OB11MessageDataType[] = []) {
let sendElements: SendMessageElement[] = []
let deleteAfterSentFiles: string[] = []
for (let sendMsg of messageData) {
if (ignoreTypes.includes(sendMsg.type)) {
continue
}
switch (sendMsg.type) {
case OB11MessageDataType.text: {
const text = sendMsg.data?.text;
if (text) {
sendElements.push(SendMsgElementConstructor.text(sendMsg.data!.text))
}
}
break;
case OB11MessageDataType.at: {
let atQQ = sendMsg.data?.qq;
if (atQQ) {
atQQ = atQQ.toString()
if (atQQ === "all") {
sendElements.push(SendMsgElementConstructor.at(atQQ, atQQ, AtType.atAll, "全体成员"))
} else {
const atMember = group?.members.find(m => m.uin == 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) {
replyMsgId = replyMsgId.toString()
const replyMsg = getHistoryMsgByShortId(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.image:
case OB11MessageDataType.file:
case OB11MessageDataType.video:
case OB11MessageDataType.voice: {
const file = sendMsg.data?.file
if (file) {
const {path, isLocal} = (await uri2local(file))
if (path) {
if (!isLocal) { // 只删除http和base64转过来的文件
deleteAfterSentFiles.push(path)
}
const constructorMap = {
[OB11MessageDataType.image]: SendMsgElementConstructor.pic,
[OB11MessageDataType.voice]: SendMsgElementConstructor.ptt,
[OB11MessageDataType.video]: SendMsgElementConstructor.video,
[OB11MessageDataType.file]: SendMsgElementConstructor.file,
}
sendElements.push(await constructorMap[sendMsg.type](path));
}
}
}
break;
}
}
return {
sendElements,
deleteAfterSentFiles
}
}
private async send(peer: Peer, sendElements: SendMessageElement[], deleteAfterSentFiles: string[], waitComplete = false) {
if (!sendElements.length) {
throw ("消息体无法解析")
}
const returnMsg = await NTQQApi.sendMsg(peer, sendElements, waitComplete, 20000);
addHistoryMsg(returnMsg)
deleteAfterSentFiles.map(f => fs.unlink(f, () => {
}))
return returnMsg
}
}
export default SendMsg

View File

@@ -1,8 +0,0 @@
import SendMsg from "./SendMsg";
import {ActionName} from "./types";
class SendPrivateMsg extends SendMsg {
actionName = ActionName.SendPrivateMsg
}
export default SendPrivateMsg

View File

@@ -1,30 +0,0 @@
import BaseAction from "./BaseAction";
import {groupNotifies} from "../../common/data";
import {GroupNotify, GroupRequestOperateTypes} from "../../ntqqapi/types";
import {NTQQApi} from "../../ntqqapi/ntcall";
import {ActionName} from "./types";
interface Payload{
flag: string,
// sub_type: "add" | "invite",
// type: "add" | "invite"
approve: boolean,
reason: string
}
export default class SetGroupAddRequest extends BaseAction<Payload, null>{
actionName = ActionName.SetGroupAddRequest
protected async _handle(payload: Payload): Promise<null> {
const seq = payload.flag.toString();
const notify: GroupNotify = groupNotifies[seq]
try{
await NTQQApi.handleGroupRequest(seq,
payload.approve ? GroupRequestOperateTypes.approve: GroupRequestOperateTypes.reject,
payload.reason
)
}catch (e) {
throw e
}
return null
}
}

View File

@@ -1,23 +0,0 @@
import BaseAction from "./BaseAction";
import {NTQQApi} from "../../ntqqapi/ntcall";
import {getGroupMember} from "../../common/data";
import {GroupMemberRole} from "../../ntqqapi/types";
import {ActionName} from "./types";
interface Payload{
group_id: number,
user_id: number,
enable: boolean
}
export default class SetGroupAdmin extends BaseAction<Payload, null>{
actionName = ActionName.SetGroupAdmin
protected async _handle(payload: Payload): Promise<null> {
const member = await getGroupMember(payload.group_id, payload.user_id)
if(!member){
throw `群成员${payload.user_id}不存在`
}
await NTQQApi.setMemberRole(payload.group_id.toString(), member.uid, payload.enable ? GroupMemberRole.admin : GroupMemberRole.normal)
return null
}
}

View File

@@ -1,22 +0,0 @@
import BaseAction from "./BaseAction";
import {NTQQApi} from "../../ntqqapi/ntcall";
import {log} from "../../common/utils";
import {ActionName} from "./types";
interface Payload{
group_id: number,
is_dismiss: boolean
}
export default class SetGroupLeave extends BaseAction<Payload, any>{
actionName = ActionName.SetGroupLeave
protected async _handle(payload: Payload): Promise<any> {
try{
await NTQQApi.quitGroup(payload.group_id.toString())
}
catch (e) {
log("退群失败", e)
throw e
}
}
}

View File

@@ -0,0 +1,110 @@
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 {ActionName} from "../types";
import {FileElement, RawMessage, VideoElement} from "../../../ntqqapi/types";
export interface GetFilePayload {
file: string // 文件名或者fileUuid
}
export interface GetFileResponse {
file?: string // path
url?: string
file_size?: string
file_name?: string
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)
if (!element){
element = msg.elements.find(e=>e.videoElement)
return {id: element.elementId, element: element.videoElement}
}
return {id: element.elementId, element: element.fileElement}
}
protected async _handle(payload: GetFilePayload): Promise<GetFileResponse> {
const cache = await dbUtil.getFileCache(payload.file)
const {autoDeleteFile, enableLocalFile2Url, autoDeleteFileSecond} = getConfigUtil().getConfig()
if (!cache) {
throw new Error('file not found')
}
if (cache.downloadFunc) {
await cache.downloadFunc()
}
try {
await fs.access(cache.filePath, fs.constants.F_OK)
} catch (e) {
log("file not found", e)
if (cache.url){
const downloadResult = await uri2local(cache.url)
if (downloadResult.success) {
cache.filePath = downloadResult.path
dbUtil.addFileCache(payload.file, cache).then()
} else {
throw new Error("file download failed. " + downloadResult.errMsg)
}
}
else{
// 没有url的可能是私聊文件或者群文件需要自己下载
log("需要调用 NTQQ 下载文件api")
if (cache.msgId) {
let msg = await dbUtil.getMsgByLongId(cache.msgId)
if (msg){
log("找到了文件 msg", msg)
let element = this.getElement(msg);
log("找到了文件 element", element);
// 构建下载函数
await NTQQFileApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid,
element.id, "", "", true)
await sleep(1000);
msg = await dbUtil.getMsgByLongId(cache.msgId)
log("下载完成后的msg", msg)
cache.filePath = this.getElement(msg).element.filePath
dbUtil.addFileCache(payload.file, cache).then()
}
}
}
}
let res: GetFileResponse = {
file: cache.filePath,
url: cache.url,
file_size: cache.fileSize,
file_name: cache.fileName
}
if (enableLocalFile2Url) {
if (!cache.url) {
try{
res.base64 = await fs.readFile(cache.filePath, 'base64')
}catch (e) {
throw new Error("文件下载失败. " + e)
}
}
}
// if (autoDeleteFile) {
// setTimeout(() => {
// fs.unlink(cache.filePath)
// }, autoDeleteFileSecond * 1000)
// }
return res
}
}
export default class GetFile extends GetFileBase {
actionName = ActionName.GetFile
protected async _handle(payload: {file_id: string, file: string}): Promise<GetFileResponse> {
if (!payload.file_id) {
throw new Error('file_id 不能为空')
}
payload.file = payload.file_id
return super._handle(payload);
}
}

View File

@@ -0,0 +1,7 @@
import {GetFileBase} from "./GetFile";
import {ActionName} from "../types";
export default class GetImage extends GetFileBase {
actionName = ActionName.GetImage
}

View File

@@ -0,0 +1,15 @@
import {GetFileBase, GetFilePayload, GetFileResponse} from "./GetFile";
import {ActionName} from "../types";
interface Payload extends GetFilePayload {
out_format: 'mp3' | 'amr' | 'wma' | 'm4a' | 'spx' | 'ogg' | 'wav' | 'flac'
}
export default class GetRecord extends GetFileBase {
actionName = ActionName.GetRecord
protected async _handle(payload: Payload): Promise<GetFileResponse> {
let res = super._handle(payload);
return res;
}
}

View File

@@ -0,0 +1,73 @@
import BaseAction from "../BaseAction";
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";
interface Payload {
thread_count?: number
url?: string
base64?: string
name?: string
headers?: string | string[]
}
interface FileResponse {
file: string
}
export default class GoCQHTTPDownloadFile extends BaseAction<Payload, FileResponse> {
actionName = ActionName.GoCQHTTP_DownloadFile
protected async _handle(payload: Payload): Promise<FileResponse> {
const isRandomName = !payload.name
let name = payload.name || uuid4();
const filePath = joinPath(TEMP_DIR, name);
if (payload.base64) {
fs.writeFileSync(filePath, payload.base64, 'base64')
} else if (payload.url) {
const headers = this.getHeaders(payload.headers);
let buffer = await httpDownload({url: payload.url, headers: headers})
fs.writeFileSync(filePath, Buffer.from(buffer), 'binary');
} else {
throw new Error("不存在任何文件, 无法下载")
}
if (fs.existsSync(filePath)) {
if (isRandomName) {
// 默认实现要名称未填写时文件名为文件 md5
const md5 = await calculateFileMD5(filePath);
const newPath = joinPath(TEMP_DIR, md5);
fs.renameSync(filePath, newPath);
return { file: newPath }
}
return { file: filePath }
} else {
throw new Error("文件写入失败, 检查权限")
}
}
getHeaders(headersIn?: string | string[]): Record<string, string> {
const headers = {};
if (typeof headersIn == 'string') {
headersIn = headersIn.split('[\\r\\n]');
}
if (Array.isArray(headersIn)) {
for (const headerItem of headersIn) {
const spilt = headerItem.indexOf('=');
if (spilt < 0) {
headers[headerItem] = "";
} else {
const key = headerItem.substring(0, spilt);
headers[key] = headerItem.substring(0, spilt + 1);
}
}
}
if (!headers['Content-Type']) {
headers['Content-Type'] = 'application/octet-stream';
}
return headers;
}
}

View File

@@ -0,0 +1,39 @@
import BaseAction from "../BaseAction";
import {OB11ForwardMessage, OB11Message, OB11MessageData} from "../../types";
import {NTQQMsgApi, Peer} from "../../../ntqqapi/api";
import {dbUtil} from "../../../common/db";
import {OB11Constructor} from "../../constructor";
import {ActionName} from "../types";
interface Payload {
message_id: string; // long msg id
}
interface Response{
messages: (OB11Message & {content: OB11MessageData})[]
}
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)
if (!rootMsg){
throw Error("msg not found")
}
let data = await NTQQMsgApi.getMultiMsg({chatType: rootMsg.chatType, peerUid: rootMsg.peerUid}, rootMsg.msgId, rootMsg.msgId)
if (data.result !== 0){
throw Error("找不到相关的聊天记录" + data.errMsg)
}
let msgList = data.msgList
let messages = await Promise.all(msgList.map(async msg => {
let resMsg = await OB11Constructor.message(msg)
resMsg.message_id = await dbUtil.addMsg(msg);
return resMsg
}))
messages.map(msg => {
(<OB11ForwardMessage>msg).content = msg.message;
delete msg.message;
})
return {messages}
}
}

View File

@@ -0,0 +1,39 @@
import BaseAction from "../BaseAction";
import {OB11Message, OB11User} from "../../types";
import {groups} from "../../../common/data";
import {ActionName} from "../types";
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
message_seq: number,
count: number
}
interface Response{
messages: OB11Message[]
}
export default class GoCQHTTPGetGroupMsgHistory extends BaseAction<Payload, Response> {
actionName = ActionName.GoCQHTTP_GetGroupMsgHistory
protected async _handle(payload: Payload): Promise<Response> {
const group = groups.find(group => group.groupCode === payload.group_id.toString())
if (!group) {
throw `${payload.group_id}不存在`
}
const startMsgId = (await dbUtil.getMsgByShortId(payload.message_seq))?.msgId || "0"
// log("startMsgId", startMsgId)
let msgList = (await NTQQMsgApi.getMsgHistory({chatType: ChatType.group, peerUid: group.groupCode}, startMsgId, parseInt(payload.count?.toString()) || 20)).msgList
await Promise.all(msgList.map(async msg => {
msg.msgShortId = await dbUtil.addMsg(msg)
}))
const ob11MsgList = await Promise.all(msgList.map(msg=>OB11Constructor.message(msg)))
return {"messages": ob11MsgList}
}
}

View File

@@ -1,24 +1,20 @@
import BaseAction from "../BaseAction";
import {OB11GroupMember, OB11User} from "../../types";
import {friends, getFriend, getGroupMember, groups} from "../../../common/data";
import {OB11User} from "../../types";
import {getUidByUin, uidMaps} from "../../../common/data";
import {OB11Constructor} from "../../constructor";
import {ActionName} from "../types";
import {NTQQUserApi} from "../../../ntqqapi/api/user";
export default class GoCQHTTPGetStrangerInfo extends BaseAction<{user_id: number}, OB11User>{
export default class GoCQHTTPGetStrangerInfo extends BaseAction<{ user_id: number }, OB11User> {
actionName = ActionName.GoCQHTTP_GetStrangerInfo
protected async _handle(payload: { user_id: number }): Promise<OB11User> {
const user_id = payload.user_id.toString()
const friend = await getFriend(user_id)
if (friend){
return OB11Constructor.friend(friend);
const uid = getUidByUin(user_id)
if (!uid) {
throw new Error("查无此人")
}
for(const group of groups){
const member = await getGroupMember(group.groupCode, user_id)
if (member){
return OB11Constructor.groupMember(group.groupCode, member) as OB11User
}
}
throw ("查无此人")
return OB11Constructor.stranger(await NTQQUserApi.getUserDetailInfo(uid, true))
}
}

View File

@@ -1,15 +1,20 @@
import SendMsg, {ReturnDataType} from "../SendMsg";
import {OB11MessageMixType, OB11PostSendMsg} from "../../types";
import {ActionName, BaseCheckResult} from "../types";
import SendMsg, {convertMessage2List} from "../msg/SendMsg";
import {OB11PostSendMsg} from "../../types";
import {ActionName} from "../types";
export class GoCQHTTPSendGroupForwardMsg extends SendMsg{
actionName = ActionName.GoCQHTTP_SendGroupForwardMsg;
protected async check(payload: OB11PostSendMsg){
payload.message = this.convertMessage2List(payload.messages);
export class GoCQHTTPSendForwardMsg extends SendMsg {
actionName = ActionName.GoCQHTTP_SendForwardMsg;
protected async check(payload: OB11PostSendMsg) {
if (payload.messages) payload.message = convertMessage2List(payload.messages);
return super.check(payload);
}
}
export class GoCQHTTPSendPrivateForwardMsg extends GoCQHTTPSendGroupForwardMsg{
export class GoCQHTTPSendPrivateForwardMsg extends GoCQHTTPSendForwardMsg {
actionName = ActionName.GoCQHTTP_SendPrivateForwardMsg;
}
export class GoCQHTTPSendGroupForwardMsg extends GoCQHTTPSendForwardMsg {
actionName = ActionName.GoCQHTTP_SendGroupForwardMsg;
}

View File

@@ -0,0 +1,37 @@
import BaseAction from "../BaseAction";
import {getGroup} from "../../../common/data";
import {ActionName} from "../types";
import {SendMsgElementConstructor} from "../../../ntqqapi/constructor";
import {ChatType, SendFileElement} from "../../../ntqqapi/types";
import fs from "fs";
import {NTQQMsgApi} from "../../../ntqqapi/api/msg";
import {uri2local} from "../../../common/utils";
interface Payload{
group_id: number
file: string
name: string
folder: string
}
export default class GoCQHTTPUploadGroupFile extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_UploadGroupFile
protected async _handle(payload: Payload): Promise<null> {
const group = await getGroup(payload.group_id.toString());
if (!group){
throw new Error(`群组${payload.group_id}不存在`)
}
let file = payload.file;
if (fs.existsSync(file)){
file = `file://${file}`
}
const downloadResult = await uri2local(file);
if (downloadResult.errMsg){
throw new Error(downloadResult.errMsg)
}
let sendFileEle: SendFileElement = await SendMsgElementConstructor.file(downloadResult.path, payload.name);
await NTQQMsgApi.sendMsg({chatType: ChatType.group, peerUid: group.groupCode}, [sendFileEle]);
return null
}
}

View File

@@ -1,8 +1,8 @@
import {OB11Group} from '../types';
import {getGroup} from "../../common/data";
import {OB11Constructor} from "../constructor";
import BaseAction from "./BaseAction";
import {ActionName} from "./types";
import {OB11Group} from '../../types';
import {getGroup} from "../../../common/data";
import {OB11Constructor} from "../../constructor";
import BaseAction from "../BaseAction";
import {ActionName} from "../types";
interface PayloadType {
group_id: number

View File

@@ -0,0 +1,22 @@
import {OB11Group} from '../../types';
import {OB11Constructor} from "../../constructor";
import {groups} from "../../../common/data";
import BaseAction from "../BaseAction";
import {ActionName} from "../types";
import {NTQQGroupApi} from "../../../ntqqapi/api";
import {log} from "../../../common/utils";
class GetGroupList extends BaseAction<null, OB11Group[]> {
actionName = ActionName.GetGroupList
protected async _handle(payload: null) {
// if (groups.length === 0) {
// const groups = await NTQQGroupApi.getGroups(true)
// log("get groups", groups)
// }
return OB11Constructor.groups(groups);
}
}
export default GetGroupList

View File

@@ -0,0 +1,35 @@
import {OB11GroupMember} from '../../types';
import {getGroupMember} from "../../../common/data";
import {OB11Constructor} from "../../constructor";
import BaseAction from "../BaseAction";
import {ActionName} from "../types";
import {NTQQUserApi} from "../../../ntqqapi/api/user";
import {log} from "../../../common/utils/log";
import {isNull} from "../../../common/utils/helper";
export interface PayloadType {
group_id: number
user_id: number
}
class GetGroupMemberInfo extends BaseAction<PayloadType, OB11GroupMember> {
actionName = ActionName.GetGroupMemberInfo
protected async _handle(payload: PayloadType) {
const member = await getGroupMember(payload.group_id.toString(), payload.user_id.toString())
if (member) {
if (isNull(member.sex)){
log("获取群成员详细信息")
let info = (await NTQQUserApi.getUserDetailInfo(member.uid, true))
log("群成员详细信息结果", info)
Object.assign(member, info);
}
return OB11Constructor.groupMember(payload.group_id.toString(), member)
} else {
throw (`群成员${payload.user_id}不存在`)
}
}
}
export default GetGroupMemberInfo

View File

@@ -1,9 +1,9 @@
import {OB11GroupMember} from '../types';
import {getGroup} from "../../common/data";
import {NTQQApi} from "../../ntqqapi/ntcall";
import {OB11Constructor} from "../constructor";
import BaseAction from "./BaseAction";
import {ActionName} from "./types";
import {OB11GroupMember} from '../../types';
import {getGroup} from "../../../common/data";
import {OB11Constructor} from "../../constructor";
import BaseAction from "../BaseAction";
import {ActionName} from "../types";
import {NTQQGroupApi} from "../../../ntqqapi/api/group";
export interface PayloadType {
group_id: number
@@ -13,15 +13,14 @@ export interface PayloadType {
class GetGroupMemberList extends BaseAction<PayloadType, OB11GroupMember[]> {
actionName = ActionName.GetGroupMemberList
protected async _handle(payload: PayloadType){
protected async _handle(payload: PayloadType) {
const group = await getGroup(payload.group_id.toString());
if (group) {
if (!group.members?.length) {
group.members = await NTQQApi.getGroupMembers(payload.group_id.toString())
group.members = await NTQQGroupApi.getGroupMembers(payload.group_id.toString())
}
return OB11Constructor.groupMembers(group);
}
else {
} else {
throw (`${payload.group_id}不存在`)
}
}

View File

@@ -1,8 +1,9 @@
import BaseAction from "./BaseAction";
import {ActionName} from "./types";
import BaseAction from "../BaseAction";
import {ActionName} from "../types";
export default class GetGuildList extends BaseAction<null, null>{
export default class GetGuildList extends BaseAction<null, null> {
actionName = ActionName.GetGuildList
protected async _handle(payload: null): Promise<null> {
return null;
}

View File

@@ -0,0 +1,18 @@
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;
payload.message_type = "group"
return super.check(payload);
}
}
export default SendGroupMsg

View File

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

View File

@@ -0,0 +1,25 @@
import BaseAction from "../BaseAction";
import {getGroupMember} from "../../../common/data";
import {GroupMemberRole} from "../../../ntqqapi/types";
import {ActionName} from "../types";
import {NTQQGroupApi} from "../../../ntqqapi/api/group";
interface Payload {
group_id: number,
user_id: number,
enable: boolean
}
export default class SetGroupAdmin extends BaseAction<Payload, null> {
actionName = ActionName.SetGroupAdmin
protected async _handle(payload: Payload): Promise<null> {
const member = await getGroupMember(payload.group_id, payload.user_id)
const enable = payload.enable.toString() === "true"
if (!member) {
throw `群成员${payload.user_id}不存在`
}
await NTQQGroupApi.setMemberRole(payload.group_id.toString(), member.uid, enable ? GroupMemberRole.admin : GroupMemberRole.normal)
return null
}
}

View File

@@ -1,23 +1,24 @@
import BaseAction from "./BaseAction";
import {NTQQApi} from "../../ntqqapi/ntcall";
import {getGroupMember} from "../../common/data";
import {ActionName} from "./types";
import BaseAction from "../BaseAction";
import {getGroupMember} from "../../../common/data";
import {ActionName} from "../types";
import {NTQQGroupApi} from "../../../ntqqapi/api/group";
interface Payload{
interface Payload {
group_id: number,
user_id: number,
duration: number
}
export default class SetGroupBan extends BaseAction<Payload, null>{
export default class SetGroupBan extends BaseAction<Payload, null> {
actionName = ActionName.SetGroupBan
protected async _handle(payload: Payload): Promise<null> {
const member = await getGroupMember(payload.group_id, payload.user_id)
if(!member){
if (!member) {
throw `群成员${payload.user_id}不存在`
}
await NTQQApi.banMember(payload.group_id.toString(),
[{uid:member.uid, timeStamp: parseInt(payload.duration.toString())}])
await NTQQGroupApi.banMember(payload.group_id.toString(),
[{uid: member.uid, timeStamp: parseInt(payload.duration.toString())}])
return null
}
}

View File

@@ -1,23 +1,23 @@
import BaseAction from "./BaseAction";
import {NTQQApi} from "../../ntqqapi/ntcall";
import {getGroupMember} from "../../common/data";
import {GroupMemberRole} from "../../ntqqapi/types";
import {ActionName} from "./types";
import BaseAction from "../BaseAction";
import {getGroupMember} from "../../../common/data";
import {ActionName} from "../types";
import {NTQQGroupApi} from "../../../ntqqapi/api/group";
interface Payload{
interface Payload {
group_id: number,
user_id: number,
card: string
}
export default class SetGroupCard extends BaseAction<Payload, null>{
export default class SetGroupCard extends BaseAction<Payload, null> {
actionName = ActionName.SetGroupCard
protected async _handle(payload: Payload): Promise<null> {
const member = await getGroupMember(payload.group_id, payload.user_id)
if(!member){
if (!member) {
throw `群成员${payload.user_id}不存在`
}
await NTQQApi.setMemberCard(payload.group_id.toString(), member.uid, payload.card || "")
await NTQQGroupApi.setMemberCard(payload.group_id.toString(), member.uid, payload.card || "")
return null
}
}

View File

@@ -1,22 +1,23 @@
import BaseAction from "./BaseAction";
import {NTQQApi} from "../../ntqqapi/ntcall";
import {getGroupMember} from "../../common/data";
import {ActionName} from "./types";
import BaseAction from "../BaseAction";
import {getGroupMember} from "../../../common/data";
import {ActionName} from "../types";
import {NTQQGroupApi} from "../../../ntqqapi/api/group";
interface Payload{
interface Payload {
group_id: number,
user_id: number,
reject_add_request: boolean
}
export default class SetGroupKick extends BaseAction<Payload, null>{
export default class SetGroupKick extends BaseAction<Payload, null> {
actionName = ActionName.SetGroupKick
protected async _handle(payload: Payload): Promise<null> {
const member = await getGroupMember(payload.group_id, payload.user_id)
if(!member){
if (!member) {
throw `群成员${payload.user_id}不存在`
}
await NTQQApi.kickMember(payload.group_id.toString(), [member.uid], !!payload.reject_add_request);
await NTQQGroupApi.kickMember(payload.group_id.toString(), [member.uid], !!payload.reject_add_request);
return null
}
}

View File

@@ -0,0 +1,22 @@
import BaseAction from "../BaseAction";
import {ActionName} from "../types";
import {NTQQGroupApi} from "../../../ntqqapi/api/group";
import {log} from "../../../common/utils/log";
interface Payload {
group_id: number,
is_dismiss: boolean
}
export default class SetGroupLeave extends BaseAction<Payload, any> {
actionName = ActionName.SetGroupLeave
protected async _handle(payload: Payload): Promise<any> {
try {
await NTQQGroupApi.quitGroup(payload.group_id.toString())
} catch (e) {
log("退群失败", e)
throw e
}
}
}

View File

@@ -1,6 +1,6 @@
import BaseAction from "./BaseAction";
import {NTQQApi} from "../../ntqqapi/ntcall";
import {ActionName} from "./types";
import BaseAction from "../BaseAction";
import {ActionName} from "../types";
import {NTQQGroupApi} from "../../../ntqqapi/api/group";
interface Payload {
group_id: number,
@@ -12,7 +12,7 @@ export default class SetGroupName extends BaseAction<Payload, null> {
protected async _handle(payload: Payload): Promise<null> {
await NTQQApi.setGroupName(payload.group_id.toString(), payload.group_name)
await NTQQGroupApi.setGroupName(payload.group_id.toString(), payload.group_name)
return null
}
}

View File

@@ -1,6 +1,6 @@
import BaseAction from "./BaseAction";
import {NTQQApi} from "../../ntqqapi/ntcall";
import {ActionName} from "./types";
import BaseAction from "../BaseAction";
import {ActionName} from "../types";
import {NTQQGroupApi} from "../../../ntqqapi/api/group";
interface Payload {
group_id: number,
@@ -11,8 +11,8 @@ export default class SetGroupWholeBan extends BaseAction<Payload, null> {
actionName = ActionName.SetGroupWholeBan
protected async _handle(payload: Payload): Promise<null> {
await NTQQApi.banGroup(payload.group_id.toString(), !!payload.enable)
const enable = payload.enable.toString() === "true"
await NTQQGroupApi.banGroup(payload.group_id.toString(), enable)
return null
}
}

View File

@@ -1,36 +1,54 @@
import GetMsg from './GetMsg'
import GetLoginInfo from './GetLoginInfo'
import GetFriendList from './GetFriendList'
import GetGroupList from './GetGroupList'
import GetGroupInfo from './GetGroupInfo'
import GetGroupMemberList from './GetGroupMemberList'
import GetGroupMemberInfo from './GetGroupMemberInfo'
import SendGroupMsg from './SendGroupMsg'
import SendPrivateMsg from './SendPrivateMsg'
import SendMsg from './SendMsg'
import DeleteMsg from "./DeleteMsg";
import GetMsg from './msg/GetMsg'
import GetLoginInfo from './system/GetLoginInfo'
import GetFriendList from './user/GetFriendList'
import GetGroupList from './group/GetGroupList'
import GetGroupInfo from './group/GetGroupInfo'
import GetGroupMemberList from './group/GetGroupMemberList'
import GetGroupMemberInfo from './group/GetGroupMemberInfo'
import SendGroupMsg from './group/SendGroupMsg'
import SendPrivateMsg from './msg/SendPrivateMsg'
import SendMsg from './msg/SendMsg'
import DeleteMsg from "./msg/DeleteMsg";
import BaseAction from "./BaseAction";
import GetVersionInfo from "./GetVersionInfo";
import CanSendRecord from "./CanSendRecord";
import CanSendImage from "./CanSendImage";
import GetStatus from "./GetStatus";
import {GoCQHTTPSendGroupForwardMsg, GoCQHTTPSendPrivateForwardMsg} from "./go-cqhttp/SendForwardMsg";
import GetVersionInfo from "./system/GetVersionInfo";
import CanSendRecord from "./system/CanSendRecord";
import CanSendImage from "./system/CanSendImage";
import GetStatus from "./system/GetStatus";
import {GoCQHTTPSendForwardMsg, GoCQHTTPSendGroupForwardMsg, GoCQHTTPSendPrivateForwardMsg} from "./go-cqhttp/SendForwardMsg";
import GoCQHTTPGetStrangerInfo from "./go-cqhttp/GetStrangerInfo";
import SendLike from "./SendLike";
import SetGroupAddRequest from "./SetGroupAddRequest";
import SetGroupLeave from "./SetGroupLeave";
import GetGuildList from "./GetGuildList";
import Debug from "./Debug";
import SetFriendAddRequest from "./SetFriendAddRequest";
import SetGroupWholeBan from "./SetGroupWholeBan";
import SetGroupName from "./SetGroupName";
import SetGroupBan from "./SetGroupBan";
import SetGroupKick from "./SetGroupKick";
import SetGroupAdmin from "./SetGroupAdmin";
import SetGroupCard from "./SetGroupCard";
import SendLike from "./user/SendLike";
import SetGroupAddRequest from "./group/SetGroupAddRequest";
import SetGroupLeave from "./group/SetGroupLeave";
import GetGuildList from "./group/GetGuildList";
import Debug from "./llonebot/Debug";
import SetFriendAddRequest from "./user/SetFriendAddRequest";
import SetGroupWholeBan from "./group/SetGroupWholeBan";
import SetGroupName from "./group/SetGroupName";
import SetGroupBan from "./group/SetGroupBan";
import SetGroupKick from "./group/SetGroupKick";
import SetGroupAdmin from "./group/SetGroupAdmin";
import SetGroupCard from "./group/SetGroupCard";
import GetImage from "./file/GetImage";
import GetRecord from "./file/GetRecord";
import GoCQHTTPMarkMsgAsRead from "./msg/MarkMsgAsRead";
import CleanCache from "./system/CleanCache";
import GoCQHTTPUploadGroupFile from "./go-cqhttp/UploadGroupFile";
import {GetConfigAction, SetConfigAction} from "./llonebot/Config";
import GetGroupAddRequest from "./llonebot/GetGroupAddRequest";
import SetQQAvatar from './llonebot/SetQQAvatar'
import GoCQHTTPDownloadFile from "./go-cqhttp/DownloadFile";
import GoCQHTTPGetGroupMsgHistory from "./go-cqhttp/GetGroupMsgHistory";
import GetFile from "./file/GetFile";
import {GoCQHTTGetForwardMsgAction} from "./go-cqhttp/GetForwardMsg";
export const actionHandlers = [
new GetFile(),
new Debug(),
new GetConfigAction(),
new SetConfigAction(),
new GetGroupAddRequest(),
new SetQQAvatar(),
// onebot11
new SendLike(),
new GetMsg(),
new GetLoginInfo(),
@@ -51,12 +69,21 @@ export const actionHandlers = [
new SetGroupAdmin(),
new SetGroupName(),
new SetGroupCard(),
new GetImage(),
new GetRecord(),
new CleanCache(),
//以下为go-cqhttp api
new GoCQHTTPSendForwardMsg(),
new GoCQHTTPSendGroupForwardMsg(),
new GoCQHTTPSendPrivateForwardMsg(),
new GoCQHTTPGetStrangerInfo(),
new GetGuildList()
new GoCQHTTPDownloadFile(),
new GetGuildList(),
new GoCQHTTPMarkMsgAsRead(),
new GoCQHTTPUploadGroupFile(),
new GoCQHTTPGetGroupMsgHistory(),
new GoCQHTTGetForwardMsgAction(),
]

View File

@@ -0,0 +1,20 @@
import BaseAction from "../BaseAction";
import {Config} from "../../../common/types";
import {ActionName} from "../types";
import {setConfig} from "../../../main/setConfig";
import {getConfigUtil} from "../../../common/config";
export class GetConfigAction extends BaseAction<null, Config> {
actionName = ActionName.GetConfig
protected async _handle(payload: null): Promise<Config> {
return getConfigUtil().getConfig()
}
}
export class SetConfigAction extends BaseAction<Config, void> {
actionName = ActionName.SetConfig
protected async _handle(payload: Config): Promise<void> {
setConfig(payload).then();
}
}

View File

@@ -0,0 +1,42 @@
import BaseAction from "../BaseAction";
// import * as ntqqApi from "../../../ntqqapi/api";
import {
NTQQMsgApi,
NTQQFriendApi,
NTQQGroupApi,
NTQQUserApi,
NTQQFileApi,
NTQQFileCacheApi,
NTQQWindowApi,
} from "../../../ntqqapi/api";
import {ActionName} from "../types";
import {log} from "../../../common/utils/log";
interface Payload {
method: string,
args: any[],
}
export default class Debug extends BaseAction<Payload, any> {
actionName = ActionName.Debug
protected async _handle(payload: Payload): Promise<any> {
log("debug call ntqq api", payload);
const ntqqApi = [NTQQMsgApi, NTQQFriendApi, NTQQGroupApi, NTQQUserApi, NTQQFileApi, NTQQFileCacheApi, NTQQWindowApi]
for (const ntqqApiClass of ntqqApi) {
log("ntqqApiClass", ntqqApiClass)
const method = ntqqApiClass[payload.method]
if (method) {
const result = method(...payload.args);
if (method.constructor.name === "AsyncFunction") {
return await result
}
return result
}
}
throw `${payload.method}方法 不存在`
// const info = await NTQQApi.getUserDetailInfo(friends[0].uid);
// return info
}
}

View File

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

View File

@@ -0,0 +1,43 @@
import BaseAction from "../BaseAction";
import {ActionName} from "../types";
import * as fs from "node:fs";
import {NTQQUserApi} from "../../../ntqqapi/api/user";
import {checkFileReceived, uri2local} from "../../../common/utils/file";
// import { log } from "../../../common/utils";
interface Payload {
file: string
}
export default class SetAvatar extends BaseAction<Payload, null> {
actionName = ActionName.SetQQAvatar
protected async _handle(payload: Payload): Promise<null> {
const {path, isLocal, errMsg} = (await uri2local(payload.file))
if (errMsg){
throw `头像${payload.file}设置失败,file字段可能格式不正确`
}
if (path) {
await checkFileReceived(path, 5000); // 文件不存在QQ会崩溃需要提前判断
const ret = await NTQQUserApi.setQQAvatar(path)
if (!isLocal){
fs.unlink(path, () => {})
}
if (!ret) {
throw `头像${payload.file}设置失败,api无返回`
}
// log(`头像设置返回:${JSON.stringify(ret)}`)
if (ret['result'] == 1004022) {
throw `头像${payload.file}设置失败,文件可能不是图片格式`
} else if(ret['result'] != 0) {
throw `头像${payload.file}设置失败,未知的错误,${ret['result']}:${ret['errMsg']}`
}
} else {
if (!isLocal){
fs.unlink(path, () => {})
}
throw `头像${payload.file}设置失败,无法获取头像,文件可能不存在`
}
return null
}
}

View File

@@ -0,0 +1,22 @@
import {ActionName} from "../types";
import BaseAction from "../BaseAction";
import {dbUtil} from "../../../common/db";
import {NTQQMsgApi} from "../../../ntqqapi/api/msg";
interface Payload {
message_id: number
}
class DeleteMsg extends BaseAction<Payload, void> {
actionName = ActionName.DeleteMsg
protected async _handle(payload: Payload) {
let msg = await dbUtil.getMsgByShortId(payload.message_id)
await NTQQMsgApi.recallMsg({
chatType: msg.chatType,
peerUid: msg.peerUid
}, [msg.msgId])
}
}
export default DeleteMsg

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