Compare commits

...

313 Commits
0.4.5 ... main

Author SHA1 Message Date
57d7e8c8cc
rm py-clone 2025-04-13 00:09:28 +08:00
f4ceef050f
warn 2025-04-11 01:05:02 +08:00
56469f6fbb
写了个 schduler 2025-04-11 00:36:58 +08:00
1f92a62e45
update dep 2025-04-10 23:37:35 +08:00
12d6254c6b
update to edition 2024 2025-04-10 23:36:16 +08:00
e10dff96d2
fix + 简化 2025-04-10 22:56:49 +08:00
ff130d7f84
挪个地方 2025-04-10 22:29:17 +08:00
d4999d1ab3
改掉一点之前写的比较粗糙的地方 2025-04-10 22:28:49 +08:00
546214f52f
fmt! 2025-04-09 23:14:17 +08:00
efc0393ed5
加上value 2025-04-09 21:27:12 +08:00
d93e8f7c9e
可能不需要那么精确(? 2025-04-09 21:22:52 +08:00
9f6206aa5c
update dep 2025-04-09 13:03:34 +08:00
9e72c8a117
fmt 2025-04-09 12:35:27 +08:00
ad66657d37
some todo 2025-04-09 12:35:20 +08:00
8b5275608b
func -> consts 2025-04-09 00:12:09 +08:00
3435b8c0fb
就喜欢花哨的 2025-04-08 00:18:19 +08:00
1711270c9f
dev 就是 dev 2025-04-08 00:07:37 +08:00
4d0a32ca0e
fmt 2025-04-07 23:39:21 +08:00
0826daf410
news! 2025-04-07 23:24:11 +08:00
eb089273c4
更好看一些 2025-04-07 23:22:44 +08:00
4c814c45bc
更新依赖 2025-04-07 23:21:48 +08:00
3c5cf2e92c
更好看的状态显示 2025-04-07 23:21:35 +08:00
15b3e328d4
一些小的重构 2025-04-07 19:40:40 +08:00
5d93e38237
修复了 Python 插件停不下来就真的停不下来的问题 2025-04-07 05:28:29 +08:00
3ce0e0004d
补充 room api 2025-04-06 20:22:42 +08:00
86ab23e45d
添加新 api,我感觉可以发版了 2025-04-06 19:27:09 +08:00
3d08a7da91
处理一下 join request 2025-03-31 06:31:48 +08:00
e41279d843
ica 2.0.1 & dev 0.9.0 2025-03-29 00:20:29 +08:00
9745590c11
update dep 2025-03-27 02:04:19 +08:00
e02918b33d
dep update 2025-03-14 00:00:31 +08:00
befc822be7
cargo update 2025-03-11 23:17:18 +08:00
fe411ba137
fix linux unuse warning and fix linux build 2025-03-11 22:32:39 +08:00
15f08a8cfb
0.8.3 dev 2025-02-13 01:31:36 +08:00
2b1c366643
remove some tokio feature 2025-02-13 01:29:54 +08:00
a63423d545
fmt 2025-02-13 00:05:56 +08:00
65b8e92ce1
2.0.0 2025-02-12 13:43:14 +08:00
c23b3ee67a
ica 1.6.7 & tailchat 1.2.6 2025-02-12 00:02:05 +08:00
ede6640aa9
fmt + 改进 msg display 2025-02-11 23:36:27 +08:00
76a3628d2d
改进一下msg display 2025-02-11 23:31:52 +08:00
4866f2ec2e
我悟了,忘记 save thread了 2025-02-11 23:25:47 +08:00
4eb553473d
a ? 2025-02-11 23:15:20 +08:00
12a32da61e
cancel -> join 2025-02-11 23:07:33 +08:00
0cec518f1d
修复了停不住的问题 2025-02-11 23:06:17 +08:00
a114a92cba
wtf 2025-02-11 23:00:35 +08:00
56a6c39df7
add -env 2025-02-11 22:06:57 +08:00
f8cd207923
add default config 2025-02-11 22:06:49 +08:00
79b306d089
cargo update 2025-02-11 21:47:04 +08:00
d8b4fe06f9
ready for 0.8.2 2025-02-11 21:46:56 +08:00
e5f67475db
add .chain(Some(0)) 2025-02-11 21:44:02 +08:00
d94841b1bd
尝试进行一个 config 2025-02-11 21:21:06 +08:00
62a0a8d3fa
加个 abi3-py38 2025-01-18 12:35:24 +08:00
6638a1f645
0.8.1 2025-01-10 23:51:28 +08:00
073c711c7c
呐呐呐 2025-01-10 23:50:48 +08:00
0275863cfe
呐呐呐 2025-01-10 22:37:48 +08:00
3ed1c3e738
开始 0.8.1 周期了 2025-01-06 23:20:10 +08:00
d4c7a55dcc
呐呐呐 2025-01-06 21:24:20 +08:00
9711af9444
看着没问题? 2025-01-06 21:23:26 +08:00
974c2577c3
还是0.8.0…… 2025-01-06 21:02:52 +08:00
c80e938a78
理论上这还是0.8.0? 2025-01-06 20:54:05 +08:00
32958031d2
clean up 2025-01-06 20:13:20 +08:00
fcf88f0ebb
能跑了吗? 2025-01-06 20:12:35 +08:00
16fee092ba
hmmm 2024-12-06 21:34:38 +08:00
9da0b37db2
ica 1.6.5 api 2024-12-06 00:49:03 +08:00
86c19bc3db
进行一个pre 刀耕火种的修改 2024-11-22 00:01:17 +08:00
75832bfa2e
迁移一些 2024-11-19 22:57:31 +08:00
c5de63f02e
真好…… 2024-11-19 22:51:18 +08:00
686291755d
fmt 2024-11-19 22:18:43 +08:00
38cfd2dce7
进行一个虚心接受批评 2024-11-19 22:17:55 +08:00
98633aa5cc
稍微去除一点 info 之类的 2024-11-19 22:05:11 +08:00
4aa969adc5
多少有点毛病( 2024-11-16 14:05:24 +08:00
9d74853d1e
release 2024-11-08 19:02:35 +08:00
52aafcab19
更新依赖, release 0.7.4 2024-11-08 19:01:34 +08:00
4cf92356c8
cargo update 2024-10-21 23:04:08 +08:00
188c357378
2.12.23 适配 2024-10-21 22:52:28 +08:00
dc936bb7fe
fix action 2024-10-07 20:29:33 +08:00
a8129181a8
update ica version 2024-10-07 20:27:35 +08:00
6407f7eb86
cargo update 2024-10-07 20:23:14 +08:00
780c9aee8d
reee 2024-10-01 19:18:28 +08:00
6d883b0d7d
0.7.3 release 2024-10-01 19:15:42 +08:00
9ba8cfaba3
看起来行了? 2024-10-01 19:13:28 +08:00
b286765213
update dependes 2024-09-28 23:07:38 +08:00
e5ff010a69
0.7.3 2024-09-28 22:40:22 +08:00
ef3fb5a8cc
忽略 download相关room信息 2024-09-27 20:33:29 +08:00
c41d796f61
reee 2024-09-26 00:48:00 +08:00
91aa3f7bd9
更新到 2.12.20 2024-09-19 23:52:45 +08:00
cb87108804
0.7.2 2024-08-18 13:25:38 +08:00
e88d1fe435
去掉 ica-py 的部分 2024-08-18 13:17:48 +08:00
538f43869f
ok 2024-08-18 13:16:20 +08:00
f0cdac3299
版本号 2024-08-18 13:15:53 +08:00
880fc4c5dd
0.7.1? 2024-08-18 13:13:52 +08:00
32f1797edc
缝缝补补又三年 2024-08-18 12:35:37 +08:00
38ba77ce42
0.7.0 2024-08-18 02:39:21 +08:00
313f859c5c
pub~ 2024-08-18 02:38:15 +08:00
ee2c51fbcc
修修补补 2024-08-18 02:34:32 +08:00
1c62e4aa0e
这下小修小补了 2024-08-18 02:14:55 +08:00
113a1518d1
逆天 2024-08-18 02:05:41 +08:00
680934ad3f
ruaaa 2024-08-18 02:04:32 +08:00
5636c8e1d9
疑似有点过于代码多了 2024-08-18 00:11:13 +08:00
d12773981d
hmmm 2024-08-17 21:49:16 +08:00
4da93570c9
丢点更新上去 2024-08-17 21:02:52 +08:00
8c52d898ff
添加 sender_name api 2024-08-09 16:15:05 +08:00
585f0ca331
0.6.11 making 2024-08-09 13:37:42 +08:00
1fa7267f3e
0.6.10! 2024-08-02 21:50:39 +08:00
4479002b8d
reee 2024-08-02 21:34:28 +08:00
4753df8ded
我的问题( 2024-08-02 19:25:55 +08:00
b8fd3cfc19
ree 2024-08-02 19:25:39 +08:00
c970e6ae45
清理插件 2024-08-02 19:25:35 +08:00
751402d5c6
remove ica-py 2024-08-02 19:03:58 +08:00
8dd5c0b7d4
可算加上我心心念念的 uptime 了,目前记录是9d不崩
当然实际上我估计能更长, 如果不怎么更新我估计一个月都没问题(不是)
2024-07-23 23:27:08 +08:00
18df7d1a89
goooo 2024-06-29 01:35:44 +08:00
127f0f0ab4
挪过来 2024-06-29 01:33:03 +08:00
e2e5142688
2024-06-29 01:31:12 +08:00
3b67d58ae2
添加 得意黑 2024-06-29 01:29:44 +08:00
d8e40bfb4d
加点error report 2024-06-29 01:25:49 +08:00
047cc110a4
aa 2024-06-29 01:04:25 +08:00
2f00a3f29a
啊? 2024-06-29 01:03:44 +08:00
0ee8091e0d
修复一下低版本兼容性 2024-06-29 01:02:44 +08:00
13995d2915
2024-06-29 00:56:40 +08:00
17f3a36540
069? 2024-06-28 23:19:10 +08:00
4b1ed03b9a
啊? 2024-06-25 23:27:23 +08:00
38d9988cdc
hmmmm 2024-06-22 20:23:23 +08:00
e6b794173f
fine 2024-06-22 19:37:36 +08:00
f1830d978a
fix? 2024-06-22 19:36:13 +08:00
2fd551e2ac
还真是围啊 2024-06-22 19:15:31 +08:00
3d338b8ba0
namerena 加上 peek 2024-06-22 19:10:53 +08:00
ed6af04570
如更 2024-06-16 22:23:29 +08:00
a02bb089a5
068 p5 2024-06-15 23:31:10 +08:00
1e81db998f
p4? 2024-06-15 02:46:46 +08:00
d9c17ea064
068 working 2024-06-15 02:25:04 +08:00
b1511a972f
p3 2024-06-15 01:34:03 +08:00
de12c495f0
p2 2024-06-15 00:35:35 +08:00
ca9033a23f
p1 2024-06-15 00:35:21 +08:00
a606db5cb0
snapshot! 2024-06-14 01:34:36 +08:00
9002103c7b
试试新的共享方式,虽然略微麻烦,但至少比之前全局共享好 2024-06-14 01:32:23 +08:00
7d2707b35e
添加macro 2024-06-14 01:19:25 +08:00
5db2978eb5
去掉自己的那个macro,准备上下一波 2024-06-14 00:04:51 +08:00
cd67c5b94d
0.6.7 2024-06-12 01:09:20 +08:00
7fbe91e55e
looks good 2024-06-10 22:08:37 +08:00
b3e9588763
避免错误信息太长,以及加点提前过滤 2024-06-10 17:33:33 +08:00
dade195e76
ruaa 2024-06-10 16:55:12 +08:00
6d6e07a48f
加s 2024-06-10 16:49:25 +08:00
fc60e5ad18
把typevar这堆改回去( 2024-06-10 16:46:07 +08:00
fca6b38972
ruaaa 2024-06-10 16:34:08 +08:00
c3be851148
加个warning 2024-06-10 16:32:49 +08:00
c72581750a
修改readme 推荐使用 rust版( 2024-06-10 16:32:09 +08:00
31a490b40c
修了,不少东西(? 2024-06-10 16:25:23 +08:00
aad9ab08f6
一些 0.6.7 的工作 2024-06-10 16:25:23 +08:00
54dfb59b16
Merge pull request #1 from cooollawf/main
readme中出现错误
2024-06-10 16:13:14 +08:00
cooollawf
d249c33d58 Update readme.md 2024-06-10 16:07:42 +08:00
2340916570
remove some TYPE_CHECKING 2024-06-09 20:01:44 +08:00
86cb098b3d
ruaaaa 2024-06-05 21:52:15 +08:00
bf40679012
add tailchat adapt for namerena plugin 2024-06-05 12:51:19 +08:00
6bbbaf4cc6
别骂了别骂了 2024-06-05 00:15:11 +08:00
3a2352d15f
bmcl 2.8.0 2024-06-05 00:14:00 +08:00
79dbebdd4e
0.6.6! 2024-06-05 00:09:08 +08:00
3451424544
几乎完活 2024-06-05 00:03:19 +08:00
ff255426f6
进度是真tm快啊 2024-06-04 23:48:01 +08:00
5a07286d2d
gogogo 2024-06-04 00:22:36 +08:00
0858085df7
坏re 2024-06-04 00:02:26 +08:00
954fbed754
ruaa 2024-06-03 23:53:29 +08:00
9ccb7ad02c
烦死了!哼! 2024-06-03 23:52:16 +08:00
996d2a327f
try tailchat 2024-06-02 23:52:12 +08:00
4669b6a378
use lower 2024-05-27 20:35:47 +08:00
2675ada851
用 in 替换掉 2024-05-27 20:31:40 +08:00
bda8a64f2b
ruaaaaa 2024-05-26 17:18:29 +08:00
f985a741f1
ruaaa 2024-05-26 01:08:48 +08:00
f8332f7761
a? 2024-05-12 21:12:27 +08:00
80c5c18bdb
添加api 2024-05-12 21:09:15 +08:00
b349cdf4af
还是在new里面去掉 sticker 2024-05-12 20:00:38 +08:00
c65a2229cc
喜报, img API 2024-05-12 19:43:03 +08:00
1eba64bf9e
感觉还行 2024-05-12 18:54:53 +08:00
99082ccfd7
先bump版本,待会再研究 2024-05-12 17:37:03 +08:00
4dfb17533d
out limit! 2024-05-10 23:09:30 +08:00
f83f7ceb0f
更新一大堆相关东西 2024-05-10 23:03:44 +08:00
c02ac6234f
加个这玩意 2024-05-10 23:01:19 +08:00
5a6e38274d
ruaaaa 2024-05-10 20:29:10 +08:00
769fd4b7cd
aaaa 2024-05-10 19:58:29 +08:00
49e89ad018
遭不住,遭不住 2024-05-10 19:46:21 +08:00
ae518e5c92
raaaaa 2024-05-10 19:42:01 +08:00
fd932e21fa
改一下 2024-05-10 19:39:46 +08:00
fdd5e03322
ennnn 2024-05-10 19:36:48 +08:00
9b96c69bbe
2024-05-10 19:34:32 +08:00
060f960320
api 大更新( 2024-05-10 19:32:51 +08:00
a99467dfbf
更新ruaa 2024-05-10 19:03:03 +08:00
6fc85322cd
修复一下这个问题 2024-05-10 12:24:21 +08:00
01187cf8d9
啊啊啊啊
a
2024-05-09 22:58:34 +08:00
924d850e01
走! 2024-05-09 22:57:23 +08:00
3defd42df8
开白! 2024-05-09 22:54:23 +08:00
b458a16c22
mts 2024-05-09 21:52:39 +08:00
85a37db3ea
希望没事( 2024-05-09 21:51:01 +08:00
71e07970c0
加一下packages 2024-05-09 21:46:17 +08:00
26133c3c00
fffff 2024-05-09 00:46:17 +08:00
83b163b685
whyyy 2024-05-09 00:41:41 +08:00
177ff4692f
gg 2024-05-09 00:40:51 +08:00
1c33a9a33b
gg 2024-05-09 00:37:56 +08:00
e1851377d9
aaa 2024-05-09 00:35:16 +08:00
62791962c2
好好好 2024-05-09 00:34:44 +08:00
0d1d815847
大为震撼 2024-05-09 00:34:07 +08:00
463169a13a
改个名? 2024-05-09 00:30:41 +08:00
bec856e9fc
试试这样呢 2024-05-09 00:27:48 +08:00
3959dd3dc7
更新namerena,解决/namerenea带了就没法用的问题 2024-05-07 21:55:30 +08:00
c41cc5e3ff
更新bmcl 2024-05-06 22:36:21 +08:00
5cba529e45
更新ica适配到 2.11.9 2024-05-06 20:32:43 +08:00
4116a38020
去掉一个\n 2024-05-05 22:57:01 +08:00
2054e6899a
不显示武器别聊 2024-05-05 22:53:19 +08:00
d564350275
稍微改一下检测 2024-05-05 22:50:03 +08:00
2449dda432
quick fix 2024-05-05 22:43:59 +08:00
c17cc006b1
去掉蹩脚的那对东西 2024-05-05 22:43:11 +08:00
fbf8411752
版本号 update 2024-05-05 22:37:36 +08:00
700c76b69a
继续去掉debug 2024-05-05 22:36:53 +08:00
cc6b52540b
阻止测号 2024-05-05 22:35:35 +08:00
660c0160ad
成! 2024-05-05 22:34:55 +08:00
93f384c0a4
raa 2024-05-05 22:34:09 +08:00
a31efda338
还得加回来 2024-05-05 22:31:57 +08:00
be7bcfd00d
去掉global? 2024-05-05 22:31:01 +08:00
c5b319730b
啊啊啊 2024-05-05 22:29:53 +08:00
69da7f4f00
fine 2024-05-05 22:27:41 +08:00
a47c66202b
ffff 2024-05-05 22:27:07 +08:00
ec4122bdf3
啥玩意啊这是 2024-05-05 22:26:12 +08:00
987f201ed1
aaa 2024-05-05 22:25:18 +08:00
2f1cffc1e6
好了吧 2024-05-05 22:22:53 +08:00
ebd9e99f8d
好了? 2024-05-05 22:21:13 +08:00
91086d01a5
成了吗? 2024-05-05 22:17:51 +08:00
63ea5f0178
rua 2024-05-04 14:17:50 +08:00
1860321b36
fix ica_typing 2024-05-04 14:16:09 +08:00
6335e73a15
2024-05-04 14:13:45 +08:00
8a9494b593
添加版本号 2024-05-04 14:13:25 +08:00
79c2e7f53d
a? 2024-05-04 14:02:47 +08:00
e592f30142
rua 2024-05-04 14:02:00 +08:00
3d0eaf9efd
update depend 2024-05-04 13:39:52 +08:00
06d29ee3d6
update! 2024-05-04 13:37:54 +08:00
84ced3b30a
tailchat p6 2024-03-30 18:30:43 +08:00
3d81fce13b
tailchat p 5 2024-03-30 16:59:06 +08:00
427b113312
添加一些tailchat的py api (虽说还没实际实现 2024-03-30 15:31:42 +08:00
b3e2da9df6
tailchat p3 2024-03-30 14:24:19 +08:00
2f535cc960
tailchat support p2 2024-03-30 12:54:52 +08:00
eaae60902d
tailchat support p1 2024-03-30 12:52:49 +08:00
6dfbc4e879
更新依赖 2024-03-29 02:35:20 +08:00
0dad42c3ca
清理一下 2024-03-29 02:06:55 +08:00
e423745a2c
remove matrix support, use tailchat 0.6.0-dev 2024-03-29 02:03:27 +08:00
aa20b7f1c3
更新矩阵同步超时时长为10分钟 2024-03-18 04:21:10 +08:00
b2229c9663
just goooo 2024-03-18 04:18:37 +08:00
2ab3f0d77b
0.5.3? 2024-03-16 16:58:18 +08:00
ddbdde5ae6
初步实现 matrix bot
还没实现 Python 侧
2024-03-16 14:04:30 +08:00
a574dcaa8a
修改一些细节 2024-03-16 00:15:49 +08:00
e0dbf7d21e
修改版本号机制 2024-03-15 12:27:00 +08:00
84c0426b05
remove matrix from default feature 2024-03-15 06:34:28 +08:00
ec9cd625d1
0.5.2 2024-03-15 01:03:24 +08:00
de09257249
我知道没修好,但是先丢上来 2024-03-15 00:40:22 +08:00
4ae11b4d4f
2024-03-14 01:12:08 +08:00
c83a2c4549
更新依赖库和修改函数命名 2024-03-13 01:32:28 +08:00
8cd8d93a28
优化插件验证逻辑和代码结构 2024-03-13 01:24:32 +08:00
0843826435
进行一个 clicppy 2024-03-13 01:20:41 +08:00
f6e760e234
继续进行一个很逆天的重构 2024-03-13 01:17:50 +08:00
53e652aa7b
扬了( 2024-03-12 22:11:59 +08:00
48d4c8fd5d
更新日志:准备接入Matrix,去掉pyo3-async的依赖,启用ica,更新版本号为0.5.0 2024-03-12 00:47:00 +08:00
51cc24e347
添加Matrix适配器 2024-03-12 00:16:12 +08:00
4656655017
改个名字 2024-02-26 06:11:17 +08:00
bdc7e3e3c3
得了, 先禁用, 才意识到我啥api都没给 2024-02-26 06:10:57 +08:00
14a3d6b3df
该干掉的要干掉( 2024-02-26 06:06:23 +08:00
a3ed392179
0.4.12!
坏了
2024-02-25 21:45:07 +08:00
47b53b245b
更新版本号为1.3.0,修复了ica-async-rs-sync-py的回复消息格式 2024-02-25 21:36:44 +08:00
469db17c3b
更新 IcaClient 类,添加 version 属性***
***更新 IcaClientPy 类,添加 get_version 方法***
***更新 base.py,修改回复消息中的版本号显示
2024-02-25 21:33:11 +08:00
e3708dc41a
大的来了 2024-02-25 18:49:39 +08:00
d362e7c155
加点料( 2024-02-25 18:20:03 +08:00
ae181425e0
优化配置数据访问方法 2024-02-25 17:29:43 +08:00
7b8db8136b
先push一波似乎能跑的( 2024-02-25 17:27:02 +08:00
d246913b4c
加个函数 2024-02-25 16:04:59 +08:00
aa641b4b82
更新上传文件名以匹配特定格式 2024-02-25 12:46:49 +08:00
e14ffbddb4
更改目录结构 2024-02-25 12:37:38 +08:00
1b5c33c1d5
add builds workflow 2024-02-25 12:29:45 +08:00
0ef4aeb4f3
0.4.11 发release吗? 2024-02-25 12:01:36 +08:00
9f5956e77a
优化计数格式化函数 2024-02-25 02:53:35 +08:00
f2624dbcca
更新消息格式和房间ID处理***
***更新消息格式和房间ID处理
2024-02-25 02:26:23 +08:00
b41617bb06
将py部分移到ica-py里 2024-02-25 02:13:22 +08:00
29f6b2efaf
优化代码结构和性能 2024-02-25 02:12:11 +08:00
5381ef598a
修复了Message解析replyMessage字段为null时解析失败的问题 2024-02-25 01:56:47 +08:00
8448b03d83
更新日志~ 2024-02-25 01:34:43 +08:00
4b3da3b85f
调整代码格式和样式 2024-02-25 01:33:56 +08:00
8b2a8ee8d2
更新代码,修复了一些bug和改进了功能。 2024-02-25 01:32:31 +08:00
fe06356bea
优化MessageFile结构体的get_name和get_fid方法 2024-02-25 01:32:15 +08:00
03fdcc300b
更新 IcalinguaStatus 结构体的方法实现 2024-02-25 01:31:56 +08:00
95c2cc377a
添加消息特性接口和实现 2024-02-25 01:31:48 +08:00
63e18e8eab
更新bmcl插件和在线数据模块 2024-02-25 01:31:39 +08:00
16ff8f534e
更新rustfmt配置文件***
***更新了rustfmt配置文件,将最大行长(max_width)从120修改为100,链式调用的最大长度(chain_width)从100修改为70,数组的最大长度(array_width)从100修改为70,函数参数的最大长度(attr_fn_like_width)从100修改为60。
2024-02-25 01:30:56 +08:00
4bad0c95c5
添加rustfmt.toml配置文件,设置代码格式化规则 2024-02-25 01:28:44 +08:00
559de2e2f6
更新bmcl.py插件中的wrap_request函数,添加了异常处理和错误提示功能 2024-02-25 00:29:48 +08:00
85608570bf
更新版本号和添加状态属性 2024-02-25 00:22:09 +08:00
0420cf36b2
更新了ica_typing.py和main.rs文件中的函数签名,添加了on_delete_message、messageSuccess和messageFailed回调函数。添加了delete_message_py和succes_message、failed_message函数来处理相应的事件。 2024-02-24 23:56:36 +08:00
3ed0f5af1e
更新类型定义和回调函数签名***
***更新类型定义和回调函数签名
2024-02-24 23:07:31 +08:00
c366f6a735
就先这样吧( 2024-02-24 23:01:49 +08:00
db3905eec3
更新SendMessage类以支持链式调用 2024-02-24 19:26:21 +08:00
d6443f27bb
先更新一部分能跑的 2024-02-23 16:48:34 +08:00
28ec8d316d
更新版本号和修复Python插件运行错误的问题 2024-02-22 23:53:23 +08:00
727e5f84dd
修复bmcl插件中的bug,现在可以正确处理消息内容中的名称参数。 2024-02-22 23:43:59 +08:00
3b280e23c0
更新bmcl插件,添加/brrs命令以显示排名信息 2024-02-22 23:36:54 +08:00
1f7ffcb2d4
0.4.8 2024-02-22 23:17:20 +08:00
152c8215c3
更新bmcl.py插件,显示名称前添加了一个标志符号,赞助者字段名称更改 2024-02-22 21:25:00 +08:00
ed9ec33ed9
aaa 2024-02-22 21:23:07 +08:00
e09a257886
没分片数据啊 2024-02-22 21:22:01 +08:00
59feec8f4e
简洁信息 2024-02-22 21:20:40 +08:00
2540ba3ee2
消息你得发出去啊 2024-02-22 21:17:42 +08:00
e57cc7f3f0
添加4~10的判定 2024-02-22 21:16:31 +08:00
75f098849e
index +1 2024-02-22 21:09:57 +08:00
64dd2d4ad2
修复syntaxError 2024-02-22 21:06:17 +08:00
954a5a1b19
改进一下bmcl 2024-02-22 21:04:20 +08:00
f254879cf0
remove blake3 2024-02-22 19:19:24 +08:00
8efc7358a3
add message on startup 2024-02-22 19:16:07 +08:00
e2f619e97f
yeeee 2024-02-22 19:10:15 +08:00
f1abfd4f9d
开摆(0.4.7 2024-02-22 19:08:09 +08:00
ef61b3a6b4
2.1.0 for bmcl! 2024-02-22 18:26:09 +08:00
09aaccf291
0.4.6 2024-02-22 15:06:16 +08:00
8c72732671
更新一下内置插件 2024-02-22 14:38:37 +08:00
60 changed files with 8011 additions and 1971 deletions

43
.github/workflows/builds.yml vendored Normal file
View File

@ -0,0 +1,43 @@
name: build and test
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
- name: 获取版本号
id: get_version
uses: sravinet/toml-select@v1.0.1
with:
file: ./ica-rs/Cargo.toml
field: "package.version"
- name: Setup Python
uses: actions/setup-python@v5.0.0
with:
# Version range or exact version of Python or PyPy to use, using SemVer's version range syntax. Reads from .python-version if unset.
python-version: 3.8
- name: Run tests
run: cargo test --verbose
- name: Build
run: cargo build --release
- name: 上传
uses: actions/upload-artifact@v4
with:
name: ica-rs-b${{ github.run_number }}-${{ steps.get_version.outputs.value }}-py38-win-x64
path: ./target/release/ica-rs.exe

45
.github/workflows/publish.yml vendored Normal file
View File

@ -0,0 +1,45 @@
name: publish
on:
workflow_dispatch:
env:
CARGO_TERM_COLOR: always
jobs:
publish: # 全都要!
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
# python-version: ["3.8", "3.9", "3.10", "3.11",]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- name: 获取版本号
id: get_version
uses: sravinet/toml-select@v1.0.1
with:
file: ./ica-rs/Cargo.toml
field: "package.version"
- name: Setup Python
uses: actions/setup-python@v5.0.0
with:
# Version range or exact version of Python or PyPy to use, using SemVer's version range syntax. Reads from .python-version if unset.
python-version: 3.8
- name: Run tests
run: cargo test --verbose
- name: Build
run: cargo build --release
- name: 上传
uses: actions/upload-artifact@v4
with:
name: ica-rs-b${{ github.run_number }}-${{ steps.get_version.outputs.value }}-py38-win-x64
path: ./target/release/ica-rs.exe

9
.gitignore vendored
View File

@ -1,8 +1,13 @@
venv venv
env env*
config.toml config*.toml
.idea
*.pyc *.pyc
*__pycache__/ *__pycache__/
make.*
# Added by cargo
/target

2626
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

10
Cargo.toml Normal file
View File

@ -0,0 +1,10 @@
[workspace]
members = [
"ica-rs"
]
resolver = "2"
[patch.crates-io]
rust_socketio = { git = "https://github.com/shenjackyuanjie/rust-socketio.git", branch = "main" }
# rust_socketio = { path = "../../rust-socketio/socketio" }
# pyo3 = { git = "https://github.com/PyO3/pyo3.git", branch = "main" }

View File

@ -1,4 +1,20 @@
# 填写 [ica] 但不填写此项则不启用 ica
enable_ica = true # 是否启用 ica
# 填写 [matrix] 但不填写此项则不启用 matrix
enable_matrix = true # 是否启用 matrix
enable_py = true # 是否启用 python 插件
[py]
# python 插件路径
plugin_path = "/path/to/your/plugin"
config_path = "/path/to/your/config"
[ica]
private_key = "" # 与 icalingua 客户端使用的 private_key 一致 private_key = "" # 与 icalingua 客户端使用的 private_key 一致
host = "" # docker 版 icalingua 服务的地址 host = "" # docker 版 icalingua 服务的地址
self_id = 0 # 机器人的 qq 号 self_id = 0 # 机器人的 qq 号
@ -10,6 +26,15 @@ notice_start = true # 是否在启动 bot 后通知
# 机器人的管理员 # 机器人的管理员
admin_list = [0] # 机器人的管理员 admin_list = [0] # 机器人的管理员
# 过滤的人
filter_list = [0]
# python 插件路径 [matrix]
py_plugin_path = "/path/to/your/plugin"
home_server = "" # matrix 服务器地址
bot_id = "" # 机器人的 id
bot_password = "" # 机器人的密码
# 启动时通知的房间
notice_room = [""] # 启动 bot 后通知的房间
notice_start = true # 是否在启动 bot 后通知

View File

@ -1,161 +0,0 @@
import time
import json
import random
import asyncio
import argparse
import traceback
from typing import Dict, List, Tuple, Any
import socketio
from colorama import Fore
from nacl.signing import SigningKey
from lib_not_dr.loggers import config
from data_struct import SendMessage, ReplyMessage, get_config, BotConfig, BotStatus
from main import BOTCONFIG, _version_
from router import route
logger = config.get_logger("icalingua")
sio: socketio.AsyncClient = socketio.AsyncClient()
@sio.on("connect") # type: ignore
def connect():
logger.info(f"{Fore.GREEN}icalingua 已连接")
@sio.on("requireAuth") # type: ignore
async def require_auth(salt: str, versions: Dict[str, str]):
logger.info(f"{Fore.BLUE}versions: {versions}\n{type(salt)}|{salt=}")
# 准备数据
sign = SigningKey(bytes.fromhex(BOTCONFIG.private_key))
signature = sign.sign(bytes.fromhex(salt))
# 发送数据
await sio.emit("auth", signature.signature)
logger.info(f"{Fore.BLUE}send auth emit")
@sio.on("auth") # type: ignore
def auth(data: Dict[str, Any]):
logger.info(f"auth: {data}")
@sio.on("authFailed") # type: ignore
async def auth_failed():
logger.info(f"{Fore.RED}authFailed")
await sio.disconnect()
@sio.on("authSucceed") # type: ignore
def auth_succeed():
logger.info(f"{Fore.GREEN}authSucceed")
@sio.on("connect_error") # type: ignore
def connect_error(*args, **kwargs):
logger.info(f"连接错误 {args}, {kwargs}")
@sio.on("updateRoom") # type: ignore
def update_room(data: Dict[str, Any]):
logger.info(f"{Fore.CYAN}update_room: {data}")
@sio.on("addMessage") # type: ignore
async def add_message(data: Dict[str, Any]):
logger.info(f"{Fore.MAGENTA}add_message: {data}")
is_self = data["message"]["senderId"] == BOTCONFIG.self_id
if not is_self:
await route(data, sio)
@sio.on("deleteMessage") # type: ignore
def delete_message(message_id: str):
logger.info(f"{Fore.MAGENTA}delete_message: {message_id}")
@sio.on("setMessages") # type: ignore
def set_messages(data: Dict[str, Any]):
logger.info(
f"{Fore.YELLOW}set_messages: {data}\nmessage_len: {len(data['messages'])}"
)
async def notice_startup(room_list: List[int]):
for notice_room in BOTCONFIG.notice_room:
if notice_room in room_list:
notice_message = SendMessage(
content=f"ica bot v{_version_}", room_id=notice_room
)
await sio.emit("sendMessage", notice_message.to_json())
BotStatus.inited = True
logger.info("inited", tag="notice room")
else:
logger.warn(f"未找到通知房间: {notice_room}", tag="notice room")
await asyncio.sleep(random.randint(2, 5))
@sio.on("setAllRooms") # type: ignore
async def set_all_rooms(rooms: List[Dict[str, Any]]):
BotStatus.running = True
room_list: List[int] = [room.get("roomId") for room in rooms] # type: ignore
if not BotStatus.inited:
logger.info("initing...", tag="setAllRooms")
logger.debug(f"room_list: {room_list}", tag="setAllRooms")
if BOTCONFIG.notice_start:
await notice_startup(room_list)
if room_list != BotStatus.rooms:
logger.info(f"{Fore.YELLOW}set_all_rooms: {rooms}\nlen: {len(rooms)}\n")
BotStatus.rooms = room_list
logger.info(f"更新房间: {room_list}", tag="setAllRooms")
@sio.on("setAllChatGroups") # type: ignore
def set_all_chat_groups(groups: List[Dict[str, Any]]):
logger.info(f"{Fore.YELLOW}set_all_chat_groups: {groups}\nlen: {len(groups)}\n")
@sio.on("notify") # type: ignore
def notify(data: List[Tuple[str, Any]]):
logger.info(f"notify: {data}")
@sio.on("closeLoading") # type: ignore
def close_loading(_):
logger.info(f"{Fore.GREEN}close_loading")
@sio.on("onlineData") # type: ignore
def online_data(data: Dict[str, Any]):
logger.info(f"{Fore.GREEN}online_data: {data}")
@sio.on("*") # type: ignore
def catch_all(event, data):
logger.info(f"{Fore.RED}catch_all: {event}|{data}")
async def main():
"""
while True:
await self.eio.wait()
await self.sleep(1) # give the reconnect task time to start up
if not self._reconnect_task:
break
await self._reconnect_task
if self.eio.state != 'connected':
break
"""
try:
await sio.connect(BOTCONFIG.host)
await sio.wait()
except KeyboardInterrupt:
logger.info("KeyboardInterrupt")
except Exception:
logger.error(traceback.format_exc())

View File

@ -1,102 +0,0 @@
from typing import List, Optional, Union, Literal, Tuple
import qtoml
from lib_not_dr.types import Options
class AtElement(Options):
text: str
id: Union[int, Literal['all']] = 'all'
class ReplyMessage(Options):
id: str
username: str = ''
content: str = ''
files: list = []
def to_json(self) -> dict:
return {
'_id': self.id,
'username': self.username,
'content': self.content,
'files': self.files
}
class SendMessage(Options):
content: str
room_id: Optional[int] = None
room: Optional[int] = None # room id 和 room 二选一 ( 实际上直接填 room id 就行了 )
file: None = None # TODO: 上传文件
reply_to: Optional[ReplyMessage] = None # 源码 给了一个 any TODO: 回复消息
b64_img: Optional[str] = None # TODO: 发送图片
at: Optional[List[AtElement]] = [] # TODO: @某人
sticker: Optional[None] = None # TODO: 发送表情
message_type: Optional[str] = None # TODO: 消息类型
def to_json(self) -> dict:
return {
'content': self.content,
'roomId': self.room_id,
'room': self.room,
'file': self.file,
'replyMessage': self.reply_to.to_json() if self.reply_to else None,
'b64img': self.b64_img,
'at': self.at,
'sticker': self.sticker,
'messageType': self.message_type
}
def to_content(self, content: str) -> "SendMessage":
self.content = content
return self
class NewMessage(Options):
sender_id: int
sender_name: str
room_id: int
content: str
msg_id: str
data: dict
def init(self, **kwargs) -> None:
data = kwargs.pop('data')
self.sender_name = data["message"]["username"]
self.sender_id = data["message"]["senderId"]
self.content = data["message"]["content"]
self.room_id = data["roomId"]
self.msg_id = data["message"]["_id"]
def is_self(self, self_id: int) -> bool:
return self.sender_id == self_id
class BotConfig(Options):
name = 'icalingua bot config'
# _check_filled = True
private_key: str
host: str
self_id: int
notice_room: List[int]
notice_start: bool = False
admin_list: List[int]
py_plugin_path: str
def init(self, **kwargs) -> None:
if self.notice_room is None:
self.notice_start = False
def get_config(config_path: str = 'config.toml') -> BotConfig:
with open(config_path, 'r', encoding='utf-8') as f:
config = qtoml.decoder.load(f)
return BotConfig(**config)
class BotStatus(Options):
inited: bool = False
running: bool = False
rooms: List[int] = []

1
ica-rs/.gitignore vendored
View File

@ -1,2 +1,3 @@
target target
Cargo.lock Cargo.lock
config/

View File

@ -1,32 +1,54 @@
[package] [package]
name = "ica-rs" name = "ica-rs"
version = "0.4.5" version = "0.9.0"
edition = "2021" edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [features]
ed25519 = "2.2.3" default = ["ica", "tailchat"]
ed25519-dalek = "2.1.1" ica = [
hex = "0.4.3" "dep:ed25519",
blake3 = "1.5.0" "dep:ed25519-dalek",
rust_socketio = { version = "0.4.4", features = ["async"]} "dep:hex",
"dep:rust_socketio",
"dep:base64",
]
tailchat = ["dep:rust_socketio", "dep:md-5", "dep:reqwest"]
[dependencies]
# ica
base64 = { version = "0.22", optional = true }
ed25519 = { version = "2.2", optional = true }
ed25519-dalek = { version = "2.1", optional = true }
hex = { version = "0.4", optional = true }
# tailchat
reqwest = { version = "0.12", optional = true, features = ["multipart"] }
md-5 = { version = "0.10", optional = true }
# ica & tailchat (socketio)
rust_socketio = { version = "0.6.0", features = ["async"], optional = true }
# data
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
chrono = "0.4.34" chrono = "0.4"
toml = "0.8.10" toml = "0.8"
colored = "2.1.0" toml_edit = "0.22"
colored = "3.0"
tokio = { version = "1.0", features = ["full"] } # runtime
futures-util = "0.3.30" tokio = { version = "1.43", features = ["rt-multi-thread", "time", "signal", "macros"] }
pyo3 = "0.20.2" futures-util = "0.3"
pyo3 = { version = "0.24", features = ["experimental-async"] }
anyhow = { version = "1.0", features = ["backtrace"] }
# async 这玩意以后在搞
# pyo3-async = "0.3.2" # pyo3-async = "0.3.2"
pyo3-asyncio = { version = "0.20.0", features = ["attributes", "tokio-runtime"] } # pyo3-asyncio = { version = "0.20.0", features = ["attributes", "tokio-runtime"] }
tracing = "0.1.40" # log
tracing-subscriber = { version = "0.3.18", features = ["time"] } tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["time"] }
[patch.crates-io] foldhash = "0.1.4"
rust_socketio = { git = "https://github.com/shenjackyuanjie/rust-socketio.git", branch = "mult_payload" }
# pyo3 = { git = "https://github.com/PyO3/pyo3.git", branch = "main" }

View File

@ -1,82 +0,0 @@
# Python 兼容版本 3.8+
from typing import Optional
class IcaStatus:
@property
def login(self) -> bool:
...
@property
def online(self) -> bool:
...
@property
def self_id(self) -> Optional[bool]:
...
@property
def nick_name(self) -> Optional[str]:
...
@property
def ica_version(self) -> Optional[str]:
...
@property
def os_info(self) -> Optional[str]:
...
@property
def resident_set_size(self) -> Optional[str]:
...
@property
def head_used(self) -> Optional[str]:
...
@property
def load_average(self) -> Optional[str]:
...
class ReplyMessage:
...
class SendMessage:
...
class NewMessage:
def reply_with(self, message: str) -> SendMessage:
...
def __str__(self) -> str:
...
@property
def content(self) -> str:
...
@property
def sender_id(self) -> int:
...
@property
def is_from_self(self) -> bool:
...
@property
def is_reply(self) -> bool:
...
class IcaClient:
@staticmethod
async def send_message_a(client: "IcaClient", message: SendMessage) -> bool:
"""
仅作占位
(因为目前来说, rust调用 Python端没法启动一个异步运行时
所以只能 tokio::task::block_in_place 转换成同步调用)
"""
def send_message(self, message: SendMessage) -> bool:
...
def debug(self, message: str) -> None:
...
def info(self, message: str) -> None:
...
def warn(self, message: str) -> None:
...
def on_message(msg: NewMessage, client: IcaClient) -> None:
...

View File

@ -1,15 +0,0 @@
from typing import TYPE_CHECKING, TypeVar
if TYPE_CHECKING:
from ica_typing import NewMessage, IcaClient
else:
NewMessage = TypeVar("NewMessage")
IcaClient = TypeVar("IcaClient")
_version_ = "1.0.0"
def on_message(msg: NewMessage, client: IcaClient) -> None:
if not msg.is_from_self:
if msg.content == "/bot-rs-py":
reply = msg.reply_with(f"ica-async-rs-sync-py {_version_}")
client.send_message(reply)

View File

@ -1,86 +0,0 @@
import time
import requests
from typing import TYPE_CHECKING, TypeVar
if TYPE_CHECKING:
from ica_typing import NewMessage, IcaClient
else:
NewMessage = TypeVar("NewMessage")
IcaClient = TypeVar("IcaClient")
_version_ = "2.0.0-rs"
def format_data_size(data_bytes: float) -> str:
data_lens = ["B", "KB", "MB", "GB", "TB"]
data_len = "0B"
for i in range(5):
if data_bytes < 1024:
data_bytes = round(data_bytes, 5)
data_len = f"{data_bytes}{data_lens[i]}"
break
else:
data_bytes /= 1024
return data_len
def format_hit_count(count: int) -> str:
"""数据分段, 四位一个下划线
Args:
count (int): 数据
Returns:
str: 格式化后的数据
1 -> 1
1000 -> 1000
10000 -> 1_0000
100000 -> 10_0000
1000000 -> 100_0000
"""
count_str = str(count)
count_len = len(count_str)
if count_len <= 4:
return count_str
else:
return "_".join(count_str[i:i + 4] for i in range(0, count_len, 4))
def bmcl(msg: NewMessage, client: IcaClient) -> None:
req_time = time.time()
# 记录请求时间
response = requests.get("https://bd.bangbang93.com/openbmclapi/metric/dashboard")
if not response.status_code == 200 or response.reason != "OK":
reply = msg.reply_with(f"请求数据失败\n{response.status_code}")
client.warn(
f"数据请求失败, 请检查网络\n{response.status}"
)
client.send_message(reply)
return
data = response.json()
data_bytes: float = data["bytes"]
data_hits: int = data["hits"]
data_bandwidth: float = data["currentBandwidth"]
load_str: float = data["load"] * 100
online_node: int = data["currentNodes"]
online_bandwidth: int = data["bandwidth"]
data_len = format_data_size(data_bytes)
hits_count = format_hit_count(data_hits)
report_msg = (
f"OpenBMCLAPI 状态面板v{_version_} :\n"
f"实时信息: {online_node} 带宽: {online_bandwidth}Mbps\n"
f"负载: {load_str:.2f}% 带宽: {data_bandwidth:.2f}Mbps\n"
f"当日请求: {hits_count} 数据量: {data_len}\n"
f"请求时间: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(req_time))}\n"
"数据源: https://bd.bangbang93.com/pages/dashboard"
)
client.info(report_msg)
reply = msg.reply_with(report_msg)
client.send_message(reply)
def on_message(msg: NewMessage, client: IcaClient) -> None:
if not (msg.is_from_self or msg.is_reply):
if msg.content == "/bmcl":
bmcl(msg, client)

25
ica-rs/rustfmt.toml Normal file
View File

@ -0,0 +1,25 @@
# cargo fmt config
# 最大行长
max_width = 100
# 链式调用的最大长度
chain_width = 80
# 数组的最大长度
array_width = 70
# 函数参数的最大长度
attr_fn_like_width = 60
# 函数调用参数的最大长度
fn_call_width = 80
# 简单函数格式化为单行
fn_single_line = true
# 自动对齐最大长度
enum_discrim_align_threshold = 5
# 字段初始化使用简写
use_field_init_shorthand = true
# 是否使用彩色输出
color = "Always"
edition = "2021"
# 这样不用 nightly 也可以使用 unstable 特性
unstable_features = true

View File

@ -1,106 +0,0 @@
use crate::config::IcaConfig;
use crate::data_struct::messages::SendMessage;
use crate::data_struct::{all_rooms::Room, online_data::OnlineData};
use crate::ClientStatus;
use colored::Colorize;
use ed25519_dalek::{Signature, Signer, SigningKey};
use rust_socketio::asynchronous::Client;
use rust_socketio::Payload;
use serde_json::Value;
use tracing::{debug, warn};
/// "安全" 的 发送一条消息
pub async fn send_message(client: &Client, message: &SendMessage) -> bool {
let value = message.as_value();
match client.emit("sendMessage", value).await {
Ok(_) => {
debug!("send_message {}", format!("{:#?}", message).cyan());
true
}
Err(e) => {
warn!("send_message faild:{}", format!("{:#?}", e).red());
false
}
}
}
#[derive(Debug, Clone)]
pub struct IcalinguaStatus {
pub login: bool,
pub online_data: Option<OnlineData>,
pub rooms: Option<Vec<Room>>,
pub config: Option<IcaConfig>,
}
impl IcalinguaStatus {
pub fn new() -> Self {
Self {
login: false,
online_data: None,
rooms: None,
config: Some(IcaConfig::new_from_cli()),
}
}
pub fn update_online_data(&mut self, online_data: OnlineData) {
self.online_data = Some(online_data);
}
pub fn update_rooms(&mut self, rooms: Vec<Room>) {
self.rooms = Some(rooms);
}
pub fn update_login_status(&mut self, login: bool) {
self.login = login;
}
pub fn update_config(&mut self, config: IcaConfig) {
self.config = Some(config);
}
pub fn get_online_data() -> &'static OnlineData {
unsafe {
ClientStatus
.online_data
.as_ref()
.expect("online_data should be set")
}
}
pub fn get_config() -> &'static IcaConfig {
unsafe { ClientStatus.config.as_ref().expect("config should be set") }
}
}
pub async fn sign_callback(payload: Payload, client: Client) {
// 获取数据
let require_data = match payload {
Payload::Text(json_value) => Some(json_value),
_ => None,
}
.expect("Payload should be Json data");
let (auth_key, version) = (&require_data[0], &require_data[1]);
debug!("auth_key: {:?}, version: {:?}", auth_key, version);
let auth_key = match &require_data.get(0) {
Some(Value::String(auth_key)) => Some(auth_key),
_ => None,
}
.expect("auth_key should be string");
let salt = hex::decode(auth_key).expect("Got an invalid salt from the server");
// 签名
let private_key = IcalinguaStatus::get_config().private_key.clone();
let array_key: [u8; 32] = hex::decode(private_key)
.expect("Not a vaild pub key")
.try_into()
.expect("Not a vaild pub key");
let signing_key: SigningKey = SigningKey::from_bytes(&array_key);
let signature: Signature = signing_key.sign(salt.as_slice());
let sign = signature.to_bytes().to_vec();
client
.emit("auth", sign)
.await
.expect("Faild to send signin data");
}

View File

@ -1,9 +1,12 @@
use std::env; use std::env;
use std::fs; use std::fs;
use colored::Colorize;
use serde::Deserialize; use serde::Deserialize;
use toml::from_str; use toml::from_str;
use crate::data_struct::{ica, tailchat};
/// Icalingua bot 的配置 /// Icalingua bot 的配置
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct IcaConfig { pub struct IcaConfig {
@ -12,27 +15,118 @@ pub struct IcaConfig {
/// icalingua 服务器地址 /// icalingua 服务器地址
pub host: String, pub host: String,
/// bot 的 qq /// bot 的 qq
pub self_id: u64, pub self_id: ica::UserId,
/// 提醒的房间 /// 提醒的房间
pub notice_room: Vec<i64>, #[serde(default = "default_empty_i64_vec")]
pub notice_room: Vec<ica::RoomId>,
/// 是否提醒 /// 是否提醒
#[serde(default = "default_false")]
pub notice_start: bool, pub notice_start: bool,
/// 管理员列表 /// 管理员列表
pub admin_list: Vec<i64>, #[serde(default = "default_empty_i64_vec")]
/// Python 插件路径 pub admin_list: Vec<ica::UserId>,
pub py_plugin_path: Option<String>, /// 过滤列表
#[serde(default = "default_empty_i64_vec")]
pub filter_list: Vec<ica::UserId>,
} }
impl IcaConfig { #[derive(Debug, Clone, Deserialize)]
pub struct TailchatConfig {
/// 服务器地址
pub host: String,
/// 机器人 App ID
pub app_id: String,
/// 机器人 App Secret
pub app_secret: String,
/// 提醒的房间
pub notice_room: Vec<(tailchat::GroupId, tailchat::ConverseId)>,
/// 是否提醒
#[serde(default = "default_false")]
pub notice_start: bool,
/// 管理员列表
#[serde(default = "default_empty_str_vec")]
pub admin_list: Vec<tailchat::UserId>,
/// 过滤列表
#[serde(default = "default_empty_str_vec")]
pub filter_list: Vec<tailchat::UserId>,
}
fn default_plugin_path() -> String { "./plugins".to_string() }
fn default_config_path() -> String { "./config".to_string() }
#[derive(Debug, Clone, Deserialize)]
pub struct PyConfig {
/// 插件路径
#[serde(default = "default_plugin_path")]
pub plugin_path: String,
/// 配置文件夹路径
#[serde(default = "default_config_path")]
pub config_path: String,
}
fn default_empty_i64_vec() -> Vec<i64> { Vec::new() }
fn default_empty_str_vec() -> Vec<String> { Vec::new() }
fn default_false() -> bool { false }
/// 主配置
#[derive(Debug, Clone, Deserialize)]
pub struct BotConfig {
/// 是否启用 icalingua
#[serde(default = "default_false")]
pub enable_ica: bool,
/// Ica 配置
pub ica: Option<IcaConfig>,
/// 是否启用 Tailchat
#[serde(default = "default_false")]
pub enable_tailchat: bool,
/// Tailchat 配置
pub tailchat: Option<TailchatConfig>,
/// 是否启用 Python 插件
#[serde(default = "default_false")]
pub enable_py: bool,
/// Python 插件配置
pub py: Option<PyConfig>,
}
impl BotConfig {
pub fn new_from_path(config_file_path: String) -> Self { pub fn new_from_path(config_file_path: String) -> Self {
// try read config from file // try read config from file
let config = fs::read_to_string(&config_file_path).expect("Failed to read config file"); let config = fs::read_to_string(&config_file_path).expect("Failed to read config file");
let ret: Self = from_str(&config) let ret: Self = from_str(&config).unwrap_or_else(|e| {
.expect(format!("Failed to parse config file {}", &config_file_path).as_str()); panic!("Failed to parse config file {}\ne:{:?}", &config_file_path, e)
});
ret ret
} }
pub fn new_from_cli() -> Self { pub fn new_from_cli() -> Self {
let config_file_path = env::args().nth(1).expect("No config path given"); // let config_file_path = env::args().nth(1).expect("No config path given");
// -c <config_file_path>
let mut config_file_path = "./config.toml".to_string();
let mut args = env::args();
while let Some(arg) = args.next() {
if arg == "-c" {
config_file_path = args.next().unwrap_or_else(|| {
panic!("{}", "No config path given\nUsage: -c <config_file_path>".red())
});
break;
}
}
Self::new_from_path(config_file_path) Self::new_from_path(config_file_path)
} }
/// 检查是否启用 ica
pub fn check_ica(&self) -> bool { self.enable_ica }
/// 检查是否启用 Tailchat
pub fn check_tailchat(&self) -> bool { self.enable_tailchat }
/// 检查是否启用 Python 插件
pub fn check_py(&self) -> bool { self.enable_py }
pub fn ica(&self) -> IcaConfig { self.ica.clone().expect("No ica config found") }
pub fn tailchat(&self) -> TailchatConfig {
self.tailchat.clone().expect("No tailchat config found")
}
pub fn py(&self) -> PyConfig { self.py.clone().expect("No py config found") }
} }

View File

@ -1,82 +0,0 @@
use crate::data_struct::messages::{At, LastMessage};
use crate::data_struct::RoomId;
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
/// export default interface Room {
/// roomId: number
/// roomName: string
/// index: number
/// unreadCount: number
/// priority: 1 | 2 | 3 | 4 | 5
/// utime: number
/// users:
/// | [{ _id: 1; username: '1' }, { _id: 2; username: '2' }]
/// | [{ _id: 1; username: '1' }, { _id: 2; username: '2' }, { _id: 3; username: '3' }]
/// at?: boolean | 'all'
/// lastMessage: LastMessage
/// autoDownload?: boolean
/// downloadPath?: string
/// }
#[derive(Debug, Clone)]
pub struct Room {
pub room_id: RoomId,
pub room_name: String,
pub index: i64,
pub unread_count: u64,
pub priority: u8,
pub utime: i64,
/// 我严重怀疑是脱裤子放屁
/// 历史遗留啊,那没事了()
// pub users: JsonValue,
pub at: At,
pub last_message: LastMessage,
pub auto_download: Option<String>,
pub download_path: Option<String>,
}
impl Room {
pub fn new_from_json(json: &JsonValue) -> Self {
let inner = serde_json::from_value::<InnerRoom>(json.clone()).unwrap();
let at = At::new_from_json(&json["at"]);
Self {
room_id: inner.room_id,
room_name: inner.room_name,
index: inner.index,
unread_count: inner.unread_count,
priority: inner.priority,
utime: inner.utime,
// users: inner.users,
at,
last_message: inner.last_message,
auto_download: inner.auto_download,
download_path: inner.download_path,
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
struct InnerRoom {
#[serde(rename = "roomId")]
pub room_id: RoomId,
#[serde(rename = "roomName")]
pub room_name: String,
#[serde(rename = "index")]
pub index: i64,
#[serde(rename = "unreadCount")]
pub unread_count: u64,
#[serde(rename = "priority")]
pub priority: u8,
#[serde(rename = "utime")]
pub utime: i64,
#[serde(rename = "users")]
pub users: JsonValue,
// 忽略 at
#[serde(rename = "lastMessage")]
pub last_message: LastMessage,
#[serde(rename = "autoDownload")]
pub auto_download: Option<String>,
#[serde(rename = "downloadPath")]
pub download_path: Option<String>,
}

View File

@ -0,0 +1,28 @@
pub mod files;
pub mod messages;
pub mod all_rooms;
pub mod online_data;
/// 房间 id
/// 群聊 < 0
/// 私聊 > 0
pub type RoomId = i64;
pub type UserId = i64;
pub type MessageId = String;
#[allow(unused)]
pub trait RoomIdTrait {
/// 判断是否是群聊
fn is_room(&self) -> bool;
/// 判断是否是私聊
fn is_chat(&self) -> bool { !self.is_room() }
fn as_room_id(&self) -> RoomId;
fn as_chat_id(&self) -> RoomId;
}
impl RoomIdTrait for RoomId {
fn is_room(&self) -> bool { (*self).is_negative() }
fn as_room_id(&self) -> RoomId { -(*self).abs() }
fn as_chat_id(&self) -> RoomId { (*self).abs() }
}

View File

@ -0,0 +1,137 @@
use crate::data_struct::ica::messages::{At, LastMessage, SendMessage};
use crate::data_struct::ica::{RoomId, UserId};
use serde::{Deserialize, Serialize};
use serde_json::{Number, Value as JsonValue};
/// export default interface Room {
/// roomId: number
/// roomName: string
/// index: number
/// unreadCount: number
/// priority: 1 | 2 | 3 | 4 | 5
/// utime: number
/// users:
/// | [{ _id: 1; username: '1' }, { _id: 2; username: '2' }]
/// | [{ _id: 1; username: '1' }, { _id: 2; username: '2' }, { _id: 3; username: '3' }]
/// at?: boolean | 'all'
/// lastMessage: LastMessage
/// autoDownload?: boolean
/// downloadPath?: string
/// }
#[derive(Debug, Clone)]
pub struct Room {
pub room_id: RoomId,
pub room_name: String,
pub index: i64,
pub unread_count: u64,
pub priority: u8,
pub utime: i64,
/// 我严重怀疑是脱裤子放屁
/// 历史遗留啊,那没事了()
// pub users: JsonValue,
pub at: At,
pub last_message: LastMessage,
// 这俩都没啥用
// pub auto_download: Option<String>,
// pub download_path: Option<String>,
}
impl Room {
pub fn new_from_json(raw_json: &JsonValue) -> Self {
let mut parse_json = raw_json.clone();
// 手动 patch 一下 roomId
// ica issue: https://github.com/Icalingua-plus-plus/Icalingua-plus-plus/issues/793
if parse_json.get("roomId").is_none_or(|id| id.is_null()) {
use tracing::warn;
warn!("Room::new_from_json roomId is None, patching it to -1, raw: {:?}", raw_json);
parse_json["roomId"] = JsonValue::Number(Number::from(-1));
}
// 现在 fix 了
let inner = match serde_json::from_value::<InnerRoom>(parse_json) {
Ok(data) => data,
Err(e) => {
panic!("Room::new_from_json error: {}, raw: {:#?}", e, raw_json);
}
};
let at = At::new_from_json(&raw_json["at"]);
Self {
room_id: inner.room_id,
room_name: inner.room_name,
index: inner.index,
unread_count: inner.unread_count,
priority: inner.priority,
utime: inner.utime,
// users: inner.users,
at,
last_message: inner.last_message,
// download_path: inner.download_path,
}
}
pub fn new_message_to(&self, content: String) -> SendMessage {
SendMessage::new(content, self.room_id, None)
}
}
fn room_id_default() -> RoomId { -1 }
#[derive(Debug, Clone, Deserialize, Serialize)]
struct InnerRoom {
#[serde(rename = "roomId", default = "room_id_default")]
pub room_id: RoomId,
#[serde(rename = "roomName")]
pub room_name: String,
#[serde(rename = "index")]
pub index: i64,
#[serde(rename = "unreadCount")]
pub unread_count: u64,
#[serde(rename = "priority")]
pub priority: u8,
#[serde(rename = "utime")]
pub utime: i64,
#[serde(rename = "users")]
pub users: JsonValue,
// 忽略 at
#[serde(rename = "lastMessage")]
pub last_message: LastMessage,
// 这俩都没啥用
// #[serde(rename = "autoDownload")]
// pub auto_download: Option<String>,
// #[serde(rename = "downloadPath")]
// pub download_path: Option<String>,
}
/// ```json
/// {
/// "comment": "问题:从哪里了解到的本群\n答案aaa",
/// "flag": "e4cd5a892ba34bed063196a0cc47a8",
/// "group_id": xxxxx,
/// "group_name": "Nuitka 和 Python 打包",
/// "nickname": "jashcken",
/// "post_type": "request",
/// "request_type": "group",
/// "self_id": 45620725,
/// "sub_type": "add",
/// "time": 1743372872,
/// "tips": "",
/// "user_id": 3838663305
/// }
/// ```
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct JoinRequestRoom {
/// 问题+答案
pub comment: String,
pub group_id: RoomId,
pub group_name: String,
pub user_id: UserId,
pub nickname: String,
// 剩下的应该没用了……吧?
pub request_type: String,
pub post_type: String,
pub sub_type: String,
pub time: i64,
pub tips: String,
pub flag: String,
}

View File

@ -19,10 +19,6 @@ pub struct MessageFile {
} }
impl MessageFile { impl MessageFile {
pub fn get_name(&self) -> Option<&String> { pub fn get_name(&self) -> Option<&String> { self.name.as_ref() }
self.name.as_ref() pub fn get_fid(&self) -> Option<&String> { self.fid.as_ref() }
}
pub fn get_fid(&self) -> Option<&String> {
self.fid.as_ref()
}
} }

View File

@ -0,0 +1,409 @@
use crate::data_struct::ica::files::MessageFile;
use crate::data_struct::ica::{MessageId, RoomId, UserId};
use chrono::DateTime;
use serde::{Deserialize, Serialize};
use serde_json::{Value as JsonValue, json};
use tracing::warn;
pub mod msg_trait;
pub use msg_trait::MessageTrait;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum At {
All,
Bool(bool),
None,
}
impl At {
#[inline]
pub fn new_from_json(json: &JsonValue) -> Self {
match json {
JsonValue::Bool(b) => Self::Bool(*b),
#[allow(non_snake_case)]
JsonValue::String(_I_dont_Care) => Self::All,
_ => Self::None,
}
}
}
/*export default interface LastMessage {
content?: string
timestamp?: string
username?: string
userId?: number
}
*/
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct LastMessage {
pub content: Option<String>,
pub timestamp: Option<String>,
pub username: Option<String>,
// 因为这玩意可能返回 raw buffer, 所以先不解析了
// #[serde(rename = "userId")]
// pub user_id: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ReplyMessage {
#[serde(rename = "_id")]
pub msg_id: String,
pub content: String,
pub files: JsonValue,
#[serde(rename = "username")]
pub sender_name: String,
}
/*
export default interface Message {
_id: string | number
senderId?: number
username: string
content: string
code?: string
timestamp?: string
date?: string
role?: string
file?: MessageFile
files: MessageFile[]
time?: number
replyMessage?: Message
at?: boolean | 'all'
deleted?: boolean
system?: boolean
mirai?: MessageMirai
reveal?: boolean
flash?: boolean
title?: string
anonymousId?: number
anonymousflag?: string
hide?: boolean
bubble_id?: number
subid?: number
head_img?: string
}*/
/// {"message": {"_id":"idddddd","anonymousId":null,"anonymousflag":null,"bubble_id":0,"content":"test","date":"2024/02/18","files":[],"role":"admin","senderId":123456,"subid":1,"time":1708267062000_i64,"timestamp":"22:37:42","title":"索引管理员","username":"shenjack"},"roomId":-123456}
#[derive(Debug, Clone)]
pub struct Message {
// /// 房间 id
// pub room_id: RoomId,
/// 消息 id
pub msg_id: MessageId,
/// 发送者 id
pub sender_id: UserId,
/// 发送者名字
pub sender_name: String,
/// 消息内容
pub content: String,
/// xml / json 内容
pub code: JsonValue,
/// 消息时间
pub time: DateTime<chrono::Utc>,
/// 身份
pub role: String,
/// 文件
pub files: Vec<MessageFile>,
/// 回复的消息
pub reply: Option<ReplyMessage>,
/// At
pub at: At,
/// 是否已撤回
pub deleted: bool,
/// 是否是系统消息
pub system: bool,
/// mirai?
pub mirai: JsonValue,
/// reveal ?
pub reveal: bool,
/// flash
pub flash: bool,
/// "群主授予的头衔"
pub title: String,
/// anonymous id
pub anonymous_id: Option<i64>,
/// 是否已被隐藏
pub hide: bool,
/// 气泡 id
pub bubble_id: i64,
/// 子? id
pub subid: i64,
/// 头像 img?
pub head_img: JsonValue,
/// 原始消息 (准确来说是 json["message"])
pub raw_msg: JsonValue,
}
impl Message {
pub fn new_from_json(json: &JsonValue) -> Self {
// 消息 id
let msg_id = json["_id"].as_str().unwrap();
// 发送者 id (Optional)
let sender_id = json["senderId"].as_i64().unwrap_or(-1);
// 发送者名字 必有
let sender_name = json["username"].as_str().unwrap();
// 消息内容
let content = json["content"].as_str().unwrap();
// xml / json 内容
let code = json["code"].clone();
// 消息时间 (怎么这个也是可选啊(恼))
// 没有就取当前时间
let current = chrono::Utc::now();
let time = json["time"]
.as_i64()
.map(|t| DateTime::from_timestamp_micros(t).unwrap_or(current))
.unwrap_or(current);
// 身份
let role = json["role"].as_str().unwrap_or("unknown");
// 文件
let value_files = json["files"].as_array().unwrap_or(&Vec::new()).to_vec();
let mut files = Vec::with_capacity(value_files.len());
for file in &value_files {
let file = serde_json::from_value::<MessageFile>(file.clone());
if let Ok(file) = file {
files.push(file);
}
}
// 回复的消息
let reply: Option<ReplyMessage> = match json.get("replyMessage") {
Some(value) => {
if !value.is_null() {
match serde_json::from_value::<ReplyMessage>(value.clone()) {
Ok(reply) => Some(reply),
Err(e) => {
warn!("Failed to parse reply message: {}", e);
None
}
}
} else {
None
}
}
None => None,
};
// At
let at = At::new_from_json(&json["at"]);
// 是否已撤回
let deleted = json["deleted"].as_bool().unwrap_or(false);
// 是否是系统消息
let system = json["system"].as_bool().unwrap_or(false);
// mirai
let mirai = json["mirai"].clone();
// reveal
let reveal = json["reveal"].as_bool().unwrap_or(false);
// flash
let flash = json["flash"].as_bool().unwrap_or(false);
// "群主授予的头衔"
let title = json["title"].as_str().unwrap_or("");
// anonymous id
let anonymous_id = json["anonymousId"].as_i64();
// 是否已被隐藏
let hide = json["hide"].as_bool().unwrap_or(false);
// 气泡 id
let bubble_id = json["bubble_id"].as_i64().unwrap_or(1);
// 子? id
let subid = json["subid"].as_i64().unwrap_or(1);
// 头像 img?
let head_img = json["head_img"].clone();
// 原始消息
let raw_msg = json["message"].clone();
Self {
msg_id: msg_id.to_string(),
sender_id,
sender_name: sender_name.to_string(),
content: content.to_string(),
code,
time,
role: role.to_string(),
files,
reply,
at,
deleted,
system,
mirai,
reveal,
flash,
title: title.to_string(),
anonymous_id,
hide,
bubble_id,
subid,
head_img,
raw_msg,
}
}
pub fn output(&self) -> String {
format!(
// >10 >10 >15
// >10 >15
"{:>12}|{:<20}|{}",
self.sender_id, self.sender_name, self.content
)
}
/// 作为回复消息使用
pub fn as_reply(&self) -> ReplyMessage {
ReplyMessage {
// 虽然其实只要这一条就行
msg_id: self.msg_id.clone(),
// 但是懒得动上面的了, 就这样吧
content: self.content.clone(),
files: json!([]),
sender_name: self.sender_name.clone(),
}
}
/// 获取回复
pub fn get_reply(&self) -> Option<&ReplyMessage> { self.reply.as_ref() }
pub fn get_reply_mut(&mut self) -> Option<&mut ReplyMessage> { self.reply.as_mut() }
}
/// 这才是 NewMessage
#[derive(Debug, Clone, Deserialize)]
pub struct NewMessage {
#[serde(rename = "roomId")]
pub room_id: RoomId,
#[serde(rename = "message")]
pub msg: Message,
}
impl NewMessage {
pub fn new(room_id: RoomId, msg: Message) -> Self { Self { room_id, msg } }
/// 创建一条对这条消息的回复
pub fn reply_with(&self, content: &str) -> SendMessage {
SendMessage::new(content.to_string(), self.room_id, Some(self.msg.as_reply()))
}
/// 作为被删除的消息
pub fn as_deleted(&self) -> DeleteMessage {
DeleteMessage {
room_id: self.room_id,
message_id: self.msg.msg_id.clone(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SendMessage {
/// 就是消息内容
pub content: String,
/// 发送的房间 id
#[serde(rename = "roomId")]
pub room_id: RoomId,
/// 回复的消息
#[serde(rename = "replyMessage")]
pub reply_to: Option<ReplyMessage>,
/// @ 谁
#[serde(rename = "at")]
pub at: JsonValue,
/// base64 的图片
#[serde(rename = "b64img")]
file_data: Option<String>,
/// 是否当作表情发送
///
/// 默认 false
pub sticker: bool,
}
impl SendMessage {
pub fn new(content: String, room_id: RoomId, reply_to: Option<ReplyMessage>) -> Self {
Self {
content,
room_id,
reply_to,
at: json!([]),
file_data: None,
sticker: false,
}
}
pub fn as_value(&self) -> JsonValue { serde_json::to_value(self).unwrap() }
/// 设置消息的图片
///
/// as_sticker: 是否当作表情发送
/// file: 图片数据
/// file_type: 图片类型(MIME) (image/png; image/jpeg)
pub fn set_img(&mut self, file: &Vec<u8>, file_type: &str, as_sticker: bool) {
self.sticker = as_sticker;
use base64::{Engine as _, engine::general_purpose};
let base64_data = general_purpose::STANDARD.encode(file);
self.file_data = Some(format!("data:{};base64,{}", file_type, base64_data));
}
}
/// 被删除的消息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeleteMessage {
#[serde(rename = "roomId")]
pub room_id: RoomId,
#[serde(rename = "messageId")]
pub message_id: MessageId,
}
impl DeleteMessage {
pub fn new(room_id: RoomId, message_id: MessageId) -> Self {
Self {
room_id,
message_id,
}
}
pub fn as_value(&self) -> JsonValue { serde_json::to_value(self).unwrap() }
}
// #[cfg(test)]
// mod test {
// use serde_json::json;
// use super::*;
// #[test]
// fn test_new_from_json() {
// let value = json!({"message": {"_id":"idddddd","anonymousId":null,"anonymousflag":null,"bubble_id":0,"content":"test","date":"2024/02/18","files":[],"role":"admin","senderId":123456,"subid":1,"time":1708267062000_i64,"timestamp":"22:37:42","title":"索引管理员","username":"shenjack"},"roomId":-123456});
// let new_message = Message::new_from_json(&value);
// assert_eq!(new_message.msg_id, "idddddd");
// assert_eq!(new_message.sender_id, 123456);
// assert_eq!(new_message.sender_name, "shenjack");
// assert_eq!(new_message.content, "test");
// assert_eq!(new_message.role, "admin");
// assert_eq!(
// new_message.time,
// NaiveDateTime::from_timestamp_micros(1708267062000_i64).unwrap()
// );
// assert!(new_message.files.is_empty());
// assert!(new_message.get_reply().is_none());
// assert!(!new_message.is_reply());
// assert!(!new_message.deleted);
// assert!(!new_message.system);
// assert!(!new_message.reveal);
// assert!(!new_message.flash);
// assert_eq!(new_message.title, "索引管理员");
// assert!(new_message.anonymous_id.is_none());
// assert!(!new_message.hide);
// assert_eq!(new_message.bubble_id, 0);
// assert_eq!(new_message.subid, 1);
// assert!(new_message.head_img.is_null());
// }
// #[test]
// fn test_parse_reply() {
// let value = json!({"message": {"_id":"idddddd","anonymousId":null,"anonymousflag":null,"bubble_id":0,"content":"test","date":"2024/02/18","files":[],"role":"admin","senderId":123456,"subid":1,"time":1708267062000_i64,"timestamp":"22:37:42","title":"索引管理员","username":"shenjack", "replyMessage": {"content": "test", "username": "jackyuanjie", "files": [], "_id": "adwadaw"}},"roomId":-123456});
// let new_message = Message::new_from_json(&value);
// assert_eq!(new_message.get_reply().unwrap().sender_name, "jackyuanjie");
// assert_eq!(new_message.get_reply().unwrap().content, "test");
// assert_eq!(new_message.get_reply().unwrap().msg_id, "adwadaw");
// assert!(new_message
// .get_reply()
// .unwrap()
// .files
// .as_array()
// .unwrap()
// .is_empty());
// }
// }

View File

@ -0,0 +1,164 @@
use std::fmt::Display;
use chrono::DateTime;
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use crate::MainStatus;
use crate::data_struct::ica::messages::{At, Message, NewMessage};
use crate::data_struct::ica::{MessageId, UserId};
impl Serialize for At {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::ser::Serializer,
{
match self {
At::All => serializer.serialize_str("all"),
At::Bool(b) => serializer.serialize_bool(*b),
At::None => serializer.serialize_none(),
}
}
}
impl<'de> Deserialize<'de> for At {
fn deserialize<D>(deserializer: D) -> Result<At, D::Error>
where
D: serde::de::Deserializer<'de>,
{
let value = JsonValue::deserialize(deserializer)?;
Ok(At::new_from_json(&value))
}
}
#[allow(unused)]
pub trait MessageTrait {
fn is_reply(&self) -> bool;
fn is_from_self(&self) -> bool {
let qq_id = MainStatus::global_ica_status().online_status.qqid;
self.sender_id() == qq_id
}
fn msg_id(&self) -> &MessageId;
fn sender_id(&self) -> UserId;
fn sender_name(&self) -> &String;
fn content(&self) -> &String;
fn time(&self) -> &DateTime<chrono::Utc>;
fn role(&self) -> &String;
fn has_files(&self) -> bool;
fn deleted(&self) -> bool;
fn system(&self) -> bool;
fn reveal(&self) -> bool;
fn flash(&self) -> bool;
fn title(&self) -> &String;
fn anonymous_id(&self) -> Option<i64>;
fn hide(&self) -> bool;
fn bubble_id(&self) -> i64;
fn subid(&self) -> i64;
}
impl MessageTrait for Message {
fn is_reply(&self) -> bool { self.reply.is_some() }
fn msg_id(&self) -> &MessageId { &self.msg_id }
fn sender_id(&self) -> UserId { self.sender_id }
fn sender_name(&self) -> &String { &self.sender_name }
fn content(&self) -> &String { &self.content }
fn time(&self) -> &DateTime<chrono::Utc> { &self.time }
fn role(&self) -> &String { &self.role }
fn has_files(&self) -> bool { !self.files.is_empty() }
fn deleted(&self) -> bool { self.deleted }
fn system(&self) -> bool { self.system }
fn reveal(&self) -> bool { self.reveal }
fn flash(&self) -> bool { self.flash }
fn title(&self) -> &String { &self.title }
fn anonymous_id(&self) -> Option<i64> { self.anonymous_id }
fn hide(&self) -> bool { self.hide }
fn bubble_id(&self) -> i64 { self.bubble_id }
fn subid(&self) -> i64 { self.subid }
}
impl<'de> Deserialize<'de> for Message {
fn deserialize<D>(deserializer: D) -> Result<Message, D::Error>
where
D: serde::de::Deserializer<'de>,
{
let value = JsonValue::deserialize(deserializer)?;
Ok(Message::new_from_json(&value))
}
}
impl Display for Message {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if !self.content.is_empty() && !self.content.trim().is_empty() {
write!(f, "{}|{}|{}|{}", self.msg_id(), self.sender_id, self.sender_name, self.content)
} else if !self.files.is_empty() {
write!(
f,
"{}|{}|{}|{:?}",
self.msg_id(),
self.sender_id,
self.sender_name,
self.files[0].name
)
} else {
write!(
f,
"{}|{}|{}|empty content & empty files",
self.msg_id(),
self.sender_id,
self.sender_name
)
}
}
}
impl MessageTrait for NewMessage {
fn is_reply(&self) -> bool { self.msg.reply.is_some() }
fn msg_id(&self) -> &MessageId { &self.msg.msg_id }
fn sender_id(&self) -> UserId { self.msg.sender_id }
fn sender_name(&self) -> &String { &self.msg.sender_name }
fn content(&self) -> &String { &self.msg.content }
fn time(&self) -> &DateTime<chrono::Utc> { &self.msg.time }
fn role(&self) -> &String { &self.msg.role }
fn has_files(&self) -> bool { !self.msg.files.is_empty() }
fn deleted(&self) -> bool { self.msg.deleted }
fn system(&self) -> bool { self.msg.system }
fn reveal(&self) -> bool { self.msg.reveal }
fn flash(&self) -> bool { self.msg.flash }
fn title(&self) -> &String { &self.msg.title }
fn anonymous_id(&self) -> Option<i64> { self.msg.anonymous_id }
fn hide(&self) -> bool { self.msg.hide }
fn bubble_id(&self) -> i64 { self.msg.bubble_id }
fn subid(&self) -> i64 { self.msg.subid }
}
impl Display for NewMessage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if !self.msg.content.trim().is_empty() {
write!(
f,
"{}|{}|{}|{}|{}",
self.msg.msg_id,
self.room_id,
self.msg.sender_id,
self.msg.sender_name,
self.msg.content
)
} else if !self.msg.files.is_empty() {
write!(
f,
"{}|{}|{}|{}|{:?}",
self.msg.msg_id,
self.room_id,
self.msg.sender_id,
self.msg.sender_name,
self.msg.files[0]
)
} else {
write!(
f,
"{}|{}|{}|{}|empty content & empty files",
self.msg.msg_id, self.room_id, self.msg.sender_id, self.msg.sender_name
)
}
}
}

View File

@ -24,7 +24,7 @@ impl IcalinguaInfo {
let mut load = None; let mut load = None;
let mut server_node = None; let mut server_node = None;
let mut client_count = None; let mut client_count = None;
let info_list = s.split("\n").collect::<Vec<&str>>(); let info_list = s.split('\n').collect::<Vec<&str>>();
for info in info_list { for info in info_list {
if info.starts_with("icalingua-bridge-oicq") { if info.starts_with("icalingua-bridge-oicq") {
ica_version = Some(info.split_at(22).1.to_string()); ica_version = Some(info.split_at(22).1.to_string());
@ -40,9 +40,9 @@ impl IcalinguaInfo {
server_node = Some(info.split_at(12).1.to_string()); server_node = Some(info.split_at(12).1.to_string());
} else if info.ends_with("clients connected") { } else if info.ends_with("clients connected") {
client_count = Some( client_count = Some(
info.split(" ") info.split(' ')
.collect::<Vec<&str>>() .collect::<Vec<&str>>()
.get(0) .first()
.unwrap_or(&"1") .unwrap_or(&"1")
.parse::<u16>() .parse::<u16>()
.unwrap_or_else(|e| { .unwrap_or_else(|e| {
@ -141,6 +141,26 @@ impl OnlineData {
} }
} }
impl Default for OnlineData {
fn default() -> Self {
OnlineData {
bkn: -1,
nick: "UNKNOWN".to_string(),
online: false,
qqid: -1,
icalingua_info: IcalinguaInfo {
ica_version: "UNKNOWN".to_string(),
os_info: "UNKNOWN".to_string(),
resident_set_size: "UNKNOWN".to_string(),
heap_used: "UNKNOWN".to_string(),
load: "UNKNOWN".to_string(),
server_node: "UNKNOWN".to_string(),
client_count: 1,
},
}
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -194,13 +214,10 @@ mod tests {
})); }));
assert_eq!(online_data.bkn, 123); assert_eq!(online_data.bkn, 123);
assert_eq!(online_data.nick, "test"); assert_eq!(online_data.nick, "test");
assert_eq!(online_data.online, true); assert!(online_data.online);
assert_eq!(online_data.qqid, 123456); assert_eq!(online_data.qqid, 123456);
assert_eq!(online_data.icalingua_info.ica_version, "2.11.1"); assert_eq!(online_data.icalingua_info.ica_version, "2.11.1");
assert_eq!( assert_eq!(online_data.icalingua_info.os_info, "Linux c038fad79f13 4.4.302+");
online_data.icalingua_info.os_info,
"Linux c038fad79f13 4.4.302+"
);
assert_eq!(online_data.icalingua_info.resident_set_size, "95.43MB"); assert_eq!(online_data.icalingua_info.resident_set_size, "95.43MB");
assert_eq!(online_data.icalingua_info.heap_used, "37.31MB"); assert_eq!(online_data.icalingua_info.heap_used, "37.31MB");
assert_eq!(online_data.icalingua_info.load, "4.23 2.15 1.59"); assert_eq!(online_data.icalingua_info.load, "4.23 2.15 1.59");
@ -213,7 +230,7 @@ mod tests {
let online_data = OnlineData::new_from_json(&serde_json::json!({})); let online_data = OnlineData::new_from_json(&serde_json::json!({}));
assert_eq!(online_data.bkn, -1); assert_eq!(online_data.bkn, -1);
assert_eq!(online_data.nick, "UNKNOWN"); assert_eq!(online_data.nick, "UNKNOWN");
assert_eq!(online_data.online, false); assert!(!online_data.online);
assert_eq!(online_data.qqid, -1); assert_eq!(online_data.qqid, -1);
assert_eq!(online_data.icalingua_info.ica_version, "UNKNOWN"); assert_eq!(online_data.icalingua_info.ica_version, "UNKNOWN");
assert_eq!(online_data.icalingua_info.os_info, "UNKNOWN"); assert_eq!(online_data.icalingua_info.os_info, "UNKNOWN");

View File

@ -1,309 +0,0 @@
use crate::client::IcalinguaStatus;
use crate::data_struct::files::MessageFile;
use crate::data_struct::{MessageId, RoomId, UserId};
use chrono::NaiveDateTime;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value as JsonValue};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum At {
All,
Bool(bool),
None,
}
impl At {
/// new_from_json(&message["at"])
pub fn new_from_json(json: &JsonValue) -> Self {
match json {
JsonValue::Bool(b) => Self::Bool(*b),
#[allow(non_snake_case)]
JsonValue::String(_I_dont_Care) => Self::All,
_ => Self::None,
}
}
}
/*export default interface LastMessage {
content?: string
timestamp?: string
username?: string
userId?: number
}
*/
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct LastMessage {
pub content: Option<String>,
pub timestamp: Option<String>,
pub username: Option<String>,
#[serde(rename = "userId")]
pub user_id: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ReplyMessage {
#[serde(rename = "_id")]
pub msg_id: String,
pub content: String,
pub files: JsonValue,
#[serde(rename = "username")]
pub sender_name: String,
}
/// {"message": {"_id":"idddddd","anonymousId":null,"anonymousflag":null,"bubble_id":0,"content":"test","date":"2024/02/18","files":[],"role":"admin","senderId":123456,"subid":1,"time":1708267062000_i64,"timestamp":"22:37:42","title":"索引管理员","username":"shenjack"},"roomId":-123456}
#[derive(Debug, Clone)]
pub struct NewMessage {
/// 房间 id
pub room_id: RoomId,
/// 消息 id
pub msg_id: MessageId,
/// 发送者 id
pub sender_id: UserId,
/// 发送者名字
pub sender_name: String,
/// 消息内容
pub content: String,
/// xml / json 内容
pub code: JsonValue,
/// 消息时间
pub time: NaiveDateTime,
/// 身份
pub role: String,
/// 文件
pub files: Vec<MessageFile>,
/// 回复的消息
pub reply: Option<ReplyMessage>,
/// At
pub at: At,
/// 是否已撤回
pub deleted: bool,
/// 是否是系统消息
pub system: bool,
/// mirai?
pub mirai: JsonValue,
/// reveal ?
pub reveal: bool,
/// flash
pub flash: bool,
/// "群主授予的头衔"
pub title: String,
/// anonymous id
pub anonymous_id: Option<i64>,
/// 是否已被隐藏
pub hide: bool,
/// 气泡 id
pub bubble_id: i64,
/// 子? id
pub subid: i64,
/// 头像 img?
pub head_img: JsonValue,
/// 原始消息 (准确来说是 json["message"])
pub raw_msg: JsonValue,
}
impl NewMessage {
pub fn new_from_json(json: &JsonValue) -> Self {
// room id 还是必定有的
let room_id = json["roomId"].as_i64().unwrap();
// message 本体也是
let message = json.get("message").unwrap();
// 消息 id
let msg_id = message["_id"].as_str().unwrap();
// 发送者 id (Optional)
let sender_id = message["senderId"].as_i64().unwrap_or(-1);
// 发送者名字 必有
let sender_name = message["username"].as_str().unwrap();
// 消息内容
let content = message["content"].as_str().unwrap();
// xml / json 内容
let code = message["code"].clone();
// 消息时间 (怎么这个也是可选啊(恼))
// 没有就取当前时间
let current = chrono::Utc::now().naive_utc();
let time = message["time"]
.as_i64()
.map(|t| NaiveDateTime::from_timestamp_micros(t).unwrap_or(current))
.unwrap_or(current);
// 身份
let role = message["role"].as_str().unwrap_or("unknown");
// 文件
let value_files = message["files"].as_array().unwrap_or(&Vec::new()).to_vec();
let mut files = Vec::with_capacity(value_files.len());
for file in &value_files {
let file = serde_json::from_value::<MessageFile>(file.clone());
if let Ok(file) = file {
files.push(file);
}
}
// 回复的消息
let reply: Option<ReplyMessage> = match message.get("replyMessage") {
Some(value) => serde_json::from_value::<ReplyMessage>(value.clone()).ok(),
None => None,
};
// At
let at = At::new_from_json(&message["at"]);
// 是否已撤回
let deleted = message["deleted"].as_bool().unwrap_or(false);
// 是否是系统消息
let system = message["system"].as_bool().unwrap_or(false);
// mirai
let mirai = message["mirai"].clone();
// reveal
let reveal = message["reveal"].as_bool().unwrap_or(false);
// flash
let flash = message["flash"].as_bool().unwrap_or(false);
// "群主授予的头衔"
let title = message["title"].as_str().unwrap_or("");
// anonymous id
let anonymous_id = message["anonymousId"].as_i64();
// 是否已被隐藏
let hide = message["hide"].as_bool().unwrap_or(false);
// 气泡 id
let bubble_id = message["bubble_id"].as_i64().unwrap_or(1);
// 子? id
let subid = message["subid"].as_i64().unwrap_or(1);
// 头像 img?
let head_img = message["head_img"].clone();
// 原始消息
let raw_msg = json["message"].clone();
Self {
room_id,
msg_id: msg_id.to_string(),
sender_id,
sender_name: sender_name.to_string(),
content: content.to_string(),
code,
time,
role: role.to_string(),
files,
reply,
at,
deleted,
system,
mirai,
reveal,
flash,
title: title.to_string(),
anonymous_id,
hide,
bubble_id,
subid,
head_img,
raw_msg,
}
}
/// 作为回复消息使用
pub fn as_reply(&self) -> ReplyMessage {
ReplyMessage {
// 虽然其实只要这一条就行
msg_id: self.msg_id.clone(),
// 但是懒得动上面的了, 就这样吧
content: self.content.clone(),
files: json!([]),
sender_name: self.sender_name.clone(),
}
}
/// 创建一条对这条消息的回复
pub fn reply_with(&self, content: &String) -> SendMessage {
SendMessage::new(content.clone(), self.room_id, Some(self.as_reply()))
}
/// 是否是回复
pub fn is_reply(&self) -> bool {
self.reply.is_some()
}
pub fn is_from_self(&self) -> bool {
let qq_id = IcalinguaStatus::get_online_data().qqid;
self.sender_id == qq_id
}
/// 获取回复
pub fn get_reply(&self) -> Option<&ReplyMessage> {
self.reply.as_ref()
}
pub fn get_reply_mut(&mut self) -> Option<&mut ReplyMessage> {
self.reply.as_mut()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SendMessage {
pub content: String,
#[serde(rename = "roomId")]
pub room_id: RoomId,
#[serde(rename = "replyMessage")]
pub reply_to: Option<ReplyMessage>,
#[serde(rename = "at")]
pub at: JsonValue,
}
impl SendMessage {
pub fn new(content: String, room_id: RoomId, reply_to: Option<ReplyMessage>) -> Self {
Self {
content,
room_id,
reply_to,
at: json!([]),
}
}
pub fn as_value(&self) -> JsonValue {
serde_json::to_value(self).unwrap()
}
}
#[cfg(test)]
mod test {
use serde_json::json;
use super::*;
#[test]
fn test_new_from_json() {
let value = json!({"message": {"_id":"idddddd","anonymousId":null,"anonymousflag":null,"bubble_id":0,"content":"test","date":"2024/02/18","files":[],"role":"admin","senderId":123456,"subid":1,"time":1708267062000_i64,"timestamp":"22:37:42","title":"索引管理员","username":"shenjack"},"roomId":-123456});
let new_message = NewMessage::new_from_json(&value);
assert_eq!(new_message.msg_id, "idddddd");
assert_eq!(new_message.sender_id, 123456);
assert_eq!(new_message.sender_name, "shenjack");
assert_eq!(new_message.content, "test");
assert_eq!(new_message.role, "admin");
assert_eq!(
new_message.time,
NaiveDateTime::from_timestamp_micros(1708267062000_i64).unwrap()
);
assert!(new_message.files.is_empty());
assert!(new_message.get_reply().is_none());
assert!(!new_message.is_reply());
assert!(!new_message.deleted);
assert!(!new_message.system);
assert!(!new_message.reveal);
assert!(!new_message.flash);
assert_eq!(new_message.title, "索引管理员");
assert!(new_message.anonymous_id.is_none());
assert!(!new_message.hide);
assert_eq!(new_message.bubble_id, 0);
assert_eq!(new_message.subid, 1);
assert!(new_message.head_img.is_null());
}
#[test]
fn test_parse_reply() {
let value = json!({"message": {"_id":"idddddd","anonymousId":null,"anonymousflag":null,"bubble_id":0,"content":"test","date":"2024/02/18","files":[],"role":"admin","senderId":123456,"subid":1,"time":1708267062000_i64,"timestamp":"22:37:42","title":"索引管理员","username":"shenjack", "replyMessage": {"content": "test", "username": "jackyuanjie", "files": [], "_id": "adwadaw"}},"roomId":-123456});
let new_message = NewMessage::new_from_json(&value);
assert_eq!(new_message.get_reply().unwrap().sender_name, "jackyuanjie");
assert_eq!(new_message.get_reply().unwrap().content, "test");
assert_eq!(new_message.get_reply().unwrap().msg_id, "adwadaw");
assert!(new_message
.get_reply()
.unwrap()
.files
.as_array()
.unwrap()
.is_empty());
}
}

View File

@ -1,9 +1,2 @@
pub mod files; pub mod ica;
pub mod messages; pub mod tailchat;
pub mod all_rooms;
pub mod online_data;
pub type RoomId = i64;
pub type UserId = i64;
pub type MessageId = String;

View File

@ -0,0 +1,8 @@
pub mod api;
pub mod messages;
pub mod status;
pub type GroupId = String;
pub type ConverseId = String;
pub type UserId = String;
pub type MessageId = String;

View File

@ -0,0 +1,8 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct FileUpload {
pub etag: String,
pub path: String,
pub url: String,
}

View File

@ -0,0 +1,238 @@
use std::fmt::Display;
use serde::{Deserialize, Serialize};
use serde_json::{Value as JsonValue, json};
use crate::data_struct::tailchat::{ConverseId, GroupId, MessageId, UserId};
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ReceiveMessage {
/// 消息ID
#[serde(rename = "_id")]
pub msg_id: MessageId,
/// 消息内容
pub content: String,
/// 发送者ID
#[serde(rename = "author")]
pub sender_id: UserId,
/// 服务器ID
/// 在私聊中不存在
#[serde(rename = "groupId")]
pub group_id: Option<GroupId>,
/// 会话ID
#[serde(rename = "converseId")]
pub converse_id: ConverseId,
/// 是否有回复?
#[serde(rename = "hasRecall")]
pub has_recall: bool,
/// 暂时懒得解析这玩意
/// 准确来说是不确定内容, 毕竟没细看 API
pub meta: Option<JsonValue>,
/// 也懒得解析这玩意
pub reactions: Vec<JsonValue>,
/// 创建时间
#[serde(rename = "createdAt")]
pub created_at: String,
/// 更新时间
#[serde(rename = "updatedAt")]
pub updated_at: String,
}
impl ReceiveMessage {
pub fn is_reply(&self) -> bool {
if let Some(meta) = &self.meta {
meta.get("reply").is_some()
} else {
false
}
}
pub fn is_from_self(&self) -> bool {
crate::MainStatus::global_tailchat_status().user_id == self.sender_id
}
/// 创建一个对这条消息的回复
pub fn as_reply(&self) -> SendingMessage {
SendingMessage::new(
"".to_string(),
self.converse_id.clone(),
self.group_id.clone(),
Some(ReplyMeta::from_receive_message(self)),
)
}
/// 回复这条消息
pub fn reply_with(&self, content: &str) -> SendingMessage {
SendingMessage::new(
content.to_string(),
self.converse_id.clone(),
self.group_id.clone(),
Some(ReplyMeta::from_receive_message(self)),
)
}
}
impl Display for ReceiveMessage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// msgid|groupid-converseid|senderid|content
write!(
f,
"{}|{:?}-{}|{}|{}",
self.msg_id, self.group_id, self.converse_id, self.sender_id, self.content
)
}
}
#[derive(Debug, Clone, Default)]
pub enum SendingFile {
#[default]
None,
/// 需要生成
/// [img height=1329 width=1918]{BACKEND}/static/files/6602e20d7b8d10675758e36b/8db505b87bdf9fb309467abcec4d8e2a.png[/img]
Image { file: Vec<u8>, name: String },
/// [card type=file url={BACKEND}/static/files/6602e20d7b8d10675758e36b/9df28943d17b9713cb0ea9625f37d015.wav]Engine.wav[/card]
File { file: Vec<u8>, name: String },
}
impl SendingFile {
pub fn is_some(&self) -> bool { !matches!(self, Self::None) }
pub fn is_image(&self) -> bool { matches!(self, Self::Image { .. }) }
pub fn is_file(&self) -> bool { matches!(self, Self::File { .. }) }
pub fn file_data(&self) -> Vec<u8> {
match self {
Self::Image { file, .. } => file.clone(),
Self::File { file, .. } => file.clone(),
_ => vec![],
}
}
pub fn file_name(&self) -> String {
match self {
Self::Image { name, .. } => name.clone(),
Self::File { name, .. } => name.clone(),
_ => "".to_string(),
}
}
pub fn gen_markdown(&self, backend_path: &str) -> String {
match self {
Self::Image { .. } => {
format!("[img]{}[/img]", backend_path)
}
Self::File { name, .. } => {
format!("[card type=file url={}]{}[/card]", backend_path, name)
}
_ => unreachable!(),
}
}
}
#[derive(Debug, Clone, Serialize)]
/// 将要发送的消息
///
/// 发送时:
/// - `content`: 回复的消息内容
/// - `converseId`: 会话ID
/// - `groupId`: 服务器ID
/// - `meta`: 回复的消息的元数据 可能为空
/// - `mentions`: 被回复的人的ID (可以是多个)
/// - `reply`: 被回复的消息
/// - `_id`: 被回复的消息ID
/// - `author`: 被回复的消息的发送者ID
/// - `content`: 被回复的消息内容
pub struct SendingMessage {
/// 消息内容
///
/// 其实还有个 plain, 就是不知道干啥的
pub content: String,
/// 会话ID
#[serde(rename = "converseId")]
pub converse_id: ConverseId,
/// 服务器ID
#[serde(rename = "groupId")]
pub group_id: Option<GroupId>,
/// 消息的元数据
pub meta: Option<ReplyMeta>,
/// 额外携带的文件
#[serde(skip)]
pub file: SendingFile,
}
impl SendingMessage {
pub fn new(
content: String,
converse_id: ConverseId,
group_id: Option<GroupId>,
meta: Option<ReplyMeta>,
) -> Self {
Self {
content,
converse_id,
group_id,
meta,
file: SendingFile::None,
}
}
pub fn new_without_meta(
content: String,
converse_id: ConverseId,
group_id: Option<GroupId>,
) -> Self {
Self {
content,
converse_id,
group_id,
meta: None,
file: SendingFile::None,
}
}
pub fn contain_file(&self) -> bool { self.file.is_some() }
pub fn add_img(&mut self, file: SendingFile) { self.file = file; }
pub fn as_value(&self) -> JsonValue { serde_json::to_value(self).unwrap() }
}
#[derive(Debug, Clone)]
pub struct ReplyMeta {
/// 被回复的人的ID (可以是多个)
pub mentions: Vec<UserId>,
/// 被回复的消息ID
pub reply_id: MessageId,
/// 被回复的消息的发送者ID
pub reply_author: UserId,
/// 被回复的消息内容
pub reply_content: String,
}
impl ReplyMeta {
pub fn from_receive_message(msg: &ReceiveMessage) -> Self {
Self {
mentions: vec![msg.sender_id.clone()],
reply_id: msg.msg_id.clone(),
reply_author: msg.sender_id.clone(),
reply_content: msg.content.clone(),
}
}
pub fn add_mention(&mut self, user_id: UserId) { self.mentions.push(user_id); }
pub fn replace_content(&mut self, content: String) { self.reply_content = content; }
}
impl Serialize for ReplyMeta {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::ser::Serializer,
{
let reply = json! {
{
"_id": self.reply_id,
"author": self.reply_author,
"content": self.reply_content,
}
};
let mut map = serde_json::Map::new();
map.insert("mentions".to_string(), serde_json::to_value(&self.mentions).unwrap());
map.insert("reply".to_string(), reply);
map.serialize(serializer)
}
}

View File

@ -0,0 +1,65 @@
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use tokio::sync::RwLock;
use crate::data_struct::tailchat::UserId;
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct LoginData {
pub jwt: String,
#[serde(rename = "userId")]
pub user_id: UserId,
pub email: String,
pub nickname: String,
pub avatar: String,
}
impl LoginData {
pub fn update_to_global(&self) {
let status = crate::status::tailchat::MainStatus {
enable: true,
login: true,
user_id: self.user_id.clone(),
nick_name: self.nickname.clone(),
email: self.email.clone(),
jwt_token: self.jwt.clone(),
avatar: self.avatar.clone(),
};
crate::MainStatus::update_tailchat_status(status);
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct UpdateDMConverse {
/// 会话ID
#[serde(rename = "_id")]
pub id: String,
/// 创建时间
#[serde(rename = "createdAt")]
pub created_at: String,
/// 成员
pub members: Vec<UserId>,
/// 类型
#[serde(rename = "type")]
pub converse_type: String,
/// 更新时间
#[serde(rename = "updatedAt")]
pub updated_at: String,
}
#[allow(unused)]
pub type Writeable<T> = Arc<RwLock<T>>;
#[allow(unused)]
#[derive(Debug, Clone)]
pub struct BotStatus {
user_id: UserId,
}
#[allow(unused)]
impl BotStatus {
pub fn new(user_id: UserId) -> Self { Self { user_id } }
pub fn get_user_id(&self) -> UserId { self.user_id.clone() }
}

122
ica-rs/src/error.rs Normal file
View File

@ -0,0 +1,122 @@
// use thiserror::Error;
pub type ClientResult<T, E> = Result<T, E>;
#[derive(Debug)]
pub enum IcaError {
/// Socket IO 链接错误
SocketIoError(rust_socketio::error::Error),
/// 登录失败
LoginFailed(String),
}
#[derive(Debug)]
pub enum TailchatError {
/// Socket IO 链接错误
SocketIoError(rust_socketio::error::Error),
/// reqwest 相关错误
ReqwestError(reqwest::Error),
/// 登录失败
LoginFailed(String),
}
#[derive(Debug)]
pub enum PyPluginError {
/// 插件内未找到指定函数
/// 函数名, 模块名
FuncNotFound(String, String),
/// 插件内函数获取错误
/// pyerr, func_name, module_name
CouldNotGetFunc(pyo3::PyErr, String, String),
/// 插件内函数不可调用
FuncNotCallable(String, String),
/// 插件内函数调用错误
/// pyerr, func_name, module_name
FuncCallError(pyo3::PyErr, String, String),
/// 插件停不下来!
PluginNotStopped,
}
impl From<rust_socketio::Error> for IcaError {
fn from(e: rust_socketio::Error) -> Self { IcaError::SocketIoError(e) }
}
impl From<rust_socketio::Error> for TailchatError {
fn from(e: rust_socketio::Error) -> Self { TailchatError::SocketIoError(e) }
}
impl From<reqwest::Error> for TailchatError {
fn from(e: reqwest::Error) -> Self { TailchatError::ReqwestError(e) }
}
impl std::fmt::Display for IcaError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
IcaError::SocketIoError(e) => write!(f, "Socket IO 链接错误: {}", e),
IcaError::LoginFailed(e) => write!(f, "登录失败: {}", e),
}
}
}
impl std::fmt::Display for TailchatError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TailchatError::SocketIoError(e) => write!(f, "Socket IO 链接错误: {}", e),
TailchatError::ReqwestError(e) => write!(f, "Reqwest 错误: {}", e),
TailchatError::LoginFailed(e) => write!(f, "登录失败: {}", e),
}
}
}
impl std::fmt::Display for PyPluginError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PyPluginError::FuncNotFound(name, module) => {
write!(f, "插件内未找到函数: {} in {}", name, module)
}
PyPluginError::CouldNotGetFunc(py_err, name, module) => {
write!(f, "插件内函数获取错误: {:#?}|{} in {}", py_err, name, module)
}
PyPluginError::FuncNotCallable(name, module) => {
write!(f, "插件内函数不可调用: {} in {}", name, module)
}
PyPluginError::FuncCallError(py_err, name, module) => {
write!(f, "插件内函数调用错误: {:#?}|{} in {}", py_err, name, module)
}
PyPluginError::PluginNotStopped => {
write!(f, "插件未停止")
}
}
}
}
impl std::error::Error for IcaError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
IcaError::SocketIoError(e) => Some(e),
IcaError::LoginFailed(_) => None,
}
}
}
impl std::error::Error for TailchatError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
TailchatError::SocketIoError(e) => Some(e),
TailchatError::ReqwestError(e) => Some(e),
TailchatError::LoginFailed(_) => None,
}
}
}
impl std::error::Error for PyPluginError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
PyPluginError::FuncNotFound(_, _) => None,
PyPluginError::CouldNotGetFunc(e, _, _) => Some(e),
PyPluginError::FuncNotCallable(_, _) => None,
PyPluginError::FuncCallError(e, _, _) => Some(e),
PyPluginError::PluginNotStopped => None,
}
}
}

View File

@ -1,158 +0,0 @@
use colored::Colorize;
use rust_socketio::asynchronous::Client;
use rust_socketio::{Event, Payload};
use tracing::{info, warn};
use crate::client::send_message;
use crate::data_struct::all_rooms::Room;
use crate::data_struct::messages::NewMessage;
use crate::data_struct::online_data::OnlineData;
use crate::{py, VERSION};
/// 获取在线数据
pub async fn get_online_data(payload: Payload, _client: Client) {
if let Payload::Text(values) = payload {
if let Some(value) = values.first() {
let online_data = OnlineData::new_from_json(value);
info!(
"update_online_data {}",
format!("{:#?}", online_data).cyan()
);
unsafe {
crate::ClientStatus.update_online_data(online_data);
}
}
}
}
/// 接收消息
pub async fn add_message(payload: Payload, client: Client) {
if let Payload::Text(values) = payload {
if let Some(value) = values.first() {
let message = NewMessage::new_from_json(value);
info!("add_message {}", format!("{:#?}", message).cyan());
// if message.is_reply() {
// return;
// }
// if message.is_from_self() {
// return;
// }
// 就在这里处理掉最基本的消息
// 之后的处理交给插件
if message.content.eq("/bot-rs") && !message.is_from_self() && !message.is_reply() {
let reply = message.reply_with(&format!("ica-async-rs pong v{}", VERSION));
send_message(&client, &reply).await;
}
// python 插件
py::new_message_py(&message, &client).await;
}
}
}
/// 撤回消息
pub async fn delete_message(payload: Payload, _client: Client) {
if let Payload::Text(values) = payload {
// 消息 id
if let Some(value) = values.first() {
if let Some(msg_id) = value.as_str() {
warn!("delete_message {}", format!("{}", msg_id).yellow());
}
}
}
}
pub async fn update_all_room(payload: Payload, _client: Client) {
if let Payload::Text(values) = payload {
if let Some(value) = values.first() {
if let Some(raw_rooms) = value.as_array() {
let rooms: Vec<Room> = raw_rooms
.iter()
.map(|room| Room::new_from_json(room))
.collect();
unsafe {
crate::ClientStatus.update_rooms(rooms.clone());
}
info!("update_all_room {}", rooms.len());
}
}
}
}
/// 所有
pub async fn any_event(event: Event, payload: Payload, _client: Client) {
let handled = vec![
// 真正处理过的
"authSucceed",
"authFailed",
"authRequired",
"requireAuth",
"onlineData",
"addMessage",
"deleteMessage",
"setAllRooms",
// 忽略的
"notify",
"closeLoading", // 发送消息/加载新聊天 有一个 loading
"updateRoom",
];
match &event {
Event::Custom(event_name) => {
if handled.contains(&event_name.as_str()) {
return;
}
}
Event::Message => {
match payload {
Payload::Text(values) => {
if let Some(value) = values.first() {
if handled.contains(&value.as_str().unwrap()) {
return;
}
info!("收到消息 {}", value.to_string().yellow());
}
}
_ => (),
}
return;
}
_ => (),
}
match payload {
Payload::Binary(ref data) => {
println!("event: {} |{:?}", event, data)
}
Payload::Text(ref data) => {
print!("event: {}", event.as_str().purple());
for value in data {
println!("|{}", value.to_string());
}
}
_ => (),
}
}
pub async fn connect_callback(payload: Payload, _client: Client) {
match payload {
Payload::Text(values) => {
if let Some(value) = values.first() {
match value.as_str() {
Some("authSucceed") => {
info!("{}", "已经登录到 icalingua!".green())
}
Some("authFailed") => {
info!("{}", "登录到 icalingua 失败!".red());
panic!("登录失败")
}
Some("authRequired") => {
warn!("{}", "需要登录到 icalingua!".yellow())
}
Some(msg) => {
warn!("未知消息 {}", msg.yellow())
}
None => (),
}
}
}
_ => (),
}
}

133
ica-rs/src/ica.rs Normal file
View File

@ -0,0 +1,133 @@
pub mod client;
pub mod events;
// use std::sync::OnceLock;
use colored::Colorize;
use rust_socketio::asynchronous::{Client, ClientBuilder};
use rust_socketio::{Event, Payload, TransportType};
use rust_socketio::{async_any_callback, async_callback};
use tracing::{Level, event, span};
use crate::config::IcaConfig;
use crate::error::{ClientResult, IcaError};
use crate::{StopGetter, version_str};
/// icalingua 客户端的兼容版本号
pub const ICA_PROTOCOL_VERSION: &str = "2.12.28";
// mod status {
// use crate::data_struct::ica::all_rooms::Room;
// pub use crate::data_struct::ica::online_data::OnlineData;
// #[derive(Debug, Clone)]
// pub struct MainStatus {
// /// 是否启用 ica
// pub enable: bool,
// /// qq 是否登录
// pub qq_login: bool,
// /// 当前已加载的消息数量
// pub current_loaded_messages_count: u64,
// /// 房间数据
// pub rooms: Vec<Room>,
// /// 在线数据 (Icalingua 信息)
// pub online_status: OnlineData,
// }
// impl MainStatus {
// pub fn update_rooms(&mut self, room: Vec<Room>) { self.rooms = room; }
// pub fn update_online_status(&mut self, status: OnlineData) { self.online_status = status; }
// }
// }
// static ICA_STATUS: OnceLock<status::MainStatus> = OnceLock::new();
pub async fn start_ica(config: &IcaConfig, stop_reciver: StopGetter) -> ClientResult<(), IcaError> {
let span = span!(Level::INFO, "Icalingua Client");
let _enter = span.enter();
event!(Level::INFO, "ica-async-rs v{} initing", crate::ICA_VERSION);
let start_connect_time = std::time::Instant::now();
let socket = match ClientBuilder::new(config.host.clone())
.transport_type(TransportType::Websocket)
.on_any(async_any_callback!(events::any_event))
.on("requireAuth", async_callback!(client::sign_callback))
.on("message", async_callback!(events::connect_callback))
.on("authSucceed", async_callback!(events::connect_callback))
.on("authFailed", async_callback!(events::connect_callback))
.on("messageSuccess", async_callback!(events::success_message))
.on("messageFailed", async_callback!(events::failed_message))
.on("onlineData", async_callback!(events::get_online_data))
.on("setAllRooms", async_callback!(events::update_all_room))
.on("setMessages", async_callback!(events::set_messages))
.on("addMessage", async_callback!(events::add_message))
.on("deleteMessage", async_callback!(events::delete_message))
.on("handleRequest", async_callback!(events::join_request))
.connect()
.await
{
Ok(client) => {
event!(
Level::INFO,
"{}",
format!("socketio connected time: {:?}", start_connect_time.elapsed()).on_cyan()
);
client
}
Err(e) => {
event!(Level::ERROR, "socketio connect failed: {}", e);
return Err(IcaError::SocketIoError(e));
}
};
if config.notice_start {
for room in config.notice_room.iter() {
let startup_msg = crate::data_struct::ica::messages::SendMessage::new(
format!("{}\n启动成功", version_str()),
*room,
None,
);
// 这可是 qq, 要保命
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
event!(Level::INFO, "发送启动消息到房间: {}", room);
if let Err(e) =
socket.emit("sendMessage", serde_json::to_value(startup_msg).unwrap()).await
{
event!(Level::INFO, "启动信息发送失败 房间:{}|e:{}", room, e);
}
}
}
// 等待停止信号
event!(Level::INFO, "{}", "ica client waiting for stop signal".purple());
stop_reciver.await.ok();
event!(Level::INFO, "{}", "socketio client stopping".yellow());
match socket.disconnect().await {
Ok(_) => {
event!(Level::INFO, "{}", "socketio client stopped".green());
Ok(())
}
Err(e) => {
// 单独处理 SocketIoError(IncompleteResponseFromEngineIo(WebsocketError(AlreadyClosed)))
match e {
rust_socketio::Error::IncompleteResponseFromEngineIo(inner_e) => {
if inner_e.to_string().contains("AlreadyClosed") {
event!(Level::INFO, "{}", "socketio client stopped".green());
Ok(())
} else {
event!(Level::ERROR, "socketio 客户端出现了 Error: {:?}", inner_e);
Err(IcaError::SocketIoError(
rust_socketio::Error::IncompleteResponseFromEngineIo(inner_e),
))
}
}
e => {
event!(Level::ERROR, "socketio client stopped with error: {}", e);
Err(IcaError::SocketIoError(e))
}
}
}
}
}

144
ica-rs/src/ica/client.rs Normal file
View File

@ -0,0 +1,144 @@
use crate::MainStatus;
use crate::data_struct::ica::messages::{DeleteMessage, SendMessage};
use crate::data_struct::ica::{RoomId, RoomIdTrait, UserId};
use crate::error::{ClientResult, IcaError};
use colored::Colorize;
use ed25519_dalek::{Signature, Signer, SigningKey};
use rust_socketio::Payload;
use rust_socketio::asynchronous::Client;
use serde_json::{Value, json};
use tracing::{Level, event, span};
/// "安全" 的 发送一条消息
pub async fn send_message(client: &Client, message: &SendMessage) -> bool {
let value = message.as_value();
match client.emit("sendMessage", value).await {
Ok(_) => {
event!(Level::INFO, "send_message {}", format!("{:#?}", message).cyan());
true
}
Err(e) => {
event!(Level::WARN, "send_message faild:{}", format!("{:#?}", e).red());
false
}
}
}
/// "安全" 的 删除一条消息
pub async fn delete_message(client: &Client, message: &DeleteMessage) -> bool {
let value = message.as_value();
match client.emit("deleteMessage", value).await {
Ok(_) => {
event!(Level::DEBUG, "delete_message {}", format!("{:#?}", message).yellow());
true
}
Err(e) => {
event!(Level::WARN, "delete_message faild:{}", format!("{:#?}", e).red());
false
}
}
}
/// "安全" 的 获取历史消息
/// ```typescript
/// async fetchHistory(messageId: string, roomId: number, currentLoadedMessagesCount: number)
/// ```
// #[allow(dead_code)]
// pub async fn fetch_history(client: &Client, roomd_id: RoomId) -> bool { false }
async fn inner_sign(payload: Payload, client: &Client) -> ClientResult<(), IcaError> {
let span = span!(Level::INFO, "signing icalingua");
let _guard = span.enter();
// 获取数据
let require_data = match payload {
Payload::Text(json_value) => Ok(json_value),
_ => Err(IcaError::LoginFailed("Got a invalid payload".to_string())),
}?;
let (auth_key, version) = (&require_data[0], &require_data[1]);
event!(
Level::INFO,
"服务器发过来的待签名key: {:?}, 服务端版本号: {:?}",
auth_key,
version
);
// 判定和自己的兼容版本号是否 一致
let server_protocol_version = version
.get("protocolVersion")
.unwrap_or(&Value::Null)
.as_str()
.unwrap_or("unknow");
if server_protocol_version != crate::ica::ICA_PROTOCOL_VERSION {
event!(
Level::WARN,
"服务器版本与兼容版本不一致\n服务器协议版本:{:?}\n兼容版本:{}",
version.get("protocolVersion"),
crate::ica::ICA_PROTOCOL_VERSION
);
}
let auth_key = match &require_data.first() {
Some(Value::String(auth_key)) => Ok(auth_key),
_ => Err(IcaError::LoginFailed("Got a invalid auth_key".to_string())),
}?;
let salt = hex::decode(auth_key).expect("Got an invalid salt from the server");
// 签名
let private_key = MainStatus::global_config().ica().private_key.clone();
let array_key: [u8; 32] = hex::decode(private_key)
.expect("配置文件设置的私钥不是一个有效的私钥, 无法使用hex解析")
.try_into()
.expect("配置文件设置的私钥不是一个有效的私钥, 无法转换为[u8; 32]数组");
let signing_key: SigningKey = SigningKey::from_bytes(&array_key);
let signature: Signature = signing_key.sign(salt.as_slice());
// 发送签名
let sign = signature.to_bytes().to_vec();
client.emit("auth", sign).await.expect("发送签名信息失败");
Ok(())
}
/// 签名回调
/// 失败的时候得 panic
pub async fn sign_callback(payload: Payload, client: Client) {
inner_sign(payload, &client).await.expect("Faild to sign");
}
/// 向指定群发送签到信息
///
/// 只能是群啊, 不能是私聊
pub async fn send_room_sign_in(client: &Client, room_id: RoomId) -> bool {
if room_id.is_chat() {
event!(Level::WARN, "不能向私聊发送签到信息");
return false;
}
let data = json!(room_id.abs());
match client.emit("sendGroupSign", data).await {
Ok(_) => {
event!(Level::INFO, "已向群 {} 发送签到信息", room_id);
true
}
Err(e) => {
event!(Level::ERROR, "向群 {} 发送签到信息失败: {}", room_id, e);
false
}
}
}
/// 向某个群/私聊的某个人发送戳一戳
pub async fn send_poke(client: &Client, room_id: RoomId, target: UserId) -> bool {
let data = vec![json!(room_id), json!(target)];
match client.emit("sendGroupPoke", data).await {
Ok(_) => {
event!(Level::INFO, "已向 {} 的 {} 发送戳一戳", room_id, target);
true
}
Err(e) => {
event!(Level::ERROR, "向 {} 的 {} 发送戳一戳失败: {}", room_id, target, e);
false
}
}
}

296
ica-rs/src/ica/events.rs Normal file
View File

@ -0,0 +1,296 @@
use colored::Colorize;
use rust_socketio::asynchronous::Client;
use rust_socketio::{Event, Payload};
use serde_json::json;
use tracing::{Level, event, info, span, warn};
use crate::data_struct::ica::RoomId;
use crate::data_struct::ica::all_rooms::{JoinRequestRoom, Room};
use crate::data_struct::ica::messages::{Message, MessageTrait, NewMessage};
use crate::data_struct::ica::online_data::OnlineData;
use crate::ica::client::send_message;
use crate::{MainStatus, VERSION, client_id, help_msg, py, version_str};
/// 获取在线数据
pub async fn get_online_data(payload: Payload, _client: Client) {
if let Payload::Text(values) = payload {
if let Some(value) = values.first() {
let online_data = OnlineData::new_from_json(value);
event!(Level::DEBUG, "update_online_data {}", format!("{:?}", online_data).cyan());
MainStatus::global_ica_status_mut().update_online_status(online_data);
}
}
}
/// 接收消息
pub async fn add_message(payload: Payload, client: Client) {
if let Payload::Text(values) = payload {
if let Some(value) = values.first() {
let message: NewMessage = serde_json::from_value(value.clone()).unwrap();
// 检测是否在过滤列表内
if MainStatus::global_config().ica().filter_list.contains(&message.msg.sender_id) {
return;
}
println!("new_msg {}", message.to_string().cyan());
// 就在这里处理掉最基本的消息
// 之后的处理交给插件
if !message.is_from_self() && !message.is_reply() {
if message.content() == "/bot-rs" {
let reply = message.reply_with(&version_str());
send_message(&client, &reply).await;
} else if message.content() == "/bot-ls" {
let reply = message.reply_with(&format!(
"shenbot-py v{}-{}\n{}",
VERSION,
client_id(),
if MainStatus::global_config().check_py() {
py::PyStatus::display()
} else {
"未启用 Python 插件".to_string()
}
));
send_message(&client, &reply).await;
} else if message.content() == "/bot-help" {
let reply = message.reply_with(&help_msg());
send_message(&client, &reply).await;
}
// else if message.content() == "/bot-uptime" {
// let duration = match start_up_time().elapsed() {
// Ok(d) => format!("{:?}", d),
// Err(e) => format!("出问题啦 {:?}", e),
// };
// let reply = message.reply_with(&format!(
// "shenbot 已运行: {}", duration
// ));
// send_message(&client, &reply).await;
// }
if MainStatus::global_config().ica().admin_list.contains(&message.sender_id()) {
// admin 区
// 先判定是否为 admin
let client_id = client_id();
if message.content().starts_with(&format!("/bot-enable-{}", client_id)) {
// 尝试获取后面的信息
if let Some((_, name)) = message.content().split_once(" ") {
match py::PyStatus::get().get_status(name) {
None => {
let reply = message.reply_with("未找到插件");
send_message(&client, &reply).await;
}
Some(true) => {
let reply = message.reply_with("无变化, 插件已经启用");
send_message(&client, &reply).await;
}
Some(false) => {
py::PyStatus::get_mut().set_status(name, true);
let reply = message.reply_with("启用插件完成");
send_message(&client, &reply).await;
}
}
}
} else if message.content().starts_with(&format!("/bot-disable-{}", client_id))
{
if let Some((_, name)) = message.content().split_once(" ") {
match py::PyStatus::get().get_status(name) {
None => {
let reply = message.reply_with("未找到插件");
send_message(&client, &reply).await;
}
Some(false) => {
let reply = message.reply_with("无变化, 插件已经禁用");
send_message(&client, &reply).await;
}
Some(true) => {
py::PyStatus::get_mut().set_status(name, false);
let reply = message.reply_with("禁用插件完成");
send_message(&client, &reply).await;
}
}
}
} else if message.content() == "/bot-fetch" {
let reply = message.reply_with("正在更新当前群消息");
send_message(&client, &reply).await;
fetch_messages(&client, message.room_id).await;
}
}
}
// python 插件
py::call::ica_new_message_py(&message, &client).await;
}
}
}
/// 理论上不会用到 (因为依赖一个客户端去请求)
/// 但反正实际上还是我去请求, 所以只是暂时
/// 加载一个房间的所有消息
pub async fn set_messages(payload: Payload, _client: Client) {
if let Payload::Text(values) = payload {
if let Some(value) = values.first() {
let messages: Vec<Message> = serde_json::from_value(value["messages"].clone()).unwrap();
let room_id = value["roomId"].as_i64().unwrap();
info!("set_messages {} len: {}", room_id.to_string().cyan(), messages.len());
}
}
}
/// 撤回消息
pub async fn delete_message(payload: Payload, client: Client) {
if let Payload::Text(values) = payload {
// 消息 id
if let Some(value) = values.first() {
if let Some(msg_id) = value.as_str() {
event!(Level::INFO, "delete_message {}", msg_id.to_string().yellow());
py::call::ica_delete_message_py(msg_id.to_string(), &client).await;
}
}
}
}
pub async fn update_all_room(payload: Payload, _client: Client) {
if let Payload::Text(values) = payload {
if let Some(value) = values.first() {
if let Some(raw_rooms) = value.as_array() {
let rooms: Vec<Room> = raw_rooms.iter().map(Room::new_from_json).collect();
event!(Level::DEBUG, "update_all_room {}", rooms.len());
MainStatus::global_ica_status_mut().update_rooms(rooms);
}
}
}
}
pub async fn success_message(payload: Payload, _client: Client) {
if let Payload::Text(values) = payload {
if let Some(value) = values.first() {
info!("messageSuccess {}", value.to_string().green());
}
}
}
pub async fn failed_message(payload: Payload, _client: Client) {
if let Payload::Text(values) = payload {
if let Some(value) = values.first() {
warn!("messageFailed {}", value.to_string().red());
}
}
}
/// 处理加群申请
///
/// add: 2.0.1
pub async fn join_request(payload: Payload, _client: Client) {
if let Payload::Text(values) = payload {
if let Some(value) = values.first() {
match serde_json::from_value::<JoinRequestRoom>(value.clone()) {
Ok(join_room) => {
event!(Level::INFO, "{}", format!("收到加群申请 {:?}", join_room).on_blue());
}
Err(e) => {
event!(
Level::WARN,
"呼叫 shenjack! JoinRequestRoom 的 serde 没写好! {}\nraw: {:#?}",
e,
value
)
}
}
}
}
}
pub async fn fetch_history(client: Client, room: RoomId) { let mut request_body = json!(room); }
pub async fn fetch_messages(client: &Client, room: RoomId) {
let mut request_body = json!(room);
match client.emit("fetchMessages", request_body).await {
Ok(_) => {}
Err(e) => {
event!(Level::WARN, "fetch_messages {}", e);
}
}
}
/// 所有
pub async fn any_event(event: Event, payload: Payload, _client: Client) {
let handled = vec![
// 真正处理过的
"authSucceed",
"authFailed",
"authRequired",
"requireAuth",
"onlineData",
"addMessage",
"deleteMessage",
"setAllRooms",
"setMessages",
"handleRequest", // 处理验证消息 (加入请求之类的)
// 也许以后会用到
"messageSuccess",
"messageFailed",
"setAllChatGroups",
// 忽略的
"notify",
"setShutUp", // 禁言
"syncRead", // 同步已读
"closeLoading", // 发送消息/加载新聊天 有一个 loading
"renewMessage", // 我也不确定到底是啥事件
"requestSetup", // 需要登录
"updateRoom", // 更新房间
];
match &event {
Event::Custom(event_name) => {
if handled.contains(&event_name.as_str()) {
return;
}
}
Event::Message => {
if let Payload::Text(values) = payload {
if let Some(value) = values.first() {
if handled.contains(&value.as_str().unwrap()) {
return;
}
info!("收到消息 {}", value.to_string().yellow());
}
}
return;
}
_ => (),
}
match payload {
Payload::Binary(ref data) => {
println!("event: {} |{:?}", event, data)
}
Payload::Text(ref data) => {
print!("event: {}", event.as_str().purple());
for value in data {
println!("|{}", value);
}
}
_ => (),
}
}
pub async fn connect_callback(payload: Payload, _client: Client) {
let span = span!(Level::INFO, "ica connect_callback");
let _enter = span.enter();
if let Payload::Text(values) = payload {
if let Some(value) = values.first() {
match value.as_str() {
Some("authSucceed") => {
event!(Level::INFO, "{}", "已经登录到 icalingua!".green())
}
Some("authFailed") => {
event!(Level::ERROR, "{}", "登录到 icalingua 失败!".red());
panic!("登录失败")
}
Some("authRequired") => {
event!(Level::INFO, "{}", "需要登录到 icalingua!".yellow())
}
Some(msg) => {
event!(Level::INFO, "{}{}", "未知消息".yellow(), msg);
}
_ => (),
}
}
}
}

View File

@ -1,93 +1,275 @@
use std::time::Duration; use std::{
hash::{DefaultHasher, Hash, Hasher},
use futures_util::FutureExt; sync::OnceLock,
use rust_socketio::asynchronous::{Client, ClientBuilder}; time::{Duration, SystemTime},
use rust_socketio::{Event, Payload, TransportType};
use tracing::info;
mod client;
mod config;
mod data_struct;
mod events;
mod py;
#[allow(non_upper_case_globals)]
pub static mut ClientStatus: client::IcalinguaStatus = client::IcalinguaStatus {
login: false,
online_data: None,
rooms: None,
config: None,
}; };
mod config;
mod data_struct;
mod error;
mod py;
mod status;
mod wasms;
#[cfg(feature = "ica")]
mod ica;
#[cfg(feature = "tailchat")]
mod tailchat;
use colored::Colorize;
use config::BotConfig;
use error::PyPluginError;
use tracing::{Level, event, span};
pub static mut MAIN_STATUS: status::BotStatus = status::BotStatus {
config: None,
ica_status: None,
tailchat_status: None,
};
pub type MainStatus = status::BotStatus;
pub type StopGetter = tokio::sync::oneshot::Receiver<()>;
pub const VERSION: &str = env!("CARGO_PKG_VERSION"); pub const VERSION: &str = env!("CARGO_PKG_VERSION");
pub const ICA_VERSION: &str = "2.0.1";
pub const TAILCHAT_VERSION: &str = "2.0.0";
macro_rules! wrap_callback { const HELP_MSG: &str = r#"/bot-rs
($f:expr) => { rust
|payload: Payload, client: Client| $f(payload, client).boxed() /bot-py
}; python (python插件启用了的话)
/bot-ls
/bot-enable-<client-id> <plugin>
()
/bot-disable-<client-id> <plugin>
()
by shenjackyuanjie"#;
/// 获取帮助信息
pub fn help_msg() -> String {
format!("{}\n{}", version_str(), HELP_MSG).replace("<client-id>", client_id().as_str())
} }
macro_rules! wrap_any_callback { static STARTUP_TIME: OnceLock<SystemTime> = OnceLock::new();
($f:expr) => {
|event: Event, payload: Payload, client: Client| $f(event, payload, client).boxed() pub fn start_up_time() -> SystemTime { *STARTUP_TIME.get().expect("WTF, why did you panic?") }
};
/// 获得当前客户端的 id
/// 防止串号
pub fn client_id() -> String {
let mut hasher = DefaultHasher::new();
start_up_time().hash(&mut hasher);
let data = hasher.finish();
// 取后6位
format!("{:06}", data % 1_000_000)
} }
#[tokio::main] /// 获取版本信息
async fn main() { pub fn version_str() -> String {
tracing_subscriber::fmt() format!(
.with_max_level(tracing::Level::DEBUG) "shenbot-rs v{}{}-[{}] ica v{}({}) tailchat v{}",
.init(); VERSION,
if STABLE { "" } else { "-开发版" },
client_id(),
ICA_VERSION,
ica::ICA_PROTOCOL_VERSION,
TAILCHAT_VERSION,
)
}
// 从命令行获取 host 和 key /// 是否为稳定版本
// 从命令行获取配置文件路径 /// 会在 release 的时候设置为 true
let ica_config = config::IcaConfig::new_from_cli(); pub const STABLE: bool = false;
unsafe {
ClientStatus.update_config(ica_config.clone()); #[macro_export]
macro_rules! async_callback_with_state {
($f:expr, $state:expr) => {{
use futures_util::FutureExt;
let state = $state.clone();
move |payload: Payload, client: Client| $f(payload, client, state.clone()).boxed()
}};
}
#[macro_export]
macro_rules! async_any_callback_with_state {
($f:expr, $state:expr) => {{
use futures_util::FutureExt;
let state = $state.clone();
move |event: Event, payload: Payload, client: Client| {
$f(event, payload, client, state.clone()).boxed()
}
}};
}
const CLI_HELP_MSG: &str = r#"{VERSION}
-d
debug
-t
trace
-h
-env <env>
-c <config_file_path>
"#;
fn main() -> anyhow::Result<()> {
let start_up_time = SystemTime::now();
STARTUP_TIME.set(start_up_time).expect("WTF, why did you panic?");
// -d -> debug
// none -> info
let args = std::env::args();
let args = args.collect::<Vec<String>>();
if args.contains(&"-h".to_string()) {
println!("{}", CLI_HELP_MSG.replace("{VERSION}", version_str().as_str()));
return Ok(());
} }
py::init_py(&ica_config); let level = {
if args.contains(&"-d".to_string()) {
Level::DEBUG
} else if args.contains(&"-t".to_string()) {
Level::TRACE
} else {
Level::INFO
}
};
let socket = ClientBuilder::new(ica_config.host.clone()) tracing_subscriber::fmt().with_max_level(level).init();
.transport_type(TransportType::Websocket)
.on_any(wrap_any_callback!(events::any_event))
.on("requireAuth", wrap_callback!(client::sign_callback))
.on("message", wrap_callback!(events::connect_callback))
.on("authSucceed", wrap_callback!(events::connect_callback))
.on("authFailed", wrap_callback!(events::connect_callback))
.on("onlineData", wrap_callback!(events::get_online_data))
.on("setAllRooms", wrap_callback!(events::update_all_room))
.on("addMessage", wrap_callback!(events::add_message))
.on("deleteMessage", wrap_callback!(events::delete_message))
.connect()
.await
.expect("Connection failed");
info!("Connected"); let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.thread_name("shenbot-rs")
.worker_threads(10)
.build()
.unwrap();
if ica_config.notice_start { let result = rt.block_on(inner_main());
for room in ica_config.notice_room.iter() {
let startup_msg = crate::data_struct::messages::SendMessage::new( event!(Level::INFO, "shenbot-rs v{} exiting", VERSION);
format!("ica-async-rs bot v{}", VERSION),
room.clone(), match result {
None, Ok(_) => {}
); Err(e) => {
std::thread::sleep(Duration::from_secs(1)); if let Some(PyPluginError::PluginNotStopped) = e.downcast_ref::<PyPluginError>() {
info!("发送启动消息到房间: {}", room); event!(Level::WARN, "Python 插件停不下来, 3s 后终止 tokio rt");
if let Err(e) = socket rt.shutdown_timeout(Duration::from_secs(3));
.emit("sendMessage", serde_json::to_value(startup_msg).unwrap()) } else {
.await event!(Level::ERROR, "shenbot-rs v{} exiting with error: {}", VERSION, e);
{
info!("启动信息发送失败 房间:{}|e:{}", room, e);
} }
} }
} }
std::thread::sleep(Duration::from_secs(3)); Ok(())
// 等待一个输入 }
info!("Press any key to exit");
let mut input = String::new(); async fn inner_main() -> anyhow::Result<()> {
std::io::stdin().read_line(&mut input).unwrap(); let span = span!(Level::INFO, "bot-main");
let _enter = span.enter();
socket.disconnect().await.expect("Disconnect failed");
info!("Disconnected"); event!(Level::INFO, "shenbot-rs v{} starting", VERSION);
if !STABLE {
event!(Level::WARN, "这是一个开发版本, 有问题记得找 shenjack");
}
let bot_config = BotConfig::new_from_cli();
MainStatus::static_init(bot_config);
let bot_config = MainStatus::global_config();
if bot_config.check_py() {
py::init_py();
}
// 准备一个用于停止 socket 的变量
let (ica_send, ica_recv) = tokio::sync::oneshot::channel::<()>();
if bot_config.check_ica() {
event!(Level::INFO, "{}", "开始启动 ICA".green());
let config = bot_config.ica();
tokio::spawn(async move {
ica::start_ica(&config, ica_recv).await.unwrap();
});
} else {
event!(Level::INFO, "{}", "ica 未启用, 不管他".cyan());
}
let (tailchat_send, tailchat_recv) = tokio::sync::oneshot::channel::<()>();
if bot_config.check_tailchat() {
event!(Level::INFO, "{}", "开始启动 tailchat".green());
let config = bot_config.tailchat();
tokio::spawn(async move {
tailchat::start_tailchat(config, tailchat_recv).await.unwrap();
});
} else {
event!(Level::INFO, "{}", "tailchat 未启用, 不管他".bright_magenta());
}
tokio::time::sleep(Duration::from_secs(1)).await;
// 等待一个输入
event!(Level::INFO, "Press ctrl+c to exit, second ctrl+c to force exit");
tokio::signal::ctrl_c().await.ok();
ica_send.send(()).ok();
tailchat_send.send(()).ok();
event!(Level::INFO, "Disconnected");
py::post_py().await?;
event!(Level::INFO, "Shenbot-rs exiting");
Ok(())
}
#[allow(dead_code, unused_variables)]
#[cfg(test)]
#[tokio::test]
async fn test_macro() {
use std::sync::Arc;
use tokio::sync::RwLock;
use rust_socketio::Payload;
use rust_socketio::asynchronous::{Client, ClientBuilder};
/// 一个简单的例子
#[derive(Clone)]
struct BotState(String);
/// 一个复杂一些的例子
#[derive(Clone)]
struct BotState2 {
pub name: Arc<RwLock<String>>,
}
async fn some_event_with_state(payload: Payload, client: Client, state: Arc<BotState>) {
// do something with your state
}
async fn some_state_change_event(payload: Payload, client: Client, state: Arc<BotState2>) {
if let Payload::Text(text) = payload {
if let Some(first_one) = text.first() {
let new_name = first_one.as_str().unwrap_or_default();
let old_name = state.name.read().await;
if new_name != *old_name {
// update your name here
*state.name.write().await = new_name.to_string();
}
}
}
}
let state = Arc::new(BotState("hello".to_string()));
let state2 = Arc::new(BotState2 {
name: Arc::new(RwLock::new("hello".to_string())),
});
let socket = ClientBuilder::new("http://example.com")
.on("message", async_callback_with_state!(some_event_with_state, state))
.on("update_status", async_callback_with_state!(some_state_change_event, state2))
.connect()
.await;
} }

255
ica-rs/src/py/call.rs Normal file
View File

@ -0,0 +1,255 @@
use std::path::PathBuf;
use std::sync::LazyLock;
use pyo3::prelude::*;
use rust_socketio::asynchronous::Client;
use tokio::sync::Mutex;
use tracing::{Level, event, info, warn};
use crate::MainStatus;
use crate::data_struct::{ica, tailchat};
use crate::error::PyPluginError;
use crate::py::consts::events_func;
use crate::py::{PyPlugin, PyStatus, class};
pub struct PyTasks {
pub ica_new_message: Vec<tokio::task::JoinHandle<()>>,
pub ica_delete_message: Vec<tokio::task::JoinHandle<()>>,
pub tailchat_new_message: Vec<tokio::task::JoinHandle<()>>,
}
impl PyTasks {
pub fn push_ica_new_message(&mut self, handle: tokio::task::JoinHandle<()>) {
self.ica_new_message.push(handle);
self.ica_new_message.retain(|handle| !handle.is_finished());
}
pub fn push_ica_delete_message(&mut self, handle: tokio::task::JoinHandle<()>) {
self.ica_delete_message.push(handle);
self.ica_delete_message.retain(|handle| !handle.is_finished());
}
pub fn push_tailchat_new_message(&mut self, handle: tokio::task::JoinHandle<()>) {
self.tailchat_new_message.push(handle);
self.tailchat_new_message.retain(|handle| !handle.is_finished());
}
pub async fn join_all(&mut self) {
for handle in self.ica_new_message.drain(..) {
let _ = handle.await;
}
for handle in self.ica_delete_message.drain(..) {
let _ = handle.await;
}
for handle in self.tailchat_new_message.drain(..) {
let _ = handle.await;
}
}
pub fn len_check(&mut self) -> usize {
self.ica_delete_message.retain(|handle| !handle.is_finished());
self.ica_new_message.retain(|handle| !handle.is_finished());
self.tailchat_new_message.retain(|handle| !handle.is_finished());
self.ica_new_message.len() + self.ica_delete_message.len() + self.tailchat_new_message.len()
}
pub fn len(&self) -> usize {
self.ica_new_message.len() + self.ica_delete_message.len() + self.tailchat_new_message.len()
}
pub fn is_empty(&self) -> bool { self.len() == 0 }
pub fn cancel_all(&mut self) {
for handle in self.ica_new_message.drain(..) {
handle.abort();
}
for handle in self.ica_delete_message.drain(..) {
handle.abort();
}
for handle in self.tailchat_new_message.drain(..) {
handle.abort();
}
}
}
pub static PY_TASKS: LazyLock<Mutex<PyTasks>> = LazyLock::new(|| {
Mutex::new(PyTasks {
ica_new_message: Vec::new(),
ica_delete_message: Vec::new(),
tailchat_new_message: Vec::new(),
})
});
pub fn get_func<'py>(
py_module: &Bound<'py, PyAny>,
name: &'py str,
) -> Result<Bound<'py, PyAny>, PyPluginError> {
// 要处理的情况:
// 1. 有这个函数
// 2. 没有这个函数
// 3. 函数不是 Callable
match py_module.hasattr(name) {
Ok(contain) => {
if contain {
match py_module.getattr(name) {
Ok(func) => {
if func.is_callable() {
Ok(func)
} else {
// warn!("function<{}>: {:#?} in {:?} is not callable", name, func, path);
Err(PyPluginError::FuncNotCallable(
name.to_string(),
py_module.getattr("__name__").unwrap().extract::<String>().unwrap(),
))
}
}
Err(e) => {
// warn!("failed to get function<{}> from {:?}: {:?}", name, path, e);
Err(PyPluginError::CouldNotGetFunc(
e,
name.to_string(),
py_module.getattr("__name__").unwrap().extract::<String>().unwrap(),
))
}
}
} else {
// debug!("no function<{}> in module {:?}", name, path);
Err(PyPluginError::FuncNotFound(
name.to_string(),
py_module.getattr("__name__").unwrap().extract::<String>().unwrap(),
))
}
}
Err(e) => {
// warn!("failed to check function<{}> from {:?}: {:?}", name, path, e);
Err(PyPluginError::CouldNotGetFunc(
e,
name.to_string(),
py_module.getattr("__name__").unwrap().extract::<String>().unwrap(),
))
}
}
}
pub fn verify_and_reload_plugins() {
let mut need_reload_files: Vec<PathBuf> = Vec::new();
let plugin_path = MainStatus::global_config().py().plugin_path.clone();
// 先检查是否有插件被删除
for path in PyStatus::get().files.keys() {
if !path.exists() {
event!(Level::INFO, "Python 插件: {:?} 已被删除", path);
PyStatus::get_mut().delete_file(path);
}
}
for entry in std::fs::read_dir(plugin_path).unwrap().flatten() {
let path = entry.path();
if let Some(ext) = path.extension() {
if ext == "py" && !PyStatus::get().verify_file(&path) {
need_reload_files.push(path);
}
}
}
if need_reload_files.is_empty() {
return;
}
event!(Level::INFO, "更改列表: {:?}", need_reload_files);
let plugins = PyStatus::get_mut();
for reload_file in need_reload_files {
if let Some(plugin) = plugins.files.get_mut(&reload_file) {
plugin.reload_from_file();
event!(Level::INFO, "重载 Python 插件: {:?} 完成", reload_file);
} else {
match PyPlugin::new_from_path(&reload_file) {
Some(plugin) => {
plugins.add_file(reload_file.clone(), plugin);
info!("加载 Python 插件: {:?} 完成", reload_file);
}
None => {
warn!("加载 Python 插件: {:?} 失败", reload_file);
}
}
}
}
}
macro_rules! call_py_func {
($args:expr, $plugin:expr, $plugin_path:expr, $func_name:expr, $client:expr) => {
tokio::spawn(async move {
Python::with_gil(|py| {
if let Ok(py_func) = get_func($plugin.py_module.bind(py), $func_name) {
if let Err(py_err) = py_func.call1($args) {
let e = PyPluginError::FuncCallError(
py_err,
$func_name.to_string(),
$plugin_path.to_string_lossy().to_string(),
);
event!(
Level::WARN,
"failed to call function<{}>: {}\ntraceback: {}",
$func_name,
e,
// 获取 traceback
match &e {
PyPluginError::FuncCallError(py_err, _, _) => match py_err.traceback(py) {
Some(traceback) => match traceback.format() {
Ok(trace) => trace,
Err(trace_e) => format!("failed to format traceback: {:?}", trace_e),
},
None => "no traceback".to_string(),
},
_ => unreachable!(),
}
);
}
}
})
})
};
}
/// 执行 new message 的 python 插件
pub async fn ica_new_message_py(message: &ica::messages::NewMessage, client: &Client) {
// 验证插件是否改变
verify_and_reload_plugins();
let plugins = PyStatus::get();
for (path, plugin) in plugins.files.iter().filter(|(_, plugin)| plugin.enabled) {
let msg = class::ica::NewMessagePy::new(message);
let client = class::ica::IcaClientPy::new(client);
let args = (msg, client);
let task = call_py_func!(args, plugin, path, events_func::ICA_NEW_MESSAGE, client);
PY_TASKS.lock().await.push_ica_new_message(task);
}
}
pub async fn ica_delete_message_py(msg_id: ica::MessageId, client: &Client) {
verify_and_reload_plugins();
let plugins = PyStatus::get();
for (path, plugin) in plugins.files.iter().filter(|(_, plugin)| plugin.enabled) {
let msg_id = msg_id.clone();
let client = class::ica::IcaClientPy::new(client);
let args = (msg_id.clone(), client);
let task = call_py_func!(args, plugin, path, events_func::ICA_DELETE_MESSAGE, client);
PY_TASKS.lock().await.push_ica_delete_message(task);
}
}
pub async fn tailchat_new_message_py(
message: &tailchat::messages::ReceiveMessage,
client: &Client,
) {
verify_and_reload_plugins();
let plugins = PyStatus::get();
for (path, plugin) in plugins.files.iter().filter(|(_, plugin)| plugin.enabled) {
let msg = class::tailchat::TailchatReceiveMessagePy::from_recive_message(message);
let client = class::tailchat::TailchatClientPy::new(client);
let args = (msg, client);
let task = call_py_func!(args, plugin, path, events_func::TAILCHAT_NEW_MESSAGE, client);
PY_TASKS.lock().await.push_tailchat_new_message(task);
}
}

View File

@ -1,242 +1,85 @@
use pyo3::prelude::*; pub mod commander;
use tracing::{debug, info, warn}; pub mod config;
use rust_socketio::asynchronous::Client; pub mod ica;
use tokio::runtime::Runtime; pub mod schdule;
pub mod tailchat;
use crate::client::send_message; use pyo3::{
use crate::data_struct::messages::{NewMessage, ReplyMessage, SendMessage}; Bound, IntoPyObject, PyAny, PyRef, PyResult, pyclass, pymethods, pymodule,
use crate::ClientStatus; types::{PyBool, PyModule, PyModuleMethods, PyString},
};
use toml::Value as TomlValue;
use tracing::{Level, event};
// #[derive(Clone)]
#[pyclass] #[pyclass]
#[pyo3(name = "IcaStatus")] #[pyo3(name = "ConfigData")]
pub struct IcaStatusPy {} pub struct ConfigDataPy {
pub data: TomlValue,
#[pymethods]
impl IcaStatusPy {
#[new]
pub fn py_new() -> Self {
Self {}
}
#[getter]
pub fn get_login(&self) -> bool {
unsafe { ClientStatus.login }
}
#[getter]
pub fn get_online(&self) -> bool {
unsafe {
match ClientStatus.online_data.as_ref() {
Some(data) => data.online,
None => false,
}
}
}
#[getter]
pub fn get_self_id(&self) -> Option<i64> {
unsafe {
match ClientStatus.online_data.as_ref() {
Some(data) => Some(data.qqid),
None => None,
}
}
}
#[getter]
pub fn get_nick_name(&self) -> Option<String> {
unsafe {
match ClientStatus.online_data.as_ref() {
Some(data) => Some(data.nick.clone()),
None => None,
}
}
}
#[getter]
pub fn get_ica_version(&self) -> Option<String> {
unsafe {
match ClientStatus.online_data.as_ref() {
Some(data) => Some(data.icalingua_info.ica_version.clone()),
None => None,
}
}
}
#[getter]
pub fn get_os_info(&self) -> Option<String> {
unsafe {
match ClientStatus.online_data.as_ref() {
Some(data) => Some(data.icalingua_info.os_info.clone()),
None => None,
}
}
}
#[getter]
pub fn get_resident_set_size(&self) -> Option<String> {
unsafe {
match ClientStatus.online_data.as_ref() {
Some(data) => Some(data.icalingua_info.resident_set_size.clone()),
None => None,
}
}
}
#[getter]
pub fn get_heap_used(&self) -> Option<String> {
unsafe {
match ClientStatus.online_data.as_ref() {
Some(data) => Some(data.icalingua_info.heap_used.clone()),
None => None,
}
}
}
#[getter]
pub fn get_load(&self) -> Option<String> {
unsafe {
match ClientStatus.online_data.as_ref() {
Some(data) => Some(data.icalingua_info.load.clone()),
None => None,
}
}
}
}
impl IcaStatusPy {
pub fn new() -> Self {
Self {}
}
}
#[derive(Clone)]
#[pyclass]
#[pyo3(name = "NewMessage")]
pub struct NewMessagePy {
pub msg: NewMessage,
} }
#[pymethods] #[pymethods]
impl NewMessagePy { impl ConfigDataPy {
pub fn reply_with(&self, content: String) -> SendMessagePy { pub fn __getitem__(self_: PyRef<'_, Self>, key: String) -> Option<Bound<PyAny>> {
SendMessagePy::new(self.msg.reply_with(&content)) match self_.data.get(&key) {
} Some(value) => match value {
TomlValue::String(s) => Some(PyString::new(self_.py(), s).into_any()),
pub fn __str__(&self) -> String { TomlValue::Integer(i) => Some(i.into_pyobject(self_.py()).unwrap().into_any()),
format!("{:?}", self.msg) TomlValue::Float(f) => Some(f.into_pyobject(self_.py()).unwrap().into_any()),
} TomlValue::Boolean(b) => {
let py_value = PyBool::new(self_.py(), *b);
#[getter] Some(py_value.as_any().clone())
pub fn get_content(&self) -> String { }
self.msg.content.clone() TomlValue::Array(a) => {
} let new_self = Self::new(TomlValue::Array(a.clone()));
#[getter] let py_value = new_self.into_pyobject(self_.py()).unwrap().into_any();
pub fn get_sender_id(&self) -> i64 { Some(py_value)
self.msg.sender_id }
} TomlValue::Table(t) => {
#[getter] let new_self = Self::new(TomlValue::Table(t.clone()));
pub fn get_is_from_self(&self) -> bool { let py_value = new_self.into_pyobject(self_.py()).unwrap().into_any();
self.msg.is_from_self() Some(py_value)
} }
#[getter] _ => None,
pub fn get_is_reply(&self) -> bool { },
self.msg.is_reply() None => None,
}
}
impl NewMessagePy {
pub fn new(msg: &NewMessage) -> Self {
Self { msg: msg.clone() }
}
}
#[pyclass]
#[pyo3(name = "ReplyMessage")]
pub struct ReplyMessagePy {
pub msg: ReplyMessage,
}
#[pymethods]
impl ReplyMessagePy {
pub fn __str__(&self) -> String {
format!("{:?}", self.msg)
}
}
impl ReplyMessagePy {
pub fn new(msg: ReplyMessage) -> Self {
Self { msg }
}
}
#[derive(Clone)]
#[pyclass]
#[pyo3(name = "SendMessage")]
pub struct SendMessagePy {
pub msg: SendMessage,
}
#[pymethods]
impl SendMessagePy {
pub fn __str__(&self) -> String {
format!("{:?}", self.msg)
}
}
impl SendMessagePy {
pub fn new(msg: SendMessage) -> Self {
Self { msg }
}
}
#[derive(Clone)]
#[pyclass]
#[pyo3(name = "IcaClient")]
pub struct IcaClientPy {
pub client: Client,
}
#[pymethods]
impl IcaClientPy {
pub fn send_message(&self, message: SendMessagePy) -> bool {
tokio::task::block_in_place(|| {
let rt = Runtime::new().unwrap();
rt.block_on(send_message(&self.client, &message.msg))
})
}
/// 仅作占位
/// (因为目前来说, rust调用 Python端没法启动一个异步运行时
/// 所以只能 tokio::task::block_in_place 转换成同步调用)
#[staticmethod]
pub fn send_message_a(
py: Python,
client: IcaClientPy,
message: SendMessagePy,
) -> PyResult<&PyAny> {
pyo3_asyncio::tokio::future_into_py(py, async move {
Ok(send_message(&client.client, &message.msg).await)
})
}
pub fn debug(&self, content: String) {
debug!("{}", content);
}
pub fn info(&self, content: String) {
info!("{}", content);
}
pub fn warn(&self, content: String) {
warn!("{}", content);
}
}
impl IcaClientPy {
pub fn new(client: &Client) -> Self {
Self {
client: client.clone(),
} }
} }
pub fn have_key(&self, key: String) -> bool { self.data.get(&key).is_some() }
}
impl ConfigDataPy {
pub fn new(data: TomlValue) -> Self { Self { data } }
}
/// Rust 侧向 Python 侧提供的 api
#[pymodule]
#[pyo3(name = "shenbot_api")]
fn rs_api_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add("__version__", crate::VERSION)?;
m.add("_version_", crate::VERSION)?;
m.add("_ica_version_", crate::ICA_VERSION)?;
m.add("_tailchat_version_", crate::TAILCHAT_VERSION)?;
m.add_class::<ConfigDataPy>()?;
m.add_class::<config::ConfigStoragePy>()?;
m.add_class::<schdule::SchedulerPy>()?;
Ok(())
}
/// 在 python 初始化之前注册所有需要的类
///
/// WARNING: 这个函数需要在 Python 初始化之前调用,否则会导致报错
///
/// (pyo3 提供的宏会检查一遍, 不过我这里就直接用原始形式了)
pub fn regist_class() {
event!(Level::INFO, "向 Python 注册 Rust 侧模块/函数");
unsafe {
// 单纯没用 macro 而已
pyo3::ffi::PyImport_AppendInittab(
rs_api_module::__PYO3_NAME.as_ptr(),
Some(rs_api_module::__pyo3_init),
);
}
event!(Level::INFO, "注册完成");
} }

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,343 @@
use std::collections::HashMap;
use pyo3::{
Bound, PyAny, PyResult, pyclass, pymethods,
types::{
PyAnyMethods, PyBool, PyBoolMethods, PyDict, PyDictMethods, PyFloat, PyInt, PyList,
PyListMethods, PyString, PyStringMethods, PyTypeMethods,
},
};
use tracing::{Level, event};
#[derive(Debug, Clone)]
pub enum ConfigItem {
None,
String(String),
Int(i64),
Float(f64),
Bool(bool),
Array(Vec<ConfigItemPy>),
Table(HashMap<String, ConfigItemPy>),
}
#[derive(Clone, Debug)]
#[pyclass]
#[pyo3(name = "ConfigItem")]
pub struct ConfigItemPy {
pub item: ConfigItem,
pub default_value: ConfigItem,
}
impl ConfigItemPy {
pub fn new(item: ConfigItem, default_value: ConfigItem) -> Self {
Self {
item,
default_value,
}
}
pub fn new_uninit(default_value: ConfigItem) -> Self {
Self {
item: ConfigItem::None,
default_value,
}
}
}
#[derive(Clone)]
#[pyclass]
#[pyo3(name = "ConfigStorage")]
pub struct ConfigStoragePy {
pub keys: HashMap<String, ConfigItemPy>,
}
/// Storage 里允许的最大层级深度
///
/// 我也不知道为啥就突然有这玩意了(
pub const MAX_CFG_DEPTH: usize = 10;
fn parse_py_string(obj: &Bound<'_, PyAny>) -> PyResult<String> {
let py_str = obj.downcast::<PyString>()?;
let value = py_str.to_str()?;
Ok(value.to_string())
}
fn parse_py_bool(obj: &Bound<'_, PyAny>) -> PyResult<bool> {
let py_bool = obj.downcast::<PyBool>()?;
Ok(py_bool.is_true())
}
fn parse_py_int(obj: &Bound<'_, PyAny>) -> PyResult<i64> {
let py_int = obj.downcast::<PyInt>()?;
py_int.extract::<i64>()
}
fn parse_py_float(obj: &Bound<'_, PyAny>) -> PyResult<f64> {
let py_float = obj.downcast::<PyFloat>()?;
py_float.extract::<f64>()
}
impl ConfigStoragePy {
/// 递归 list 解析配置
///
/// 用个 Result 来标记递归过深
fn parse_py_list(
args: &Bound<'_, PyList>,
list: &mut Vec<ConfigItemPy>,
current_deepth: usize,
) -> Result<(), usize> {
if current_deepth > MAX_CFG_DEPTH {
return Err(current_deepth);
} else {
for value in args.iter() {
// 匹配 item
let value_type = value.get_type();
if value_type.is_instance_of::<PyDict>() {
let py_dict = value.downcast::<PyDict>().unwrap();
let mut new_map = HashMap::new();
match Self::parse_py_dict(py_dict, &mut new_map, current_deepth + 1) {
Ok(_) => {
list.push(ConfigItemPy::new_uninit(ConfigItem::Table(new_map)));
}
Err(e) => {
event!(
Level::WARN,
"value(dict) 解析时出现错误: {}\nraw: {}",
e,
value
);
}
}
} else if value_type.is_instance_of::<PyList>() {
let py_list = value.downcast::<PyList>().unwrap();
let mut new_list = Vec::new();
match Self::parse_py_list(py_list, &mut new_list, current_deepth + 1) {
Ok(_) => {
list.push(ConfigItemPy::new_uninit(ConfigItem::Array(new_list)));
}
Err(e) => {
event!(
Level::WARN,
"value(list) 解析时出现错误: {}\nraw: {}",
e,
value
);
}
}
} else if value_type.is_instance_of::<PyString>() {
match parse_py_string(&value) {
Ok(value) => {
list.push(ConfigItemPy::new_uninit(ConfigItem::String(value)));
}
Err(e) => {
event!(
Level::WARN,
"value(string) 解析时出现错误: {}\nraw: {}",
e,
value
);
}
}
} else if value_type.is_instance_of::<PyBool>() {
match parse_py_bool(&value) {
Ok(value) => {
list.push(ConfigItemPy::new_uninit(ConfigItem::Bool(value)));
}
Err(e) => {
event!(
Level::WARN,
"value(bool) 解析时出现错误: {}\nraw: {}",
e,
value
);
}
}
} else if value_type.is_instance_of::<PyInt>() {
match parse_py_int(&value) {
Ok(value) => {
list.push(ConfigItemPy::new_uninit(ConfigItem::Int(value)));
}
Err(e) => {
event!(Level::WARN, "value(int) 解析时出现错误: {}\nraw: {}", e, value);
}
}
} else if value_type.is_instance_of::<PyFloat>() {
match parse_py_float(&value) {
Ok(value) => {
list.push(ConfigItemPy::new_uninit(ConfigItem::Float(value)));
}
Err(e) => {
event!(
Level::WARN,
"value(float) 解析时出现错误: {}\nraw: {}",
e,
value
);
}
}
} else {
// 先丢个 warning 出去
match value_type.name() {
Ok(type_name) => {
event!(
Level::WARN,
"value 为不支持的 {} 类型\nraw: {}",
type_name,
value
)
}
Err(e) => {
event!(
Level::WARN,
"value 为不支持的类型 (获取类型名失败: {})\nraw: {}",
e,
value
)
}
}
}
}
}
Ok(())
}
/// 递归 dict 解析配置
///
/// 用个 Result 来标记递归过深
fn parse_py_dict(
kwargs: &Bound<'_, PyDict>,
map: &mut HashMap<String, ConfigItemPy>,
current_deepth: usize,
) -> Result<(), usize> {
if current_deepth > MAX_CFG_DEPTH {
Err(current_deepth)
} else {
for (key, value) in kwargs.iter() {
if let Ok(name) = key.downcast::<PyString>() {
let name = name.to_string();
// 匹配 item
let value_type = value.get_type();
if value_type.is_instance_of::<PyDict>() {
let py_dict = value.downcast::<PyDict>().unwrap();
let mut new_map = HashMap::new();
match Self::parse_py_dict(py_dict, &mut new_map, current_deepth + 1) {
Ok(_) => {
map.insert(
name.clone(),
ConfigItemPy::new_uninit(ConfigItem::Table(new_map)),
);
}
Err(e) => {
event!(Level::WARN, "value(dict) {} 解析时出现错误: {}", name, e);
}
}
} else if value_type.is_instance_of::<PyList>() {
let py_list = value.downcast::<PyList>().unwrap();
let mut new_list = Vec::new();
match Self::parse_py_list(py_list, &mut new_list, current_deepth + 1) {
Ok(_) => {
map.insert(
name.clone(),
ConfigItemPy::new_uninit(ConfigItem::Array(new_list)),
);
}
Err(e) => {
event!(Level::WARN, "value(list) {} 解析时出现错误: {}", name, e);
}
}
} else if value_type.is_instance_of::<PyString>() {
match parse_py_string(&value) {
Ok(value) => {
map.insert(
name.clone(),
ConfigItemPy::new_uninit(ConfigItem::String(value)),
);
}
Err(e) => {
event!(Level::WARN, "value(string) {} 解析时出现错误: {}", name, e);
}
}
} else if value_type.is_instance_of::<PyBool>() {
match parse_py_bool(&value) {
Ok(value) => {
map.insert(
name.clone(),
ConfigItemPy::new_uninit(ConfigItem::Bool(value)),
);
}
Err(e) => {
event!(Level::WARN, "value(bool) {} 解析时出现错误: {}", name, e);
}
}
} else if value_type.is_instance_of::<PyInt>() {
match parse_py_int(&value) {
Ok(value) => {
map.insert(
name.clone(),
ConfigItemPy::new_uninit(ConfigItem::Int(value)),
);
}
Err(e) => {
event!(Level::WARN, "value(int) {} 解析时出现错误: {}", name, e);
}
}
} else if value_type.is_instance_of::<PyFloat>() {
match parse_py_float(&value) {
Ok(value) => {
map.insert(
name.clone(),
ConfigItemPy::new_uninit(ConfigItem::Float(value)),
);
}
Err(e) => {
event!(Level::WARN, "value(float) {} 解析时出现错误: {}", name, e);
}
}
} else {
// 先丢个 warning 出去
match value_type.name() {
Ok(type_name) => {
event!(Level::WARN, "value {} 为不支持的 {} 类型", name, type_name)
}
Err(e) => event!(
Level::WARN,
"value {} 为不支持的类型 (获取类型名失败: {})",
name,
e
),
}
continue;
}
}
}
Ok(())
}
}
}
#[pymethods]
impl ConfigStoragePy {
#[new]
#[pyo3(signature = (**kwargs))]
pub fn new(kwargs: Option<&Bound<'_, PyDict>>) -> PyResult<Self> {
match kwargs {
Some(kwargs) => {
let mut keys = HashMap::new();
// 解析 kwargs
Self::parse_py_dict(kwargs, &mut keys, 0).map_err(|e| {
event!(Level::ERROR, "配置解析过深: {}", e);
pyo3::exceptions::PyValueError::new_err(format!("配置解析过深: {}", e))
})?;
// 解析完成
Ok(Self { keys })
}
None => Ok(Self {
keys: HashMap::new(),
}),
}
}
#[getter]
/// 获取最大允许的层级深度
pub fn get_max_allowed_depth(&self) -> usize { MAX_CFG_DEPTH }
}

369
ica-rs/src/py/class/ica.rs Normal file
View File

@ -0,0 +1,369 @@
use std::time::SystemTime;
use pyo3::{pyclass, pymethods};
use rust_socketio::asynchronous::Client;
use tokio::runtime::Runtime;
use tracing::{Level, event};
use crate::MainStatus;
use crate::data_struct::ica::messages::{
DeleteMessage, MessageTrait, NewMessage, ReplyMessage, SendMessage,
};
use crate::data_struct::ica::{MessageId, RoomId, RoomIdTrait, UserId};
use crate::ica::client::{delete_message, send_message, send_poke, send_room_sign_in};
use crate::py::PyStatus;
#[pyclass]
#[pyo3(name = "IcaStatus")]
pub struct IcaStatusPy {}
#[pymethods]
impl IcaStatusPy {
#[new]
pub fn py_new() -> Self { Self {} }
#[getter]
pub fn get_qq_login(&self) -> bool { MainStatus::global_ica_status().qq_login }
#[getter]
pub fn get_online(&self) -> bool { MainStatus::global_ica_status().online_status.online }
#[getter]
pub fn get_self_id(&self) -> i64 { MainStatus::global_ica_status().online_status.qqid }
#[getter]
pub fn get_nick_name(&self) -> String {
MainStatus::global_ica_status().online_status.nick.clone()
}
#[getter]
pub fn get_loaded_messages_count(&self) -> u64 {
MainStatus::global_ica_status().current_loaded_messages_count
}
#[getter]
pub fn get_ica_version(&self) -> String {
MainStatus::global_ica_status().online_status.icalingua_info.ica_version.clone()
}
#[getter]
pub fn get_os_info(&self) -> String {
MainStatus::global_ica_status().online_status.icalingua_info.os_info.clone()
}
#[getter]
pub fn get_resident_set_size(&self) -> String {
MainStatus::global_ica_status()
.online_status
.icalingua_info
.resident_set_size
.clone()
}
#[getter]
pub fn get_heap_used(&self) -> String {
MainStatus::global_ica_status().online_status.icalingua_info.heap_used.clone()
}
#[getter]
pub fn get_load(&self) -> String {
MainStatus::global_ica_status().online_status.icalingua_info.load.clone()
}
#[getter]
/// 获取当前用户加入的所有房间
///
/// 添加自 2.0.1
pub fn get_rooms(&self) -> Vec<IcaRoomPy> {
MainStatus::global_ica_status().rooms.iter().map(|r| r.into()).collect()
}
#[getter]
/// 获取所有管理员
///
/// 添加自 2.0.1
pub fn get_admins(&self) -> Vec<UserId> { MainStatus::global_config().ica().admin_list.clone() }
#[getter]
/// 获取所有被屏蔽的人
///
/// (好像没啥用就是了, 反正被过滤的不会给到插件)
///
/// 添加自 2.0.1
pub fn get_filtered(&self) -> Vec<UserId> {
MainStatus::global_config().ica().filter_list.clone()
}
}
impl Default for IcaStatusPy {
fn default() -> Self { Self::new() }
}
impl IcaStatusPy {
pub fn new() -> Self { Self {} }
}
#[derive(Clone)]
#[pyclass]
#[pyo3(name = "IcaRoom")]
/// Room api
///
/// 添加自 2.0.1
pub struct IcaRoomPy {
pub inner: crate::data_struct::ica::all_rooms::Room,
}
impl From<crate::data_struct::ica::all_rooms::Room> for IcaRoomPy {
fn from(inner: crate::data_struct::ica::all_rooms::Room) -> Self { Self { inner } }
}
impl From<&crate::data_struct::ica::all_rooms::Room> for IcaRoomPy {
fn from(inner: &crate::data_struct::ica::all_rooms::Room) -> Self {
Self {
inner: inner.clone(),
}
}
}
#[pymethods]
impl IcaRoomPy {
#[getter]
pub fn get_room_id(&self) -> i64 { self.inner.room_id }
#[getter]
pub fn get_room_name(&self) -> String { self.inner.room_name.clone() }
#[getter]
pub fn get_unread_count(&self) -> u64 { self.inner.unread_count }
#[getter]
pub fn get_priority(&self) -> u8 { self.inner.priority }
#[getter]
pub fn get_utime(&self) -> i64 { self.inner.utime }
pub fn is_group(&self) -> bool { self.inner.room_id.is_room() }
pub fn is_chat(&self) -> bool { self.inner.room_id.is_chat() }
pub fn new_message_to(&self, content: String) -> SendMessagePy {
SendMessagePy::new(self.inner.new_message_to(content))
}
}
#[derive(Clone)]
#[pyclass]
#[pyo3(name = "NewMessage")]
pub struct NewMessagePy {
pub msg: NewMessage,
}
#[pymethods]
impl NewMessagePy {
pub fn reply_with(&self, content: String) -> SendMessagePy {
SendMessagePy::new(self.msg.reply_with(&content))
}
pub fn as_deleted(&self) -> DeleteMessagePy { DeleteMessagePy::new(self.msg.as_deleted()) }
pub fn __str__(&self) -> String { format!("{:?}", self.msg) }
#[getter]
pub fn get_id(&self) -> MessageId { self.msg.msg_id().clone() }
#[getter]
pub fn get_content(&self) -> String { self.msg.content().clone() }
#[getter]
pub fn get_sender_id(&self) -> i64 { self.msg.sender_id() }
#[getter]
pub fn get_sender_name(&self) -> String { self.msg.sender_name().clone() }
#[getter]
pub fn get_is_from_self(&self) -> bool { self.msg.is_from_self() }
#[getter]
pub fn get_is_reply(&self) -> bool { self.msg.is_reply() }
#[getter]
pub fn get_is_room_msg(&self) -> bool { self.msg.room_id.is_room() }
#[getter]
pub fn get_is_chat_msg(&self) -> bool { self.msg.room_id.is_chat() }
#[getter]
pub fn get_room_id(&self) -> RoomId { self.msg.room_id }
}
impl NewMessagePy {
pub fn new(msg: &NewMessage) -> Self { Self { msg: msg.clone() } }
}
#[pyclass]
#[pyo3(name = "ReplyMessage")]
pub struct ReplyMessagePy {
pub msg: ReplyMessage,
}
#[pymethods]
impl ReplyMessagePy {
pub fn __str__(&self) -> String { format!("{:?}", self.msg) }
}
impl ReplyMessagePy {
pub fn new(msg: ReplyMessage) -> Self { Self { msg } }
}
#[derive(Clone)]
#[pyclass]
#[pyo3(name = "SendMessage")]
pub struct SendMessagePy {
pub msg: SendMessage,
}
#[pymethods]
impl SendMessagePy {
pub fn __str__(&self) -> String { format!("{:?}", self.msg) }
/// 设置消息内容
/// 用于链式调用
pub fn with_content(&mut self, content: String) -> Self {
self.msg.content = content;
self.clone()
}
#[getter]
pub fn get_content(&self) -> String { self.msg.content.clone() }
#[setter]
pub fn set_content(&mut self, content: String) { self.msg.content = content; }
#[getter]
pub fn get_room_id(&self) -> RoomId { self.msg.room_id }
#[setter]
pub fn set_room_id(&mut self, room_id: RoomId) { self.msg.room_id = room_id; }
/// 设置消息图片
pub fn set_img(&mut self, file: Vec<u8>, file_type: String, as_sticker: bool) {
self.msg.set_img(&file, &file_type, as_sticker);
}
pub fn remove_reply(&mut self) -> Self {
self.msg.reply_to = None;
self.clone()
}
}
impl SendMessagePy {
pub fn new(msg: SendMessage) -> Self { Self { msg } }
}
#[derive(Clone)]
#[pyclass]
#[pyo3(name = "DeleteMessage")]
pub struct DeleteMessagePy {
pub msg: DeleteMessage,
}
#[pymethods]
impl DeleteMessagePy {
pub fn __str__(&self) -> String { format!("{:?}", self.msg) }
}
impl DeleteMessagePy {
pub fn new(msg: DeleteMessage) -> Self { Self { msg } }
}
#[derive(Clone)]
#[pyclass]
#[pyo3(name = "IcaClient")]
pub struct IcaClientPy {
pub client: Client,
}
#[pymethods]
impl IcaClientPy {
/// 签到
///
/// 添加自 1.6.5 版本
pub fn send_room_sign_in(&self, room_id: RoomId) -> bool {
tokio::task::block_in_place(|| {
let rt = Runtime::new().unwrap();
rt.block_on(send_room_sign_in(&self.client, room_id))
})
}
/// 戳一戳
///
/// 添加自 1.6.5 版本
pub fn send_poke(&self, room_id: RoomId, user_id: UserId) -> bool {
tokio::task::block_in_place(|| {
let rt = Runtime::new().unwrap();
rt.block_on(send_poke(&self.client, room_id, user_id))
})
}
pub fn send_message(&self, message: SendMessagePy) -> bool {
tokio::task::block_in_place(|| {
let rt = Runtime::new().unwrap();
rt.block_on(send_message(&self.client, &message.msg))
})
}
pub fn send_and_warn(&self, message: SendMessagePy) -> bool {
event!(Level::WARN, message.msg.content);
self.send_message(message)
}
pub fn delete_message(&self, message: DeleteMessagePy) -> bool {
tokio::task::block_in_place(|| {
let rt = Runtime::new().unwrap();
rt.block_on(delete_message(&self.client, &message.msg))
})
}
/// 仅作占位
/// (因为目前来说, rust调用 Python端没法启动一个异步运行时
/// 所以只能 tokio::task::block_in_place 转换成同步调用)
// #[staticmethod]
// pub fn send_message_a(
// py: Python,
// client: IcaClientPy,
// message: SendMessagePy,
// ) -> PyResult<&PyAny> {
// pyo3_asyncio::tokio::future_into_py(py, async move {
// Ok(send_message(&client.client, &message.msg).await)
// })
// }
#[getter]
pub fn get_status(&self) -> IcaStatusPy { IcaStatusPy::new() }
#[getter]
pub fn get_version(&self) -> String { crate::VERSION.to_string() }
#[getter]
pub fn get_version_str(&self) -> String { crate::version_str() }
#[getter]
pub fn get_client_id(&self) -> String { crate::client_id() }
#[getter]
pub fn get_ica_version(&self) -> String { crate::ICA_VERSION.to_string() }
#[getter]
pub fn get_startup_time(&self) -> SystemTime { crate::start_up_time() }
#[getter]
pub fn get_py_tasks_count(&self) -> usize {
tokio::task::block_in_place(|| {
let rt = Runtime::new().unwrap();
rt.block_on(async { crate::py::call::PY_TASKS.lock().await.len_check() })
})
}
/// 重新加载插件状态
/// 返回是否成功
pub fn reload_plugin_status(&self) -> bool { PyStatus::get_mut().config.reload_from_default() }
/// 设置某个插件的状态
pub fn set_plugin_status(&self, plugin_name: String, status: bool) {
PyStatus::get_mut().set_status(&plugin_name, status);
}
pub fn get_plugin_status(&self, plugin_name: String) -> Option<bool> {
PyStatus::get().get_status(&plugin_name)
}
/// 同步状态到配置文件
/// 这样关闭的时候就会保存状态
pub fn sync_status_to_config(&self) { PyStatus::get_mut().config.sync_status_to_config(); }
/// 重新加载插件
///
/// 返回是否成功
pub fn reload_plugin(&self, plugin_name: String) -> bool {
PyStatus::get_mut().reload_plugin(&plugin_name)
}
pub fn debug(&self, content: String) {
event!(Level::DEBUG, "{}", content);
}
pub fn info(&self, content: String) {
event!(Level::INFO, "{}", content);
}
pub fn warn(&self, content: String) {
event!(Level::WARN, "{}", content);
}
}
impl IcaClientPy {
pub fn new(client: &Client) -> Self {
Self {
client: client.clone(),
}
}
}

View File

@ -0,0 +1,59 @@
use std::time::Duration;
use pyo3::{Bound, Py, PyTraverseError, PyVisit, Python, pyclass, pymethods, types::PyFunction};
use tracing::{Level, event};
#[derive(Debug)]
#[pyclass]
#[pyo3(name = "Scheduler")]
/// 用于计划任务的类
///
/// 给 Python 侧使用
///
/// add: 0.9.0
pub struct SchedulerPy {
/// 回调函数
///
/// 你最好不要把他清理掉
pub callback: Py<PyFunction>,
/// 预计等待时间
pub schdule_time: Duration,
}
#[pymethods]
impl SchedulerPy {
fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
visit.call(&self.callback)?;
Ok(())
}
#[new]
pub fn new(func: Bound<'_, PyFunction>, schdule_time: Duration) -> Self {
Self {
callback: func.unbind(),
schdule_time,
}
}
/// 开始
pub fn start(&self, py: Python<'_>) {
let wait = self.schdule_time;
let cb = self.callback.clone_ref(py);
tokio::spawn(async move {
let second = Duration::from_secs(1);
if wait > second {
let big_sleep = wait.checked_sub(second).unwrap();
tokio::time::sleep(big_sleep).await;
tokio::time::sleep(second).await;
} else {
tokio::time::sleep(wait).await;
}
Python::with_gil(|py| {
event!(Level::INFO, "正在调用计划 {:?}", wait);
if let Err(e) = cb.call0(py) {
event!(Level::WARN, "调用时出现错误 {}", e);
}
});
});
}
}

View File

@ -0,0 +1,192 @@
use std::time::SystemTime;
use pyo3::prelude::*;
use rust_socketio::asynchronous::Client;
use tokio::runtime::Runtime;
use tracing::{debug, info, warn};
use crate::data_struct::tailchat::messages::{ReceiveMessage, SendingFile, SendingMessage};
use crate::data_struct::tailchat::{ConverseId, GroupId, MessageId, UserId};
use crate::py::PyStatus;
use crate::tailchat::client::send_message;
#[pyclass]
#[pyo3(name = "TailchatClient")]
pub struct TailchatClientPy {
pub client: Client,
}
impl TailchatClientPy {
pub fn new(client: &Client) -> Self {
Self {
client: client.clone(),
}
}
}
#[pyclass]
#[pyo3(name = "TailchatStatus")]
/// 预留?
pub struct TailchatStatusPy {}
#[pyclass]
#[pyo3(name = "TailchatReceiveMessage")]
pub struct TailchatReceiveMessagePy {
pub message: ReceiveMessage,
}
impl TailchatReceiveMessagePy {
pub fn from_recive_message(msg: &ReceiveMessage) -> Self {
Self {
message: msg.clone(),
}
}
}
#[derive(Clone)]
#[pyclass]
#[pyo3(name = "TailchatSendingMessage")]
pub struct TailchatSendingMessagePy {
pub message: SendingMessage,
}
#[pymethods]
impl TailchatClientPy {
pub fn send_message(&self, message: TailchatSendingMessagePy) -> bool {
tokio::task::block_in_place(|| {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(send_message(&self.client, &message.message))
})
}
pub fn send_and_warn(&self, message: TailchatSendingMessagePy) -> bool {
warn!("{}", message.message.content);
self.send_message(message)
}
#[getter]
pub fn get_version(&self) -> String { crate::VERSION.to_string() }
#[getter]
pub fn get_version_str(&self) -> String { crate::version_str() }
#[getter]
pub fn get_client_id(&self) -> String { crate::client_id() }
#[getter]
pub fn get_tailchat_version(&self) -> String { crate::TAILCHAT_VERSION.to_string() }
#[getter]
pub fn get_startup_time(&self) -> SystemTime { crate::start_up_time() }
#[getter]
pub fn get_py_tasks_count(&self) -> usize {
tokio::task::block_in_place(|| {
let rt = Runtime::new().unwrap();
rt.block_on(async { crate::py::call::PY_TASKS.lock().await.len_check() })
})
}
/// 重新加载插件状态
/// 返回是否成功
pub fn reload_plugin_status(&self) -> bool { PyStatus::get_mut().config.reload_from_default() }
/// 设置某个插件的状态
pub fn set_plugin_status(&self, plugin_name: String, status: bool) {
PyStatus::get_mut().set_status(&plugin_name, status);
}
pub fn get_plugin_status(&self, plugin_name: String) -> Option<bool> {
PyStatus::get().get_status(&plugin_name)
}
/// 同步状态到配置文件
/// 这样关闭的时候就会保存状态
pub fn sync_status_to_config(&self) { PyStatus::get_mut().config.sync_status_to_config(); }
/// 重新加载插件
///
/// 返回是否成功
pub fn reload_plugin(&self, plugin_name: String) -> bool {
PyStatus::get_mut().reload_plugin(&plugin_name)
}
#[pyo3(signature = (content, converse_id, group_id = None))]
pub fn new_message(
&self,
content: String,
converse_id: ConverseId,
group_id: Option<GroupId>,
) -> TailchatSendingMessagePy {
TailchatSendingMessagePy {
message: SendingMessage::new(content, converse_id, group_id, None),
}
}
pub fn debug(&self, content: String) {
debug!("{}", content);
}
pub fn info(&self, content: String) {
info!("{}", content);
}
pub fn warn(&self, content: String) {
warn!("{}", content);
}
}
#[pymethods]
impl TailchatReceiveMessagePy {
#[getter]
pub fn get_is_reply(&self) -> bool { self.message.is_reply() }
#[getter]
pub fn get_is_from_self(&self) -> bool { self.message.is_from_self() }
#[getter]
pub fn get_msg_id(&self) -> MessageId { self.message.msg_id.clone() }
#[getter]
pub fn get_content(&self) -> String { self.message.content.clone() }
#[getter]
pub fn get_sender_id(&self) -> UserId { self.message.sender_id.clone() }
#[getter]
pub fn get_group_id(&self) -> Option<GroupId> { self.message.group_id.clone() }
#[getter]
pub fn get_converse_id(&self) -> ConverseId { self.message.converse_id.clone() }
/// 作为回复
pub fn as_reply(&self) -> TailchatSendingMessagePy {
TailchatSendingMessagePy {
message: self.message.as_reply(),
}
}
pub fn reply_with(&self, content: String) -> TailchatSendingMessagePy {
TailchatSendingMessagePy {
message: self.message.reply_with(&content),
}
}
}
#[pymethods]
impl TailchatSendingMessagePy {
#[getter]
pub fn get_content(&self) -> String { self.message.content.clone() }
#[setter]
pub fn set_content(&mut self, content: String) { self.message.content = content; }
#[getter]
pub fn get_converse_id(&self) -> ConverseId { self.message.converse_id.clone() }
#[setter]
pub fn set_converse_id(&mut self, converse_id: ConverseId) {
self.message.converse_id = converse_id;
}
#[getter]
pub fn get_group_id(&self) -> Option<GroupId> { self.message.group_id.clone() }
#[setter]
pub fn set_group_id(&mut self, group_id: Option<GroupId>) { self.message.group_id = group_id; }
pub fn with_content(&mut self, content: String) -> Self {
self.message.content = content;
self.clone()
}
pub fn clear_meta(&mut self) -> Self {
self.message.meta = None;
self.clone()
}
pub fn set_img(&mut self, file: Vec<u8>, file_name: String) {
let file = SendingFile::Image {
file,
name: file_name,
};
self.message.add_img(file);
}
}

192
ica-rs/src/py/config.rs Normal file
View File

@ -0,0 +1,192 @@
use std::{path::Path, str::FromStr};
use colored::Colorize;
use toml_edit::{DocumentMut, Key, Table, TomlError, Value, value};
use tracing::{Level, event};
use crate::MainStatus;
use crate::py::PyStatus;
/// ```toml
/// # 这个文件是由 shenbot 自动生成的, 请 **谨慎** 修改
/// # 请不要修改这个文件, 除非你知道你在做什么
///
/// [plugins]
/// "xxxxxxx" = false # 被禁用的插件
/// "xxxxxxx" = true # 被启用的插件
/// ```
#[derive(Debug, Clone)]
pub struct PluginConfigFile {
pub data: DocumentMut,
}
const CONFIG_KEY: &str = "plugins";
pub const CONFIG_FILE_NAME: &str = "plugins.toml";
pub const DEFAULT_CONFIG: &str = r#"
# shenbot , ****
# ,
[plugins]
"#;
#[allow(unused)]
impl PluginConfigFile {
pub fn from_str(data: &str) -> Result<Self, TomlError> {
let mut data = DocumentMut::from_str(data)?;
if data.get(CONFIG_KEY).is_none() {
event!(Level::WARN, "插件配置文件缺少 plugins 字段, 正在初始化");
data.insert_formatted(
&Key::from_str(CONFIG_KEY).unwrap(),
toml_edit::Item::Table(Table::new()),
);
}
Ok(Self { data })
}
pub fn default_init() -> anyhow::Result<Self> {
let config_path = MainStatus::global_config().py().config_path.clone();
let path = Path::new(&config_path);
Self::from_config_path(path)
}
pub fn reload_from_default(&mut self) -> bool {
let new_config = Self::default_init();
if let Err(e) = new_config {
event!(Level::ERROR, "从配置文件重加载时遇到错误: {}", e);
return false;
}
let new_config = new_config.unwrap();
self.data = new_config.data;
true
}
pub fn from_config_path(path: &Path) -> anyhow::Result<Self> {
let config_path = path.join(CONFIG_FILE_NAME);
if !config_path.exists() {
event!(Level::WARN, "插件配置文件不存在, 正在创建");
std::fs::write(&config_path, DEFAULT_CONFIG)?;
Ok(Self::from_str(DEFAULT_CONFIG)?)
} else {
let data = std::fs::read_to_string(&config_path)?;
Ok(Self::from_str(&data)?)
}
}
fn get_table(&self) -> Option<&Table> {
self.data.get(CONFIG_KEY).and_then(|item| item.as_table())
}
fn get_table_mut(&mut self) -> Option<&mut Table> {
self.data.get_mut(CONFIG_KEY).and_then(|item| item.as_table_mut())
}
/// 获取插件状态
/// 默认为 true
pub fn get_status(&self, plugin_id: &str) -> bool {
if let Some(item) = self.data.get(CONFIG_KEY) {
if let Some(table) = item.as_table() {
if let Some(item) = table.get(plugin_id) {
if let Some(bool) = item.as_bool() {
return bool;
}
}
}
}
true
}
/// 删掉一个状态
pub fn remove_status(&mut self, path: &Path) -> Option<bool> {
let path_str = path.to_str().unwrap();
if let Some(table) = self.get_table_mut() {
if let Some(item) = table.get_mut(path_str) {
if let Some(bool) = item.as_bool() {
table.remove(path_str);
return Some(bool);
} else {
table.remove(path_str);
return Some(false);
}
}
}
None
}
/// 设置插件状态
pub fn set_status(&mut self, path: &Path, status: bool) {
let path_str = path.to_str().unwrap();
let table = self.data.get_mut(CONFIG_KEY).unwrap().as_table_mut().unwrap();
if table.contains_key(path_str) {
match table.get_mut(path_str).unwrap().as_value_mut() {
Some(value) => *value = Value::from(status),
None => {
table.insert(path_str, value(status));
}
}
} else {
table.insert(path_str, value(status));
}
}
/// 从默认文件读取状态
///
/// 返回是否成功
pub fn read_status_from_default(&mut self) -> bool {
if !self.reload_from_default() {
return false;
}
event!(Level::INFO, "同步插件状态");
let plugins = PyStatus::get_mut();
fn fmt_bool(b: bool) -> String {
if b {
"启用".green().to_string()
} else {
"禁用".red().to_string()
}
}
plugins.files.iter_mut().for_each(|(path, status)| {
let plugin_id = status.get_id();
let config_status = self.get_status(&plugin_id);
if config_status != status.enabled {
event!(
Level::INFO,
"插件状态: {} {} -> {}",
status.get_id(),
fmt_bool(status.enabled),
fmt_bool(config_status)
);
status.enabled = config_status;
} else {
event!(
Level::INFO,
"插件状态: {} {} (没变)",
status.get_id(),
fmt_bool(status.enabled)
);
}
});
true
}
pub fn sync_status_to_config(&mut self) {
let plugins = PyStatus::get();
let table = self.data.get_mut(CONFIG_KEY).unwrap().as_table_mut().unwrap();
table.clear();
plugins.files.iter().for_each(|(_, status)| {
table.insert(&status.get_id(), value(status.enabled));
});
}
pub fn write_to_default(&self) -> Result<(), std::io::Error> {
let config_path = MainStatus::global_config().py().config_path.clone();
let config_path = Path::new(&config_path);
self.write_to_file(config_path)
}
pub fn write_to_file(&self, path: &Path) -> Result<(), std::io::Error> {
let config_path = path.join(CONFIG_FILE_NAME);
std::fs::write(config_path, self.data.to_string())?;
Ok(())
}
}

21
ica-rs/src/py/consts.rs Normal file
View File

@ -0,0 +1,21 @@
pub mod events_func {
/// icalingua 的 加群请求
///
/// added: 2.0.1
pub const ICA_JOIN_REQUEST: &str = "on_ica_join_request";
/// icalingua 的 新消息
pub const ICA_NEW_MESSAGE: &str = "on_ica_message";
/// icalingua 的 消息撤回
pub const ICA_DELETE_MESSAGE: &str = "on_ica_delete_message";
/// tailchat 的 新消息
pub const TAILCHAT_NEW_MESSAGE: &str = "on_tailchat_message";
}
pub mod config_func {
/// 请求配置用的函数
pub const REQUIRE_CONFIG: &str = "require_config";
/// 接受配置用的函数
pub const ON_CONFIG: &str = "on_config";
}

View File

@ -1,115 +1,441 @@
pub mod call;
pub mod class; pub mod class;
pub mod config;
pub mod consts;
use std::ffi::CString;
use std::fmt::Display;
use std::path::Path;
use std::sync::OnceLock;
use std::time::SystemTime; use std::time::SystemTime;
use std::{collections::HashMap, path::PathBuf}; use std::{collections::HashMap, path::PathBuf};
use pyo3::prelude::*; use colored::Colorize;
use rust_socketio::asynchronous::Client; use pyo3::{
use tracing::{debug, info, warn}; Bound, Py, PyErr, PyResult, Python,
exceptions::PyTypeError,
intern,
types::{PyAnyMethods, PyModule, PyTracebackMethods, PyTuple},
};
use tracing::{Level, event, span, warn};
use crate::config::IcaConfig; use crate::MainStatus;
use crate::data_struct::messages::NewMessage; use crate::error::PyPluginError;
#[derive(Debug, Clone)] use consts::config_func;
#[derive(Debug)]
pub struct PyStatus { pub struct PyStatus {
pub files: Option<HashMap<PathBuf, (Option<SystemTime>, Py<PyAny>)>>, pub files: PyPlugins,
pub config: config::PluginConfigFile,
} }
pub type PyPlugins = HashMap<PathBuf, PyPlugin>;
pub type RawPyPlugin = (PathBuf, Option<SystemTime>, String);
#[allow(non_upper_case_globals)]
static mut PyPluginStatus: OnceLock<PyStatus> = OnceLock::new();
#[allow(static_mut_refs)]
impl PyStatus { impl PyStatus {
pub fn get_files() -> &'static HashMap<PathBuf, (Option<SystemTime>, Py<PyAny>)> { pub fn init() {
unsafe { let config =
match PYSTATUS.files.as_ref() { config::PluginConfigFile::default_init().expect("初始化 Python 插件配置文件失败");
Some(files) => files, let status = PyStatus {
None => { files: HashMap::new(),
debug!("No files in py status"); config,
PYSTATUS.files = Some(HashMap::new()); };
PYSTATUS.files.as_ref().unwrap() let _ = unsafe { PyPluginStatus.get_or_init(|| status) };
} }
pub fn get() -> &'static PyStatus { unsafe { PyPluginStatus.get().unwrap() } }
pub fn get_mut() -> &'static mut PyStatus { unsafe { PyPluginStatus.get_mut().unwrap() } }
/// 添加一个插件
pub fn add_file(&mut self, path: PathBuf, plugin: PyPlugin) { self.files.insert(path, plugin); }
/// 重新加载一个插件
pub fn reload_plugin(&mut self, plugin_name: &str) -> bool {
let plugin = self.files.iter_mut().find_map(|(_, plugin)| {
if plugin.get_id() == plugin_name {
Some(plugin)
} else {
None
} }
});
if let Some(plugin) = plugin {
plugin.reload_from_file()
} else {
event!(Level::WARN, "没有找到插件: {}", plugin_name);
false
} }
} }
pub fn add_file(path: PathBuf, changed_time: Option<SystemTime>, py_module: Py<PyAny>) { /// 删除一个插件
unsafe { pub fn delete_file(&mut self, path: &PathBuf) -> Option<PyPlugin> { self.files.remove(path) }
match PYSTATUS.files.as_mut() {
Some(files) => { pub fn get_status(&self, pluging_id: &str) -> Option<bool> {
files.insert(path, (changed_time, py_module)); self.files.iter().find_map(|(_, plugin)| {
debug!("Added file to py status, {:?}", files); if plugin.get_id() == pluging_id {
} return Some(plugin.enabled);
None => {
warn!("No files in py status, creating new");
let mut files = HashMap::new();
files.insert(path, (changed_time, py_module));
PYSTATUS.files = Some(files);
}
} }
} None
})
} }
pub fn verify_file(path: &PathBuf) -> bool { pub fn set_status(&mut self, pluging_id: &str, status: bool) {
unsafe { self.files.iter_mut().for_each(|(_, plugin)| {
match PYSTATUS.files.as_ref() { if plugin.get_id() == pluging_id {
Some(files) => match files.get(path) { plugin.enabled = status;
Some((changed_time, _)) => {
if let Some(changed_time) = changed_time {
if let Some(new_changed_time) = get_change_time(path) {
if new_changed_time != *changed_time {
return false;
}
}
}
true
},
None => false,
},
None => false,
} }
} });
}
pub fn verify_file(&self, path: &PathBuf) -> bool {
self.files.get(path).is_some_and(|plugin| plugin.verifiy())
}
pub fn display() -> String {
format!(
"Python 插件 {{ {} }}",
Self::get()
.files
.values()
.map(|v| v.to_string())
.collect::<Vec<String>>()
.join("\n")
)
} }
} }
pub static mut PYSTATUS: PyStatus = PyStatus { files: None }; pub fn get_py_err_traceback(py_err: &PyErr) -> String {
Python::with_gil(|py| match py_err.traceback(py) {
Some(traceback) => match traceback.format() {
Ok(trace) => trace,
Err(e) => format!("{:?}", e),
},
None => "".to_string(),
})
.red()
.to_string()
}
#[derive(Debug)]
pub struct PyPlugin {
pub file_path: PathBuf,
pub modify_time: Option<SystemTime>,
pub py_module: Py<PyModule>,
pub enabled: bool,
}
impl PyPlugin {
pub fn new(path: PathBuf, modify_time: Option<SystemTime>, module: Py<PyModule>) -> Self {
PyPlugin {
file_path: path.clone(),
modify_time,
py_module: module,
enabled: false,
}
}
/// 从文件创建一个新的
pub fn new_from_path(path: &PathBuf) -> Option<Self> {
let raw_file = load_py_file(path);
match raw_file {
Ok(raw_file) => match Self::try_from(raw_file) {
Ok(plugin) => Some(plugin),
Err(e) => {
warn!(
"加载 Python 插件文件{:?}: {:?} 失败\n{}",
path,
e,
get_py_err_traceback(&e)
);
None
}
},
Err(e) => {
warn!("加载插件 {:?}: {:?} 失败", path, e);
None
}
}
}
/// 从文件更新
pub fn reload_from_file(&mut self) -> bool {
let raw_file = load_py_file(&self.file_path);
match raw_file {
Ok(raw_file) => match Self::try_from(raw_file) {
Ok(plugin) => {
self.py_module = plugin.py_module;
self.modify_time = plugin.modify_time;
self.enabled = PyStatus::get().config.get_status(&self.get_id());
event!(Level::INFO, "更新 Python 插件文件 {:?} 完成", self.file_path);
true
}
Err(e) => {
warn!(
"更新 Python 插件文件{:?}: {:?} 失败\n{}",
self.file_path,
e,
get_py_err_traceback(&e)
);
false
}
},
Err(e) => {
warn!("更新插件 {:?}: {:?} 失败", self.file_path, e);
false
}
}
}
/// 检查文件是否被修改
pub fn verifiy(&self) -> bool {
match get_change_time(&self.file_path) {
None => false,
Some(time) => {
if let Some(changed_time) = self.modify_time {
time.eq(&changed_time)
} else {
true
}
}
}
}
pub fn get_id(&self) -> String { plugin_path_as_id(&self.file_path) }
}
impl Display for PyPlugin {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}({:?})-{}", self.get_id(), self.file_path, self.enabled)
}
}
pub const CONFIG_DATA_NAME: &str = "CONFIG_DATA";
fn set_str_cfg_default_plugin(
module: &Bound<'_, PyModule>,
default: String,
path: String,
) -> PyResult<()> {
let base_path = MainStatus::global_config().py().config_path;
let mut base_path: PathBuf = PathBuf::from(base_path);
if !base_path.exists() {
event!(Level::WARN, "python 插件路径不存在, 创建: {:?}", base_path);
std::fs::create_dir_all(&base_path)?;
}
base_path.push(&path);
let config_str: String = if base_path.exists() {
event!(Level::INFO, "加载 {:?} 的配置文件 {:?} 中", path, base_path);
match std::fs::read_to_string(&base_path) {
Ok(v) => v,
Err(e) => {
event!(Level::WARN, "配置文件 {:?} 读取失败 {}, 创建默认配置", base_path, e);
// 写入默认配置
std::fs::write(&base_path, &default)?;
default
}
}
} else {
event!(Level::WARN, "配置文件 {:?} 不存在, 创建默认配置", base_path);
// 写入默认配置
std::fs::write(base_path, &default)?;
default
};
if let Err(e) = module.setattr(intern!(module.py(), CONFIG_DATA_NAME), &config_str) {
event!(Level::WARN, "Python 插件 {:?} 的配置文件信息设置失败:{:?}", path, e);
return Err(PyTypeError::new_err(format!(
"Python 插件 {:?} 的配置文件信息设置失败:{:?}",
path, e
)));
}
// 给到 on config
if let Ok(attr) = module.getattr(intern!(module.py(), config_func::ON_CONFIG)) {
if !attr.is_callable() {
event!(
Level::WARN,
"Python 插件 {:?} 的 {} 函数不是 Callable",
path,
config_func::ON_CONFIG
);
return Ok(());
}
let args = (config_str.as_bytes(),);
if let Err(e) = attr.call1(args) {
event!(
Level::WARN,
"Python 插件 {:?} 的 {} 函数返回了一个报错 {}",
path,
config_func::ON_CONFIG,
e
);
}
}
Ok(())
}
fn set_bytes_cfg_default_plugin(
module: &Bound<'_, PyModule>,
default: Vec<u8>,
path: String,
) -> PyResult<()> {
let base_path = MainStatus::global_config().py().config_path;
let mut base_path: PathBuf = PathBuf::from(base_path);
if !base_path.exists() {
event!(Level::WARN, "python 插件路径不存在, 创建: {:?}", base_path);
std::fs::create_dir_all(&base_path)?;
}
base_path.push(&path);
let config_vec: Vec<u8> = if base_path.exists() {
event!(Level::INFO, "加载 {:?} 的配置文件 {:?} 中", path, base_path);
match std::fs::read(&base_path) {
Ok(v) => v,
Err(e) => {
event!(Level::WARN, "配置文件 {:?} 读取失败 {}, 创建默认配置", base_path, e);
// 写入默认配置
std::fs::write(&base_path, &default)?;
default
}
}
} else {
event!(Level::WARN, "配置文件 {:?} 不存在, 创建默认配置", base_path);
// 写入默认配置
std::fs::write(base_path, &default)?;
default
};
match module.setattr(intern!(module.py(), CONFIG_DATA_NAME), &config_vec) {
Ok(()) => (),
Err(e) => {
warn!("Python 插件 {:?} 的配置文件信息设置失败:{:?}", path, e);
return Err(PyTypeError::new_err(format!(
"Python 插件 {:?} 的配置文件信息设置失败:{:?}",
path, e
)));
}
}
// 给到 on config
if let Ok(attr) = module.getattr(intern!(module.py(), config_func::ON_CONFIG)) {
if !attr.is_callable() {
event!(
Level::WARN,
"Python 插件 {:?} 的 {} 函数不是 Callable",
path,
config_func::ON_CONFIG
);
return Ok(());
}
let args = (&config_vec,);
if let Err(e) = attr.call1(args) {
event!(
Level::WARN,
"Python 插件 {:?} 的 {} 函数返回了一个报错 {}",
path,
config_func::ON_CONFIG,
e
);
}
}
Ok(())
}
impl TryFrom<RawPyPlugin> for PyPlugin {
type Error = PyErr;
fn try_from(value: RawPyPlugin) -> Result<Self, Self::Error> {
let (path, modify_time, content) = value;
let py_module: Py<PyModule> = match py_module_from_code(&content, &path) {
Ok(module) => module,
Err(e) => {
warn!("加载 Python 插件: {:?} 失败", e);
return Err(e);
}
};
Python::with_gil(|py| {
let module = py_module.bind(py);
if let Ok(config_func) = call::get_func(module, config_func::REQUIRE_CONFIG) {
match config_func.call0() {
Ok(config) => {
if config.is_instance_of::<PyTuple>() {
// let (config, default) = config.extract::<(String, Vec<u8>)>().unwrap();
// let (config, default) = config.extract::<(String, String)>().unwrap();
if let Ok((config, default)) = config.extract::<(String, String)>() {
set_str_cfg_default_plugin(module, default, config)?;
} else if let Ok((config, default)) =
config.extract::<(String, Vec<u8>)>()
{
set_bytes_cfg_default_plugin(module, default, config)?;
} else {
warn!(
"加载 Python 插件 {:?} 的配置文件信息时失败:返回的不是 [str, bytes | str]",
path
);
return Err(PyTypeError::new_err(
"返回的不是 [str, bytes | str]".to_string(),
));
}
Ok(PyPlugin::new(path, modify_time, module.clone().unbind()))
} else if config.is_none() {
// 没有配置文件
Ok(PyPlugin::new(path, modify_time, module.clone().unbind()))
} else {
warn!(
"加载 Python 插件 {:?} 的配置文件信息时失败:返回的不是 [str, str]",
path
);
Err(PyTypeError::new_err("返回的不是 [str, str]".to_string()))
}
}
Err(e) => {
warn!("加载 Python 插件 {:?} 的配置文件信息时失败:{:?}", path, e);
Err(e)
}
}
} else {
Ok(PyPlugin::new(path, modify_time, module.clone().unbind()))
}
})
}
}
/// 插件路径转换为 id
pub fn plugin_path_as_id(path: &Path) -> String {
path.file_name()
.unwrap_or_default()
.to_str()
.unwrap_or("decode-failed")
.to_string()
}
pub fn load_py_plugins(path: &PathBuf) { pub fn load_py_plugins(path: &PathBuf) {
let plugins = PyStatus::get_mut();
if path.exists() { if path.exists() {
info!("finding plugins in: {:?}", path); event!(Level::INFO, "找到位于 {:?} 的插件", path);
// 搜索所有的 py 文件 和 文件夹单层下面的 py 文件 // 搜索所有的 py 文件 和 文件夹单层下面的 py 文件
match path.read_dir() { match path.read_dir() {
Err(e) => { Err(e) => {
warn!("failed to read plugin path: {:?}", e); event!(Level::WARN, "读取插件路径失败 {:?}", e);
} }
Ok(dir) => { Ok(dir) => {
for entry in dir { for entry in dir {
if let Ok(entry) = entry { let entry = entry.unwrap();
let path = entry.path(); let path = entry.path();
if let Some(ext) = path.extension() { if let Some(ext) = path.extension() {
if ext == "py" { if ext == "py" {
match load_py_file(&path) { if let Some(plugin) = PyPlugin::new_from_path(&path) {
Ok((changed_time, content)) => { plugins.add_file(path, plugin);
let py_module: PyResult<Py<PyAny>> = Python::with_gil(|py| -> PyResult<Py<PyAny>> {
let module: PyResult<Py<PyAny>> = PyModule::from_code(
py,
&content,
&path.to_string_lossy(),
&path.to_string_lossy()
)
.map(|module| module.into());
module
});
match py_module {
Ok(py_module) => {
info!("加载到插件: {:?}", path);
PyStatus::add_file(path, changed_time, py_module);
}
Err(e) => {
warn!("failed to load file: {:?} | e: {:?}", path, e);
}
}
}
Err(e) => {
warn!("failed to load file: {:?} | e: {:?}", path, e);
}
}
} }
} }
} }
@ -117,98 +443,159 @@ pub fn load_py_plugins(path: &PathBuf) {
} }
} }
} else { } else {
warn!("plugin path not exists: {:?}", path); event!(Level::WARN, "插件加载目录不存在: {:?}", path);
} }
info!( plugins.config.read_status_from_default();
plugins.config.sync_status_to_config();
event!(
Level::INFO,
"python 插件目录: {:?} 加载完成, 加载到 {} 个插件", "python 插件目录: {:?} 加载完成, 加载到 {} 个插件",
path, path,
PyStatus::get_files().len() plugins.files.len()
); );
} }
pub fn verify_plugins() { pub fn get_change_time(path: &Path) -> Option<SystemTime> { path.metadata().ok()?.modified().ok() }
let plugins = PyStatus::get_files();
for (path, _) in plugins.iter() {
if !PyStatus::verify_file(path) {
info!("file changed: {:?}", path);
if let Ok((changed_time, content)) = load_py_file(path) {
let py_module = Python::with_gil(|py| -> Py<PyAny> {
let module: Py<PyAny> = PyModule::from_code(
py,
&content,
&path.to_string_lossy(),
&path.to_string_lossy(),
// !!!! 请注意, 一定要给他一个名字, cpython 会自动把后面的重名模块覆盖掉前面的
)
.unwrap()
.into();
module
});
PyStatus::add_file(path.clone(), changed_time, py_module);
}
}
}
} pub fn py_module_from_code(content: &str, path: &Path) -> PyResult<Py<PyModule>> {
Python::with_gil(|py| -> PyResult<Py<PyModule>> {
pub fn get_change_time(path: &PathBuf) -> Option<SystemTime> { let module = PyModule::from_code(
path.metadata().ok()?.modified().ok() py,
CString::new(content).unwrap().as_c_str(),
CString::new(path.to_string_lossy().as_bytes()).unwrap().as_c_str(),
CString::new(path.file_name().unwrap().to_string_lossy().as_bytes())
.unwrap()
.as_c_str(),
// !!!! 请注意, 一定要给他一个名字, cpython 会自动把后面的重名模块覆盖掉前面的
)?;
Ok(module.unbind())
})
} }
/// 传入文件路径 /// 传入文件路径
/// 返回 hash 和 文件内容 /// 返回 hash 和 文件内容
pub fn load_py_file(path: &PathBuf) -> std::io::Result<(Option<SystemTime>, String)> { pub fn load_py_file(path: &PathBuf) -> std::io::Result<RawPyPlugin> {
let changed_time = get_change_time(&path); let changed_time = get_change_time(path);
let content = std::fs::read_to_string(path)?; let content = std::fs::read_to_string(path)?;
Ok((changed_time, content)) Ok((path.clone(), changed_time, content))
} }
pub fn init_py(config: &IcaConfig) { fn init_py_with_env_path(path: &str) {
debug!("initing python threads"); unsafe {
pyo3::prepare_freethreaded_python(); #[cfg(target_os = "windows")]
if let Some(plugin_path) = &config.py_plugin_path { use std::ffi::OsStr;
let path = PathBuf::from(plugin_path); #[cfg(target_os = "windows")]
load_py_plugins(&path); use std::os::windows::ffi::OsStrExt;
debug!("python 插件列表: {:#?}", PyStatus::get_files());
}
info!("python inited") let mut config = std::mem::zeroed::<pyo3::ffi::PyConfig>();
} let config_ptr = &mut config as *mut pyo3::ffi::PyConfig;
// 初始化配置
// pyo3::ffi::PyConfig_InitIsolatedConfig(config_ptr);
pyo3::ffi::PyConfig_InitPythonConfig(config_ptr);
/// 执行 new message 的 python 插件 #[cfg(target_os = "linux")]
pub async fn new_message_py(message: &NewMessage, client: &Client) { let wide_path = path.as_bytes().iter().map(|i| *i as i32).collect::<Vec<i32>>();
// 验证插件是否改变 #[cfg(target_os = "windows")]
verify_plugins(); let wide_path = OsStr::new(path).encode_wide().chain(Some(0)).collect::<Vec<u16>>();
let cwd = std::env::current_dir().unwrap();
let plugins = PyStatus::get_files(); // 设置 prefix 和 exec_prefix
for (path, (_, py_module)) in plugins.iter() { pyo3::ffi::PyConfig_SetString(config_ptr, &mut config.prefix as *mut _, wide_path.as_ptr());
// 切换工作目录到运行的插件的位置 pyo3::ffi::PyConfig_SetString(
let mut goto = cwd.clone(); config_ptr,
goto.push(path.parent().unwrap()); &mut config.exec_prefix as *mut _,
wide_path.as_ptr(),
if let Err(e) = std::env::set_current_dir(&goto) { );
warn!("移动工作目录到 {:?} 失败 {:?} cwd: {:?}", goto, e, cwd);
}
Python::with_gil(|py| { // 使用 Py_InitializeFromConfig 初始化 python
let msg = class::NewMessagePy::new(message); let status = pyo3::ffi::Py_InitializeFromConfig(&config as *const _);
let client = class::IcaClientPy::new(client); pyo3::ffi::PyEval_SaveThread();
let args = (msg, client); // 清理配置
let async_py_func = py_module.getattr(py, "on_message"); pyo3::ffi::PyConfig_Clear(config_ptr);
match async_py_func { match status._type {
Ok(async_py_func) => { pyo3::ffi::_PyStatus_TYPE::_PyStatus_TYPE_OK => {
async_py_func.as_ref(py).call1(args).unwrap(); event!(Level::INFO, "根据配置初始化 python 完成");
}
Err(e) => {
warn!("failed to get on_message function: {:?}", e);
}
} }
}); pyo3::ffi::_PyStatus_TYPE::_PyStatus_TYPE_EXIT => {
} event!(Level::ERROR, "不对啊, 怎么刚刚初始化 Python 就 EXIT 了");
}
// 最后切换回来 pyo3::ffi::_PyStatus_TYPE::_PyStatus_TYPE_ERROR => {
if let Err(e) = std::env::set_current_dir(&cwd) { event!(Level::ERROR, "初始化 python 时发生错误: ERROR");
warn!("设置工作目录{:?} 失败:{:?}", cwd, e); pyo3::ffi::Py_ExitStatusException(status);
}
}
}
}
/// Python 侧初始化
pub fn init_py() {
// 从 全局配置中获取 python 插件路径
let span = span!(Level::INFO, "py init");
let _enter = span.enter();
event!(Level::INFO, "开始初始化 python");
// 注册东西
class::regist_class();
let plugin_path = MainStatus::global_config().py().plugin_path;
let cli_args = std::env::args().collect::<Vec<String>>();
if cli_args.contains(&"-env".to_string()) {
let env_path = cli_args.iter().find(|&arg| arg != "-env").expect("未找到 -env 参数的值");
event!(Level::INFO, "找到 -env 参数: {} 正在初始化", env_path);
// 判断一下是否有 VIRTUAL_ENV 环境变量
if let Ok(virtual_env) = std::env::var("VIRTUAL_ENV") {
event!(Level::WARN, "找到 VIRTUAL_ENV 环境变量: {} 将会被 -env 参数覆盖", virtual_env);
}
init_py_with_env_path(env_path);
} else {
// 根据 VIRTUAL_ENV 环境变量 进行一些处理
match std::env::var("VIRTUAL_ENV") {
Ok(virtual_env) => {
event!(Level::INFO, "找到 VIRTUAL_ENV 环境变量: {} 正在初始化", virtual_env);
init_py_with_env_path(&virtual_env);
}
Err(_) => {
event!(Level::INFO, "未找到 VIRTUAL_ENV 环境变量, 正常初始化");
pyo3::prepare_freethreaded_python();
event!(Level::INFO, "prepare_freethreaded_python 完成");
}
}
}
PyStatus::init();
let plugin_path = PathBuf::from(plugin_path);
load_py_plugins(&plugin_path);
event!(Level::DEBUG, "python 插件列表: {}", PyStatus::display());
event!(Level::INFO, "python 初始化完成")
}
pub async fn post_py() -> anyhow::Result<()> {
let status = PyStatus::get_mut();
status.config.sync_status_to_config();
status.config.write_to_default()?;
stop_tasks().await?;
Ok(())
}
async fn stop_tasks() -> Result<(), PyPluginError> {
if call::PY_TASKS.lock().await.is_empty() {
return Ok(());
}
let waiter = tokio::spawn(async {
call::PY_TASKS.lock().await.join_all().await;
});
tokio::select! {
_ = waiter => {
event!(Level::INFO, "Python 任务完成");
Ok(())
}
_ = tokio::signal::ctrl_c() => {
event!(Level::WARN, "正在强制结束 Python 任务");
Err(PyPluginError::PluginNotStopped)
}
} }
} }

127
ica-rs/src/status.rs Normal file
View File

@ -0,0 +1,127 @@
use crate::MAIN_STATUS;
use crate::config::BotConfig;
#[derive(Debug, Clone)]
pub struct BotStatus {
pub config: Option<BotConfig>,
pub ica_status: Option<ica::MainStatus>,
pub tailchat_status: Option<tailchat::MainStatus>,
}
impl BotStatus {
pub fn update_static_config(config: BotConfig) {
unsafe {
MAIN_STATUS.config = Some(config);
}
}
pub fn update_ica_status(status: ica::MainStatus) {
unsafe {
MAIN_STATUS.ica_status = Some(status);
}
}
pub fn update_tailchat_status(status: tailchat::MainStatus) {
unsafe {
MAIN_STATUS.tailchat_status = Some(status);
}
}
pub fn static_init(config: BotConfig) {
unsafe {
MAIN_STATUS.ica_status = Some(ica::MainStatus {
enable: config.check_ica(),
qq_login: false,
current_loaded_messages_count: 0,
rooms: Vec::new(),
online_status: ica::OnlineData::default(),
});
MAIN_STATUS.config = Some(config);
}
}
pub fn global_config() -> &'static BotConfig {
unsafe {
let ptr = &raw const MAIN_STATUS.config;
(*ptr).as_ref().unwrap()
}
}
pub fn global_ica_status() -> &'static ica::MainStatus {
unsafe {
let ptr = &raw const MAIN_STATUS.ica_status;
(*ptr).as_ref().unwrap()
}
}
pub fn global_tailchat_status() -> &'static tailchat::MainStatus {
unsafe {
let ptr = &raw const MAIN_STATUS.tailchat_status;
(*ptr).as_ref().unwrap()
}
}
pub fn global_ica_status_mut() -> &'static mut ica::MainStatus {
unsafe {
let ptr = &raw mut MAIN_STATUS.ica_status;
(*ptr).as_mut().unwrap()
}
}
pub fn global_tailchat_status_mut() -> &'static mut tailchat::MainStatus {
unsafe {
let ptr = &raw mut MAIN_STATUS.tailchat_status;
(*ptr).as_mut().unwrap()
}
}
}
pub mod ica {
use crate::data_struct::ica::all_rooms::Room;
pub use crate::data_struct::ica::online_data::OnlineData;
#[derive(Debug, Clone)]
pub struct MainStatus {
/// 是否启用 ica
pub enable: bool,
/// qq 是否登录
pub qq_login: bool,
/// 当前已加载的消息数量
pub current_loaded_messages_count: u64,
/// 房间数据
pub rooms: Vec<Room>,
/// 在线数据 (Icalingua 信息)
pub online_status: OnlineData,
}
impl MainStatus {
pub fn update_rooms(&mut self, room: Vec<Room>) { self.rooms = room; }
pub fn update_online_status(&mut self, status: OnlineData) { self.online_status = status; }
}
}
pub mod tailchat {
use crate::data_struct::tailchat::UserId;
#[derive(Debug, Clone)]
pub struct MainStatus {
/// 是否启用 tailchat
pub enable: bool,
/// 是否登录
pub login: bool,
/// 用户 ID
pub user_id: UserId,
/// 昵称
pub nick_name: String,
/// 邮箱
pub email: String,
/// JWT Token
pub jwt_token: String,
/// avatar
pub avatar: String,
}
impl MainStatus {
pub fn update_user_id(&mut self, user_id: UserId) { self.user_id = user_id; }
pub fn update_nick_name(&mut self, nick_name: String) { self.nick_name = nick_name; }
pub fn update_email(&mut self, email: String) { self.email = email; }
pub fn update_jwt_token(&mut self, jwt_token: String) { self.jwt_token = jwt_token; }
pub fn update_avatar(&mut self, avatar: String) { self.avatar = avatar; }
}
}

143
ica-rs/src/tailchat.rs Normal file
View File

@ -0,0 +1,143 @@
pub mod client;
pub mod events;
use std::sync::Arc;
use colored::Colorize;
use md5::{Digest, Md5};
use reqwest::ClientBuilder as reqwest_ClientBuilder;
use rust_socketio::async_callback;
use rust_socketio::asynchronous::{Client, ClientBuilder};
use rust_socketio::{Event, Payload, TransportType};
use serde_json::{Value, json};
use tracing::{Level, event, span};
use crate::config::TailchatConfig;
use crate::data_struct::tailchat::status::{BotStatus, LoginData};
use crate::error::{ClientResult, TailchatError};
use crate::{StopGetter, async_any_callback_with_state, async_callback_with_state, version_str};
pub async fn start_tailchat(
config: TailchatConfig,
stop_reciver: StopGetter,
) -> ClientResult<(), TailchatError> {
let span = span!(Level::INFO, "Tailchat Client");
let _enter = span.enter();
event!(Level::INFO, "tailchat-async-rs v{} initing", crate::TAILCHAT_VERSION);
let mut hasher = Md5::new();
hasher.update(config.app_id.as_bytes());
hasher.update(config.app_secret.as_bytes());
let token = format!("{:x}", hasher.finalize());
let mut header_map = reqwest::header::HeaderMap::new();
header_map.append("Content-Type", "application/json".parse().unwrap());
let client = reqwest_ClientBuilder::new().default_headers(header_map.clone()).build()?;
let status = match client
.post(format!("{}/api/openapi/bot/login", config.host))
.body(json! {{"appId": config.app_id, "token": token}}.to_string())
.send()
.await
{
Ok(resp) => {
if resp.status().is_success() {
let raw_data = resp.text().await?;
let json_data = serde_json::from_str::<Value>(&raw_data).unwrap();
let login_data = serde_json::from_value::<LoginData>(json_data["data"].clone());
match login_data {
Ok(data) => data,
Err(e) => {
event!(Level::ERROR, "login failed: {}|{}", e, raw_data);
return Err(TailchatError::LoginFailed(e.to_string()));
}
}
} else {
return Err(TailchatError::LoginFailed(resp.text().await?));
}
}
Err(e) => return Err(TailchatError::LoginFailed(e.to_string())),
};
status.update_to_global();
let sharded_status = BotStatus::new(status.user_id.clone());
let sharded_status = Arc::new(sharded_status);
let socket = ClientBuilder::new(config.host)
.auth(json!({"token": status.jwt.clone()}))
.transport_type(TransportType::Websocket)
.on_any(async_any_callback_with_state!(events::any_event, sharded_status.clone()))
.on(
"notify:chat.message.add",
async_callback_with_state!(events::on_message, sharded_status.clone()),
)
.on("notify:chat.message.delete", async_callback!(events::on_msg_delete))
.on(
"notify:chat.converse.updateDMConverse",
async_callback!(events::on_converse_update),
)
// .on("notify:chat.message.update", wrap_callback!(events::on_message))
// .on("notify:chat.message.addReaction", wrap_callback!(events::on_msg_update))
.connect()
.await
.unwrap();
event!(Level::INFO, "{}", "已经连接到 tailchat!".green());
// sleep for 500ms to wait for the connection to be established
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
socket.emit("chat.converse.findAndJoinRoom", json!([])).await.unwrap();
event!(Level::INFO, "{}", "tailchat 已经加入房间".green());
if config.notice_start {
event!(Level::INFO, "正在发送启动消息");
for (group, con) in config.notice_room {
event!(Level::INFO, "发送启动消息到: {}|{}", con, group);
let startup_msg =
crate::data_struct::tailchat::messages::SendingMessage::new_without_meta(
format!("{}\n启动成功", version_str()),
con.clone(),
Some(group.clone()),
);
// 反正是 tailchat, 不需要等, 直接发
if let Err(e) = socket.emit("chat.message.sendMessage", startup_msg.as_value()).await {
event!(Level::ERROR, "发送启动消息失败: {}", e);
}
}
}
stop_reciver.await.ok();
event!(Level::INFO, "socketio client stopping");
match socket.disconnect().await {
Ok(_) => {
event!(Level::INFO, "socketio client stopped");
Ok(())
}
Err(e) => {
// 单独处理 SocketIoError(IncompleteResponseFromEngineIo(WebsocketError(AlreadyClosed)))
match e {
rust_socketio::Error::IncompleteResponseFromEngineIo(inner_e) => {
if inner_e.to_string().contains("AlreadyClosed") {
event!(Level::INFO, "socketio client stopped");
Ok(())
} else {
event!(Level::ERROR, "socketio client stopped with error: {:?}", inner_e);
Err(TailchatError::SocketIoError(
rust_socketio::Error::IncompleteResponseFromEngineIo(inner_e),
))
}
}
e => {
event!(Level::ERROR, "socketio client stopped with error: {}", e);
Err(TailchatError::SocketIoError(e))
}
}
}
}
}

View File

@ -0,0 +1,106 @@
use crate::data_struct::tailchat::messages::SendingMessage;
// use crate::data_struct::tailchat::{ConverseId, GroupId, MessageId, UserId};
use colored::Colorize;
use reqwest::multipart;
use rust_socketio::asynchronous::Client;
use serde_json::{Value, json};
use tracing::{Level, event, span};
pub async fn send_message(client: &Client, message: &SendingMessage) -> bool {
let span = span!(Level::INFO, "tailchat send message");
let _enter = span.enter();
let mut value: Value = message.as_value();
if message.contain_file() {
// 处理文件
let mut header = reqwest::header::HeaderMap::new();
header.append(
"X-Token",
crate::MainStatus::global_tailchat_status().jwt_token.clone().parse().unwrap(),
);
let file_client = match reqwest::ClientBuilder::new().default_headers(header).build() {
Ok(client) => client,
Err(e) => {
event!(Level::ERROR, "file_client build failed:{}", format!("{:#?}", e).red());
return false;
}
};
// 感谢 https://stackoverflow.com/questions/65814450/how-to-post-a-file-using-reqwest
let upload_url =
format!("{}/upload", crate::MainStatus::global_config().tailchat().host.clone());
let file_body =
multipart::Part::stream(message.file.file_data()).file_name(message.file.file_name());
let form_data = multipart::Form::new().part("file", file_body);
event!(Level::INFO, "sending file message");
let data = match file_client.post(&upload_url).multipart(form_data).send().await {
Ok(resp) => {
if resp.status().is_success() {
match resp.text().await {
Ok(text) => match serde_json::from_str::<Value>(&text) {
Ok(json) => json,
Err(e) => {
event!(
Level::ERROR,
"file uploaded, but response parse failed:{}",
format!("{:#?}", e).red()
);
return false;
}
},
Err(e) => {
event!(
Level::ERROR,
"file uploaded, but failed to get response:{}",
format!("{:#?}", e).red()
);
return false;
}
}
} else {
event!(Level::ERROR, "file upload faild:{}", format!("{:#?}", resp).red());
return false;
}
}
Err(e) => {
event!(
Level::ERROR,
"file upload failed while posting data:{}",
format!("{:#?}", e).red()
);
return false;
}
};
let content = format!(
"{}{}",
message.content,
message.file.gen_markdown(data["url"].as_str().unwrap())
);
value["content"] = json!(content);
}
match client.emit("chat.message.sendMessage", value).await {
Ok(_) => {
event!(Level::DEBUG, "send message {}", format!("{:#?}", message).cyan());
true
}
Err(e) => {
event!(Level::WARN, "send message failed:{}", format!("{:#?}", e).red());
false
}
}
}
pub async fn emit_join_room(client: &Client) -> bool {
let span = span!(Level::INFO, "tailchat findAndJoinRoom");
let _enter = span.enter();
match client.emit("chat.converse.findAndJoinRoom", json!([])).await {
Ok(_) => {
event!(Level::INFO, "emiting join room");
true
}
Err(e) => {
event!(Level::WARN, "emit_join_room faild:{}", format!("{:#?}", e).red());
false
}
}
}

View File

@ -0,0 +1,171 @@
use std::sync::Arc;
use colored::Colorize;
use rust_socketio::asynchronous::Client;
use rust_socketio::{Event, Payload};
use tracing::{Level, event, info};
use crate::data_struct::tailchat::messages::ReceiveMessage;
use crate::data_struct::tailchat::status::{BotStatus, UpdateDMConverse};
use crate::py::PyStatus;
use crate::py::call::tailchat_new_message_py;
use crate::tailchat::client::{emit_join_room, send_message};
use crate::{MainStatus, VERSION, client_id, help_msg, version_str};
/// 所有
pub async fn any_event(event: Event, payload: Payload, _client: Client, _status: Arc<BotStatus>) {
let handled = [
// 真正处理过的
"notify:chat.message.add",
"notify:chat.message.delete",
"notify:chat.converse.updateDMConverse",
// 也许以后会用到
"notify:chat.message.update",
"notify:chat.message.addReaction",
"notify:chat.message.removeReaction",
// 忽略的
"notify:chat.inbox.append", // 被 @ 之类的事件
];
match &event {
Event::Custom(event_name) => {
if handled.contains(&event_name.as_str()) {
return;
}
}
Event::Message => {
match payload {
Payload::Text(values) => {
if let Some(value) = values.first() {
if handled.contains(&value.as_str().unwrap()) {
return;
}
info!("收到消息 {}", value.to_string().yellow());
}
}
_ => {
return;
}
}
return;
}
_ => (),
}
match payload {
Payload::Binary(ref data) => {
println!("event: {} |{:?}", event, data)
}
Payload::Text(ref data) => {
print!("event: {}", event.as_str().purple());
for value in data {
println!("|{}", value);
}
}
_ => (),
}
}
pub async fn on_message(payload: Payload, client: Client, _status: Arc<BotStatus>) {
if let Payload::Text(values) = payload {
if let Some(value) = values.first() {
let message: ReceiveMessage = match serde_json::from_value(value.clone()) {
Ok(v) => v,
Err(e) => {
event!(Level::WARN, "tailchat_msg {}", value.to_string().red());
event!(Level::WARN, "tailchat_msg {}", format!("{:?}", e).red());
return;
}
};
event!(Level::INFO, "tailchat_msg {}", message.to_string().yellow());
if !message.is_reply() {
if message.content == "/bot-rs" {
let reply = message.reply_with(&version_str());
send_message(&client, &reply).await;
} else if message.content == "/bot-ls" {
let reply = message.reply_with(&format!(
"shenbot-py v{}-{}\n{}",
VERSION,
client_id(),
if MainStatus::global_config().check_py() {
PyStatus::display()
} else {
"未启用 Python 插件".to_string()
}
));
send_message(&client, &reply).await;
} else if message.content == "/bot-help" {
let reply = message.reply_with(&help_msg());
send_message(&client, &reply).await;
}
if MainStatus::global_config().tailchat().admin_list.contains(&message.sender_id) {
// admin 区
let client_id = client_id();
if message.content.starts_with(&format!("/bot-enable-{}", client_id)) {
// 先判定是否为 admin
// 尝试获取后面的信息
if let Some((_, name)) = message.content.split_once(" ") {
match PyStatus::get().get_status(name) {
None => {
let reply = message.reply_with("未找到插件");
send_message(&client, &reply).await;
}
Some(true) => {
let reply = message.reply_with("无变化, 插件已经启用");
send_message(&client, &reply).await;
}
Some(false) => {
PyStatus::get_mut().set_status(name, true);
let reply = message.reply_with("启用插件完成");
send_message(&client, &reply).await;
}
}
}
} else if message.content.starts_with(&format!("/bot-disable-{}", client_id)) {
if let Some((_, name)) = message.content.split_once(" ") {
match PyStatus::get().get_status(name) {
None => {
let reply = message.reply_with("未找到插件");
send_message(&client, &reply).await;
}
Some(false) => {
let reply = message.reply_with("无变化, 插件已经禁用");
send_message(&client, &reply).await;
}
Some(true) => {
PyStatus::get_mut().set_status(name, false);
let reply = message.reply_with("禁用插件完成");
send_message(&client, &reply).await;
}
}
}
}
}
}
tailchat_new_message_py(&message, &client).await;
}
}
}
pub async fn on_msg_delete(payload: Payload, _client: Client) {
if let Payload::Text(values) = payload {
if let Some(value) = values.first() {
info!("删除消息 {}", value.to_string().red());
}
}
}
pub async fn on_converse_update(payload: Payload, client: Client) {
if let Payload::Text(values) = payload {
if let Some(value) = values.first() {
emit_join_room(&client).await;
let update_info: UpdateDMConverse = match serde_json::from_value(value.clone()) {
Ok(value) => value,
Err(e) => {
event!(Level::WARN, "tailchat updateDMConverse {}", value.to_string().red());
event!(Level::WARN, "tailchat updateDMConverse {}", format!("{:?}", e).red());
return;
}
};
info!("更新会话 {}", format!("{:?}", update_info).cyan());
}
}
}

1
ica-rs/src/wasms.rs Normal file
View File

@ -0,0 +1 @@

36
main.py
View File

@ -1,36 +0,0 @@
import asyncio
import argparse
# from lib_not_dr.types import Options
from lib_not_dr.loggers import config
from data_struct import get_config, BotConfig, BotStatus
_version_ = "0.3.3"
logger = config.get_logger("bot")
BOTCONFIG: BotConfig = get_config()
BotStatus = BotStatus()
if __name__ == "__main__":
# --debug
# --config=config.toml
# -n --no-notice
parser = argparse.ArgumentParser(description=f"icalingua bot v{_version_}")
parser.add_argument("-d", "--debug", action="store_true")
parser.add_argument("-n", "--no-notice", action="store_true")
parser.add_argument("-c", "--config", type=str)
args = parser.parse_args()
if args.debug:
logger.global_level = 0
if args.config:
# global BOTCONFIG
BOTCONFIG: BotConfig = get_config(args.config)
if args.no_notice:
BOTCONFIG.notice_start = False
from connect import main
asyncio.run(main())

47
news.md
View File

@ -1,48 +1,5 @@
# 更新日志 # 更新日志
## 0.4.5 ## [0.9](./news/0-9.md)
添加 `is_reply` api 到 `NewMessagePy` ## [0.2 ~ 0.8](./news/old.md)
## 0.4.4
现在正式支持 Python 插件了
`/bmcl` 也迁移到了 Python 插件版本
## 0.4.3
噫! 好! 我成了!
## 0.4.2
现在是 async 版本啦!
## 0.4.1
现在能发送登录信息啦
## 0.4.0
使用 Rust 从头实现一遍
\能登录啦/
## 0.3.3
适配 Rust 端的配置文件修改
## 0.3.1/2
改进 `/bmcl` 的细节
## 0.3.0
合并了 dongdigua 的代码, 把消息处理部分分离
现在代码更阳间了(喜
## 0.2.3
添加了 `/bmcl` 请求 bmclapi 状态
## 0.2.2
重构了一波整体代码

38
news/0-9.md Normal file
View File

@ -0,0 +1,38 @@
# 0.9 更新日志
## 0.9.0
- 修复了 Python 插件停不下来就真的停不下来的问题
- 让初始化的时候 插件启/禁状态显示更明显了
- 有颜色啦!
- 加了不少颜色
### ica 2.0.1
> 添加了 `Room` 相关的 api
- `IcaStatus` 添加了 `rooms(self) -> list[IcaRoom]` 方法
- 用于获取当前所有的房间
- 添加了 `IcaRoom`
- 用于表示一个房间
- `room_id -> int` 群号 (i64)
- `def is_group(self) -> bool` 是否为群聊
- `def is_chat(self) -> bool` 是否为私聊
- `room_name -> int` 群名 (String)
- `unread_count -> int` 未读消息数 (u64)
- `priority -> int` 优先级 (u8)
- `utime -> int` 最后活跃时间 (unix sec * 1000)
- `def new_message_to(self, content: str) -> IcaSendMessage`
- 用于创建一条指向这个房间的消息
> 添加了 Ica 侧的相关配置获取
- `IcaStatus` 添加了 `admins(self) -> list[UserId]` 方法
- 用于获取当前所有的管理员
- `IcaStatus` 添加了 `blocked(self) -> list[UserId]` 方法
- 用于获取当前所有的被屏蔽的人

383
news/olds.md Normal file
View File

@ -0,0 +1,383 @@
# 更新日志 (0.2 ~ 0.8)
## 0.8.2
- ica 兼容版本号更新到 `2.12.28`
- 现在支持通过读取环境变量里的 `VIRTUAL_ENV` 来获取虚拟环境路径
- 用于在虚拟环境中运行插件
- 添加了 `-h` 参数
- 用于展示帮助信息
- 添加了 `-env` 参数
- 用于指定 python 插件的虚拟环境路径
- 会覆盖环境变量中的 `VIRTUAL_ENV`
- 现在加入了默认的配置文件路径 `./config.toml`
- 现在会记录所有的 python 运行中 task 了
- 也会在退出的时候等待所有的 task 结束
- 二次 ctrl-c 会立即退出
- 改进了一下 ica 的新消息显示
- 添加了 ica 链接用时的显示
### ica&tailchat 2.0.0
- BREAKING CHANGE
- 现在 `CONFIG_DATA` 为一个 `str | bytes`
- 用于存储插件的配置信息
- 需要自行解析
- 现在 `on_config` 函数签名为 `on_config = Callable[[bytes], None]`
- 用于接收配置信息
- 现在使用 `require_config = Callable[[None], str, bytes | str]` 函数来请求配置信息
- 用于请求配置信息
- `str` 为配置文件名
- `bytes | str` 为配置文件默认内容
- 以 `bytes` 形式或者 `str` 形式均可
### ica 1.6.7
- 为 `IcaClinet` 添加了 `py_tasks_count -> int` 属性
- 用于获取当前运行中的 python task 数量
### tailchat 1.2.6
- 为 `TailchatClient` 添加了 `py_tasks_count -> int` 属性
- 用于获取当前运行中的 python task 数量
## 0.8.1
- 修复了 Python 插件状态写入的时候写入路径错误的问题
- `ica-typing` 加入了 `from __future__ import annotations`
- 这样就可以随便用 typing 了
- 把 NewType 都扬了
### ica 1.6.6
- 修复了 `send_poke` api 的问题
- 现在可以正常使用了
## 0.8.0
- ica 兼容版本号更新到 ~~`2.12.24`~~ `2.12.26`
- 从 `py::PyStatus` 开始进行一个 `static mut` -> `static mut OnceLock` 的改造
- 用于看着更舒服(逃)
- 部分重构了一下 读取插件启用状态 的配置文件的代码
- 现在 `/bot-help` 会直接输出实际的 client id, 而不是给你一个默认的 `<client-id>`
### ica 1.6.5
- 添加了 `send_room_sign_in` api
- 用于发送群签到信息
- socketio event: `sendGroupSign`
- 添加了 `send_poke` api
- 用于发送戳一戳
- 可以指定群的某个人
- 或者指定好友
- 目前还是有点问题
- socketio event: `sendGroupPoke`
- 添加了 `reload_plugin_status` api
- 用于重新加载插件状态
- 添加了 `reload_plugin(plugin_name: str)` api
- 用于重新加载指定插件
- 添加了 `set_plugin_status(plugin_name: str, status: bool)` api
- 用于设置插件的启用状态
- 添加了 `get_plugin_status(plugin_name: str) -> bool` api
- 用于获取插件的启用状态
- 添加了 `sync_status_to_config` api
- 用于将内存中的插件状态同步到配置文件中
### tailchat 1.2.5
- 添加了 `reload_plugin_status` api
- 用于重新加载插件状态
- 添加了 `reload_plugin(plugin_name: str)` api
- 用于重新加载指定插件
- 添加了 `set_plugin_status(plugin_name: str, status: bool)` api
- 用于设置插件的启用状态
- 添加了 `get_plugin_status(plugin_name: str) -> bool` api
- 用于获取插件的启用状态
- 添加了 `sync_status_to_config` api
- 用于将内存中的插件状态同步到配置文件中
## 0.7.4
- ica 兼容版本号更新到 `2.12.23`
- 通过一个手动 json patch 修复了因为 icalingua 的奇怪类型问题导致的 bug
- [icalingua issue](https://github.com/Icalingua-plus-plus/Icalingua-plus-plus/issues/793)
## 0.7.3
- 也许修复了删除插件不会立即生效的问题
- ica 兼容版本号更新到 `2.12.21`
添加了一些新的 api
### ica 1.6.4
- 给 `SendMessagePy`
- 添加了 `remove_reply` 方法
- 用于取消回复状态
- 删除了 `Room``auto_download``download_path` 字段
- 因为这两个字段也没啥用
### tailcaht 1.2.4
- 给 `TailchatClientPy`
- 添加了 `new_message` 方法
- 用于创建新的消息
- 给 `TailchatSendingMessagePy`
- 添加了 `clear_meta` 功能
- 用于清除 `meta` 字段
- 可以用来取消回复状态
## 0.7.2
- 修复了一些 ica 和 tailchat 表现不一致的问题(捂脸)
## 0.7.1
- 两个 api 版本号分别升级到 `1.6.3(ica)``1.2.3(tailchat)`
- 加入了 `client_id`
- 用的 startup time hash 一遍取后六位
- 以及也有 python 侧的 `client_id` api
- 修复了上个版本其实没有写 python 侧 `version_str` api 的问题
## 0.7.0
> 我决定叫他 0.7.0
> 因为修改太多了.png
- 加入了 禁用/启用 插件功能
- 现在会在插件加载时警告你的插件原来定义了 `CONFIG_DATA` 这一项
- `IcaNewMessage` 添加了新的 api
- `get_sender_name` 获取发送人昵称
- `ica` 兼容版本号 `2.12.11` -> `2.12.12`
- 加入了 `STABLE` 信息, 用于标记稳定版本
- 不少配置文件项加上了默认值
- 添加了 `version_str() -> String` 用于方便的获取版本信息
- 同样在 `py` 侧也有 `version_str` 方法
- 加入了 `/help` 命令
- 用于获取帮助信息
- 加入了 `/bot-ls`
- 用于展示所有插件的信息
- 加入了 `/bot-enable``/bot-disable`
- 用于启用/禁用插件
## 0.6.10
- 加了点东西 (?)
## 0.6.9
我决定立即发布 0.6.9
- 添加了 `Client.startup_time() -> datetime` 方法
- 用于获取 bot 启动时间
- 这样就可以经常吹我 bot 跑了多久了 ( ˘•ω•˘ )
- 顺手加上了 `/bot-uptime` 命令
- 可以获取 bot 运行时间
- 谢谢 GitHub Copilot 的帮助
## 0.6.8
- 修复了一堆拼写错误
- 太难绷了
- `TailchatReciveMessagePy` -> `TailchatReceiveMessagePy`
- `ReciveMessage` -> `ReceiveMessage`
- `ReceiveMessage::meta`
- 从 `JsonValue` 改成 `Option<JsonValue>`
- 用来解决发图片的时候没有 `meta` 字段的问题
- 去除了自带的两个 macro
- `wrap_callback``wrap_any_callback`
- 因为现在他俩已经进到 `rust_socketio` 里啦
- 添加了新的 macro
- 支持了 `TailchatReceiveMessagePy``is_from_self` 方法
- 用于判断是否是自己发的消息
## 0.6.7
游学回来啦
- 处理了一些 tailchat 的特殊情况
- 比如 message 里面的 `GroupId` 实际上是可选的, 在私聊中没有这一项
- 忽略了所有的 `__v` (用于数据库记录信息的, bot不需要管)
- 作者原话 `不用管。数据库记录版本`
- 修复了如果没法解析新的信息, 会 panic 的问题
- `ica_typing.py`
- 补充了 `TailchatSendingMessage``group_id``converse_id` 字段
- 把 `group_id` 的设置和返回都改成了 `Optional[GroupId]`
- tailchat 的 API 也差点意思就是了(逃)
- 处理了 icalingua 的 `renewMessage` 事件 (其实就是直接忽略掉了)
## 0.6.6
游学之前最后一次更新
其实也就五天
正式支持了 tailchat 端
好耶!
[!note]
```text
notice_room = []
notice_start = true
admin_list = []
filter_list = []
```
的功能暂时不支持
## 0.6.5
怎么就突然 0.6.5 了
我也不造啊
- 反正支持了 tailchat 的信息接受
- 但是需要你在对面服务端打开 `DISABLE_MESSAGEPACK` 环境变量
- 能用就行
- 现在 `update_online_data` 不会再以 INFO 级别显示了
- `update_all_room` 同上
## 0.6.2
- 添加 API
- `NewMessage.set_img` 用于设置消息的图片
- `IcaSendMessage.set_img` 用于设置消息的图片 (python)
## 0.6.1
还是没写完 tailchat 支持
因为 rust_socketio 还是没写好 serdelizer 的支持
- 正在添加发送图片的 api
## 0.6.0-dev
- 去除了 matrix 的支持
- 淦哦
- 去除了相应代码和依赖
- 去除了 Python 侧代码
- 向 tailchat (typescript 低头)
- 修复了没法编译的问题(
## 0.5.3
修复了 Icalingua 断开时 如果 socketio 已经断开会导致程序 返回 Error 的问题
以及还有一些别的修复就是了
- Python 端修改
- `on_message` -> `on_ica_message`
- `on_delete_message` -> `on_ica_delete_message`
- 添加 `on_matrix_message`
## 0.5.1/2
重构了一整波, 还没改 `ica-typing.py` 的代码
但至少能用了
- Ica 版本号 `1.4.0`
- Matrix 版本号 `0.1.0`
## 0.5.0
准备接入 `Matrix`
去掉 `pyo3-async` 的依赖
## 0.4.12
把 0.4.11 的遗留问题修完了
## 0.4.11
这几天就是在刷版本号的感觉
- 添加
- `DeleteMessage` 用于删除消息
- `NewMessage.as_delete` 用于将消息转换为删除消息
- `client::delete_message` 用于删除消息
- `client::fetch_history` 用于获取历史消息 TODO
- `py::class::DeleteMessagePy` 用于删除消息 的 Python 侧 API
- `py::class::IcaClientPy.delete_message` 用于删除消息 的 Python 侧 API
- `IcalinguaStatus.current_loaded_messages_count`
- 用于以后加载信息计数
- 修改
- `py::class::IcaStatusPy`
- 大部分方法从手动 `unsafe` + `Option`
- 改成直接调用 `IcalinguaStatus` 的方法
- `IcalinguaStatus`
- 所有方法均改成 直接对着 `IcalinguaStatus` 的方法调用
- 补全没有的方法
## 0.4.10
好家伙, 我感觉都快能叫 0.5 了
修改了一些内部数据结构, 使得插件更加稳定
添加了 `rustfmt.toml` 用于格式化代码
**注意**: 请在提交代码前使用 `cargo +nightly fmt` 格式化代码
修复了 `Message` 解析 `replyMessage` 字段是 如果是 null 则会解析失败的问题
## 0.4.9
修复了 Python 插件运行错误会导致整个程序崩溃的问题
## 0.4.8
添加了 `filter_list` 用于过滤特定人的消息
## 0.4.7
修复了重载时如果代码有问题会直接 panic 的问题
## 0.4.6
现在更适合部署了
## 0.4.5
添加 `is_reply` api 到 `NewMessagePy`
## 0.4.4
现在正式支持 Python 插件了
`/bmcl` 也迁移到了 Python 插件版本
## 0.4.3
噫! 好! 我成了!
## 0.4.2
现在是 async 版本啦!
## 0.4.1
现在能发送登录信息啦
## 0.4.0
使用 Rust 从头实现一遍
\能登录啦/
## 0.3.3
适配 Rust 端的配置文件修改
## 0.3.1/2
改进 `/bmcl` 的细节
## 0.3.0
合并了 dongdigua 的代码, 把消息处理部分分离
现在代码更阳间了(喜
## 0.2.3
添加了 `/bmcl` 请求 bmclapi 状态
## 0.2.2
重构了一波整体代码

View File

@ -1,98 +0,0 @@
import time
import json
import aiohttp
from lib_not_dr.loggers import config
from data_struct import NewMessage, SendMessage
logger = config.get_logger("bmcl")
_version_ = "1.1.1"
def format_data_size(data_bytes: float) -> str:
data_lens = ["B", "KB", "MB", "GB", "TB"]
data_len = "0B"
for i in range(5):
if data_bytes < 1024:
data_bytes = round(data_bytes, 5)
data_len = f"{data_bytes}{data_lens[i]}"
break
else:
data_bytes /= 1024
return data_len
def format_hit_count(count: int) -> str:
"""数据分段, 四位一个下划线
Args:
count (int): 数据
Returns:
str: 格式化后的数据
1 -> 1
1000 -> 1000
10000 -> 1_0000
100000 -> 10_0000
1000000 -> 100_0000
"""
count_str = str(count)
count_len = len(count_str)
if count_len <= 4:
return count_str
else:
return "_".join(count_str[i:i + 4] for i in range(0, count_len, 4))
async def bmcl(sio, reply_msg: SendMessage, msg: NewMessage):
req_time = time.time()
# 记录请求时间
async with aiohttp.ClientSession() as session:
async with session.get(
"https://bd.bangbang93.com/openbmclapi/metric/dashboard"
) as response:
if not response.status == 200 or response.reason != "OK":
await sio.emit(
"sendMessage",
reply_msg.to_content(
f"请求数据失败\n{response.status}"
).to_json(),
)
logger.warn(
f"数据请求失败, 请检查网络\n{response.status}",
tag="bmclapi_dashboard",
)
return
raw_data = await response.text()
try:
data = json.loads(raw_data)
data_bytes: float = data["bytes"]
data_hits: int = data["hits"]
data_bandwidth: float = data["currentBandwidth"]
load_str: float = data["load"] * 100
online_node: int = data["currentNodes"]
online_bandwidth: int = data["bandwidth"]
data_len = format_data_size(data_bytes)
hits_count = format_hit_count(data_hits)
report_msg = (
f"OpenBMCLAPI 状态面板v{_version_} :\n"
f"实时信息: {online_node} 带宽: {online_bandwidth}Mbps\n"
f"负载: {load_str:.2f}% 带宽: {data_bandwidth:.2f}Mbps\n"
f"当日请求: {hits_count} 数据量: {data_len}\n"
f"请求时间: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(req_time))}\n"
"数据源: https://bd.bangbang93.com/pages/dashboard"
)
await sio.emit(
"sendMessage",
reply_msg.to_content(report_msg).to_json()
)
except (json.JSONDecodeError, AttributeError, ValueError) as e:
await sio.emit(
"sendMessage",
reply_msg.to_content(f"返回数据解析错误\n{e}").to_json(),
)
logger.warn(f"返回数据解析错误\n{e}", tag="bmclapi_dashboard")

View File

@ -1,72 +0,0 @@
import time
import random
import traceback
from main import BOTCONFIG
from colorama import Fore
from lib_not_dr.loggers import config
logger = config.get_logger("safe_eval")
def safe_eval(code: str) -> str:
try:
# code = code.replace('help', '坏东西!\n')
# code = code.replace('bytes', '坏东西!\n')
# code = code.replace('encode', '坏东西!\n')
# code = code.replace('decode', '坏东西!\n')
# code = code.replace('compile', '屑的!\n')
# code = code.replace('globals', '拿不到!\n')
code = code.replace("os", "坏东西!\n")
code = code.replace("sys", "坏东西!\n")
# code = code.replace('input', '坏东西!\n')
# code = code.replace('__', '啊哈!\n')
# code = code.replace('import', '很坏!\n')
code = code.replace(" kill", "别跑!\n")
code = code.replace(" rm ", "别跑!\n")
code = code.replace("exit", "好坏!\n")
code = code.replace("eval", "啊哈!\n")
code = code.replace("exec", "抓住!\n")
start_time = time.time()
try:
import os
import math
import decimal
global_val = {
"time": time,
"math": math,
"decimal": decimal,
"random": random,
"__import__": "<built-in function __import__>",
"globals": "<built-in function globals>",
"compile": "<built-in function compile>",
"help": "<built-in function help>",
"exit": "<built-in function exit>",
"input": "<built-in function input>",
"return": "别惦记你那个 return 了",
"getattr": "<built-in function getattr>",
"setattr": "<built-in function setattr>",
}
os.system = "不许"
result = str(eval(code, global_val, {}))
limit = 500
if len(result) > limit:
result = result[:limit]
except:
result = traceback.format_exc()
end_time = time.time()
result = result.replace(BOTCONFIG.private_key, "***")
result = result.replace(BOTCONFIG.host, "***")
logger.info(f"{Fore.MAGENTA}safe_eval: {result}")
if result == "6" or result == 6:
result = "他确实等于 6"
result = f"{code}\neval result:\n{result}\n耗时: {end_time - start_time} s"
return result
except:
error = traceback.format_exc()
result = f"error:\n{error}"
return result

View File

@ -1,47 +1,33 @@
# icalingua bot # icalingua bot
这是一个基于 icalingua docker 版的 bot 这是一个基于 icalingua-bridge 的 bot
> 出于某个企鹅, 和保护 作者 和 原项目 ( ica ) 的原因 \ [插件市场(确信)](https://github.com/shenjackyuanjie/shenbot-plugins)
> 功能请自行理解
## 使用方法 ( Python 版 ) ## 通用环境准备
- 安装依赖 - 安装 Python 3.8+
```powershell ```powershell
python -m pip install -r requirement.txt # 你可以使用你自己的方法安装 Python
# 例如
choco install python
# 或者
scoop install python
# 又或者
uv venv
``` ```
> 如果你想使用虚拟环境 \ - 启动 icalingua 后端
> 可以使用 `python -m venv venv` 创建虚拟环境 \
> 然后使用 `venv\Scripts\activate` 激活虚拟环境 \
> 最后使用 `python -m pip install -r requirement.txt` 安装依赖
- 修改配置文件
```powershell
Copy-Item config-temp.toml config.toml
# 欸我就用 powershell
```
- icalingua 启动!
```bash ```bash
# 用你自己的方法启动你的 icalingua 后端 # 用你自己的方法启动你的 icalingua-bridge
# 例如 # 例如
docker start icalingua docker start icalingua
# 或者 docker-compose up -d
docker up -d
``` ```
- bot 启动! ## 使用方法
```powershell
python connect.py
```
## 使用方法 ( Rust 版 )
- 准备一个 Python 环境 - 准备一个 Python 环境
@ -58,3 +44,7 @@ cargo build --release
``` ```
运行 运行
```powershell
cargo run --release -- -c config.toml
```

View File

@ -1,7 +0,0 @@
lib-not-dr >= 0.3.13
colorama
qtoml
pynacl
python-socketio
aiohttp

View File

@ -1,62 +0,0 @@
import random
import asyncio
from lib_not_dr.loggers import config
from main import BOTCONFIG, _version_
from data_struct import SendMessage, ReplyMessage, NewMessage
from plugins import bmcl, safe_eval
logger = config.get_logger("router")
async def route(data, sio):
is_self = data["message"]["senderId"] == BOTCONFIG.self_id
sender_name = data["message"]["username"]
sender_id = data["message"]["senderId"]
content = data["message"]["content"]
room_id = data["roomId"]
msg_id = data["message"]["_id"]
msg = NewMessage(data=data)
reply_msg = SendMessage(content="", room_id=room_id, reply_to=ReplyMessage(id=msg_id))
if content == "/bot":
message = reply_msg.to_content(f"icalingua bot-python pong v{_version_}")
await sio.emit("sendMessage", message.to_json())
elif content.startswith("=="):
evals: str = content[2:]
result = safe_eval.safe_eval(evals)
# whitelist = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ' ', '.', '+', '-', '*', '/', '(', ')', '<',
# '>', '=']
# evals = evals.replace('**', '')
# express = ''
# for text in evals:
# if text in whitelist:
# express += text
# if express == '':
# result = '你在干嘛'
# else:
# result = str(eval(express))
message = reply_msg.to_content(result)
await asyncio.sleep(random.random() * 2)
await sio.emit("sendMessage", message.to_json())
elif content == "!!jrrp":
randomer = random.Random(
f'{sender_id}-{data["message"]["date"]}-jrrp-{_version_}'
)
result = randomer.randint(0, 50) + randomer.randint(0, 50)
logger.info(f"{sender_name} 今日人品值为 {result}")
message = reply_msg.to_content(f"{sender_name} 今日人品为 {result}")
await asyncio.sleep(0.5)
await sio.emit("sendMessage", message.to_json())
elif content == "/bmcl":
await bmcl.bmcl(sio, reply_msg, msg)

2
rust-toolchain.toml Normal file
View File

@ -0,0 +1,2 @@
[toolchain]
channel = "stable"