Compare commits

...

135 Commits

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

7
.gitattributes vendored
View File

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

354
LICENSE
View File

@@ -1,25 +1,339 @@
MIT Without Public Social Media Promotion License
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (c) 2024 LLOneBot
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
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:
Preamble
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
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.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
You may use this software in accordance with the above terms, but you are not
allowed to promote this project or your projects based on this project on any
public social media.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

343
NapCatQQ-LICENSE Normal file
View File

@@ -0,0 +1,343 @@
GNU GENERAL PUBLIC Without Social media promotion LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
dYou may use this software in accordance with the above terms,
but you are not allowed to promote this project or your projects
based on this project on any public social media.
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

View File

@@ -1,6 +1,6 @@
# LLOneBot
LiteLoaderQQNT 插件,实现 OneBot 11 协议,用于 QQ 机器人开发
LiteLoaderQQNT 插件,实现 OneBot 11 和 Satori 协议,用于 QQ 机器人开发
> [!CAUTION]\
> 请不要在 QQ 官方群聊和任何影响力较大的简中互联网平台(包括但不限于: 哔哩哔哩,微博,知乎,抖音等)发布和讨论任何与本插件存在相关性的信息
@@ -23,9 +23,13 @@ TG 群:<https://t.me/+nLZEnpne-pQ1OWFl>
[![Stargazers over time](https://starchart.cc/LLOneBot/LLOneBot.svg?variant=adaptive)](https://starchart.cc/LLOneBot/LLOneBot)
## 贡献者
[![Contributors](https://contributors-img.web.app/image?repo=LLOneBot/LLOneBot)](https://github.com/LOneBot/LLOneBot/graphs/contributors)
## 鸣谢
- [NapCatQQ](https://github.com/NapNeko/NapCatQQ)
- [NapCatQQ](https://github.com/NapNeko/NapCatQQ),依照开源协议参考了其部分代码
- [LiteLoaderQQNT](https://liteloaderqqnt.github.io/guide/install.html)
- [Chronocat](https://github.com/chrononeko/chronocat)
- [koishi-plugin-adapter-onebot](https://github.com/koishijs/koishi-plugin-adapter-onebot)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,3 @@
import { QQLevel, Sex } from './user'
export enum GroupListUpdateType {
REFRESHALL,
GETALL,
MODIFIED,
REMOVE
}
export interface Group {
groupCode: string
maxMember: number
@@ -22,9 +13,9 @@ export interface Group {
hasModifyConfGroupName: boolean
remarkName: string
hasMemo: boolean
groupShutupExpireTime: string //"0",
personShutupExpireTime: string //"0",
discussToGroupUin: string //"0",
groupShutupExpireTime: string
personShutupExpireTime: string
discussToGroupUin: string
discussToGroupMaxMsgSeq: number
discussToGroupTime: number
groupFlagExt: number //1073938496,
@@ -32,39 +23,57 @@ export interface Group {
groupCreditLevel: number //0,
groupFlagExt3: number //0,
groupOwnerId: {
memberUin: string //"0",
memberUid: string //"u_fbf8N7aeuZEnUiJAbQ9R8Q"
memberUin: string
memberUid: string
}
members: GroupMember[] // 原始数据是没有这个的,为了方便自己加了这个字段
createTime: string
}
export enum GroupMemberRole {
normal = 2,
admin = 3,
owner = 4,
Normal = 2,
Admin = 3,
Owner = 4,
}
export interface GroupMember {
memberSpecialTitle?: string
avatarPath: string
cardName: string
cardType: number
isDelete: boolean
nick: string
uid: string
qid: string
uin: string
nick: string
remark: string
role: GroupMemberRole // 群主:4, 管理员:3群员:2
shutUpTime: number // 禁言时间,单位是什么暂时不清楚
uid: string // 加密的字符串
uin: string // QQ号
cardType: number
cardName: string
role: GroupMemberRole
avatarPath: string
shutUpTime: number
isDelete: boolean
isSpecialConcerned: boolean
isSpecialShield: boolean
isRobot: boolean
sex?: Sex
qqLevel?: QQLevel
isChangeRole: boolean
groupHonor: Uint8Array
memberRealLevel: number
memberLevel: number
globalGroupLevel: number
globalGroupPoint: number
memberTitleId: number
memberSpecialTitle: string
specialTitleExpireTime: string
userShowFlag: number
userShowFlagNew: number
richFlag: number
mssVipType: number
bigClubLevel: number
bigClubFlag: number
autoRemark: string
creditLevel: number
joinTime: number
lastSpeakTime: number
memberLevel: number
memberFlag: number
memberFlagExt: number
memberMobileFlag: number
memberFlagExt2: number
isSpecialShielded: boolean
cardNameId: number
}
export interface PublishGroupBulletinReq {
@@ -120,3 +129,62 @@ export interface GroupAllInfo {
joinGroupAuth: string
isAllowModifyConfGroupName: number
}
export interface GroupBulletinListResult {
groupCode: string
srvCode: number
readOnly: number
role: number
inst: unknown[]
feeds: {
uin: string
feedId: string
publishTime: string
msg: {
text: string
textFace: string
pics: {
id: string
width: number
height: number
}[]
title: string
}
type: number
fn: number
cn: number
vn: number
settings: {
isShowEditCard: number
remindTs: number
tipWindowType: number
confirmRequired: number
}
pinned: number
readNum: number
is_read: number
is_all_confirm: number
}[]
groupInfo: {
groupCode: string
classId: number
}
gln: number
tst: number
publisherInfos: {
uin: string
nick: string
avatar: string
}[]
server_time: string
svrt: string
nextIndex: number
jointime: string
}
export enum GroupMsgMask {
AllowNotify = 1, // 允许提醒
AllowNotNotify = 4, // 接受消息不提醒
BoxNotNotify = 2, // 收进群助手不提醒
NotAllow = 3, // 屏蔽
}

View File

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

View File

@@ -1,19 +1,19 @@
export enum GroupNotifyType {
INVITED_BY_MEMBER = 1,
REFUSE_INVITED,
REFUSED_BY_ADMINI_STRATOR,
AGREED_TOJOIN_DIRECT, // 有人接受了邀请入群
INVITED_NEED_ADMINI_STRATOR_PASS, // 有人邀请了别人入群
AGREED_TO_JOIN_BY_ADMINI_STRATOR,
REQUEST_JOIN_NEED_ADMINI_STRATOR_PASS,
SET_ADMIN,
KICK_MEMBER_NOTIFY_ADMIN,
KICK_MEMBER_NOTIFY_KICKED,
MEMBER_LEAVE_NOTIFY_ADMIN, // 主动退出
CANCEL_ADMIN_NOTIFY_CANCELED, // 我被取消管理员
CANCEL_ADMIN_NOTIFY_ADMIN, // 其他人取消管理员
TRANSFER_GROUP_NOTIFY_OLDOWNER,
TRANSFER_GROUP_NOTIFY_ADMIN
InvitedByMember = 1,
RefuseInvited,
RefusedByAdminiStrator,
AgreedTojoinDirect, // 有人接受了邀请入群
InvitedNeedAdminiStratorPass, // 有人邀请了别人入群
AgreedToJoinByAdminiStrator,
RequestJoinNeedAdminiStratorPass,
SetAdmin,
KickMemberNotifyAdmin,
KickMemberNotifyKicked,
MemberLeaveNotifyAdmin, // 主动退出
CancelAdminNotifyCanceled, // 我被取消管理员
CancelAdminNotifyAdmin, // 其他人取消管理员
TransferGroupNotifyOldowner,
TransferGroupNotifyAdmin
}
export interface GroupNotifies {
@@ -23,11 +23,11 @@ export interface GroupNotifies {
}
export enum GroupNotifyStatus {
KINIT, // 初始化
KUNHANDLE, // 未处理
KAGREED, // 同意
KREFUSED, // 拒绝
KIGNORED // 忽略
Init, // 初始化
Unhandle, // 未处理
Agreed, // 同意
Refused, // 拒绝
Ignored // 忽略
}
export interface GroupNotify {
@@ -56,20 +56,8 @@ export enum GroupRequestOperateTypes {
}
export enum BuddyReqType {
KMEINITIATOR,
KPEERINITIATOR,
KMEAGREED,
KMEAGREEDANDADDED,
KPEERAGREED,
KPEERAGREEDANDADDED,
KPEERREFUSED,
KMEREFUSED,
KMEIGNORED,
KMEAGREEANYONE,
KMESETQUESTION,
KMEAGREEANDADDFAILED,
KMSGINFO,
KMEINITIATORWAITPEERCONFIRM
MsgInfo = 12,
MeInitiatorWaitPeerConfirm = 13,
}
export interface FriendRequest {
@@ -91,41 +79,3 @@ export interface FriendRequestNotify {
buddyReqs: FriendRequest[]
}
}
export enum MemberExtSourceType {
DEFAULTTYPE = 0,
TITLETYPE = 1,
NEWGROUPTYPE = 2,
}
export interface GroupExtParam {
groupCode: string
seq: string
beginUin: string
dataTime: string
uinList: Array<string>
uinNum: string
groupType: string
richCardNameVer: string
sourceType: MemberExtSourceType
memberExtFilter: {
memberLevelInfoUin: number
memberLevelInfoPoint: number
memberLevelInfoActiveDay: number
memberLevelInfoLevel: number
memberLevelInfoName: number
levelName: number
dataTime: number
userShowFlag: number
sysShowFlag: number
timeToUpdate: number
nickName: number
specialTitle: number
levelNameNew: number
userShowFlagNew: number
msgNeedField: number
cmdUinFlagExt3Grocery: number
memberIcon: number
memberInfoSeq: number
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -50,7 +50,7 @@ export class GetGroupFileUrl extends BaseAction<Payload, Response> {
private async search(groupId: string, fileId: string, folderId?: string) {
let modelId: string | undefined
let nextIndex: number | undefined
let folders: GroupFileInfo['item'] = []
const folders: GroupFileInfo['item'] = []
while (nextIndex !== 0) {
const res = await this.ctx.ntGroupApi.getGroupFileList(groupId, {
sortType: 1,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,19 @@
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
import { selfInfo } from '@/common/globalVars'
interface Payload {
group_id: number | string
}
export class SendGroupSign extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_SendGroupSign
payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required(),
})
async _handle(payload: Payload) {
throw new Error('暂未实现群签到功能')
return null
}
}

View File

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

View File

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

View File

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

View File

@@ -15,8 +15,16 @@ class GetGroupInfo extends BaseAction<Payload, OB11Group> {
protected async _handle(payload: Payload) {
const groupCode = payload.group_id.toString()
const group = (await this.ctx.ntGroupApi.getGroups()).find(e => e.groupCode === groupCode)
let group = (await this.ctx.ntGroupApi.getGroups()).find(e => e.groupCode === groupCode)
if (group) {
try{
const groupAllInfo = await this.ctx.ntGroupApi.getGroupAllInfo(groupCode)
this.ctx.logger.info(groupAllInfo)
return {...OB11Entities.group(group), ...groupAllInfo}
}
catch (e) {
this.ctx.logger.error('获取群完整详细信息失败', e)
}
return OB11Entities.group(group)
}
throw new Error(`${payload.group_id}不存在`)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,25 @@
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
import { getBuildVersion } from '@/common/utils'
interface Payload {
user_id: number | string
}
export class FriendPoke extends BaseAction<Payload, null> {
actionName = ActionName.FriendPoke
payloadSchema = Schema.object({
user_id: Schema.union([Number, String]).required()
})
async _handle(payload: Payload) {
if (!this.ctx.app.native.checkPlatform() || !this.ctx.app.native.checkVersion()) {
// await this.ctx.app.packet.sendPokePacket(+payload.user_id)
throw new Error('戳一戳暂时只支持Windows QQ 27333 ~ 275970版本')
}
else{
await this.ctx.app.native.sendFriendPoke(+payload.user_id)
}
return null
}
}

View File

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

View File

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

View File

@@ -0,0 +1,26 @@
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
import { getBuildVersion } from '@/common/utils'
interface Payload {
group_id: number | string
user_id: number | string
}
export class GroupPoke extends BaseAction<Payload, null> {
actionName = ActionName.GroupPoke
payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required(),
user_id: Schema.union([Number, String]).required()
})
async _handle(payload: Payload) {
if (!this.ctx.app.native.checkPlatform() || !this.ctx.app.native.checkVersion()) {
throw new Error('戳一戳暂时只支持Windows QQ 27333 ~ 275970版本')
}
else{
await this.ctx.app.native.sendGroupPoke(+payload.group_id, +payload.user_id)
}
return null
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -24,6 +24,15 @@ export enum ActionName {
FetchEmojiLike = 'fetch_emoji_like',
FetchCustomFace = 'fetch_custom_face',
GetFriendMsgHistory = 'get_friend_msg_history',
SendForwardMsg = 'send_forward_msg',
SetMsgEmojiLike = 'set_msg_emoji_like',
GetRobotUinRange = 'get_robot_uin_range',
GroupPoke = 'group_poke',
FriendPoke = 'friend_poke',
SetFriendRemark = 'set_friend_remark',
SetFriendCategory = 'set_friend_category',
SetGroupMsgMask = 'set_group_msg_mask',
SetGroupRemark = 'set_group_remark',
// onebot 11
SendLike = 'send_like',
GetLoginInfo = 'get_login_info',
@@ -37,12 +46,12 @@ export enum ActionName {
SendGroupMsg = 'send_group_msg',
SendPrivateMsg = 'send_private_msg',
DeleteMsg = 'delete_msg',
SetMsgEmojiLike = 'set_msg_emoji_like',
SetGroupAddRequest = 'set_group_add_request',
SetFriendAddRequest = 'set_friend_add_request',
SetGroupLeave = 'set_group_leave',
GetVersionInfo = 'get_version_info',
GetStatus = 'get_status',
SetRestart = 'set_restart',
CanSendRecord = 'can_send_record',
CanSendImage = 'can_send_image',
SetGroupKick = 'set_group_kick',
@@ -57,8 +66,8 @@ export enum ActionName {
GetCookies = 'get_cookies',
ForwardFriendSingleMsg = 'forward_friend_single_msg',
ForwardGroupSingleMsg = 'forward_group_single_msg',
// 以下为go-cqhttp api
GoCQHTTP_SendForwardMsg = 'send_forward_msg',
GetCredentials = 'get_credentials',
// go-cqhttp
GoCQHTTP_SendGroupForwardMsg = 'send_group_forward_msg',
GoCQHTTP_SendPrivateForwardMsg = 'send_private_forward_msg',
GoCQHTTP_GetStrangerInfo = 'get_stranger_info',
@@ -82,5 +91,11 @@ export enum ActionName {
GoCQHTTP_GetGroupRootFiles = 'get_group_root_files',
GoCQHTTP_SendGroupNotice = '_send_group_notice',
GoCQHTTP_GetGroupFilesByFolder = 'get_group_files_by_folder',
GoCQHTTP_GetGroupFileUrl = 'get_group_file_url'
GoCQHTTP_GetGroupFileUrl = 'get_group_file_url',
GoCQHTTP_GetGroupNotice = '_get_group_notice',
GoCQHTTP_DeleteFriend = 'delete_friend',
GoCQHTTP_OCRImage = 'ocr_image',
GoCQHTTP_GetGroupFileSystemInfo = 'get_group_file_system_info',
GoCQHTTP_SetGroupSpecialTitle = 'set_group_special_title',
GoCQHTTP_SendGroupSign = 'send_group_sign',
}

View File

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

View File

@@ -23,11 +23,11 @@ export default class SetFriendAddRequest extends BaseAction<Payload, null> {
if (payload.remark) {
await this.ctx.ntFriendApi.setBuddyRemark(uid, payload.remark)
}
await this.ctx.ntMsgApi.activateChat({
/*await this.ctx.ntMsgApi.activateChat({
peerUid: uid,
chatType: ChatType.C2C,
guildId: ''
})
})*/
return null
}
}

View File

@@ -4,30 +4,25 @@ import {
GroupNotify,
GroupNotifyType,
RawMessage,
BuddyReqType,
Peer,
FriendRequest,
GroupMember,
GroupMemberRole,
GroupNotifyStatus
} from '../ntqqapi/types'
import { OB11GroupRequestEvent } from './event/request/OB11GroupRequest'
import { OB11FriendRequestEvent } from './event/request/OB11FriendRequest'
import { GroupDecreaseSubType, OB11GroupDecreaseEvent } from './event/notice/OB11GroupDecreaseEvent'
import { OB11GroupDecreaseEvent } from './event/notice/OB11GroupDecreaseEvent'
import { selfInfo } from '../common/globalVars'
import { OB11Config, Config as LLOBConfig } from '../common/types'
import { OB11WebSocket, OB11WebSocketReverseManager } from './connect/ws'
import { OB11Http, OB11HttpPost } from './connect/http'
import { OB11BaseEvent } from './event/OB11BaseEvent'
import { OB11Message } from './types'
import { OB11BaseMetaEvent } from './event/meta/OB11BaseMetaEvent'
import { postHttpEvent } from './helper/eventForHttp'
import { initActionMap } from './action'
import { llonebotError } from '../common/globalVars'
import { OB11GroupCardEvent } from './event/notice/OB11GroupCardEvent'
import { OB11GroupAdminNoticeEvent } from './event/notice/OB11GroupAdminNoticeEvent'
import { OB11ProfileLikeEvent } from './event/notice/OB11ProfileLikeEvent'
import { SysMsg } from '@/ntqqapi/proto/compiled'
import { Msg, SysMsg } from '@/ntqqapi/proto/compiled'
import { OB11GroupIncreaseEvent } from './event/notice/OB11GroupIncreaseEvent'
declare module 'cordis' {
interface Context {
@@ -36,14 +31,15 @@ declare module 'cordis' {
}
class OneBot11Adapter extends Service {
static inject = ['ntMsgApi', 'ntFileApi', 'ntFileCacheApi', 'ntFriendApi', 'ntGroupApi', 'ntUserApi', 'ntWindowApi', 'ntWebApi', 'store']
public messages: Map<string, RawMessage> = new Map()
public startTime = 0
private ob11WebSocket: OB11WebSocket
private ob11WebSocketReverseManager: OB11WebSocketReverseManager
private ob11Http: OB11Http
private ob11HttpPost: OB11HttpPost
static inject = [
'ntMsgApi', 'ntFileApi', 'ntFileCacheApi', 'ntFriendApi',
'ntGroupApi', 'ntUserApi', 'ntWindowApi', 'ntWebApi',
'store', 'app'
]
private ob11WebSocket
private ob11WebSocketReverseManager
private ob11Http
private ob11HttpPost
constructor(public ctx: Context, public config: OneBot11Adapter.Config) {
super(ctx, 'onebot', true)
@@ -75,25 +71,7 @@ class OneBot11Adapter extends Service {
})
}
/** 缓存近期消息内容 */
public async addMsgCache(msg: RawMessage) {
const expire = this.config.msgCacheExpire * 1000
if (expire === 0) {
return
}
const id = msg.msgId
this.messages.set(id, msg)
setTimeout(() => {
this.messages.delete(id)
}, expire)
}
/** 获取近期消息内容 */
public getMsgCache(msgId: string) {
return this.messages.get(msgId)
}
public dispatch(event: OB11BaseEvent | OB11Message) {
public dispatch(event: OB11BaseEvent) {
if (this.config.enableWs) {
this.ob11WebSocket.emitEvent(event)
}
@@ -109,115 +87,103 @@ class OneBot11Adapter extends Service {
}
}
private async handleGroupNotify(notifies: GroupNotify[]) {
for (const notify of notifies) {
try {
const notifyTime = parseInt(notify.seq) / 1000
if (notifyTime < this.startTime) {
continue
}
const flag = notify.group.groupCode + '|' + notify.seq + '|' + notify.type
if ([GroupNotifyType.MEMBER_LEAVE_NOTIFY_ADMIN, GroupNotifyType.KICK_MEMBER_NOTIFY_ADMIN].includes(notify.type)) {
this.ctx.logger.info('有成员退出通知', notify)
const member1Uin = await this.ctx.ntUserApi.getUinByUid(notify.user1.uid)
let operatorId = member1Uin
let subType: GroupDecreaseSubType = 'leave'
if (notify.user2.uid) {
// 是被踢的
const member2Uin = await this.ctx.ntUserApi.getUinByUid(notify.user2.uid)
if (member2Uin) {
operatorId = member2Uin
}
subType = 'kick'
}
private async handleGroupNotify(notify: GroupNotify, doubt: boolean) {
try {
const flag = `${notify.group.groupCode}|${notify.seq}|${notify.type}|${doubt === true ? '1' : '0'}`
if ([GroupNotifyType.MemberLeaveNotifyAdmin, GroupNotifyType.KickMemberNotifyAdmin].includes(notify.type)) {
if (notify.user2.uid) {
this.ctx.logger.info('有群成员被踢', notify.group.groupCode, notify.user1.uid, notify.user2.uid)
const memberUin = await this.ctx.ntUserApi.getUinByUid(notify.user1.uid)
const adminUin = await this.ctx.ntUserApi.getUinByUid(notify.user2.uid)
const event = new OB11GroupDecreaseEvent(
parseInt(notify.group.groupCode),
parseInt(member1Uin),
parseInt(operatorId),
subType,
parseInt(memberUin),
parseInt(adminUin),
'kick',
)
this.dispatch(event)
}
else if (notify.type === GroupNotifyType.REQUEST_JOIN_NEED_ADMINI_STRATOR_PASS && notify.status === GroupNotifyStatus.KUNHANDLE) {
this.ctx.logger.info('有加群请求')
const requestUin = await this.ctx.ntUserApi.getUinByUid(notify.user1.uid)
const event = new OB11GroupRequestEvent(
parseInt(notify.group.groupCode),
parseInt(requestUin) || 0,
flag,
notify.postscript,
)
this.dispatch(event)
}
else if (notify.type === GroupNotifyType.INVITED_BY_MEMBER && notify.status === GroupNotifyStatus.KUNHANDLE) {
this.ctx.logger.info('收到邀请我加群通知')
const userId = await this.ctx.ntUserApi.getUinByUid(notify.user2.uid)
const event = new OB11GroupRequestEvent(
parseInt(notify.group.groupCode),
parseInt(userId) || 0,
flag,
notify.postscript,
undefined,
'invite'
)
this.dispatch(event)
}
else if (notify.type === GroupNotifyType.INVITED_NEED_ADMINI_STRATOR_PASS && notify.status === GroupNotifyStatus.KUNHANDLE) {
this.ctx.logger.info('收到群员邀请加群通知')
const userId = await this.ctx.ntUserApi.getUinByUid(notify.user1.uid)
const event = new OB11GroupRequestEvent(
parseInt(notify.group.groupCode),
parseInt(userId) || 0,
flag,
notify.postscript
)
this.dispatch(event)
}
} catch (e) {
this.ctx.logger.error('解析群通知失败', (e as Error).stack)
}
else if (notify.type === GroupNotifyType.RequestJoinNeedAdminiStratorPass && notify.status === GroupNotifyStatus.Unhandle) {
this.ctx.logger.info('有加群请求')
const requestUin = await this.ctx.ntUserApi.getUinByUid(notify.user1.uid)
const event = new OB11GroupRequestEvent(
parseInt(notify.group.groupCode),
parseInt(requestUin) || 0,
flag,
notify.postscript,
)
this.dispatch(event)
}
else if (notify.type === GroupNotifyType.InvitedByMember && notify.status === GroupNotifyStatus.Unhandle) {
this.ctx.logger.info('收到邀请我加群通知')
const userId = await this.ctx.ntUserApi.getUinByUid(notify.user2.uid)
const event = new OB11GroupRequestEvent(
parseInt(notify.group.groupCode),
parseInt(userId) || 0,
flag,
notify.postscript,
undefined,
'invite'
)
this.dispatch(event)
}
else if (notify.type === GroupNotifyType.InvitedNeedAdminiStratorPass && notify.status === GroupNotifyStatus.Unhandle) {
this.ctx.logger.info('收到群员邀请加群通知')
const userId = await this.ctx.ntUserApi.getUinByUid(notify.user1.uid)
const event = new OB11GroupRequestEvent(
parseInt(notify.group.groupCode),
parseInt(userId) || 0,
flag,
notify.postscript
)
this.dispatch(event)
}
else if ([
GroupNotifyType.SetAdmin,
GroupNotifyType.CancelAdminNotifyCanceled,
GroupNotifyType.CancelAdminNotifyAdmin
].includes(notify.type)) {
this.ctx.logger.info('收到管理员变动通知')
const uin = await this.ctx.ntUserApi.getUinByUid(notify.user1.uid)
const event = new OB11GroupAdminNoticeEvent(
notify.type === GroupNotifyType.SetAdmin ? 'set' : 'unset',
parseInt(notify.group.groupCode),
parseInt(uin),
)
this.dispatch(event)
}
} catch (e) {
this.ctx.logger.error('解析群通知失败', (e as Error).stack)
}
}
private handleMsg(msgList: RawMessage[]) {
for (const message of msgList) {
// 过滤启动之前的消息
if (parseInt(message.msgTime) < this.startTime / 1000) {
continue
private handleMsg(message: RawMessage) {
OB11Entities.message(this.ctx, message).then(msg => {
if (!msg) {
return
}
this.addMsgCache(message)
if (!this.config.debug && msg.message.length === 0) {
return
}
const isSelfMsg = msg.user_id.toString() === selfInfo.uin
if (isSelfMsg) {
msg.target_id = parseInt(message.peerUin)
}
this.dispatch(msg)
}).catch(e => this.ctx.logger.error('handling incoming messages', e))
OB11Entities.message(this.ctx, message)
.then((msg) => {
if (!msg) {
return
}
if (!this.config.debug && msg.message.length === 0) {
return
}
const isSelfMsg = msg.user_id.toString() === selfInfo.uin
if (isSelfMsg && !this.config.reportSelfMessage) {
return
}
if (isSelfMsg) {
msg.target_id = parseInt(message.peerUin)
}
this.dispatch(msg)
})
.catch((e) => this.ctx.logger.error('constructMessage error: ', e.stack.toString()))
OB11Entities.groupEvent(this.ctx, message).then(groupEvent => {
if (groupEvent) {
this.dispatch(groupEvent)
}
}).catch(e => this.ctx.logger.error('handling incoming group events', e))
OB11Entities.groupEvent(this.ctx, message).then((groupEvent) => {
if (groupEvent) {
this.dispatch(groupEvent)
}
})
OB11Entities.privateEvent(this.ctx, message).then((privateEvent) => {
if (privateEvent) {
this.dispatch(privateEvent)
}
})
}
OB11Entities.privateEvent(this.ctx, message).then(privateEvent => {
if (privateEvent) {
this.dispatch(privateEvent)
}
}).catch(e => this.ctx.logger.error('handling incoming buddy events', e))
}
private handleRecallMsg(message: RawMessage) {
@@ -236,30 +202,22 @@ class OneBot11Adapter extends Service {
})
}
private async handleFriendRequest(buddyReqs: FriendRequest[]) {
for (const req of buddyReqs) {
if (!!req.isInitiator || (req.isDecide && req.reqType !== BuddyReqType.KMEINITIATORWAITPEERCONFIRM)) {
continue
}
if (+req.reqTime < this.startTime / 1000) {
continue
}
let userId = 0
try {
const requesterUin = await this.ctx.ntUserApi.getUinByUid(req.friendUid)
userId = parseInt(requesterUin)
} catch (e) {
this.ctx.logger.error('获取加好友者QQ号失败', e)
}
const flag = req.friendUid + '|' + req.reqTime
const comment = req.extWords
const friendRequestEvent = new OB11FriendRequestEvent(
userId,
comment,
flag
)
this.dispatch(friendRequestEvent)
private async handleFriendRequest(req: FriendRequest) {
let userId = 0
try {
const requesterUin = await this.ctx.ntUserApi.getUinByUid(req.friendUid)
userId = parseInt(requesterUin)
} catch (e) {
this.ctx.logger.error('获取加好友者QQ号失败', e)
}
const flag = req.friendUid + '|' + req.reqTime
const comment = req.extWords
const friendRequestEvent = new OB11FriendRequestEvent(
userId,
comment,
flag
)
this.dispatch(friendRequestEvent)
}
private async handleConfigUpdated(config: LLOBConfig) {
@@ -343,7 +301,6 @@ class OneBot11Adapter extends Service {
heartInterval: config.heartInterval,
token: config.token,
debug: config.debug,
reportSelfMessage: config.reportSelfMessage,
msgCacheExpire: config.msgCacheExpire,
musicSignUrl: config.musicSignUrl,
enableLocalFile2Url: config.enableLocalFile2Url,
@@ -351,31 +308,7 @@ class OneBot11Adapter extends Service {
})
}
private async handleGroupMemberInfoUpdated(groupCode: string, members: GroupMember[]) {
for (const member of members) {
const existMember = await this.ctx.ntGroupApi.getGroupMember(groupCode, member.uin)
if (existMember) {
if (member.cardName !== existMember.cardName) {
this.ctx.logger.info('群成员名片变动', `${groupCode}: ${existMember.uin}`, existMember.cardName, '->', member.cardName)
this.dispatch(
new OB11GroupCardEvent(parseInt(groupCode), parseInt(member.uin), member.cardName, existMember.cardName),
)
} else if (member.role !== existMember.role) {
this.ctx.logger.info('有管理员变动通知')
const groupAdminNoticeEvent = new OB11GroupAdminNoticeEvent(
member.role == GroupMemberRole.admin ? 'set' : 'unset',
parseInt(groupCode),
parseInt(member.uin)
)
this.dispatch(groupAdminNoticeEvent)
}
Object.assign(existMember, member)
}
}
}
public start() {
this.startTime = Date.now()
if (this.config.enableWs) {
this.ob11WebSocket.start()
}
@@ -388,7 +321,7 @@ class OneBot11Adapter extends Service {
if (this.config.enableHttpPost) {
this.ob11HttpPost.start()
}
this.ctx.on('llonebot/config-updated', input => {
this.ctx.on('llob/config-updated', input => {
this.handleConfigUpdated(input)
})
this.ctx.on('nt/message-created', input => {
@@ -398,28 +331,45 @@ class OneBot11Adapter extends Service {
this.handleRecallMsg(input)
})
this.ctx.on('nt/message-sent', input => {
this.handleMsg([input])
if (!this.config.reportSelfMessage) {
return
}
this.handleMsg(input)
})
this.ctx.on('nt/group-notify', input => {
this.handleGroupNotify(input)
const { doubt, notify } = input
this.handleGroupNotify(notify, doubt)
})
this.ctx.on('nt/friend-request', input => {
this.handleFriendRequest(input)
})
this.ctx.on('nt/group-member-info-updated', input => {
this.handleGroupMemberInfoUpdated(input.groupCode, input.members)
})
this.ctx.on('nt/system-message-created', input => {
const sysMsg = SysMsg.SystemMessage.decode(input)
const { msgType, subType, subSubType } = sysMsg.msgSpec[0] ?? {}
if (msgType === 528 && subType === 39 && subSubType === 39) {
const tip = SysMsg.ProfileLikeTip.decode(sysMsg.bodyWrapper!.body!)
this.ctx.on('nt/system-message-created', async input => {
const sysMsg = Msg.Message.decode(input)
const { msgType, subType } = sysMsg.contentHead ?? {}
if (msgType === 528 && subType === 39) {
const tip = SysMsg.ProfileLikeTip.decode(sysMsg.body!.msgContent!)
if (tip.msgType !== 0 || tip.subType !== 203) return
const detail = tip.content?.msg?.detail
if (!detail) return
const [times] = detail.txt?.match(/\d+/) ?? ['0']
const profileLikeEvent = new OB11ProfileLikeEvent(detail.uin!, detail.nickname!, +times)
this.dispatch(profileLikeEvent)
const event = new OB11ProfileLikeEvent(detail.uin!, detail.nickname!, +times)
this.dispatch(event)
} else if (msgType === 33) {
const tip = SysMsg.GroupMemberChange.decode(sysMsg.body!.msgContent!)
if (tip.type !== 130) return
this.ctx.logger.info('群成员增加', tip)
const memberUin = await this.ctx.ntUserApi.getUinByUid(tip.memberUid)
const operatorUin = await this.ctx.ntUserApi.getUinByUid(tip.adminUid)
const event = new OB11GroupIncreaseEvent(tip.groupCode, +memberUin, +operatorUin)
this.dispatch(event)
} else if (msgType === 34) {
const tip = SysMsg.GroupMemberChange.decode(sysMsg.body!.msgContent!)
if (tip.type !== 130) return // adminUid: 0
this.ctx.logger.info('群成员减少', tip)
const memberUin = await this.ctx.ntUserApi.getUinByUid(tip.memberUid)
const userId = Number(memberUin)
const event = new OB11GroupDecreaseEvent(tip.groupCode, userId, userId)
this.dispatch(event)
}
})
}
@@ -430,8 +380,6 @@ namespace OneBot11Adapter {
heartInterval: number
token: string
debug: boolean
reportSelfMessage: boolean
msgCacheExpire: number
musicSignUrl?: string
enableLocalFile2Url: boolean
ffmpeg?: string

View File

@@ -1,7 +1,7 @@
import http from 'node:http'
import cors from 'cors'
import crypto from 'node:crypto'
import express, { Express, Request, Response } from 'express'
import express, { Express, Request, Response, NextFunction } from 'express'
import { BaseAction } from '../action/BaseAction'
import { Context } from 'cordis'
import { llonebotError, selfInfo } from '@/common/globalVars'
@@ -76,7 +76,7 @@ class OB11Http {
Object.assign(this.config, config)
}
private authorize(req: Request, res: Response, next: () => void) {
private authorize(req: Request, res: Response, next: NextFunction) {
const serverToken = this.config.token
if (!serverToken) return next()
@@ -95,12 +95,13 @@ class OB11Http {
}
if (clientToken !== serverToken) {
return res.status(403).json({ message: 'token verify failed!' })
res.status(403).json({ message: 'token verify failed!' })
} else {
next()
}
next()
}
private async handleRequest(req: Request, res: Response, next: () => void) {
private async handleRequest(req: Request, res: Response, next: NextFunction) {
if (req.path === '/') return next()
let payload = req.body
if (req.method === 'GET') {
@@ -133,11 +134,13 @@ namespace OB11Http {
class OB11HttpPost {
private disposeInterval?: () => void
private activated = false
constructor(protected ctx: Context, public config: OB11HttpPost.Config) {
}
public start() {
this.activated = true
if (this.config.enableHttpHeart && !this.disposeInterval) {
this.disposeInterval = this.ctx.setInterval(() => {
// ws的心跳是ws自己维护的
@@ -147,10 +150,14 @@ class OB11HttpPost {
}
public stop() {
this.activated = false
this.disposeInterval?.()
}
public async emitEvent(event: OB11BaseEvent | OB11Message) {
public async emitEvent(event: OB11BaseEvent) {
if (!this.activated || !this.config.hosts.length) {
return
}
const msgStr = JSON.stringify(event)
const headers: Dict = {
'Content-Type': 'application/json',
@@ -170,7 +177,8 @@ class OB11HttpPost {
}).then(
async (res) => {
if (event.post_type) {
this.ctx.logger.info(`HTTP 事件上报: ${host}`, event.post_type, res.status)
const eventName = event.post_type + '.' + event[event.post_type + '_type']
this.ctx.logger.info(`HTTP 事件上报: ${host}`, eventName, res.status)
}
try {
const resJson = await res.json()

View File

@@ -57,11 +57,12 @@ class OB11WebSocket {
})
}
public async emitEvent(event: OB11BaseEvent | OB11Message) {
public async emitEvent(event: OB11BaseEvent) {
this.wsClients.forEach(({ socket, emitEvent }) => {
if (emitEvent && socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify(event))
this.ctx.logger.info('WebSocket 事件上报', socket.url ?? '', event.post_type)
const eventName = event.post_type + '.' + event[event.post_type + '_type']
this.ctx.logger.info('WebSocket 事件上报', socket.url ?? '', eventName)
}
})
}
@@ -193,10 +194,11 @@ class OB11WebSocketReverse {
this.wsClient?.close()
}
public emitEvent(event: OB11BaseEvent | OB11Message) {
public emitEvent(event: OB11BaseEvent) {
if (this.wsClient && this.wsClient.readyState === WebSocket.OPEN) {
this.wsClient.send(JSON.stringify(event))
this.ctx.logger.info('WebSocket 事件上报', this.wsClient.url ?? '', event.post_type)
const eventName = event.post_type + '.' + event[event.post_type + '_type']
this.ctx.logger.info('WebSocket 事件上报', this.wsClient.url ?? '', eventName)
}
}
@@ -214,7 +216,7 @@ class OB11WebSocketReverse {
let receive: { action: ActionName | null; params: unknown; echo?: unknown } = { action: null, params: {} }
try {
receive = JSON.parse(msg.toString())
this.ctx.logger.info('收到反向Websocket消息', receive)
this.ctx.logger.info('收到反向 Websocket 消息', receive)
} catch (e) {
return this.reply(this.wsClient!, OB11Response.error('json解析失败请检查数据格式', 1400, receive.echo))
}

View File

@@ -1,4 +1,3 @@
import { XMLParser } from 'fast-xml-parser'
import {
OB11Group,
OB11GroupMember,
@@ -20,7 +19,7 @@ import {
Sex,
TipGroupElementType,
User,
FriendV2
SimpleInfo
} from '../ntqqapi/types'
import { EventType } from './event/OB11BaseEvent'
import { encodeCQCode } from './cqcode'
@@ -30,7 +29,6 @@ import { OB11GroupUploadNoticeEvent } from './event/notice/OB11GroupUploadNotice
import { OB11GroupNoticeEvent } from './event/notice/OB11GroupNoticeEvent'
import { calcQQLevel } from '../common/utils/misc'
import { OB11GroupTitleEvent } from './event/notice/OB11GroupTitleEvent'
import { OB11GroupCardEvent } from './event/notice/OB11GroupCardEvent'
import { OB11GroupDecreaseEvent } from './event/notice/OB11GroupDecreaseEvent'
import { OB11GroupMsgEmojiLikeEvent } from './event/notice/OB11MsgEmojiLikeEvent'
import { OB11FriendAddNoticeEvent } from './event/notice/OB11FriendAddNoticeEvent'
@@ -39,7 +37,7 @@ import { OB11GroupRecallNoticeEvent } from './event/notice/OB11GroupRecallNotice
import { OB11FriendPokeEvent, OB11GroupPokeEvent } from './event/notice/OB11PokeEvent'
import { OB11BaseNoticeEvent } from './event/notice/OB11BaseNoticeEvent'
import { OB11GroupEssenceEvent } from './event/notice/OB11GroupEssenceEvent'
import { omit, isNullable, pick, Dict } from 'cosmokit'
import { omit, pick, Dict } from 'cosmokit'
import { Context } from 'cordis'
import { selfInfo } from '@/common/globalVars'
import { pathToFileURL } from 'node:url'
@@ -72,7 +70,7 @@ export namespace OB11Entities {
sub_type: 'friend',
message: messagePostFormat === 'string' ? '' : [],
message_format: messagePostFormat === 'string' ? 'string' : 'array',
post_type: selfUin == msg.senderUin ? EventType.MESSAGE_SENT : EventType.MESSAGE,
post_type: selfUin === msg.senderUin ? EventType.MESSAGE_SENT : EventType.MESSAGE,
}
if (debug) {
resMsg.raw = msg
@@ -80,21 +78,24 @@ export namespace OB11Entities {
if (msg.chatType === ChatType.Group) {
resMsg.sub_type = 'normal'
resMsg.group_id = parseInt(msg.peerUin)
const member = await ctx.ntGroupApi.getGroupMember(msg.peerUin, msg.senderUin)
if (member) {
resMsg.sender.role = groupMemberRole(member.role)
resMsg.sender.nickname = member.nick
resMsg.sender.title = member.memberSpecialTitle ?? ''
// 284840486: 合并转发内部
if (msg.peerUin !== '284840486') {
const member = await ctx.ntGroupApi.getGroupMember(msg.peerUin, msg.senderUid)
if (member) {
resMsg.sender.role = groupMemberRole(member.role)
resMsg.sender.nickname = member.nick
resMsg.sender.title = member.memberSpecialTitle ?? ''
}
}
}
else if (msg.chatType === ChatType.C2C) {
resMsg.sub_type = 'friend'
resMsg.sender.nickname = (await ctx.ntUserApi.getUserDetailInfo(msg.senderUid)).nick
resMsg.sender.nickname = (await ctx.ntUserApi.getUserSimpleInfo(msg.senderUid)).nick
}
else if (msg.chatType === ChatType.TempC2CFromGroup) {
resMsg.sub_type = 'group'
resMsg.temp_source = 0 //群聊
resMsg.sender.nickname = (await ctx.ntUserApi.getUserDetailInfo(msg.senderUid)).nick
resMsg.sender.nickname = (await ctx.ntUserApi.getUserSimpleInfo(msg.senderUid)).nick
const ret = await ctx.ntMsgApi.getTempChatInfo(ChatType.TempC2CFromGroup, msg.senderUid)
if (ret?.result === 0) {
resMsg.sender.group_id = Number(ret.tmpChatInfo?.groupCode)
@@ -120,7 +121,7 @@ export namespace OB11Entities {
name = content.replace('@', '')
}
messageSegment = {
type: OB11MessageDataType.at,
type: OB11MessageDataType.At,
data: {
qq,
name
@@ -133,7 +134,7 @@ export namespace OB11Entities {
continue
}
messageSegment = {
type: OB11MessageDataType.text,
type: OB11MessageDataType.Text,
data: {
text
}
@@ -169,7 +170,7 @@ export namespace OB11Entities {
throw new Error('回复消息验证失败')
}
messageSegment = {
type: OB11MessageDataType.reply,
type: OB11MessageDataType.Reply,
data: {
id: ctx.store.createMsgShortId(peer, replyMsg ? replyMsg.msgId : records.msgId).toString()
}
@@ -183,7 +184,7 @@ export namespace OB11Entities {
const { picElement } = element
const fileSize = picElement.fileSize ?? '0'
messageSegment = {
type: OB11MessageDataType.image,
type: OB11MessageDataType.Image,
data: {
file: picElement.fileName,
subType: picElement.picSubType,
@@ -211,7 +212,7 @@ export namespace OB11Entities {
}, msg.msgId, element.elementId)
const fileSize = videoElement.fileSize ?? '0'
messageSegment = {
type: OB11MessageDataType.video,
type: OB11MessageDataType.Video,
data: {
file: videoElement.fileName,
url: videoUrl || pathToFileURL(videoElement.filePath).href,
@@ -235,7 +236,7 @@ export namespace OB11Entities {
const { fileElement } = element
const fileSize = fileElement.fileSize ?? '0'
messageSegment = {
type: OB11MessageDataType.file,
type: OB11MessageDataType.File,
data: {
file: fileElement.fileName,
url: pathToFileURL(fileElement.filePath).href,
@@ -260,7 +261,7 @@ export namespace OB11Entities {
const { pttElement } = element
const fileSize = pttElement.fileSize ?? '0'
messageSegment = {
type: OB11MessageDataType.voice,
type: OB11MessageDataType.Record,
data: {
file: pttElement.fileName,
url: pathToFileURL(pttElement.filePath).href,
@@ -283,7 +284,7 @@ export namespace OB11Entities {
else if (element.arkElement) {
const { arkElement } = element
messageSegment = {
type: OB11MessageDataType.json,
type: OB11MessageDataType.Json,
data: {
data: arkElement.bytesData
}
@@ -294,14 +295,14 @@ export namespace OB11Entities {
const { faceIndex, pokeType } = faceElement
if (faceIndex === FaceIndex.Dice) {
messageSegment = {
type: OB11MessageDataType.dice,
type: OB11MessageDataType.Dice,
data: {
result: faceElement.resultId!
}
}
} else if (faceIndex === FaceIndex.RPS) {
messageSegment = {
type: OB11MessageDataType.RPS,
type: OB11MessageDataType.Rps,
data: {
result: faceElement.resultId!
}
@@ -313,7 +314,7 @@ export namespace OB11Entities {
}*/
} else {
messageSegment = {
type: OB11MessageDataType.face,
type: OB11MessageDataType.Face,
data: {
id: faceIndex.toString()
}
@@ -329,7 +330,7 @@ export namespace OB11Entities {
// const url = `https://p.qpic.cn/CDN_STATIC/0/data/imgcache/htdocs/club/item/parcel/item/${dir}/${md5}/300x300.gif?max_age=31536000`
const url = `https://gxh.vip.qq.com/club/item/parcel/item/${dir}/${emojiId}/raw300.gif`
messageSegment = {
type: OB11MessageDataType.mface,
type: OB11MessageDataType.Mface,
data: {
summary: marketFaceElement.faceName!,
url,
@@ -338,20 +339,19 @@ export namespace OB11Entities {
key: marketFaceElement.key
}
}
//mFaceCache.set(emojiId, element.marketFaceElement.faceName!)
}
else if (element.markdownElement) {
const { markdownElement } = element
messageSegment = {
type: OB11MessageDataType.markdown,
type: OB11MessageDataType.Markdown,
data: {
data: markdownElement.content
content: markdownElement.content
}
}
}
else if (element.multiForwardMsgElement) {
messageSegment = {
type: OB11MessageDataType.forward,
type: OB11MessageDataType.Forward,
data: {
id: msg.msgId
}
@@ -375,11 +375,23 @@ export namespace OB11Entities {
if (msg.chatType !== ChatType.C2C) {
return
}
if (msg.msgType !== 5) {
return
}
for (const element of msg.elements) {
if (element.grayTipElement) {
const { grayTipElement } = element
if (grayTipElement.jsonGrayTipElement?.busiId === '1061') {
const json = JSON.parse(grayTipElement.jsonGrayTipElement.jsonStr)
const param = grayTipElement.jsonGrayTipElement.xmlToJsonParam
if (param) {
return new OB11FriendPokeEvent(
Number(param.templParam.get('uin_str1')),
Number(param.templParam.get('uin_str2')),
json.items
)
}
const pokedetail: Dict[] = json.items
//筛选item带有uid的元素
const poke_uid = pokedetail.filter(item => item.uid)
@@ -404,38 +416,17 @@ export namespace OB11Entities {
if (msg.chatType !== ChatType.Group) {
return
}
if (msg.senderUin) {
const member = await ctx.ntGroupApi.getGroupMember(msg.peerUid, msg.senderUin)
if (member && member.cardName !== msg.sendMemberName) {
const event = new OB11GroupCardEvent(
parseInt(msg.peerUid),
parseInt(msg.senderUin),
msg.sendMemberName!,
member.cardName,
)
member.cardName = msg.sendMemberName!
return event
}
if (msg.msgType !== 5 && msg.msgType !== 3) {
return
}
for (const element of msg.elements) {
const grayTipElement = element.grayTipElement
const groupElement = grayTipElement?.groupElement
const xmlElement = grayTipElement?.xmlElement
if (groupElement) {
if (groupElement.type === TipGroupElementType.MemberIncrease) {
ctx.logger.info('收到群成员增加消息', groupElement)
await ctx.sleep(1000)
const member = await ctx.ntGroupApi.getGroupMember(msg.peerUid, groupElement.memberUid)
let memberUin = member?.uin
if (!memberUin) {
memberUin = (await ctx.ntUserApi.getUserDetailInfo(groupElement.memberUid)).uin
}
const adminMember = await ctx.ntGroupApi.getGroupMember(msg.peerUid, groupElement.adminUid)
if (memberUin) {
const operatorUin = adminMember?.uin || memberUin
return new OB11GroupIncreaseEvent(parseInt(msg.peerUid), parseInt(memberUin), parseInt(operatorUin))
}
}
else if (groupElement.type === TipGroupElementType.Ban) {
if (groupElement.type === TipGroupElementType.Ban) {
ctx.logger.info('收到群成员禁言提示', groupElement)
const memberUid = groupElement.shutUp?.member.uid
const adminUid = groupElement.shutUp?.admin.uid
@@ -487,6 +478,13 @@ export namespace OB11Entities {
)
}
}
else if (groupElement.type === TipGroupElementType.MemberIncrease) {
const { memberUid, adminUid } = groupElement
if (memberUid !== selfInfo.uid) return
ctx.logger.info('收到群成员增加消息', groupElement)
const adminUin = adminUid ? await ctx.ntUserApi.getUinByUid(adminUid) : selfInfo.uin
return new OB11GroupIncreaseEvent(+msg.peerUid, +selfInfo.uin, +adminUin)
}
}
else if (element.fileElement) {
return new OB11GroupUploadNoticeEvent(+msg.peerUid, +msg.senderUin!, {
@@ -496,81 +494,70 @@ export namespace OB11Entities {
busid: element.fileElement.fileBizId || 0,
})
}
if (grayTipElement) {
const xmlElement = grayTipElement.xmlElement
if (xmlElement?.templId === '10382') {
const emojiLikeData = new XMLParser({
ignoreAttributes: false,
attributeNamePrefix: '',
}).parse(xmlElement.content)
ctx.logger.info('收到表情回应我的消息', emojiLikeData)
else if (xmlElement) {
if (xmlElement.templId === '10382') {
ctx.logger.info('收到表情回应我的消息', xmlElement.templParam)
try {
const senderUin: string = emojiLikeData.gtip.qq.jp
const msgSeq: string = emojiLikeData.gtip.url.msgseq
const emojiId: string = emojiLikeData.gtip.face.id
const senderUin = xmlElement.templParam.get('jp_uin')
const msgSeq = xmlElement.templParam.get('msg_seq')
const emojiId = xmlElement.templParam.get('face_id')
const peer = {
chatType: ChatType.Group,
guildId: '',
peerUid: msg.peerUid,
}
const replyMsgList = (await ctx.ntMsgApi.queryFirstMsgBySeq(peer, msgSeq)).msgList
const replyMsgList = (await ctx.ntMsgApi.queryFirstMsgBySeq(peer, msgSeq!)).msgList
if (!replyMsgList?.length) {
return
}
const shortId = ctx.store.getShortIdByMsgInfo(peer, replyMsgList[0].msgId)
const shortId = ctx.store.createMsgShortId(peer, replyMsgList[0].msgId)
return new OB11GroupMsgEmojiLikeEvent(
parseInt(msg.peerUid),
parseInt(senderUin),
shortId!,
parseInt(senderUin!),
shortId,
[{
emoji_id: emojiId,
emoji_id: emojiId!,
count: 1,
}]
)
} catch (e) {
ctx.logger.error('解析表情回应消息失败', (e as Error).stack)
}
}
if (
grayTipElement.subElementType == GrayTipElementSubType.XmlMsg &&
xmlElement?.templId == '10179'
) {
ctx.logger.info('收到新人被邀请进群消息', grayTipElement)
if (xmlElement?.content) {
const regex = /jp="(\d+)"/g
const matches: string[] = []
let match: RegExpExecArray | null = null
while ((match = regex.exec(xmlElement.content)) !== null) {
matches.push(match[1])
}
// log("新人进群匹配到的QQ号", matches)
if (matches.length === 2) {
const [inviter, invitee] = matches
return new OB11GroupIncreaseEvent(parseInt(msg.peerUid), parseInt(invitee), parseInt(inviter), 'invite')
}
} else if (xmlElement.templId == '10179') {
ctx.logger.info('收到新人被邀请进群消息', xmlElement)
const invitor = xmlElement.templParam.get('invitor')
const invitee = xmlElement.templParam.get('invitee')
if (invitor && invitee) {
return new OB11GroupIncreaseEvent(+msg.peerUid, +invitee, +invitor, 'invite')
}
}
else if (grayTipElement.subElementType == GrayTipElementSubType.JSON) {
}
if (grayTipElement) {
if (grayTipElement.subElementType == GrayTipElementSubType.JSON) {
const json = JSON.parse(grayTipElement.jsonGrayTipElement!.jsonStr)
if (grayTipElement.jsonGrayTipElement?.busiId === '1061') {
const param = grayTipElement.jsonGrayTipElement.xmlToJsonParam
if (param) {
return new OB11GroupPokeEvent(
Number(msg.peerUid),
Number(param.templParam.get('uin_str1')),
Number(param.templParam.get('uin_str2')),
json.items
)
}
const pokedetail: Dict[] = json.items
//筛选item带有uid的元素
const poke_uid = pokedetail.filter(item => item.uid)
if (poke_uid.length == 2) {
return new OB11GroupPokeEvent(
parseInt(msg.peerUid),
parseInt(await ctx.ntUserApi.getUinByUid(poke_uid[0].uid) ?? 0),
parseInt(await ctx.ntUserApi.getUinByUid(poke_uid[1].uid) ?? 0),
Number(msg.peerUid),
Number(await ctx.ntUserApi.getUinByUid(poke_uid[0].uid) ?? 0),
Number(await ctx.ntUserApi.getUinByUid(poke_uid[1].uid) ?? 0),
pokedetail
)
}
}
if (grayTipElement.jsonGrayTipElement?.busiId === '2401' && json.items[2]) {
} else if (grayTipElement.jsonGrayTipElement?.busiId === '2401' && json.items[2]) {
ctx.logger.info('收到群精华消息', json)
const searchParams = new URL(json.items[2].jp).searchParams
const msgSeq = searchParams.get('seq')
@@ -592,17 +579,16 @@ export namespace OB11Entities {
parseInt(essence.items[0]?.msgSenderUin ?? sourceMsg.senderUin),
parseInt(essence.items[0]?.opUin ?? '0'),
)
}
if (grayTipElement.jsonGrayTipElement?.busiId === '2407') {
} else if (grayTipElement.jsonGrayTipElement?.busiId === '2407') {
const memberUin = json.items[1].param[0]
const title = json.items[3].txt
ctx.logger.info('收到群成员新头衔消息', json)
ctx.ntGroupApi.getGroupMember(msg.peerUid, memberUin).then(member => {
if (!isNullable(member)) {
member.memberSpecialTitle = title
}
})
return new OB11GroupTitleEvent(parseInt(msg.peerUid), parseInt(memberUin), title)
} else if (grayTipElement.jsonGrayTipElement?.busiId === '19217') {
ctx.logger.info('收到新人被邀请进群消息', grayTipElement)
const userId = new URL(json.items[2].jp).searchParams.get('robot_uin')
const operatorId = new URL(json.items[0].jp).searchParams.get('uin')
return new OB11GroupIncreaseEvent(Number(msg.peerUid), Number(userId), Number(operatorId), 'invite')
}
}
}
@@ -614,13 +600,7 @@ export namespace OB11Entities {
msg: RawMessage,
shortId: number
): Promise<OB11FriendRecallNoticeEvent | OB11GroupRecallNoticeEvent | undefined> {
const msgElement = msg.elements.find(
(element) => element.grayTipElement?.subElementType === GrayTipElementSubType.Revoke,
)
if (!msgElement) {
return
}
const revokeElement = msgElement.grayTipElement!.revokeElement
const revokeElement = msg.elements[0].grayTipElement?.revokeElement
if (msg.chatType === ChatType.Group) {
const operator = await ctx.ntGroupApi.getGroupMember(msg.peerUid, revokeElement!.operatorUid)
return new OB11GroupRecallNoticeEvent(
@@ -649,7 +629,7 @@ export namespace OB11Entities {
return friends.map(friend)
}
export function friendV2(raw: FriendV2): OB11User {
export function friendV2(raw: SimpleInfo): OB11User {
return {
...omit(raw.baseInfo, ['richBuffer', 'phoneNum']),
...omit(raw.coreInfo, ['nick']),
@@ -661,60 +641,50 @@ export namespace OB11Entities {
}
}
export function friendsV2(raw: FriendV2[]): OB11User[] {
export function friendsV2(raw: SimpleInfo[]): OB11User[] {
return raw.map(friendV2)
}
export function groupMemberRole(role: number): OB11GroupMemberRole | undefined {
export function groupMemberRole(role: number): OB11GroupMemberRole {
return {
4: OB11GroupMemberRole.owner,
3: OB11GroupMemberRole.admin,
2: OB11GroupMemberRole.member,
}[role]
4: OB11GroupMemberRole.Owner,
3: OB11GroupMemberRole.Admin,
2: OB11GroupMemberRole.Member,
}[role] ?? OB11GroupMemberRole.Member
}
export function sex(sex: Sex): OB11UserSex {
const sexMap = {
[Sex.male]: OB11UserSex.male,
[Sex.female]: OB11UserSex.female,
[Sex.unknown]: OB11UserSex.unknown,
[Sex.Unknown]: OB11UserSex.Unknown,
[Sex.Male]: OB11UserSex.Male,
[Sex.Female]: OB11UserSex.Female,
[Sex.Hidden]: OB11UserSex.Unknown
}
return sexMap[sex] || OB11UserSex.unknown
return sexMap[sex] ?? OB11UserSex.Unknown
}
export function groupMember(group_id: string, member: GroupMember): OB11GroupMember {
export function groupMember(groupId: number, member: GroupMember): OB11GroupMember {
const titleExpireTime = +member.specialTitleExpireTime
const int32Max = 2147483647
return {
group_id: parseInt(group_id),
group_id: groupId,
user_id: parseInt(member.uin),
nickname: member.nick,
card: member.cardName,
sex: sex(member.sex!),
card: member.cardName || member.nick,
sex: OB11UserSex.Unknown,
age: 0,
area: '',
level: String(member.memberLevel ?? 0),
qq_level: (member.qqLevel && calcQQLevel(member.qqLevel)) || 0,
qq_level: 0,
join_time: member.joinTime,
last_sent_time: member.lastSpeakTime,
title_expire_time: 0,
title_expire_time: titleExpireTime > int32Max ? 0 : titleExpireTime,
unfriendly: false,
card_changeable: true,
is_robot: member.isRobot,
shut_up_timestamp: member.shutUpTime,
role: groupMemberRole(member.role),
title: member.memberSpecialTitle || '',
}
}
export function stranger(user: User): OB11User {
return {
...user,
user_id: parseInt(user.uin),
nickname: user.nick,
sex: sex(user.sex!),
age: 0,
qid: user.qid,
login_days: 0,
level: (user.qqLevel && calcQQLevel(user.qqLevel)) || 0,
title: member.memberSpecialTitle,
}
}

View File

@@ -9,6 +9,7 @@ export enum EventType {
}
export abstract class OB11BaseEvent {
[index: string]: any
time = Math.floor(Date.now() / 1000)
self_id = parseInt(selfInfo.uin)
abstract post_type: EventType

View File

@@ -12,7 +12,7 @@ export class OB11GroupDecreaseEvent extends OB11GroupNoticeEvent {
constructor(groupId: number, userId: number, operatorId: number, subType: GroupDecreaseSubType = 'leave') {
super()
this.group_id = groupId
this.operator_id = operatorId // 实际上不应该这么实现,但是现在还没有办法识别用户是被踢出的,还是自己主动退出的
this.operator_id = operatorId
this.user_id = userId
this.sub_type = subType
}

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