mirror of
http://shenjack.top:5100/shenjack/icalingua-python-bot.git
synced 2025-04-20 09:39:54 +08:00
Compare commits
313 Commits
Author | SHA1 | Date | |
---|---|---|---|
57d7e8c8cc | |||
f4ceef050f | |||
56469f6fbb | |||
1f92a62e45 | |||
12d6254c6b | |||
e10dff96d2 | |||
ff130d7f84 | |||
d4999d1ab3 | |||
546214f52f | |||
efc0393ed5 | |||
d93e8f7c9e | |||
9f6206aa5c | |||
9e72c8a117 | |||
ad66657d37 | |||
8b5275608b | |||
3435b8c0fb | |||
1711270c9f | |||
4d0a32ca0e | |||
0826daf410 | |||
eb089273c4 | |||
4c814c45bc | |||
3c5cf2e92c | |||
15b3e328d4 | |||
5d93e38237 | |||
3ce0e0004d | |||
86ab23e45d | |||
3d08a7da91 | |||
e41279d843 | |||
9745590c11 | |||
e02918b33d | |||
befc822be7 | |||
fe411ba137 | |||
15f08a8cfb | |||
2b1c366643 | |||
a63423d545 | |||
65b8e92ce1 | |||
c23b3ee67a | |||
ede6640aa9 | |||
76a3628d2d | |||
4866f2ec2e | |||
4eb553473d | |||
12a32da61e | |||
0cec518f1d | |||
a114a92cba | |||
56a6c39df7 | |||
f8cd207923 | |||
79b306d089 | |||
d8b4fe06f9 | |||
e5f67475db | |||
d94841b1bd | |||
62a0a8d3fa | |||
6638a1f645 | |||
073c711c7c | |||
0275863cfe | |||
3ed1c3e738 | |||
d4c7a55dcc | |||
9711af9444 | |||
974c2577c3 | |||
c80e938a78 | |||
32958031d2 | |||
fcf88f0ebb | |||
16fee092ba | |||
9da0b37db2 | |||
86c19bc3db | |||
75832bfa2e | |||
c5de63f02e | |||
686291755d | |||
38cfd2dce7 | |||
98633aa5cc | |||
4aa969adc5 | |||
9d74853d1e | |||
52aafcab19 | |||
4cf92356c8 | |||
188c357378 | |||
dc936bb7fe | |||
a8129181a8 | |||
6407f7eb86 | |||
780c9aee8d | |||
6d883b0d7d | |||
9ba8cfaba3 | |||
b286765213 | |||
e5ff010a69 | |||
ef3fb5a8cc | |||
c41d796f61 | |||
91aa3f7bd9 | |||
cb87108804 | |||
e88d1fe435 | |||
538f43869f | |||
f0cdac3299 | |||
880fc4c5dd | |||
32f1797edc | |||
38ba77ce42 | |||
313f859c5c | |||
ee2c51fbcc | |||
1c62e4aa0e | |||
113a1518d1 | |||
680934ad3f | |||
5636c8e1d9 | |||
d12773981d | |||
4da93570c9 | |||
8c52d898ff | |||
585f0ca331 | |||
1fa7267f3e | |||
4479002b8d | |||
4753df8ded | |||
b8fd3cfc19 | |||
c970e6ae45 | |||
751402d5c6 | |||
8dd5c0b7d4 | |||
18df7d1a89 | |||
127f0f0ab4 | |||
e2e5142688 | |||
3b67d58ae2 | |||
d8e40bfb4d | |||
047cc110a4 | |||
2f00a3f29a | |||
0ee8091e0d | |||
13995d2915 | |||
17f3a36540 | |||
4b1ed03b9a | |||
38d9988cdc | |||
e6b794173f | |||
f1830d978a | |||
2fd551e2ac | |||
3d338b8ba0 | |||
ed6af04570 | |||
a02bb089a5 | |||
1e81db998f | |||
d9c17ea064 | |||
b1511a972f | |||
de12c495f0 | |||
ca9033a23f | |||
a606db5cb0 | |||
9002103c7b | |||
7d2707b35e | |||
5db2978eb5 | |||
cd67c5b94d | |||
7fbe91e55e | |||
b3e9588763 | |||
dade195e76 | |||
6d6e07a48f | |||
fc60e5ad18 | |||
fca6b38972 | |||
c3be851148 | |||
c72581750a | |||
31a490b40c | |||
aad9ab08f6 | |||
54dfb59b16 | |||
![]() |
d249c33d58 | ||
2340916570 | |||
86cb098b3d | |||
bf40679012 | |||
6bbbaf4cc6 | |||
3a2352d15f | |||
79dbebdd4e | |||
3451424544 | |||
ff255426f6 | |||
5a07286d2d | |||
0858085df7 | |||
954fbed754 | |||
9ccb7ad02c | |||
996d2a327f | |||
4669b6a378 | |||
2675ada851 | |||
bda8a64f2b | |||
f985a741f1 | |||
f8332f7761 | |||
80c5c18bdb | |||
b349cdf4af | |||
c65a2229cc | |||
1eba64bf9e | |||
99082ccfd7 | |||
4dfb17533d | |||
f83f7ceb0f | |||
c02ac6234f | |||
5a6e38274d | |||
769fd4b7cd | |||
49e89ad018 | |||
ae518e5c92 | |||
fd932e21fa | |||
fdd5e03322 | |||
9b96c69bbe | |||
060f960320 | |||
a99467dfbf | |||
6fc85322cd | |||
01187cf8d9 | |||
924d850e01 | |||
3defd42df8 | |||
b458a16c22 | |||
85a37db3ea | |||
71e07970c0 | |||
26133c3c00 | |||
83b163b685 | |||
177ff4692f | |||
1c33a9a33b | |||
e1851377d9 | |||
62791962c2 | |||
0d1d815847 | |||
463169a13a | |||
bec856e9fc | |||
3959dd3dc7 | |||
c41cc5e3ff | |||
5cba529e45 | |||
4116a38020 | |||
2054e6899a | |||
d564350275 | |||
2449dda432 | |||
c17cc006b1 | |||
fbf8411752 | |||
700c76b69a | |||
cc6b52540b | |||
660c0160ad | |||
93f384c0a4 | |||
a31efda338 | |||
be7bcfd00d | |||
c5b319730b | |||
69da7f4f00 | |||
a47c66202b | |||
ec4122bdf3 | |||
987f201ed1 | |||
2f1cffc1e6 | |||
ebd9e99f8d | |||
91086d01a5 | |||
63ea5f0178 | |||
1860321b36 | |||
6335e73a15 | |||
8a9494b593 | |||
79c2e7f53d | |||
e592f30142 | |||
3d0eaf9efd | |||
06d29ee3d6 | |||
84ced3b30a | |||
3d81fce13b | |||
427b113312 | |||
b3e2da9df6 | |||
2f535cc960 | |||
eaae60902d | |||
6dfbc4e879 | |||
0dad42c3ca | |||
e423745a2c | |||
aa20b7f1c3 | |||
b2229c9663 | |||
2ab3f0d77b | |||
ddbdde5ae6 | |||
a574dcaa8a | |||
e0dbf7d21e | |||
84c0426b05 | |||
ec9cd625d1 | |||
de09257249 | |||
4ae11b4d4f | |||
c83a2c4549 | |||
8cd8d93a28 | |||
0843826435 | |||
f6e760e234 | |||
53e652aa7b | |||
48d4c8fd5d | |||
51cc24e347 | |||
4656655017 | |||
bdc7e3e3c3 | |||
14a3d6b3df | |||
a3ed392179 | |||
47b53b245b | |||
469db17c3b | |||
e3708dc41a | |||
d362e7c155 | |||
ae181425e0 | |||
7b8db8136b | |||
d246913b4c | |||
aa641b4b82 | |||
e14ffbddb4 | |||
1b5c33c1d5 | |||
0ef4aeb4f3 | |||
9f5956e77a | |||
f2624dbcca | |||
b41617bb06 | |||
29f6b2efaf | |||
5381ef598a | |||
8448b03d83 | |||
4b3da3b85f | |||
8b2a8ee8d2 | |||
fe06356bea | |||
03fdcc300b | |||
95c2cc377a | |||
63e18e8eab | |||
16ff8f534e | |||
4bad0c95c5 | |||
559de2e2f6 | |||
85608570bf | |||
0420cf36b2 | |||
3ed0f5af1e | |||
c366f6a735 | |||
db3905eec3 | |||
d6443f27bb | |||
28ec8d316d | |||
727e5f84dd | |||
3b280e23c0 | |||
1f7ffcb2d4 | |||
152c8215c3 | |||
ed9ec33ed9 | |||
e09a257886 | |||
59feec8f4e | |||
2540ba3ee2 | |||
e57cc7f3f0 | |||
75f098849e | |||
64dd2d4ad2 | |||
954a5a1b19 | |||
f254879cf0 | |||
8efc7358a3 | |||
e2f619e97f | |||
f1abfd4f9d | |||
ef61b3a6b4 | |||
09aaccf291 | |||
8c72732671 |
43
.github/workflows/builds.yml
vendored
Normal file
43
.github/workflows/builds.yml
vendored
Normal 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
45
.github/workflows/publish.yml
vendored
Normal 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
9
.gitignore
vendored
|
@ -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
2626
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
10
Cargo.toml
Normal file
10
Cargo.toml
Normal 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" }
|
|
@ -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 后通知
|
||||||
|
|
161
connect.py
161
connect.py
|
@ -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())
|
|
102
data_struct.py
102
data_struct.py
|
@ -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
1
ica-rs/.gitignore
vendored
|
@ -1,2 +1,3 @@
|
||||||
target
|
target
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
|
config/
|
||||||
|
|
|
@ -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" }
|
|
||||||
|
|
|
@ -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:
|
|
||||||
...
|
|
|
@ -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)
|
|
|
@ -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
25
ica-rs/rustfmt.toml
Normal 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
|
|
@ -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");
|
|
||||||
}
|
|
|
@ -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") }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>,
|
|
||||||
}
|
|
28
ica-rs/src/data_struct/ica.rs
Normal file
28
ica-rs/src/data_struct/ica.rs
Normal 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() }
|
||||||
|
}
|
137
ica-rs/src/data_struct/ica/all_rooms.rs
Normal file
137
ica-rs/src/data_struct/ica/all_rooms.rs
Normal 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,
|
||||||
|
}
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
}
|
409
ica-rs/src/data_struct/ica/messages.rs
Normal file
409
ica-rs/src/data_struct/ica/messages.rs
Normal 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());
|
||||||
|
// }
|
||||||
|
// }
|
164
ica-rs/src/data_struct/ica/messages/msg_trait.rs
Normal file
164
ica-rs/src/data_struct/ica/messages/msg_trait.rs
Normal 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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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");
|
|
@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
|
|
8
ica-rs/src/data_struct/tailchat.rs
Normal file
8
ica-rs/src/data_struct/tailchat.rs
Normal 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;
|
8
ica-rs/src/data_struct/tailchat/api.rs
Normal file
8
ica-rs/src/data_struct/tailchat/api.rs
Normal 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,
|
||||||
|
}
|
238
ica-rs/src/data_struct/tailchat/messages.rs
Normal file
238
ica-rs/src/data_struct/tailchat/messages.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
65
ica-rs/src/data_struct/tailchat/status.rs
Normal file
65
ica-rs/src/data_struct/tailchat/status.rs
Normal 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
122
ica-rs/src/error.rs
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
133
ica-rs/src/ica.rs
Normal 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
144
ica-rs/src/ica/client.rs
Normal 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
296
ica-rs/src/ica/events.rs
Normal 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);
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
255
ica-rs/src/py/call.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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, "注册完成");
|
||||||
}
|
}
|
||||||
|
|
1
ica-rs/src/py/class/commander.rs
Normal file
1
ica-rs/src/py/class/commander.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
|
343
ica-rs/src/py/class/config.rs
Normal file
343
ica-rs/src/py/class/config.rs
Normal 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
369
ica-rs/src/py/class/ica.rs
Normal 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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
59
ica-rs/src/py/class/schdule.rs
Normal file
59
ica-rs/src/py/class/schdule.rs
Normal 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
192
ica-rs/src/py/class/tailchat.rs
Normal file
192
ica-rs/src/py/class/tailchat.rs
Normal 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
192
ica-rs/src/py/config.rs
Normal 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
21
ica-rs/src/py/consts.rs
Normal 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";
|
||||||
|
}
|
|
@ -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
127
ica-rs/src/status.rs
Normal 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
143
ica-rs/src/tailchat.rs
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
106
ica-rs/src/tailchat/client.rs
Normal file
106
ica-rs/src/tailchat/client.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
171
ica-rs/src/tailchat/events.rs
Normal file
171
ica-rs/src/tailchat/events.rs
Normal 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
1
ica-rs/src/wasms.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
|
36
main.py
36
main.py
|
@ -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
47
news.md
|
@ -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
38
news/0-9.md
Normal 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
383
news/olds.md
Normal 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
|
||||||
|
|
||||||
|
重构了一波整体代码
|
|
@ -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")
|
|
|
@ -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
|
|
48
readme.md
48
readme.md
|
@ -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
|
||||||
|
```
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
lib-not-dr >= 0.3.13
|
|
||||||
colorama
|
|
||||||
qtoml
|
|
||||||
pynacl
|
|
||||||
|
|
||||||
python-socketio
|
|
||||||
aiohttp
|
|
62
router.py
62
router.py
|
@ -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
2
rust-toolchain.toml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
[toolchain]
|
||||||
|
channel = "stable"
|
Loading…
Reference in New Issue
Block a user