mirror of
http://shenjack.top:5100/shenjack/icalingua-python-bot.git
synced 2025-04-20 05:39:54 +08:00
Compare commits
No commits in common. "main" and "0.6.9" have entirely different histories.
2
.github/workflows/builds.yml
vendored
2
.github/workflows/builds.yml
vendored
|
@ -37,7 +37,7 @@ jobs:
|
|||
run: cargo build --release
|
||||
|
||||
- name: 上传
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: ica-rs-b${{ github.run_number }}-${{ steps.get_version.outputs.value }}-py38-win-x64
|
||||
path: ./target/release/ica-rs.exe
|
||||
|
|
2
.github/workflows/publish.yml
vendored
2
.github/workflows/publish.yml
vendored
|
@ -39,7 +39,7 @@ jobs:
|
|||
run: cargo build --release
|
||||
|
||||
- name: 上传
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: ica-rs-b${{ github.run_number }}-${{ steps.get_version.outputs.value }}-py38-win-x64
|
||||
path: ./target/release/ica-rs.exe
|
||||
|
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -1,13 +1,13 @@
|
|||
venv
|
||||
env*
|
||||
|
||||
config*.toml
|
||||
config.toml
|
||||
|
||||
.idea
|
||||
*.pyc
|
||||
*__pycache__/
|
||||
|
||||
make.*
|
||||
make.ps1
|
||||
|
||||
# Added by cargo
|
||||
/target
|
||||
|
|
1408
Cargo.lock
generated
1408
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
|
@ -5,6 +5,6 @@ members = [
|
|||
resolver = "2"
|
||||
|
||||
[patch.crates-io]
|
||||
rust_socketio = { git = "https://github.com/shenjackyuanjie/rust-socketio.git", branch = "main" }
|
||||
# rust_socketio = { git = "https://github.com/shenjackyuanjie/rust-socketio.git", branch = "message_pack" }
|
||||
# rust_socketio = { path = "../../rust-socketio/socketio" }
|
||||
# pyo3 = { git = "https://github.com/PyO3/pyo3.git", branch = "main" }
|
||||
|
|
161
ica-py/connect.py
Normal file
161
ica-py/connect.py
Normal file
|
@ -0,0 +1,161 @@
|
|||
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
ica-py/data_struct.py
Normal file
102
ica-py/data_struct.py
Normal file
|
@ -0,0 +1,102 @@
|
|||
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] = []
|
36
ica-py/main.py
Normal file
36
ica-py/main.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
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())
|
||||
|
98
ica-py/plugins/bmcl.py
Normal file
98
ica-py/plugins/bmcl.py
Normal file
|
@ -0,0 +1,98 @@
|
|||
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")
|
72
ica-py/plugins/safe_eval.py
Normal file
72
ica-py/plugins/safe_eval.py
Normal file
|
@ -0,0 +1,72 @@
|
|||
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
|
7
ica-py/requirements.txt
Normal file
7
ica-py/requirements.txt
Normal file
|
@ -0,0 +1,7 @@
|
|||
lib-not-dr >= 0.3.13
|
||||
colorama
|
||||
qtoml
|
||||
pynacl
|
||||
|
||||
python-socketio
|
||||
aiohttp
|
62
ica-py/router.py
Normal file
62
ica-py/router.py
Normal file
|
@ -0,0 +1,62 @@
|
|||
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)
|
||||
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
[package]
|
||||
name = "ica-rs"
|
||||
version = "0.9.0"
|
||||
edition = "2024"
|
||||
version = "0.6.9"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
|
@ -25,8 +25,8 @@ 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 }
|
||||
reqwest = { version = "0.12.4", optional = true, features = ["multipart"] }
|
||||
md-5 = { version = "0.10.6", optional = true }
|
||||
|
||||
# ica & tailchat (socketio)
|
||||
rust_socketio = { version = "0.6.0", features = ["async"], optional = true }
|
||||
|
@ -36,19 +36,17 @@ serde = { version = "1.0", features = ["derive"] }
|
|||
serde_json = "1.0"
|
||||
chrono = "0.4"
|
||||
toml = "0.8"
|
||||
toml_edit = "0.22"
|
||||
colored = "3.0"
|
||||
colored = "2.1"
|
||||
|
||||
# runtime
|
||||
tokio = { version = "1.43", features = ["rt-multi-thread", "time", "signal", "macros"] }
|
||||
futures-util = "0.3"
|
||||
pyo3 = { version = "0.24", features = ["experimental-async"] }
|
||||
tokio = { version = "1.37", features = ["full"] }
|
||||
futures-util = "0.3.30"
|
||||
pyo3 = { version = "0.21.2", features = ["experimental-async"] }
|
||||
anyhow = { version = "1.0", features = ["backtrace"] }
|
||||
# async 这玩意以后在搞
|
||||
# pyo3-async = "0.3.2"
|
||||
# pyo3-asyncio = { version = "0.20.0", features = ["attributes", "tokio-runtime"] }
|
||||
|
||||
# log
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["time"] }
|
||||
foldhash = "0.1.4"
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["time"] }
|
||||
|
|
306
ica-rs/ica_typing.py
Normal file
306
ica-rs/ica_typing.py
Normal file
|
@ -0,0 +1,306 @@
|
|||
# Python 兼容版本 3.8+
|
||||
|
||||
from typing import Callable, Tuple, NewType, Optional, Union
|
||||
|
||||
"""
|
||||
ica.rs
|
||||
pub type RoomId = i64;
|
||||
pub type UserId = i64;
|
||||
pub type MessageId = String;
|
||||
"""
|
||||
class IcaType:
|
||||
RoomId = NewType('RoomId', int)
|
||||
UserId = NewType('UserId', int)
|
||||
MessageId = NewType('MessageId', str)
|
||||
|
||||
"""
|
||||
tailchat.rs
|
||||
pub type GroupId = String;
|
||||
pub type ConverseId = String;
|
||||
pub type UserId = String;
|
||||
pub type MessageId = String;
|
||||
"""
|
||||
class TailchatType:
|
||||
GroupId = NewType('GroupId', str)
|
||||
ConverseId = NewType('ConverseId', str)
|
||||
UserId = NewType('UserId', str)
|
||||
MessageId = NewType('MessageId', str)
|
||||
|
||||
class IcaStatus:
|
||||
"""
|
||||
ica状态信息
|
||||
此类并不存储信息, 所有方法都是实时获取
|
||||
"""
|
||||
@property
|
||||
def qq_login(self) -> bool:
|
||||
...
|
||||
@property
|
||||
def online(self) -> bool:
|
||||
...
|
||||
@property
|
||||
def self_id(self) -> IcaType.UserId:
|
||||
...
|
||||
@property
|
||||
def nick_name(self) -> str:
|
||||
...
|
||||
@property
|
||||
def ica_version(self) -> str:
|
||||
...
|
||||
@property
|
||||
def os_info(self) -> str:
|
||||
...
|
||||
@property
|
||||
def resident_set_size(self) -> str:
|
||||
...
|
||||
@property
|
||||
def head_used(self) -> str:
|
||||
...
|
||||
@property
|
||||
def load(self) -> str:
|
||||
...
|
||||
|
||||
|
||||
class IcaReplyMessage:
|
||||
...
|
||||
|
||||
|
||||
class IcaSendMessage:
|
||||
@property
|
||||
def content(self) -> str:
|
||||
...
|
||||
@content.setter
|
||||
def content(self, value: str) -> None:
|
||||
...
|
||||
def with_content(self, content: str) -> "IcaSendMessage":
|
||||
"""
|
||||
为了链式调用, 返回自身
|
||||
"""
|
||||
self.content = content
|
||||
return self
|
||||
def set_img(self, file: bytes, file_type: str, as_sticker: bool):
|
||||
"""
|
||||
设置消息的图片
|
||||
@param file: 图片文件 (实际上是 vec<u8>)
|
||||
@param file_type: 图片类型 (MIME) (image/png; image/jpeg)
|
||||
@param as_sticker: 是否作为贴纸发送
|
||||
"""
|
||||
|
||||
|
||||
class IcaDeleteMessage:
|
||||
def __str__(self) -> str:
|
||||
...
|
||||
|
||||
|
||||
class IcaNewMessage:
|
||||
"""
|
||||
Icalingua 接收到新消息
|
||||
"""
|
||||
def reply_with(self, message: str) -> IcaSendMessage:
|
||||
"""回复这条消息"""
|
||||
...
|
||||
def as_deleted(self) -> IcaDeleteMessage:
|
||||
...
|
||||
def __str__(self) -> str:
|
||||
...
|
||||
@property
|
||||
def id(self) -> IcaType.MessageId:
|
||||
...
|
||||
@property
|
||||
def content(self) -> str:
|
||||
...
|
||||
@property
|
||||
def sender_id(self) -> IcaType.UserId:
|
||||
...
|
||||
@property
|
||||
def is_from_self(self) -> bool:
|
||||
...
|
||||
@property
|
||||
def is_reply(self) -> bool:
|
||||
...
|
||||
@property
|
||||
def is_room_msg(self) -> bool:
|
||||
"""是否是群聊消息"""
|
||||
...
|
||||
@property
|
||||
def is_chat_msg(self) -> bool:
|
||||
"""是否是私聊消息"""
|
||||
...
|
||||
@property
|
||||
def room_id(self) -> IcaType.RoomId:
|
||||
"""
|
||||
如果是群聊消息, 返回 (-群号)
|
||||
如果是私聊消息, 返回 对面qq
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class IcaClient:
|
||||
"""
|
||||
Icalingua 的客户端
|
||||
"""
|
||||
# @staticmethod
|
||||
# async def send_message_a(client: "IcaClient", message: SendMessage) -> bool:
|
||||
# """
|
||||
# 仅作占位, 不能使用
|
||||
# (因为目前来说, rust调用 Python端没法启动一个异步运行时
|
||||
# 所以只能 tokio::task::block_in_place 转换成同步调用)
|
||||
# """
|
||||
def send_message(self, message: IcaSendMessage) -> bool:
|
||||
...
|
||||
def send_and_warn(self, message: IcaSendMessage) -> bool:
|
||||
"""发送消息, 并在日志中输出警告信息"""
|
||||
self.warn(message.content)
|
||||
return self.send_message(message)
|
||||
def delete_message(self, message: IcaDeleteMessage) -> bool:
|
||||
...
|
||||
|
||||
@property
|
||||
def status(self) -> IcaStatus:
|
||||
...
|
||||
@property
|
||||
def version(self) -> str:
|
||||
...
|
||||
@property
|
||||
def ica_version(self) -> str:
|
||||
"""shenbot ica 的版本号"""
|
||||
...
|
||||
def debug(self, message: str) -> None:
|
||||
"""向日志中输出调试信息"""
|
||||
...
|
||||
def info(self, message: str) -> None:
|
||||
"""向日志中输出信息"""
|
||||
...
|
||||
def warn(self, message: str) -> None:
|
||||
"""向日志中输出警告信息"""
|
||||
...
|
||||
|
||||
|
||||
class TailchatReciveMessage:
|
||||
"""
|
||||
Tailchat 接收到的新消息
|
||||
"""
|
||||
@property
|
||||
def id(self) -> TailchatType.MessageId:
|
||||
...
|
||||
@property
|
||||
def content(self) -> str:
|
||||
...
|
||||
@property
|
||||
def sender_id(self) -> TailchatType.UserId:
|
||||
...
|
||||
@property
|
||||
def is_from_self(self) -> bool:
|
||||
...
|
||||
@property
|
||||
def is_reply(self) -> bool:
|
||||
...
|
||||
@property
|
||||
def group_id(self) -> Optional[TailchatType.GroupId]:
|
||||
...
|
||||
@property
|
||||
def converse_id(self) -> TailchatType.ConverseId:
|
||||
...
|
||||
def reply_with(self, message: str) -> "TailchatSendingMessage":
|
||||
"""回复这条消息"""
|
||||
...
|
||||
def as_reply(self, message: str) -> "TailchatSendingMessage":
|
||||
"""回复这条消息"""
|
||||
...
|
||||
|
||||
|
||||
class TailchatSendingMessage:
|
||||
"""
|
||||
Tailchat 将要发送的信息
|
||||
"""
|
||||
@property
|
||||
def content(self) -> str:
|
||||
...
|
||||
@content.setter
|
||||
def content(self, value: str) -> None:
|
||||
...
|
||||
@property
|
||||
def group_id(self) -> Optional[TailchatType.GroupId]:
|
||||
...
|
||||
@group_id.setter
|
||||
def group_id(self, value: Optional[TailchatType.GroupId]) -> None:
|
||||
...
|
||||
@property
|
||||
def converse_id(self) -> TailchatType.ConverseId:
|
||||
...
|
||||
@converse_id.setter
|
||||
def converse_id(self, value: TailchatType.ConverseId) -> None:
|
||||
...
|
||||
def with_content(self, content: str) -> "TailchatSendingMessage":
|
||||
"""
|
||||
为了链式调用, 返回自身
|
||||
"""
|
||||
self.content = content
|
||||
return self
|
||||
def set_img(self, file: bytes, file_name: str):
|
||||
"""
|
||||
设置消息的图片
|
||||
@param file: 图片文件 (实际上是 vec<u8>)
|
||||
@param file_name: 图片名称 (just_img.png)
|
||||
"""
|
||||
|
||||
|
||||
class TailchatClient:
|
||||
"""
|
||||
Tailchat 的客户端
|
||||
"""
|
||||
def send_message(self, message: TailchatSendingMessage) -> bool:
|
||||
...
|
||||
def send_and_warn(self, message: TailchatSendingMessage) -> bool:
|
||||
"""发送消息, 并在日志中输出警告信息"""
|
||||
self.warn(message.content)
|
||||
return self.send_message(message)
|
||||
@property
|
||||
def version(self) -> str:
|
||||
...
|
||||
@property
|
||||
def tailchat_version(self) -> str:
|
||||
"""tailchat 的版本号"""
|
||||
...
|
||||
def debug(self, message: str) -> None:
|
||||
"""向日志中输出调试信息"""
|
||||
def info(self, message: str) -> None:
|
||||
"""向日志中输出信息"""
|
||||
def warn(self, message: str) -> None:
|
||||
"""向日志中输出警告信息"""
|
||||
|
||||
|
||||
class ReciveMessage(TailchatReciveMessage, IcaNewMessage):
|
||||
"""
|
||||
继承了两边的消息
|
||||
只是用来类型标记, 不能实例化
|
||||
"""
|
||||
def reply_with(self, message: str) -> Union["IcaReplyMessage", "TailchatSendingMessage"]: # type: ignore
|
||||
...
|
||||
|
||||
|
||||
class ConfigData:
|
||||
def __getitem__(self, key: str):
|
||||
...
|
||||
def have_key(self, key: str) -> bool:
|
||||
...
|
||||
|
||||
|
||||
on_load = Callable[[IcaClient], None]
|
||||
# def on_load(client: IcaClient) -> None:
|
||||
# ...
|
||||
|
||||
on_ica_message = Callable[[IcaNewMessage, IcaClient], None]
|
||||
# def on_message(msg: NewMessage, client: IcaClient) -> None:
|
||||
# ...
|
||||
|
||||
on_ica_delete_message = Callable[[IcaType.MessageId, IcaClient], None]
|
||||
# def on_delete_message(msg_id: MessageId, client: IcaClient) -> None:
|
||||
# ...
|
||||
|
||||
on_tailchat_message = Callable[[TailchatClient, TailchatReciveMessage], None]
|
||||
# def on_tailchat_message(client: TailchatClient, msg: TailchatReciveMessage) -> None:
|
||||
# ...
|
||||
|
||||
on_config = Callable[[None], Tuple[str, str]]
|
||||
|
||||
CONFIG_DATA: ConfigData = ConfigData()
|
36
ica-rs/plugins/base.py
Normal file
36
ica-rs/plugins/base.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
from typing import TYPE_CHECKING, TypeVar
|
||||
import platform
|
||||
import PIL.Image
|
||||
import io
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ica_typing import IcaNewMessage, IcaClient
|
||||
from ica_typing import TailchatReciveMessage, TailchatClient
|
||||
else:
|
||||
IcaNewMessage = TypeVar("NewMessage")
|
||||
IcaClient = TypeVar("IcaClient")
|
||||
TailchatReciveMessage = TypeVar("TailchatReciveMessage")
|
||||
TailchatClient = TypeVar("TailchatClient")
|
||||
|
||||
def on_ica_message(msg: IcaNewMessage, client: IcaClient) -> None:
|
||||
if not (msg.is_from_self or msg.is_reply):
|
||||
if msg.content == "/bot":
|
||||
reply = msg.reply_with(f"ica-async-rs({client.version})-sync-py {client.ica_version}")
|
||||
client.send_message(reply)
|
||||
|
||||
|
||||
def on_tailchat_message(msg: TailchatReciveMessage, client: TailchatClient) -> None:
|
||||
# if not (msg.is_from_self or msg.is_reply):
|
||||
if not (msg.is_reply):
|
||||
if msg.content == "/bot":
|
||||
reply = msg.reply_with(f"tailchat-async-rs({client.version})-sync-py {client.tailchat_version}")
|
||||
client.send_message(reply)
|
||||
elif msg.content == "/image":
|
||||
image = PIL.Image.new("RGB", (100, 100), (255, 255, 255))
|
||||
img_cache = io.BytesIO()
|
||||
image.save(img_cache, format="JPEG")
|
||||
raw_img = img_cache.getvalue()
|
||||
img_cache.close()
|
||||
reply = msg.reply_with("Here is an image")
|
||||
reply.set_img(raw_img, "just_img.png")
|
||||
client.send_message(reply)
|
378
ica-rs/plugins/bmcl.py
Normal file
378
ica-rs/plugins/bmcl.py
Normal file
|
@ -0,0 +1,378 @@
|
|||
import io
|
||||
import time
|
||||
import requests
|
||||
import traceback
|
||||
import urllib.parse
|
||||
|
||||
# import PIL
|
||||
|
||||
from typing import TYPE_CHECKING, TypeVar, Optional, Tuple, List
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ica_typing import IcaNewMessage, IcaClient, ConfigData
|
||||
CONFIG_DATA: ConfigData
|
||||
else:
|
||||
CONFIG_DATA = None # type: ignore
|
||||
IcaNewMessage = TypeVar("NewMessage")
|
||||
IcaClient = TypeVar("IcaClient")
|
||||
|
||||
_version_ = "2.8.0-rs"
|
||||
backend_version = "unknown"
|
||||
|
||||
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:
|
||||
# 先倒序
|
||||
# 再插入
|
||||
# 最后再倒序
|
||||
count_str = count_str[::-1]
|
||||
count_str = "_".join([count_str[i:i+4] for i in range(0, count_len, 4)])
|
||||
count_str = count_str[::-1]
|
||||
return count_str
|
||||
|
||||
|
||||
def wrap_request(url: str, msg: IcaNewMessage, client: IcaClient) -> Optional[dict]:
|
||||
try:
|
||||
cookie = CONFIG_DATA["cookie"] # type: ignore
|
||||
if cookie == "填写你的 cookie" or cookie is None:
|
||||
response = requests.get(url)
|
||||
else:
|
||||
response = requests.get(url, cookies={"openbmclapi-jwt": cookie})
|
||||
except requests.exceptions.RequestException:
|
||||
warn_msg = f"数据请求失败, 请检查网络\n{traceback.format_exc()}"
|
||||
reply = msg.reply_with(warn_msg)
|
||||
client.send_and_warn(reply)
|
||||
return None
|
||||
except Exception as _:
|
||||
warn_msg = f"数据请求中发生未知错误, 请呼叫 shenjack\n{traceback.format_exc()}"
|
||||
reply = msg.reply_with(warn_msg)
|
||||
client.send_and_warn(reply)
|
||||
return None
|
||||
if not response.status_code == 200 or response.reason != "OK":
|
||||
warn_msg = f"请求失败, 请检查网络\n{response.status_code} {response.reason}"
|
||||
reply = msg.reply_with(warn_msg)
|
||||
client.send_and_warn(reply)
|
||||
return None
|
||||
return response.json()
|
||||
|
||||
|
||||
def bmcl_dashboard(msg: IcaNewMessage, client: IcaClient) -> None:
|
||||
req_time = time.time()
|
||||
# 记录请求时间
|
||||
data = wrap_request("https://bd.bangbang93.com/openbmclapi/metric/dashboard", msg, client)
|
||||
dashboard_status = wrap_request("https://bd.bangbang93.com/openbmclapi/metric/version", msg, client)
|
||||
if data is None or dashboard_status is None:
|
||||
return
|
||||
global backend_version
|
||||
backend_version = dashboard_status["version"]
|
||||
backend_commit = dashboard_status["_resolved"].split("#")[1][:7]
|
||||
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"api版本 {backend_version} commit:{backend_commit}\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.debug(report_msg)
|
||||
reply = msg.reply_with(report_msg)
|
||||
client.send_message(reply)
|
||||
|
||||
|
||||
def check_is_full_data(data: list) -> bool:
|
||||
return 'user' in data[0]
|
||||
|
||||
|
||||
def display_rank_min(ranks: list, req_time) -> str:
|
||||
cache = io.StringIO()
|
||||
cache.write(f"bmclapi v{_version_}-排名({len(ranks)})")
|
||||
if check_is_full_data(ranks):
|
||||
cache.write("完整\n")
|
||||
for rank in ranks:
|
||||
cache.write('✅' if rank['isEnabled'] else '❌')
|
||||
if 'fullSize' in rank:
|
||||
cache.write('🌕' if rank['fullSize'] else '🌘')
|
||||
if 'version' in rank:
|
||||
cache.write('🟢' if rank['version'] == backend_version else '🟠')
|
||||
cache.write(f"-{rank['index']+1:3}")
|
||||
cache.write(f"|{rank['name']}\n")
|
||||
else:
|
||||
cache.write("无cookie\n")
|
||||
for rank in ranks:
|
||||
cache.write('✅' if rank['isEnabled'] else '❌')
|
||||
cache.write(f"-{rank['index']+1:3}")
|
||||
cache.write(f"|{rank['name']}\n")
|
||||
cache.write(f"请求时间: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(req_time))}")
|
||||
return cache.getvalue()
|
||||
|
||||
|
||||
def display_rank_full(ranks: list, req_time) -> str:
|
||||
cache = io.StringIO()
|
||||
cache.write(f"bmclapi v{_version_}-排名({len(ranks)})")
|
||||
if check_is_full_data(ranks):
|
||||
cache.write("完整\n")
|
||||
for rank in ranks:
|
||||
# 基本信息
|
||||
cache.write('✅' if rank['isEnabled'] else '❌')
|
||||
if 'fullSize' in rank:
|
||||
cache.write('🌕' if rank['fullSize'] else '🌘')
|
||||
cache.write(f"|{rank['index']+1:3}|")
|
||||
cache.write(f"{rank['name']}")
|
||||
if 'version' in rank:
|
||||
cache.write(f"|{rank['version']}")
|
||||
cache.write('🟢' if rank['version'] == backend_version else '🟠')
|
||||
cache.write('\n')
|
||||
# 用户/赞助信息
|
||||
if ('user' in rank) and (rank['user'] is not None):
|
||||
cache.write(f"所有者:{rank['user']['name']}")
|
||||
if 'sponsor' in rank:
|
||||
cache.write(f"|赞助者:{rank['sponsor']['name']}")
|
||||
if 'sponsor' in rank or ('user' in rank and rank['user'] is not None):
|
||||
cache.write('\n')
|
||||
# 数据信息
|
||||
if 'metric' in rank:
|
||||
hits = format_hit_count(rank['metric']['hits'])
|
||||
data = format_data_size(rank['metric']['bytes'])
|
||||
cache.write(f"hit/data|{hits}|{data}")
|
||||
cache.write('\n')
|
||||
else:
|
||||
cache.write("无cookie\n")
|
||||
for rank in ranks:
|
||||
cache.write('✅' if rank['isEnabled'] else '❌')
|
||||
cache.write(f"-{rank['index']+1:3}")
|
||||
cache.write(f"|{rank['name']}|\n")
|
||||
if 'sponsor' in rank:
|
||||
cache.write(f"赞助者: {rank['sponsor']['name']}|")
|
||||
if 'metric' in rank:
|
||||
cache.write(f"hit/data|{format_hit_count(rank['metric']['hits'])}|{format_data_size(rank['metric']['bytes'])}\n")
|
||||
cache.write(f"请求时间: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(req_time))}")
|
||||
return cache.getvalue()
|
||||
|
||||
|
||||
def bmcl_rank_general(msg, client):
|
||||
req_time = time.time()
|
||||
# 记录请求时间
|
||||
rank_data = wrap_request("https://bd.bangbang93.com/openbmclapi/metric/rank", msg, client)
|
||||
if rank_data is None:
|
||||
return
|
||||
# 预处理数据
|
||||
for i, r in enumerate(rank_data):
|
||||
r['index'] = i
|
||||
# 显示前3名
|
||||
ranks = rank_data[:3]
|
||||
# ranks = rank_data
|
||||
|
||||
# image = PIL.Image.new("RGB", (100, 100), (255, 255, 255))
|
||||
# img_cache = io.BytesIO()
|
||||
# image.save(img_cache, format="JPEG")
|
||||
# raw_img = img_cache.getvalue()
|
||||
# img_cache.close()
|
||||
|
||||
report_msg = display_rank_full(ranks, req_time)
|
||||
client.debug(report_msg)
|
||||
reply = msg.reply_with(display_rank_full(ranks, req_time))
|
||||
# reply.set_img(raw_img, "image/jpeg", False)
|
||||
client.send_message(reply)
|
||||
|
||||
|
||||
def bmcl_rank(msg: IcaNewMessage, client: IcaClient, name: str) -> None:
|
||||
req_time = time.time()
|
||||
# 记录请求时间
|
||||
rank_data = wrap_request("https://bd.bangbang93.com/openbmclapi/metric/rank", msg, client)
|
||||
if rank_data is None:
|
||||
return
|
||||
# 预处理数据
|
||||
for i, r in enumerate(rank_data):
|
||||
r['index'] = i
|
||||
# 搜索是否有这个名字的节点
|
||||
names: List[str] = [r["name"].lower() for r in rank_data]
|
||||
# try:
|
||||
# import regexrs
|
||||
# pattern = regexrs.compile(name)
|
||||
# finds = [pattern.match(n) for n in names]
|
||||
# except Exception as e:
|
||||
finds = [name.lower() in n for n in names]
|
||||
if not any(finds):
|
||||
reply = msg.reply_with(f"未找到名为{name}的节点")
|
||||
client.send_message(reply)
|
||||
return
|
||||
# 如果找到 > 3 个节点, 则提示 不显示
|
||||
counts = [f for f in finds if f]
|
||||
ranks = [rank_data[i] for i, f in enumerate(finds) if f]
|
||||
if len(counts) > 3:
|
||||
if len(counts) > 10:
|
||||
reply = msg.reply_with(f"搜索|{name}|到{len(counts)}个节点, 请用更精确的名字")
|
||||
else:
|
||||
# 4~10 个节点 只显示名称和次序
|
||||
report_msg = display_rank_min(ranks, req_time)
|
||||
reply = msg.reply_with(report_msg)
|
||||
client.send_message(reply)
|
||||
return
|
||||
# 如果找到 <= 3 个节点, 则显示全部信息
|
||||
report_msg = display_rank_full(ranks, req_time)
|
||||
client.debug(report_msg)
|
||||
reply = msg.reply_with(report_msg)
|
||||
client.send_message(reply)
|
||||
|
||||
|
||||
def bangbang_img(msg: IcaNewMessage, client: IcaClient) -> None:
|
||||
data = requests.get("https://api.bangbang93.top/api/link")
|
||||
if data.status_code != 200:
|
||||
reply = msg.reply_with(f"请求失败 {data.status_code} {data.reason}")
|
||||
client.send_message(reply)
|
||||
return
|
||||
raw_name = data.url.split("/")[-1]
|
||||
img_suffix = raw_name.split(".")[-1]
|
||||
# mine 映射一下
|
||||
if img_suffix.lower() in ("jpeg", "jpg"):
|
||||
img_suffix = "jpeg"
|
||||
img_name = raw_name[:-len(img_suffix) - 1]
|
||||
img_name = urllib.parse.unquote(img_name)
|
||||
mime_format = f"image/{img_suffix}"
|
||||
client.info(f"获取到随机怪图: {img_name} {img_suffix}")
|
||||
reply = msg.reply_with(img_name)
|
||||
reply.set_img(data.content, mime_format, True)
|
||||
client.send_message(reply)
|
||||
|
||||
|
||||
help = """/bmcl -> dashboard
|
||||
/bmcl rank -> all rank
|
||||
/bmcl rank <name> -> rank of <name>
|
||||
/bm93 -> 随机怪图
|
||||
/brrs <name> -> rank of <name>
|
||||
搜索限制:
|
||||
1- 3 显示全部信息
|
||||
4-10 显示状态、名称
|
||||
11+ 不显示
|
||||
"""
|
||||
|
||||
|
||||
def on_ica_message(msg: IcaNewMessage, client: IcaClient) -> None:
|
||||
if not (msg.is_from_self or msg.is_reply):
|
||||
if '\n' in msg.content:
|
||||
return
|
||||
try:
|
||||
if not msg.content.startswith("/b"):
|
||||
return
|
||||
global backend_version
|
||||
if backend_version == "unknown":
|
||||
dashboard_status = wrap_request("https://bd.bangbang93.com/openbmclapi/metric/version", msg, client)
|
||||
if dashboard_status is None:
|
||||
return
|
||||
backend_version = dashboard_status["version"]
|
||||
if msg.content.startswith("/bmcl"):
|
||||
if msg.content == "/bmcl":
|
||||
bmcl_dashboard(msg, client)
|
||||
elif msg.content == "/bmcl rank":
|
||||
bmcl_rank_general(msg, client)
|
||||
elif msg.content.startswith("/bmcl rank") and len(msg.content) > 11:
|
||||
name = msg.content[11:]
|
||||
bmcl_rank(msg, client, name)
|
||||
else:
|
||||
reply = msg.reply_with(help)
|
||||
client.send_message(reply)
|
||||
elif msg.content.startswith("/brrs"):
|
||||
if msg.content == "/brrs":
|
||||
reply = msg.reply_with(help)
|
||||
client.send_message(reply)
|
||||
else:
|
||||
name = msg.content.split(" ")
|
||||
if len(name) > 1:
|
||||
name = name[1]
|
||||
bmcl_rank(msg, client, name)
|
||||
elif msg.content == "/bm93":
|
||||
bangbang_img(msg, client)
|
||||
except: # noqa
|
||||
report_msg = f"bmcl插件发生错误,请呼叫shenjack\n{traceback.format_exc()}"
|
||||
if len(report_msg) > 200:
|
||||
report_msg = report_msg[:200] + "..." # 防止消息过长
|
||||
reply = msg.reply_with(report_msg)
|
||||
client.send_and_warn(reply)
|
||||
|
||||
|
||||
def on_tailchat_message(msg, client) -> None:
|
||||
if not msg.is_reply:
|
||||
if '\n' in msg.content:
|
||||
return
|
||||
try:
|
||||
if not msg.content.startswith("/b"):
|
||||
return
|
||||
global backend_version
|
||||
if backend_version == "unknown":
|
||||
dashboard_status = wrap_request("https://bd.bangbang93.com/openbmclapi/metric/version", msg, client)
|
||||
if dashboard_status is None:
|
||||
return
|
||||
backend_version = dashboard_status["version"]
|
||||
if msg.content.startswith("/bmcl"):
|
||||
if msg.content == "/bmcl":
|
||||
bmcl_dashboard(msg, client)
|
||||
elif msg.content == "/bmcl rank":
|
||||
bmcl_rank_general(msg, client)
|
||||
elif msg.content.startswith("/bmcl rank") and len(msg.content) > 11:
|
||||
name = msg.content[11:]
|
||||
bmcl_rank(msg, client, name)
|
||||
else:
|
||||
reply = msg.reply_with(help)
|
||||
client.send_message(reply)
|
||||
elif msg.content.startswith("/brrs"):
|
||||
if msg.content == "/brrs":
|
||||
reply = msg.reply_with(help)
|
||||
client.send_message(reply)
|
||||
else:
|
||||
name = msg.content.split(" ")
|
||||
if len(name) > 1:
|
||||
name = name[1]
|
||||
bmcl_rank(msg, client, name)
|
||||
elif msg.content == "/bm93":
|
||||
bangbang_img(msg, client)
|
||||
except: # noqa
|
||||
report_msg = f"bmcl插件发生错误,请呼叫shenjack\n{traceback.format_exc()}"
|
||||
if len(report_msg) > 200:
|
||||
report_msg = report_msg[:200] + "..." # 防止消息过长
|
||||
reply = msg.reply_with(report_msg)
|
||||
client.send_and_warn(reply)
|
||||
|
||||
def on_config() -> Tuple[str, str]:
|
||||
return (
|
||||
"bmcl.toml",
|
||||
"""cookie = \"填写你的 cookie\""""
|
||||
)
|
4
ica-rs/plugins/md5/.gitignore
vendored
Normal file
4
ica-rs/plugins/md5/.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
input.txt
|
||||
node_modules
|
||||
package.json
|
||||
package-lock.json
|
1
ica-rs/plugins/md5/assets/gAd.md
Normal file
1
ica-rs/plugins/md5/assets/gAd.md
Normal file
|
@ -0,0 +1 @@
|
|||
@@ABMECIGUB@OK@CHBAA@FCMFNBEABCICEG@DJDGBGHMDALBAHHEDIEFB@AACFLCFDC@ABIC@DBLIABFPCFDCTD@B@@ID@@EHDI@GDGFDAH@ABBAGBBF@PKALADHG@AAOC@EJ@@FCHC@MLKBAHBGDAKDGA\C@A@FHE@TTHWCGHJBBAFBSGCAAF@I@D@A@AC@AGIFXHBAEU@@@AMTGFF@AAIBJGAKAAAE@BJIMH@CAAHAABC@DD@L@AABEDFECBCT@BGED@GDF@CFDDGH@ACDBDH@DAFOBBIKD@ICJGCAH@GBADBDGDH@@DIY@BFDEOEAS@G@DIN@GABNHECOCBEAHPBC@AIBDAFBDWB@GCAB@EACD@DE@@FFDB@JBFAENJ@L@JMM@R@JD@@DBCFCDAB@@@EBABCEA@PAEBKB@@@DXBCACFABCDFBIAFDBAEAFCY@LB@EHH@B@BIIDFGDOCH@EB@LFCCCDCE@BF@DG@LNDQKKCC@FACDFCGBCSC@CLI@CHLBHELHA@BCA@AABSBBBNBFI@MBCMB@UB@PNA@DEJ_GOD@DKDBIMC@BMBBADFVEB@DCBA@AG@HE@FDCE@AC@EBBL]@AF@@A@FBHBECJAFDAMBA@EBEICGFDCEBADMC@LC@FH@D@@EEBAFPAHIAAACBBD@BBBCCDCB@GDHCMHAGAGA@HVBFCRCDHCJBERJBBEBEIGOAD@WBDCDKAACK@BAMAOADFE@@EUFAF@NKICDFBFWLAIICBABBKWBGLLZAGHBA@AB@A@DEBG@HER@CDAO@@FCDA@EOFB@DBOJRAAAABCAFFDBAAKDB@ACD@FC@I@@EB@HHCBGCCLCB@BDBA@BFBBHNIA@DDQA@@@EAO@EHIGBOAFSAZCGJRCN@CMJL@CACE@HCIDTBGSFAA@AA@DFBA@J@DJDGEBEFBBHIJ@GCFGDLBA@@DBBD@FA@TBCAIHAJ@CCLHDA@DHFF@HJCF@DEAJE@JGI@ABFJEDKD@DGEICHECPMDBIELA@D[F@HDGDABBND@CAKHBCCDOAJACEE@CIDFVALABQAC@P@HBBBKBA@AM@@RCAC@PKD@HC@@IABD@PCFFA@NQD@DBJFJCAAA@HACFFBAAJ@AM@GDBECBADD@D@@CNK@@FPEJGDATACFJQANKADFA@LKVGEAJQAAB@@KDA@C@DCAEBAADBNHCCE@@@AACGED@JBBFBHANAFMAASHDC@DBIJJHDECAD@FDAFECDGC@GHQB@@BBCEFEXABBA@@EIIJF@@DEC@AAQCA@A@CIBGNACK@CHLEFBCBD@BOIB@BJDEGFD@AL@INECCAUAXHBBEDWBBA@BBLDADD@BHB@D@HHFB@GGAKABAAEGCKFJHJC@@@HECDDVBL@BCLB@DEEE@BDFN[@DPA@DF@AAFF@B@EGEDG@ABBBFCAAA@FPAJBBACBBFGA@@@D@LOOBBQDGDOJRACGABCUCAHGBF@BBKDAA@FCCO[AAIALA@SKHKB@ABRLDDBC@CAEAFBGFCLA@DBFFB@A@EG]DEGEBQWKMB@NBBMEADKI^@@@KHED@P@CC@FACZNADAM@IADC@V@@@A@IO@CKEEKCBLIBCBG@AEBCANA@BJAFBAVCLDGHCKPDEDLIFHBL@@FF@DH@ABDGFDF@HLDIBAEBCF@@BLFBA@AAFBCDBBFA@FBL@AA@AABBCFEEAFCJUICDI@@@I@AA@FBBDFE@C@AKAEBBBC@EGAEKACA@@B@AGHMHHD@ACGABHBEFVCT@FGADC@DADGDADLPNGBAAAEACF@D@PAH@PELHAEJ@CCKEICAAG@DNDE@CDBF@AEJK@O@DEC@FCICLDNHHDBCANBCE@AGLCDGCHCBCCCA@PECE@K@AJJ@QBNBO@@DODCEBNLFBACBEBFABJCFBOBPKRBB@EAFACGGIC@CGG^DDAFVACFHBBACAJDP@GDM@ABB@GG@@@B@K@EBFFAFB@DGB@FANBH@BQDE@FEBBGJ@FBGBAOAI@DQEJA@KE@ATC@JUG@QADH@@HAB@DDA@JCFFB@AAOCL@MPBAC@@CAQCCAA@@DIUHG@EBUEAIHSERCJK@@GTAD@N@AHRCMEL@HCA@@AICAB@HI@AF@ABLAEW@GBZ@CFNKDCHN@@BDO@@CFL@NDBN@L@AUBHBAEFEEQGAKHQ@@@CAB@KIOCA@CF@P@MA@DLBABARQBA@KCAIDGIDG@JCCBBFBFKED@ABEFBBAHGDBDBBMBDFBDBDDABABD@DPCF\DB@G@DD@E@G@CV@EBF@O@@CA@BQFDBLAB@BDGHF@@@@BI@DADEBBCDEGCD@A_@EAKACCA@CICDEPKGCEADNABFDCG@DFCBPE@FEDECBAFGBAGCBH@JDAENADAF@CBHACBA@@BCCQCF@FNNBAD@GABKAEBFACGCBFKCGOEBG@DBI@B@GBAJADIOWA@KKLCGFAABAR@TH@BBDAF@DMDA@AHAABREIABAHCED@@EAFHHEBEDSEABNMBAOREEJ@A@ALACIGHFBNPIEDDFDF@@KBHCGBBBOBCAILBFHDMATBDAD@ADFBWHJ@@BACCRC@FJAB@DCFM@IBHCIAF@ED@HAG@@@ECDHBAAHBEKDC@@@BCENC@C@ACDEBDG@A@A@K@BBCNOK@FBCAFIAPFECBBIOEGHDACGBB@@J@ADEECBEG@AACEADD@@JEGACHJ@BDE@JE@IFAQGEBFDDDKUECEEHFD_@CDCG@BJALEB@B@@QEKE@LKBD@ABW@QBNNAL@B@KCADDADA@BD@QBEQ@DQUC@EC@BOGB@AM@DGBAANEIAGFNPEICA@CHEDGDCD@A@LEP@N@CEMFJ@@BIACB@NIBJG@NG^A@EAIMCDHH@BCFEPOJGC@CSNAAAJ@FCGT@FAAOKHXFVFACJHFMAADAA@LA@MHLKTGSZ@HAAJ@AABD@BCBEPDHCJJB@JAABKEBCCEH@D@DBHIAACABAGBBB@EGCACFUDFC@LB@AAFHDBC@G@E@DQD@DKD@CFEBHABANEQ@CCLDAGCAHBA@@@CKC@DAHCGEMJOBGZ@A@CB@AD@ECQ@DAZNN@ACC@@CECDCJAFNJRGBGG@NCBJ@@BI@DBIDCO@B@CCDAAMTGGGCAKDGG@A@ALBA@@FDQ@BA@AB@AVKDJED@@@CDLFC@@C@PCBMCDK@IEHCEAC@CRCOBCAFAGEFUK@@BADGAUFBFIA@H@AI@DDB@BGAAHLVC@KCKBDCBBACHC@A@NI@BEBDIFVF@AGBCIELDIZABAJAFD@AUKS@DJ@@DD@BMHABFBE@IBBDBADAM@GMDBHD@@EAD@BEADGPGGAAFEEABBECKIBAW@BEEQE@DD@FCCAEHJFB@C@E@BMJCB@ALGD@CBC@F@AFJDECAAA@BEO@EBNACDCUH@@K@A@DB@BFECFMAEHDBAEEADFB@EE@CI@ABDBDCAD@A@@B@JCB@FAGJCK@GG@ABFLQ@BHDEHOAB@DBIDSBDBIVA@FD@@ABCAAAA
|
176
ica-rs/plugins/md5/assets/zh.json
Normal file
176
ica-rs/plugins/md5/assets/zh.json
Normal file
|
@ -0,0 +1,176 @@
|
|||
{
|
||||
"recover": "[1]回复体力[2]点",
|
||||
"sklAbsorb": "[0]发起[吸血攻击]",
|
||||
"sklAccumulate": "[0]开始[聚气]",
|
||||
"sklAccumulated": "[1]攻击力上升",
|
||||
"sklAccumulateCancel": "[1]的[聚气]被打消了",
|
||||
"sklAssassinate1": "[0][潜行]到[1]身后",
|
||||
"sklAssassinate2": "[0]发动[背刺]",
|
||||
"dodge": "[0][回避]了攻击(通用)",
|
||||
"sklAssassinateFailed": "[0]的[潜行]被识破",
|
||||
"sklBerserkEnd": "[1]从[狂暴]中解除",
|
||||
"sklBerserkAttack": "[0]发起[狂暴攻击]",
|
||||
"sklBerserkHit": "[1]进入[狂暴]状态",
|
||||
"sklBerserk": "[0]使用[狂暴术]",
|
||||
"sklCharge": "[0]开始[蓄力]",
|
||||
"sklChargeCancel": "[1]的[蓄力]被中止了",
|
||||
"sklCharmEnd": "[1]从[魅惑]中解除",
|
||||
"sklCharm": "[0]使用[魅惑]",
|
||||
"sklCharmHit": "[1]被[魅惑]了",
|
||||
"sklClone": "[0]使用[分身]",
|
||||
"sklCloned": "出现一个新的[1]",
|
||||
"sklCritical": "[0]发动[会心一击]",
|
||||
"sklCurseDamage": "[诅咒]使伤害加倍",
|
||||
"sklCurseEnd": "[1]从[诅咒]中解除",
|
||||
"sklCurseHit": "[1]被[诅咒]了",
|
||||
"sklCurse": "[0]使用[诅咒]",
|
||||
"sklDisperse": "[0]使用[净化]",
|
||||
"sklExchange": "[0]使用[生命之轮]",
|
||||
"sklExchanged": "[1]的体力值与[0]互换",
|
||||
"sklFire": "[0]使用[火球术]",
|
||||
"sklHalf": "[0]使用[瘟疫]",
|
||||
"sklHalfDamage": "[1]体力减少[2]%",
|
||||
"sklHasteEnd": "[1]从[疾走]中解除",
|
||||
"sklHaste": "[0]使用[加速术]",
|
||||
"sklHasteHit": "[1]进入[疾走]状态",
|
||||
"sklHeal": "[0]使用[治愈魔法]",
|
||||
"sklIceEnd": "[1]从[冰冻]中解除",
|
||||
"sklIceHit": "[1]被[冰冻]了",
|
||||
"sklIce": "[0]使用[冰冻术]",
|
||||
"sklIron": "[0]发动[铁壁]",
|
||||
"sklIrond": "[0]防御力大幅上升",
|
||||
"sklIronCancel": "[1]的[铁壁]被打消了",
|
||||
"sklIronEnd": "[0]从[铁壁]中解除",
|
||||
"sklPoisonDamage": "[1][毒性发作]",
|
||||
"sklPoisonEnd": "[1]从[中毒]中解除",
|
||||
"sklPoisonHit": "[1][中毒]",
|
||||
"sklPoison": "[0][投毒]",
|
||||
"sklQuake": "[0]使用[地裂术]",
|
||||
"SklRapid": "[0]发起攻击",
|
||||
"SklRapidNext": "[0][连击]",
|
||||
"sklRevive": "[0]使用[苏生术]",
|
||||
"sklRevived": "[1][复活]了",
|
||||
"sklPossess": "[0]使用[附体]",
|
||||
"sklShadow": "[0]使用[幻术]",
|
||||
"sklShadowName": "幻影",
|
||||
"sklShadowed": "召唤出[1]",
|
||||
"sklSlowEnd": "[1]从[迟缓]中解除",
|
||||
"sklSlow": "[0]使用[减速术]",
|
||||
"sklSlowHit": "[1]进入[迟缓]状态",
|
||||
"sklExplode": "[0]使用[自爆]",
|
||||
"sklSummon": "[0]使用[血祭]",
|
||||
"sklSummonName": "使魔",
|
||||
"sklSummoned": "召唤出[1]",
|
||||
"sklThunder": "[0]使用[雷击术]",
|
||||
"sklThunderEnd": "[0][回避]了攻击(雷击)",
|
||||
"benchmarking": "实力评估中...[2]%",
|
||||
"benchmarkRatio": "》 胜率: [2]%",
|
||||
"benchmarkScore": "》 实力评分: [2]",
|
||||
"benchmarkSkill": "频率: [2]%",
|
||||
"searchInvalid": "错误,目前最多支持8000人搜索",
|
||||
"searchStart": "搜索开始...",
|
||||
"searchEnd": "搜索结束",
|
||||
"searchFailed": "但是一无所获",
|
||||
"bossName_aokiji": "青雉",
|
||||
"sklAokijiDefend": "[0][吸收]所有冰冻伤害",
|
||||
"sklAokijiIceAge": "[0]使用[冰河时代]",
|
||||
"bossName_conan": "柯南",
|
||||
"sklConanKillUnknown": "[0]在一间密室中发现了一具无名尸体",
|
||||
"sklConanThinking": "[0]正在进行推理",
|
||||
"sklConanThinkingFinish": "[0]推理完毕",
|
||||
"sklConanThinkingFinish2": "真相只有一个",
|
||||
"sklConanThinkingFinish3": "凶手就是你",
|
||||
"sklConanKillLast": "[1]",
|
||||
"sklConanKill": "[0]在一间密室中发现了[1]的尸体",
|
||||
"bossName_covid": "新冠病毒",
|
||||
"sklCovidDamage": "[1][肺炎]发作",
|
||||
"sklCovidICU": "[1]在重症监护室无法行动",
|
||||
"sklCovidStayHome": "[1]在家中自我隔离",
|
||||
"sklCovidInfect": "[0]和[1]近距离接触",
|
||||
"sklCovidPrevent": "但[1]没被感染",
|
||||
"sklAttack": "[0]发起攻击",
|
||||
"sklCovidMutate": "[1]所感染的病毒发生变异",
|
||||
"sklCovidHit": "[1]感染了[新冠病毒]",
|
||||
"bossName_ikaruga": "斑鸠",
|
||||
"sklIkarugaDefend": "[0][吸收]所有奇数伤害",
|
||||
"sklIkarugaAttack": "[0]使用[能量释放]",
|
||||
"bossName_lazy": "懒癌",
|
||||
"sklLazyDamage": "[1][懒癌]发作",
|
||||
"sklLazySkipTurn1": "[0]打开了[Steam]",
|
||||
"sklLazySkipTurn2": "[0]打开了[守望先锋]",
|
||||
"sklLazySkipTurn3": "[0]打开了[文明6]",
|
||||
"sklLazySkipTurn4": "[0]打开了[英雄联盟]",
|
||||
"sklLazySkipTurn5": "[0]打开了[微博]",
|
||||
"sklLazySkipTurn6": "[0]打开了[朋友圈]",
|
||||
"sklLazySkipTurn0": "这回合什么也没做",
|
||||
"sklLazyHit": "[1]感染了[懒癌]",
|
||||
"bossName_mario": "马里奥",
|
||||
"bossMarioGrow10": "[0]得到[蘑菇]",
|
||||
"bossMarioGrow11": "[0]攻击力上升",
|
||||
"bossMarioGrow20": "[0]得到[火焰花]",
|
||||
"bossMarioGrow21": "[0]学会[火球术]",
|
||||
"bossMarioGrow30": "[0]得到[奖命蘑菇]",
|
||||
"bossMarioLife": "[0]还剩[2]条命",
|
||||
"bossMarioRevive": "[0]满血复活",
|
||||
"bossName_mosquito": "蚊",
|
||||
"bossName_saitama": "一拳超人",
|
||||
"saitamaHungry": "[0]觉得有点饿",
|
||||
"saitamaLeave": "[0]离开了战场",
|
||||
"bossName_slime": "史莱姆",
|
||||
"sklSlimeSpawn": "[0][分裂]",
|
||||
"sklSlimeSpawned": "分成了[0] 和 [1]",
|
||||
"bossName_sonic": "索尼克",
|
||||
"bossName_yuri": "尤里",
|
||||
"sklYuriControl": "[0]使用[心灵控制]",
|
||||
"endMessage": "你已经玩了[0]局了",
|
||||
"continueGame": "继续游戏",
|
||||
"navigationLink": "navigation.html",
|
||||
"errorMaxPlayer": "错误,目前最多支持1000人PK",
|
||||
"errorMinPlayer": "错误,请至少输入两行名字",
|
||||
"welcome": "名字竞技场",
|
||||
"welcome2": "(MD5大作战10周年纪念)",
|
||||
"winnerName": "胜者",
|
||||
"score": "得分",
|
||||
"killedCount": "击杀",
|
||||
"killerName": "致命一击",
|
||||
"loserName": "败者",
|
||||
"returnTitle": "返回",
|
||||
"shareTitle": "分享",
|
||||
"helpTitle": "帮助",
|
||||
"HP": "HP",
|
||||
"detail": " 攻 [] 防 [] 速 [] 敏 [] 魔 [] 抗 [] 智 []",
|
||||
"inputTitle": "名字竞技场",
|
||||
"inputPlaceholder": "修改by shenjackyuanjie&超导体元素\n\n版本: latest\n可能会有一些问题, 稳定版请使用根目录下版本",
|
||||
"startFight": "开 始",
|
||||
"closeTitle": "关闭",
|
||||
"fastTitle": "快进",
|
||||
"challengeLabel": "挑战Boss",
|
||||
"selectBossHint": "选择Boss",
|
||||
"win": "[2]获得胜利",
|
||||
"minionDie": "[1]消失了",
|
||||
"damage": "[1]受到[2]点伤害",
|
||||
"die": "[1]被击倒了",
|
||||
"sklMagicAttack": "[0]发起攻击",
|
||||
"sklCounter": "[0]发起[反击]",
|
||||
"defend": "[0][防御]",
|
||||
"sklHide": "[0]发动[隐匿]",
|
||||
"sklMerge": "[0][吞噬]了[1]",
|
||||
"sklMerged": "[0]属性上升",
|
||||
"sklProtect": "[0][守护][1]",
|
||||
"sklReflect": "[0]使用[伤害反弹]",
|
||||
"sklReraise": "[0]使用[护身符]抵挡了一次死亡",
|
||||
"sklUpgrade": "[0]做出[垂死]抗争",
|
||||
"sklUpgraded": "[0]所有属性上升",
|
||||
"sklUpgradeCancel": "[1]的[垂死]属性被打消",
|
||||
"sklZombieName": "丧尸",
|
||||
"sklZombie": "[0][召唤亡灵]",
|
||||
"sklZombied": "[2]变成了[1]",
|
||||
"weaponDeathNoteAtk": "[0]在[死亡笔记]写下[1]的名字",
|
||||
"weaponRModifierUse": "[0]使用[属性修改器]",
|
||||
"weaponS11_0": "[0]在促销日[购买]了武器",
|
||||
"weaponS11_1": "但是并没有什么用",
|
||||
"weaponS11_2": "增加了[2]点",
|
||||
"weaponS11Done1": "[0]信用卡刷爆",
|
||||
"weaponS11Done3": "[0]砍下了自己的左手",
|
||||
"weaponS11Done2": "[0]砍下了自己的右手"
|
||||
}
|
230
ica-rs/plugins/md5/md5-api.js
Normal file
230
ica-rs/plugins/md5/md5-api.js
Normal file
|
@ -0,0 +1,230 @@
|
|||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
var __generator = (this && this.__generator) || function (thisArg, body) {
|
||||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
||||
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
||||
function verb(n) { return function (v) { return step([n, v]); }; }
|
||||
function step(op) {
|
||||
if (f) throw new TypeError("Generator is already executing.");
|
||||
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
||||
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
||||
if (y = 0, t) op = [op[0] & 2, t.value];
|
||||
switch (op[0]) {
|
||||
case 0: case 1: t = op; break;
|
||||
case 4: _.label++; return { value: op[1], done: false };
|
||||
case 5: _.label++; y = op[1]; op = [0]; continue;
|
||||
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
||||
default:
|
||||
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
||||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
||||
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
||||
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
||||
if (t[2]) _.ops.pop();
|
||||
_.trys.pop(); continue;
|
||||
}
|
||||
op = body.call(thisArg, _);
|
||||
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
||||
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
||||
}
|
||||
};
|
||||
var md5_module = require("./md5.js");
|
||||
/**
|
||||
*
|
||||
* @param names 原始的输入框输入
|
||||
* @returns 对战结果
|
||||
*/
|
||||
function fight(names) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0:
|
||||
// 检查一下输入是否合法
|
||||
// 比如里面有没有 !test!
|
||||
if (names.indexOf("!test!") !== -1) {
|
||||
throw new Error("你怎么在对战输入里加 !test!(恼)\n${names}");
|
||||
}
|
||||
return [4 /*yield*/, md5_module.fight(names)];
|
||||
case 1: return [2 /*return*/, _a.sent()];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 对于胜率/评分的输入检查
|
||||
* @param names
|
||||
* @returns
|
||||
*/
|
||||
function test_check(names) {
|
||||
var have_test = names.trim().startsWith("!test!");
|
||||
return have_test;
|
||||
}
|
||||
/**
|
||||
* 测量胜率
|
||||
* @param names 原始的输入框输入
|
||||
* @param round 战斗的回合数
|
||||
* @returns 胜率结果
|
||||
*/
|
||||
function win_rate(names, round) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0:
|
||||
// 检查 round 是否合法
|
||||
if (round <= 0) {
|
||||
throw new Error("round 必须大于 0");
|
||||
}
|
||||
if (!test_check(names)) {
|
||||
throw new Error("你怎么在胜率输入里丢了 !test!(恼)\n${names}");
|
||||
}
|
||||
return [4 /*yield*/, md5_module.win_rate(names, round)];
|
||||
case 1: return [2 /*return*/, _a.sent()];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param names 原始的输入框输入
|
||||
* @param callback 用于接收胜率的回调函数
|
||||
* @returns 胜率结果
|
||||
*/
|
||||
function win_rate_callback(names, callback) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0:
|
||||
if (!test_check(names)) {
|
||||
throw new Error("你怎么在胜率输入里丢了 !test!(恼)\n${names}");
|
||||
}
|
||||
return [4 /*yield*/, md5_module.win_rate_callback(names, callback)];
|
||||
case 1: return [2 /*return*/, _a.sent()];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
function score(names, round) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0:
|
||||
// 检查 round 是否合法
|
||||
if (round <= 0) {
|
||||
throw new Error("round 必须大于 0");
|
||||
}
|
||||
if (!test_check(names)) {
|
||||
throw new Error("你怎么在分数输入里丢了 !test!(恼)\n${names}");
|
||||
}
|
||||
return [4 /*yield*/, md5_module.score(names, round)];
|
||||
case 1: return [2 /*return*/, _a.sent()];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
function score_callback(names, callback) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0:
|
||||
if (!test_check(names)) {
|
||||
throw new Error("你怎么在分数输入里加 !test!(恼)\n${names}");
|
||||
}
|
||||
return [4 /*yield*/, md5_module.score_callback(names, callback)];
|
||||
case 1: return [2 /*return*/, _a.sent()];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
function run_any(names, round) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0: return [4 /*yield*/, md5_module.run_any(names, round)];
|
||||
case 1: return [2 /*return*/, _a.sent()];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
var out_limit = 1000;
|
||||
function wrap_any(names, round) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var result, win_rate_1, win_rate_str, output_str_1, output_datas_1, win_rate_2, output_str_2, output_datas_2;
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0: return [4 /*yield*/, run_any(names, round)];
|
||||
case 1:
|
||||
result = _a.sent();
|
||||
if ('message' in result) {
|
||||
// 对战结果
|
||||
return [2 /*return*/, "\u8D62\u5BB6:|".concat(result.source_plr, "|")];
|
||||
}
|
||||
else if ('win_count' in result) {
|
||||
win_rate_1 = result.win_count * 100 / round;
|
||||
win_rate_str = win_rate_1.toFixed(4);
|
||||
output_str_1 = "\u6700\u7EC8\u80DC\u7387:|".concat(win_rate_str, "%|(").concat(round, "\u8F6E)");
|
||||
// 每 500 轮, 输出一次
|
||||
if (round > out_limit) {
|
||||
output_datas_1 = [];
|
||||
result.raw_data.forEach(function (data, index) {
|
||||
if (data.round % out_limit === 0) {
|
||||
output_datas_1.push(data);
|
||||
}
|
||||
});
|
||||
output_datas_1.forEach(function (data, index) {
|
||||
var win_rate = data.win_count * 100 / data.round;
|
||||
output_str_1 += "\n".concat(win_rate.toFixed(2), "%(").concat(data.round, ")");
|
||||
});
|
||||
}
|
||||
return [2 /*return*/, output_str_1];
|
||||
// } else if ('score' in result) {
|
||||
}
|
||||
else {
|
||||
win_rate_2 = (result.score * 10000 / round).toFixed(2);
|
||||
output_str_2 = "\u5206\u6570:|".concat(win_rate_2, "|(").concat(round, "\u8F6E)");
|
||||
if (round > out_limit) {
|
||||
output_datas_2 = [];
|
||||
result.raw_data.forEach(function (data, index) {
|
||||
if (data.round % out_limit === 0) {
|
||||
output_datas_2.push(data);
|
||||
}
|
||||
});
|
||||
output_datas_2.forEach(function (data, index) {
|
||||
var win_rate = (data.score / data.round * 10000).toFixed(2);
|
||||
output_str_2 += "\n".concat(win_rate, "(").concat(data.round, ")");
|
||||
});
|
||||
}
|
||||
return [2 /*return*/, output_str_2];
|
||||
}
|
||||
return [2 /*return*/];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
function main() {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var fs, path, names, start_time, result, end_time;
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0:
|
||||
fs = require("fs");
|
||||
path = require("path");
|
||||
names = fs.readFileSync(path.resolve(__dirname, "input.txt"), "utf-8");
|
||||
start_time = Date.now();
|
||||
return [4 /*yield*/, wrap_any(names, 10000)];
|
||||
case 1:
|
||||
result = _a.sent();
|
||||
end_time = Date.now();
|
||||
console.log(result);
|
||||
console.log("Node.js \u8017\u65F6: ".concat(end_time - start_time, " ms"));
|
||||
return [2 /*return*/];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
main();
|
207
ica-rs/plugins/md5/md5-api.ts
Normal file
207
ica-rs/plugins/md5/md5-api.ts
Normal file
|
@ -0,0 +1,207 @@
|
|||
const md5_module = require("./md5.js");
|
||||
// import * as md5_module from "./md5.js";
|
||||
|
||||
/**
|
||||
* 对战结果的数据结构
|
||||
* 其实只有 source_plr 是有用的, 是赢家之一
|
||||
*/
|
||||
type FightResult = {
|
||||
message: string;
|
||||
source_plr: string;
|
||||
target_plr: string;
|
||||
affect: string | number;
|
||||
};
|
||||
|
||||
/**
|
||||
* 每一行具体的胜率结果
|
||||
*/
|
||||
type WinRate = {
|
||||
round: number;
|
||||
win_count: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* 胜率的数据结构
|
||||
*/
|
||||
type WinRateResult = {
|
||||
win_count: number;
|
||||
raw_data: WinRate[];
|
||||
};
|
||||
|
||||
/**
|
||||
* 用于接收胜率的回调函数
|
||||
* 返回一个 bool, true 表示继续, false 表示停止
|
||||
*/
|
||||
type WinRateCallback = (run_round: number, win_count: number) => boolean;
|
||||
|
||||
/**
|
||||
* 分数的数据结构
|
||||
*/
|
||||
type Score = {
|
||||
round: number;
|
||||
score: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* 分数的数据结构
|
||||
*/
|
||||
type ScoreResult = {
|
||||
score: number;
|
||||
raw_data: Score[];
|
||||
};
|
||||
|
||||
/**
|
||||
* 用于接收分数的回调函数
|
||||
* 返回一个 bool, true 表示继续, false 表示停止
|
||||
*/
|
||||
type ScoreCallback = (run_round: number, score: number) => boolean;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param names 原始的输入框输入
|
||||
* @returns 对战结果
|
||||
*/
|
||||
async function fight(names: string): Promise<FightResult> {
|
||||
// 检查一下输入是否合法
|
||||
// 比如里面有没有 !test!
|
||||
if (names.indexOf("!test!") !== -1) {
|
||||
throw new Error("你怎么在对战输入里加 !test!(恼)\n${names}");
|
||||
}
|
||||
return await md5_module.fight(names);
|
||||
}
|
||||
|
||||
/**
|
||||
* 对于胜率/评分的输入检查
|
||||
* @param names
|
||||
* @returns
|
||||
*/
|
||||
function test_check(names: string): boolean {
|
||||
const have_test = names.trim().startsWith("!test!");
|
||||
|
||||
return have_test;
|
||||
}
|
||||
|
||||
/**
|
||||
* 测量胜率
|
||||
* @param names 原始的输入框输入
|
||||
* @param round 战斗的回合数
|
||||
* @returns 胜率结果
|
||||
*/
|
||||
async function win_rate(names: string, round: number): Promise<WinRateResult> {
|
||||
// 检查 round 是否合法
|
||||
if (round <= 0) {
|
||||
throw new Error("round 必须大于 0");
|
||||
}
|
||||
if (!test_check(names)) {
|
||||
throw new Error("你怎么在胜率输入里丢了 !test!(恼)\n${names}");
|
||||
}
|
||||
return await md5_module.win_rate(names, round);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param names 原始的输入框输入
|
||||
* @param callback 用于接收胜率的回调函数
|
||||
* @returns 胜率结果
|
||||
*/
|
||||
async function win_rate_callback(
|
||||
names: string,
|
||||
callback: WinRateCallback,
|
||||
): Promise<WinRateResult> {
|
||||
if (!test_check(names)) {
|
||||
throw new Error("你怎么在胜率输入里丢了 !test!(恼)\n${names}");
|
||||
}
|
||||
return await md5_module.win_rate_callback(names, callback);
|
||||
}
|
||||
|
||||
async function score(names: string, round: number): Promise<ScoreResult> {
|
||||
// 检查 round 是否合法
|
||||
if (round <= 0) {
|
||||
throw new Error("round 必须大于 0");
|
||||
}
|
||||
if (!test_check(names)) {
|
||||
throw new Error("你怎么在分数输入里丢了 !test!(恼)\n${names}");
|
||||
}
|
||||
return await md5_module.score(names, round);
|
||||
}
|
||||
|
||||
async function score_callback(
|
||||
names: string,
|
||||
callback: ScoreCallback,
|
||||
): Promise<ScoreResult> {
|
||||
if (!test_check(names)) {
|
||||
throw new Error("你怎么在分数输入里加 !test!(恼)\n${names}");
|
||||
}
|
||||
return await md5_module.score_callback(names, callback);
|
||||
}
|
||||
|
||||
async function run_any(names: string, round: number): Promise<FightResult | WinRateResult | ScoreResult> {
|
||||
return await md5_module.run_any(names, round);
|
||||
}
|
||||
|
||||
const out_limit: number = 1000;
|
||||
|
||||
async function wrap_any(names: string, round: number): Promise<string> {
|
||||
const result = await run_any(names, round);
|
||||
if ('message' in result) {
|
||||
// 对战结果
|
||||
return `赢家:|${result.source_plr}|`;
|
||||
} else if ('win_count' in result) {
|
||||
// 胜率结果
|
||||
const win_rate = result.win_count * 100 / round;
|
||||
let win_rate_str = win_rate.toFixed(4);
|
||||
let output_str = `最终胜率:|${win_rate_str}%|(${round}轮)`;
|
||||
// 每 500 轮, 输出一次
|
||||
if (round > out_limit) {
|
||||
// 把所有要找的数据拿出来
|
||||
let output_datas: WinRate[] = [];
|
||||
result.raw_data.forEach((data, index) => {
|
||||
if (data.round % out_limit === 0) {
|
||||
output_datas.push(data);
|
||||
}
|
||||
});
|
||||
output_datas.forEach((data, index) => {
|
||||
const win_rate = data.win_count * 100 / data.round;
|
||||
output_str += `\n${win_rate.toFixed(2)}%(${data.round})`;
|
||||
});
|
||||
}
|
||||
return output_str;
|
||||
// } else if ('score' in result) {
|
||||
} else {
|
||||
// 分数结果其实还是个胜率, 不过需要 * 100
|
||||
const win_rate = (result.score * 10000 / round).toFixed(2);
|
||||
let output_str = `分数:|${win_rate}|(${round}轮)`;
|
||||
if (round > out_limit) {
|
||||
// 把所有要找的数据拿出来
|
||||
let output_datas: Score[] = [];
|
||||
result.raw_data.forEach((data, index) => {
|
||||
if (data.round % out_limit === 0) {
|
||||
output_datas.push(data);
|
||||
}
|
||||
});
|
||||
output_datas.forEach((data, index) => {
|
||||
const win_rate = (data.score / data.round * 10000).toFixed(2);
|
||||
output_str += `\n${win_rate}(${data.round})`;
|
||||
});
|
||||
}
|
||||
return output_str;
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// 从相对位置导入内容
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const names = fs.readFileSync(path.resolve(__dirname, "input.txt"), "utf-8");
|
||||
// const result = await fight(names);
|
||||
// const result = await md5_module.run_any(names, 50000);
|
||||
// console.log(`赢家:|${result.source_plr}|`);
|
||||
const start_time = Date.now();
|
||||
const result = await wrap_any(names, 10000);
|
||||
const end_time = Date.now();
|
||||
console.log(result);
|
||||
console.log(`Node.js 耗时: ${end_time - start_time} ms`);
|
||||
}
|
||||
|
||||
main();
|
21903
ica-rs/plugins/md5/md5.js
Normal file
21903
ica-rs/plugins/md5/md5.js
Normal file
File diff suppressed because one or more lines are too long
208
ica-rs/plugins/name_utils/__init__.py
Normal file
208
ica-rs/plugins/name_utils/__init__.py
Normal file
|
@ -0,0 +1,208 @@
|
|||
import io
|
||||
|
||||
sklname = [
|
||||
"火球术",
|
||||
"冰冻术",
|
||||
"雷击术",
|
||||
"地裂术",
|
||||
"吸血攻击",
|
||||
"投毒",
|
||||
"连击",
|
||||
"会心一击",
|
||||
"瘟疫",
|
||||
"生命之轮",
|
||||
"狂暴术",
|
||||
"魅惑",
|
||||
"加速术",
|
||||
"减速术",
|
||||
"诅咒",
|
||||
"治愈魔法",
|
||||
"苏生术",
|
||||
"净化",
|
||||
"铁壁",
|
||||
"蓄力",
|
||||
"聚气",
|
||||
"潜行",
|
||||
"血祭",
|
||||
"分身",
|
||||
"幻术",
|
||||
"防御",
|
||||
"守护",
|
||||
"伤害反弹",
|
||||
"护身符",
|
||||
"护盾",
|
||||
"反击",
|
||||
"吞噬",
|
||||
"召唤亡灵",
|
||||
"垂死抗争",
|
||||
"隐匿",
|
||||
"sklvoid1",
|
||||
"sklvoid2",
|
||||
"sklvoid3",
|
||||
"sklvoid4",
|
||||
"sklvoid5",
|
||||
]
|
||||
|
||||
prop_names = [
|
||||
"HP",
|
||||
"攻",
|
||||
"防",
|
||||
"速",
|
||||
"敏",
|
||||
"魔",
|
||||
"抗",
|
||||
"智",
|
||||
"八围",
|
||||
]
|
||||
|
||||
|
||||
class Player:
|
||||
def __init__(self) -> None:
|
||||
self.name = ""
|
||||
self.team = ""
|
||||
self.val = [i for i in range(0, 256)]
|
||||
self.name_base = [0] * 128
|
||||
self.name_str = [0] * 256
|
||||
self.team_str = [0] * 256
|
||||
self.name_len = 0
|
||||
self.team_len = 0
|
||||
self.name_prop = [0] * 8
|
||||
self.skl_id = [i for i in range(0, 40)]
|
||||
self.skl_freq = [0] * 40
|
||||
|
||||
def load(self, raw_name: str):
|
||||
if raw_name == "":
|
||||
print("错误:输入不能为空。")
|
||||
return False
|
||||
if raw_name.count("@") > 1:
|
||||
print("错误:无法分割名字与战队名,请检查输入。")
|
||||
return False
|
||||
name_lst = list(raw_name.rpartition("@"))
|
||||
if len(name_lst[0]) > 256 or len(name_lst[2]) > 256:
|
||||
print("错误:名字或战队名长度过大。")
|
||||
return False
|
||||
if name_lst[1] == "@":
|
||||
if name_lst[2] == "":
|
||||
name_lst[2] = name_lst[0]
|
||||
else:
|
||||
name_lst[0] = name_lst[2]
|
||||
name_bytes = name_lst[0].encode(encoding="utf-8")
|
||||
team_bytes = name_lst[2].encode(encoding="utf-8")
|
||||
self.name = name_lst[0]
|
||||
self.team = name_lst[2]
|
||||
self.name_len = len(name_bytes)
|
||||
self.team_len = len(team_bytes)
|
||||
for i in range(self.name_len):
|
||||
self.name_str[i + 1] = name_bytes[i]
|
||||
for i in range(self.team_len):
|
||||
self.team_str[i + 1] = team_bytes[i]
|
||||
self.name_len += 1
|
||||
self.team_len += 1
|
||||
|
||||
s = 0
|
||||
for i in range(256):
|
||||
s += self.team_str[i % self.team_len] + self.val[i]
|
||||
s %= 256
|
||||
self.val[i], self.val[s] = self.val[s], self.val[i]
|
||||
|
||||
for i in range(2):
|
||||
s = 0
|
||||
for j in range(256):
|
||||
s += self.name_str[j % self.name_len] + self.val[j]
|
||||
s %= 256
|
||||
self.val[j], self.val[s] = self.val[s], self.val[j]
|
||||
s = 0
|
||||
for i in range(256):
|
||||
m = ((self.val[i] * 181) + 160) % 256
|
||||
if m >= 89 and m < 217:
|
||||
self.name_base[s] = m & 63
|
||||
s += 1
|
||||
|
||||
propcnt = 0
|
||||
r = self.name_base[0:32]
|
||||
for i in range(10, 31, 3):
|
||||
r[i : i + 3] = sorted(r[i : i + 3])
|
||||
self.name_prop[propcnt] = r[i + 1]
|
||||
propcnt += 1
|
||||
r[0:10] = sorted(r[0:10])
|
||||
self.name_prop[propcnt] = 154
|
||||
propcnt += 1
|
||||
for i in range(3, 7):
|
||||
self.name_prop[propcnt - 1] += r[i]
|
||||
for i in range(7):
|
||||
self.name_prop[i] += 36
|
||||
|
||||
self.skl_id = list(range(0, 40))
|
||||
self.skl_freq = [0] * 40
|
||||
a = b = 0
|
||||
randbase = []
|
||||
randbase[:] = self.val[:]
|
||||
|
||||
def randgen():
|
||||
def m():
|
||||
nonlocal a, b, randbase
|
||||
a = (a + 1) % 256
|
||||
b = (b + randbase[a]) % 256
|
||||
randbase[a], randbase[b] = randbase[b], randbase[a]
|
||||
return randbase[(randbase[a] + randbase[b]) & 255]
|
||||
|
||||
return ((m() << 8) | m()) % 40
|
||||
|
||||
s = 0
|
||||
for i in range(2):
|
||||
for j in range(40):
|
||||
rand = randgen()
|
||||
s = (s + rand + self.skl_id[j]) % 40
|
||||
self.skl_id[j], self.skl_id[s] = self.skl_id[s], self.skl_id[j]
|
||||
last = -1
|
||||
j = 0
|
||||
for i in range(64, 128, 4):
|
||||
p = (
|
||||
min(
|
||||
self.name_base[i],
|
||||
self.name_base[i + 1],
|
||||
self.name_base[i + 2],
|
||||
self.name_base[i + 3],
|
||||
)
|
||||
% 256
|
||||
)
|
||||
if p > 10 and self.skl_id[j] < 35:
|
||||
self.skl_freq[j] = p - 10
|
||||
if self.skl_id[j] < 25:
|
||||
last = j
|
||||
j += 1
|
||||
if last != -1:
|
||||
self.skl_freq[last] *= 2
|
||||
if self.skl_freq[14] > 0 and last != 14:
|
||||
self.skl_freq[14] += min(
|
||||
self.name_base[60], self.name_base[61], self.skl_freq[14]
|
||||
)
|
||||
if self.skl_freq[15] > 0 and last != 15:
|
||||
self.skl_freq[15] += min(
|
||||
self.name_base[62], self.name_base[63], self.skl_freq[15]
|
||||
)
|
||||
return True
|
||||
|
||||
def display(self) -> str:
|
||||
cache = io.StringIO()
|
||||
cache.write(f"{self.name}@{self.team}|")
|
||||
full = sum(self.name_prop[0:7]) + round(self.name_prop[7] / 3)
|
||||
datas = [self.name_prop[7], *self.name_prop[0:7], full]
|
||||
cache.write(
|
||||
"|".join(
|
||||
[f"{prop_names[index]}:{value}" for index, value in enumerate(datas)]
|
||||
)
|
||||
)
|
||||
cache.write("\n")
|
||||
cache.write(
|
||||
"|".join(
|
||||
[
|
||||
f"{sklname[self.skl_id[index]]}:{self.skl_freq[index]}"
|
||||
for index, value in sorted(
|
||||
enumerate(self.skl_freq), key=lambda x: x[1], reverse=True
|
||||
)
|
||||
if value > 0
|
||||
]
|
||||
)
|
||||
)
|
||||
return cache.getvalue()
|
129
ica-rs/plugins/namerena.py
Normal file
129
ica-rs/plugins/namerena.py
Normal file
|
@ -0,0 +1,129 @@
|
|||
import io
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
import subprocess
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from typing import TYPE_CHECKING, TypeVar
|
||||
|
||||
if str(Path(__file__).parent.absolute()) not in sys.path:
|
||||
sys.path.append(str(Path(__file__).parent.absolute()))
|
||||
|
||||
import name_utils
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ica_typing import (
|
||||
IcaNewMessage,
|
||||
IcaClient,
|
||||
ConfigData,
|
||||
ReciveMessage,
|
||||
TailchatReciveMessage,
|
||||
)
|
||||
|
||||
CONFIG_DATA: ConfigData
|
||||
else:
|
||||
CONFIG_DATA = None # type: ignore
|
||||
IcaNewMessage = TypeVar("NewMessage")
|
||||
IcaClient = TypeVar("IcaClient")
|
||||
ReciveMessage = TypeVar("ReciveMessage")
|
||||
TailchatReciveMessage = TypeVar("TailchatReciveMessage")
|
||||
|
||||
|
||||
_version_ = "0.5.0"
|
||||
|
||||
EVAL_PREFIX = "/namerena"
|
||||
CONVERT_PREFIX = "/namer-peek"
|
||||
|
||||
|
||||
def convert_name(msg: ReciveMessage, client) -> None:
|
||||
# 也是多行
|
||||
if msg.content.find("\n") == -1:
|
||||
client.send_message(
|
||||
msg.reply_with(
|
||||
f"请使用 {CONVERT_PREFIX} 命令,然后换行输入名字,例如:\n{CONVERT_PREFIX}\n张三\n李四\n王五\n"
|
||||
)
|
||||
)
|
||||
return
|
||||
# 去掉 prefix
|
||||
names = msg.content[len(CONVERT_PREFIX) :]
|
||||
# 去掉第一个 \n
|
||||
names = names[names.find("\n") + 1 :]
|
||||
cache = io.StringIO()
|
||||
raw_players = [x for x in names.split("\n") if x != ""]
|
||||
players = [name_utils.Player() for _ in raw_players]
|
||||
for i, player in enumerate(players):
|
||||
if not player.load(raw_players[i]):
|
||||
cache.write(f"{i+1} {raw_players[i]} 无法解析\n")
|
||||
raw_players[i] = ""
|
||||
for i, player in enumerate(players):
|
||||
if raw_players[i] == "":
|
||||
continue
|
||||
cache.write(player.display())
|
||||
cache.write("\n")
|
||||
reply = msg.reply_with(f"{cache.getvalue()}版本:{_version_}")
|
||||
client.send_message(reply)
|
||||
|
||||
|
||||
def eval_fight(msg: ReciveMessage, client) -> None:
|
||||
if msg.content.find("\n") == -1:
|
||||
# 在判断一下是不是 /xxx xxxx
|
||||
if msg.content.find(" ") != -1:
|
||||
client.send_message(
|
||||
msg.reply_with(
|
||||
f"请使用 {EVAL_PREFIX} 命令,然后换行输入名字,例如:\n{EVAL_PREFIX}\n张三\n李四\n王五\n"
|
||||
)
|
||||
)
|
||||
return
|
||||
# 去掉 prefix
|
||||
names = msg.content[len(EVAL_PREFIX) :]
|
||||
# 去掉第一个 \n
|
||||
names = names[names.find("\n") + 1 :]
|
||||
|
||||
start_time = time.time()
|
||||
# 开始 try
|
||||
try:
|
||||
# 内容写入到 ./md5/input.txt
|
||||
# 路径是插件文件的相对路径
|
||||
root_path = Path(__file__).parent
|
||||
with open(root_path / "md5" / "input.txt", "w") as f:
|
||||
f.write(names)
|
||||
# 执行 node md5.js
|
||||
runner_path = root_path / "md5" / "md5-api.js"
|
||||
# input_path = root_path / "md5" / "input.txt"
|
||||
result = subprocess.run(
|
||||
["node", runner_path.absolute()],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
# 获取结果
|
||||
out_result = result.stdout.decode("utf-8")
|
||||
err_result = result.stderr.decode("utf-8")
|
||||
# 发送结果
|
||||
end_time = time.time()
|
||||
reply = msg.reply_with(
|
||||
f"{out_result}{err_result}外部耗时:{end_time - start_time:.2f}s\n版本:{_version_}"
|
||||
)
|
||||
client.send_message(reply)
|
||||
except Exception as e:
|
||||
# 发送错误
|
||||
reply = msg.reply_with(f"发生错误:{e}\n{traceback.format_exc()}")
|
||||
client.send_message(reply)
|
||||
|
||||
|
||||
def dispatch_msg(msg: ReciveMessage, client) -> None:
|
||||
if msg.is_reply or msg.is_from_self:
|
||||
return
|
||||
if msg.content.startswith(EVAL_PREFIX):
|
||||
eval_fight(msg, client)
|
||||
elif msg.content.startswith(CONVERT_PREFIX):
|
||||
convert_name(msg, client)
|
||||
|
||||
|
||||
def on_ica_message(msg: IcaNewMessage, client: IcaClient) -> None:
|
||||
dispatch_msg(msg, client) # type: ignore
|
||||
|
||||
|
||||
def on_tailchat_message(msg: TailchatReciveMessage, client) -> None:
|
||||
dispatch_msg(msg, client) # type: ignore
|
80
ica-rs/plugins/save_eval.pyi
Normal file
80
ica-rs/plugins/save_eval.pyi
Normal file
|
@ -0,0 +1,80 @@
|
|||
import time
|
||||
import random
|
||||
import traceback
|
||||
|
||||
from typing import TYPE_CHECKING, TypeVar
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ica_typing import IcaNewMessage, IcaClient
|
||||
else:
|
||||
IcaNewMessage = TypeVar("NewMessage")
|
||||
IcaClient = TypeVar("IcaClient")
|
||||
|
||||
def safe_eval(code: str, msg: IcaNewMessage) -> 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>",
|
||||
"open": "<built-in function open>",
|
||||
"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>",
|
||||
"msg": msg,
|
||||
}
|
||||
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()
|
||||
|
||||
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
|
||||
|
||||
|
||||
def on_message(message: IcaNewMessage, client: IcaClient) -> None:
|
||||
if not (message.is_from_self or message.is_reply):
|
||||
if message.content.startswith("/="):
|
||||
code = message.content[2:]
|
||||
result = safe_eval(code, message)
|
||||
reply = message.reply_with(result)
|
||||
client.send_message(reply)
|
|
@ -21,5 +21,3 @@ use_field_init_shorthand = true
|
|||
color = "Always"
|
||||
|
||||
edition = "2021"
|
||||
# 这样不用 nightly 也可以使用 unstable 特性
|
||||
unstable_features = true
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
use std::env;
|
||||
use std::fs;
|
||||
|
||||
use colored::Colorize;
|
||||
use serde::Deserialize;
|
||||
use toml::from_str;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::data_struct::{ica, tailchat};
|
||||
|
||||
|
@ -17,16 +17,12 @@ pub struct IcaConfig {
|
|||
/// bot 的 qq
|
||||
pub self_id: ica::UserId,
|
||||
/// 提醒的房间
|
||||
#[serde(default = "default_empty_i64_vec")]
|
||||
pub notice_room: Vec<ica::RoomId>,
|
||||
/// 是否提醒
|
||||
#[serde(default = "default_false")]
|
||||
pub notice_start: bool,
|
||||
/// 管理员列表
|
||||
#[serde(default = "default_empty_i64_vec")]
|
||||
pub admin_list: Vec<ica::UserId>,
|
||||
/// 过滤列表
|
||||
#[serde(default = "default_empty_i64_vec")]
|
||||
pub filter_list: Vec<ica::UserId>,
|
||||
}
|
||||
|
||||
|
@ -41,51 +37,36 @@ pub struct TailchatConfig {
|
|||
/// 提醒的房间
|
||||
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,
|
||||
pub enable_ica: Option<bool>,
|
||||
/// Ica 配置
|
||||
pub ica: Option<IcaConfig>,
|
||||
|
||||
/// 是否启用 Tailchat
|
||||
#[serde(default = "default_false")]
|
||||
pub enable_tailchat: bool,
|
||||
pub enable_tailchat: Option<bool>,
|
||||
/// Tailchat 配置
|
||||
pub tailchat: Option<TailchatConfig>,
|
||||
|
||||
/// 是否启用 Python 插件
|
||||
#[serde(default = "default_false")]
|
||||
pub enable_py: bool,
|
||||
pub enable_py: Option<bool>,
|
||||
/// Python 插件配置
|
||||
pub py: Option<PyConfig>,
|
||||
}
|
||||
|
@ -94,21 +75,18 @@ impl BotConfig {
|
|||
pub fn new_from_path(config_file_path: String) -> Self {
|
||||
// try read config from file
|
||||
let config = fs::read_to_string(&config_file_path).expect("Failed to read config file");
|
||||
let ret: Self = from_str(&config).unwrap_or_else(|e| {
|
||||
panic!("Failed to parse config file {}\ne:{:?}", &config_file_path, e)
|
||||
});
|
||||
let ret: Self = from_str(&config)
|
||||
.unwrap_or_else(|_| panic!("Failed to parse config file {}", &config_file_path));
|
||||
ret
|
||||
}
|
||||
pub fn new_from_cli() -> Self {
|
||||
// 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 config_file_path = String::new();
|
||||
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())
|
||||
});
|
||||
config_file_path = args.next().expect("No config path given");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -116,13 +94,64 @@ impl BotConfig {
|
|||
}
|
||||
|
||||
/// 检查是否启用 ica
|
||||
pub fn check_ica(&self) -> bool { self.enable_ica }
|
||||
pub fn check_ica(&self) -> bool {
|
||||
match self.enable_ica {
|
||||
Some(enable) => {
|
||||
if enable && self.ica.is_none() {
|
||||
warn!("enable_ica 为 true 但未填写 [ica] 配置\n将不启用 ica");
|
||||
false
|
||||
} else {
|
||||
enable
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if self.ica.is_some() {
|
||||
warn!("未填写 enable_ica 但填写了 [ica] 配置\n将不启用 ica");
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查是否启用 Tailchat
|
||||
pub fn check_tailchat(&self) -> bool { self.enable_tailchat }
|
||||
pub fn check_tailchat(&self) -> bool {
|
||||
match self.enable_tailchat {
|
||||
Some(enable) => {
|
||||
if enable && self.tailchat.is_none() {
|
||||
warn!("enable_tailchat 为 true 但未填写 [tailchat] 配置\n将不启用 Tailchat");
|
||||
false
|
||||
} else {
|
||||
enable
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if self.tailchat.is_some() {
|
||||
warn!("未填写 enable_tailchat 但填写了 [tailchat] 配置\n将不启用 Tailchat");
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查是否启用 Python 插件
|
||||
pub fn check_py(&self) -> bool { self.enable_py }
|
||||
pub fn check_py(&self) -> bool {
|
||||
match self.enable_py {
|
||||
Some(enable) => {
|
||||
if enable && self.py.is_none() {
|
||||
warn!("enable_py 为 true 但未填写 [py] 配置\n将不启用 Python 插件");
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if self.py.is_some() {
|
||||
warn!("未填写 enable_py 但填写了 [py] 配置\n将不启用 Python 插件");
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ica(&self) -> IcaConfig { self.ica.clone().expect("No ica config found") }
|
||||
pub fn tailchat(&self) -> TailchatConfig {
|
||||
|
|
|
@ -13,9 +13,7 @@ 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;
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
use crate::data_struct::ica::messages::{At, LastMessage, SendMessage};
|
||||
use crate::data_struct::ica::{RoomId, UserId};
|
||||
use crate::data_struct::ica::messages::{At, LastMessage};
|
||||
use crate::data_struct::ica::RoomId;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Number, Value as JsonValue};
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
/// export default interface Room {
|
||||
/// roomId: number
|
||||
|
@ -32,30 +32,14 @@ pub struct Room {
|
|||
// pub users: JsonValue,
|
||||
pub at: At,
|
||||
pub last_message: LastMessage,
|
||||
// 这俩都没啥用
|
||||
// pub auto_download: Option<String>,
|
||||
// pub download_path: Option<String>,
|
||||
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"]);
|
||||
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,
|
||||
|
@ -66,19 +50,15 @@ impl Room {
|
|||
// users: inner.users,
|
||||
at,
|
||||
last_message: inner.last_message,
|
||||
// download_path: inner.download_path,
|
||||
auto_download: inner.auto_download,
|
||||
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")]
|
||||
#[serde(rename = "roomId")]
|
||||
pub room_id: RoomId,
|
||||
#[serde(rename = "roomName")]
|
||||
pub room_name: String,
|
||||
|
@ -95,43 +75,8 @@ struct InnerRoom {
|
|||
// 忽略 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,
|
||||
#[serde(rename = "autoDownload")]
|
||||
pub auto_download: Option<String>,
|
||||
#[serde(rename = "downloadPath")]
|
||||
pub download_path: Option<String>,
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ use crate::data_struct::ica::{MessageId, RoomId, UserId};
|
|||
|
||||
use chrono::DateTime;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Value as JsonValue, json};
|
||||
use serde_json::{json, Value as JsonValue};
|
||||
use tracing::warn;
|
||||
|
||||
pub mod msg_trait;
|
||||
|
@ -41,9 +41,8 @@ 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>,
|
||||
#[serde(rename = "userId")]
|
||||
pub user_id: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
|
@ -331,7 +330,7 @@ impl SendMessage {
|
|||
/// 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};
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
let base64_data = general_purpose::STANDARD.encode(file);
|
||||
self.file_data = Some(format!("data:{};base64,{}", file_type, base64_data));
|
||||
}
|
||||
|
|
|
@ -4,9 +4,9 @@ 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};
|
||||
use crate::MainStatus;
|
||||
|
||||
impl Serialize for At {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
|
@ -88,26 +88,7 @@ impl<'de> Deserialize<'de> for Message {
|
|||
|
||||
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
|
||||
)
|
||||
}
|
||||
write!(f, "{}|{}|{}|{}", self.msg_id(), self.sender_id, self.sender_name, self.content)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -133,32 +114,14 @@ impl MessageTrait for NewMessage {
|
|||
|
||||
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
|
||||
)
|
||||
}
|
||||
write!(
|
||||
f,
|
||||
"{}|{}|{}|{}|{}",
|
||||
self.msg_id(),
|
||||
self.room_id,
|
||||
self.msg.sender_id,
|
||||
self.msg.sender_name,
|
||||
self.msg.content
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use std::fmt::Display;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Value as JsonValue, json};
|
||||
use serde_json::{json, Value as JsonValue};
|
||||
|
||||
use crate::data_struct::tailchat::{ConverseId, GroupId, MessageId, UserId};
|
||||
|
||||
|
|
|
@ -51,13 +51,11 @@ pub struct UpdateDMConverse {
|
|||
#[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 } }
|
||||
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
// use thiserror::Error;
|
||||
|
||||
pub type ClientResult<T, E> = Result<T, E>;
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -33,8 +31,6 @@ pub enum PyPluginError {
|
|||
/// 插件内函数调用错误
|
||||
/// pyerr, func_name, module_name
|
||||
FuncCallError(pyo3::PyErr, String, String),
|
||||
/// 插件停不下来!
|
||||
PluginNotStopped,
|
||||
}
|
||||
|
||||
impl From<rust_socketio::Error> for IcaError {
|
||||
|
@ -83,9 +79,6 @@ impl std::fmt::Display for PyPluginError {
|
|||
PyPluginError::FuncCallError(py_err, name, module) => {
|
||||
write!(f, "插件内函数调用错误: {:#?}|{} in {}", py_err, name, module)
|
||||
}
|
||||
PyPluginError::PluginNotStopped => {
|
||||
write!(f, "插件未停止")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -116,7 +109,6 @@ impl std::error::Error for PyPluginError {
|
|||
PyPluginError::CouldNotGetFunc(e, _, _) => Some(e),
|
||||
PyPluginError::FuncNotCallable(_, _) => None,
|
||||
PyPluginError::FuncCallError(e, _, _) => Some(e),
|
||||
PyPluginError::PluginNotStopped => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,46 +1,16 @@
|
|||
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 rust_socketio::{Event, Payload, TransportType};
|
||||
use tracing::{event, span, Level};
|
||||
|
||||
use crate::config::IcaConfig;
|
||||
use crate::error::{ClientResult, IcaError};
|
||||
use crate::{StopGetter, version_str};
|
||||
use crate::StopGetter;
|
||||
|
||||
/// 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();
|
||||
const ICA_PROTOCOL_VERSION: &str = "2.12.6";
|
||||
|
||||
pub async fn start_ica(config: &IcaConfig, stop_reciver: StopGetter) -> ClientResult<(), IcaError> {
|
||||
let span = span!(Level::INFO, "Icalingua Client");
|
||||
|
@ -48,7 +18,6 @@ pub async fn start_ica(config: &IcaConfig, stop_reciver: StopGetter) -> ClientRe
|
|||
|
||||
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))
|
||||
|
@ -63,16 +32,11 @@ pub async fn start_ica(config: &IcaConfig, stop_reciver: StopGetter) -> ClientRe
|
|||
.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()
|
||||
);
|
||||
event!(Level::INFO, "socketio connected");
|
||||
client
|
||||
}
|
||||
Err(e) => {
|
||||
|
@ -84,11 +48,10 @@ pub async fn start_ica(config: &IcaConfig, stop_reciver: StopGetter) -> ClientRe
|
|||
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()),
|
||||
format!("shenbot v {}\nica-async-rs v{}", crate::VERSION, crate::ICA_VERSION),
|
||||
*room,
|
||||
None,
|
||||
);
|
||||
// 这可是 qq, 要保命
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
|
||||
event!(Level::INFO, "发送启动消息到房间: {}", room);
|
||||
|
@ -101,12 +64,11 @@ pub async fn start_ica(config: &IcaConfig, stop_reciver: StopGetter) -> ClientRe
|
|||
}
|
||||
}
|
||||
// 等待停止信号
|
||||
event!(Level::INFO, "{}", "ica client waiting for stop signal".purple());
|
||||
stop_reciver.await.ok();
|
||||
event!(Level::INFO, "{}", "socketio client stopping".yellow());
|
||||
event!(Level::INFO, "socketio client stopping");
|
||||
match socket.disconnect().await {
|
||||
Ok(_) => {
|
||||
event!(Level::INFO, "{}", "socketio client stopped".green());
|
||||
event!(Level::INFO, "socketio client stopped");
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
|
@ -114,10 +76,10 @@ pub async fn start_ica(config: &IcaConfig, stop_reciver: StopGetter) -> ClientRe
|
|||
match e {
|
||||
rust_socketio::Error::IncompleteResponseFromEngineIo(inner_e) => {
|
||||
if inner_e.to_string().contains("AlreadyClosed") {
|
||||
event!(Level::INFO, "{}", "socketio client stopped".green());
|
||||
event!(Level::INFO, "socketio client stopped");
|
||||
Ok(())
|
||||
} else {
|
||||
event!(Level::ERROR, "socketio 客户端出现了 Error: {:?}", inner_e);
|
||||
event!(Level::ERROR, "socketio client stopped with error: {:?}", inner_e);
|
||||
Err(IcaError::SocketIoError(
|
||||
rust_socketio::Error::IncompleteResponseFromEngineIo(inner_e),
|
||||
))
|
||||
|
|
|
@ -1,52 +1,50 @@
|
|||
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 crate::MainStatus;
|
||||
|
||||
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};
|
||||
use rust_socketio::Payload;
|
||||
use serde_json::Value;
|
||||
use tracing::{debug, info, span, warn, Level};
|
||||
|
||||
/// "安全" 的 发送一条消息
|
||||
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());
|
||||
debug!("send_message {}", format!("{:#?}", message).cyan());
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
event!(Level::WARN, "send_message faild:{}", format!("{:#?}", e).red());
|
||||
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());
|
||||
debug!("delete_message {}", format!("{:#?}", message).yellow());
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
event!(Level::WARN, "delete_message faild:{}", format!("{:#?}", e).red());
|
||||
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> {
|
||||
|
||||
async fn inner_sign(payload: Payload, client: Client) -> ClientResult<(), IcaError> {
|
||||
let span = span!(Level::INFO, "signing icalingua");
|
||||
let _guard = span.enter();
|
||||
|
||||
|
@ -58,12 +56,7 @@ async fn inner_sign(payload: Payload, client: &Client) -> ClientResult<(), IcaEr
|
|||
|
||||
let (auth_key, version) = (&require_data[0], &require_data[1]);
|
||||
|
||||
event!(
|
||||
Level::INFO,
|
||||
"服务器发过来的待签名key: {:?}, 服务端版本号: {:?}",
|
||||
auth_key,
|
||||
version
|
||||
);
|
||||
info!("auth_key: {:?}, server_version: {:?}", auth_key, version);
|
||||
// 判定和自己的兼容版本号是否 一致
|
||||
let server_protocol_version = version
|
||||
.get("protocolVersion")
|
||||
|
@ -71,8 +64,7 @@ async fn inner_sign(payload: Payload, client: &Client) -> ClientResult<(), IcaEr
|
|||
.as_str()
|
||||
.unwrap_or("unknow");
|
||||
if server_protocol_version != crate::ica::ICA_PROTOCOL_VERSION {
|
||||
event!(
|
||||
Level::WARN,
|
||||
warn!(
|
||||
"服务器版本与兼容版本不一致\n服务器协议版本:{:?}\n兼容版本:{}",
|
||||
version.get("protocolVersion"),
|
||||
crate::ica::ICA_PROTOCOL_VERSION
|
||||
|
@ -89,56 +81,20 @@ async fn inner_sign(payload: Payload, client: &Client) -> ClientResult<(), IcaEr
|
|||
let private_key = MainStatus::global_config().ica().private_key.clone();
|
||||
|
||||
let array_key: [u8; 32] = hex::decode(private_key)
|
||||
.expect("配置文件设置的私钥不是一个有效的私钥, 无法使用hex解析")
|
||||
.expect("Not a vaild pub key")
|
||||
.try_into()
|
||||
.expect("配置文件设置的私钥不是一个有效的私钥, 无法转换为[u8; 32]数组");
|
||||
.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("发送签名信息失败");
|
||||
client.emit("auth", sign).await.expect("Faild to send signin data");
|
||||
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
|
||||
}
|
||||
}
|
||||
inner_sign(payload, client).await.expect("Faild to sign");
|
||||
}
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
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 tracing::{event, info, span, warn, Level};
|
||||
|
||||
use crate::data_struct::ica::RoomId;
|
||||
use crate::data_struct::ica::all_rooms::{JoinRequestRoom, Room};
|
||||
use crate::data_struct::ica::all_rooms::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};
|
||||
use crate::{py, MainStatus, ICA_VERSION, VERSION};
|
||||
|
||||
/// 获取在线数据
|
||||
pub async fn get_online_data(payload: Payload, _client: Client) {
|
||||
|
@ -22,96 +20,29 @@ pub async fn get_online_data(payload: Payload, _client: Client) {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::collapsible_if)]
|
||||
/// 接收消息
|
||||
pub async fn add_message(payload: Payload, client: Client) {
|
||||
if let Payload::Text(values) = payload {
|
||||
if let Some(value) = values.first() {
|
||||
let span = span!(Level::INFO, "ica new_msg");
|
||||
let _enter = span.enter();
|
||||
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());
|
||||
event!(Level::INFO, "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()
|
||||
}
|
||||
"shenbot v{}\nica-async-rs pong v{}",
|
||||
VERSION, ICA_VERSION
|
||||
));
|
||||
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 插件
|
||||
|
@ -175,41 +106,6 @@ pub async fn failed_message(payload: Payload, _client: Client) {
|
|||
}
|
||||
}
|
||||
|
||||
/// 处理加群申请
|
||||
///
|
||||
/// 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![
|
||||
|
@ -223,19 +119,17 @@ pub async fn any_event(event: Event, payload: Payload, _client: Client) {
|
|||
"deleteMessage",
|
||||
"setAllRooms",
|
||||
"setMessages",
|
||||
"handleRequest", // 处理验证消息 (加入请求之类的)
|
||||
// 也许以后会用到
|
||||
"messageSuccess",
|
||||
"messageFailed",
|
||||
"setAllChatGroups",
|
||||
// 忽略的
|
||||
"notify",
|
||||
"setShutUp", // 禁言
|
||||
"syncRead", // 同步已读
|
||||
"closeLoading", // 发送消息/加载新聊天 有一个 loading
|
||||
"renewMessage", // 我也不确定到底是啥事件
|
||||
"requestSetup", // 需要登录
|
||||
"updateRoom", // 更新房间
|
||||
"updateRoom",
|
||||
];
|
||||
match &event {
|
||||
Event::Custom(event_name) => {
|
||||
|
@ -289,7 +183,7 @@ pub async fn connect_callback(payload: Payload, _client: Client) {
|
|||
Some(msg) => {
|
||||
event!(Level::INFO, "{}{}", "未知消息".yellow(), msg);
|
||||
}
|
||||
_ => (),
|
||||
None => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,25 +1,18 @@
|
|||
use std::{
|
||||
hash::{DefaultHasher, Hash, Hasher},
|
||||
sync::OnceLock,
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
use std::time::Duration;
|
||||
|
||||
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};
|
||||
use tracing::{event, span, Level};
|
||||
|
||||
pub static mut MAIN_STATUS: status::BotStatus = status::BotStatus {
|
||||
config: None,
|
||||
|
@ -32,57 +25,8 @@ pub type MainStatus = status::BotStatus;
|
|||
pub type StopGetter = tokio::sync::oneshot::Receiver<()>;
|
||||
|
||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
pub const ICA_VERSION: &str = "2.0.1";
|
||||
pub const TAILCHAT_VERSION: &str = "2.0.0";
|
||||
|
||||
const HELP_MSG: &str = r#"/bot-rs
|
||||
展示 rust 侧信息
|
||||
/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())
|
||||
}
|
||||
|
||||
static STARTUP_TIME: OnceLock<SystemTime> = OnceLock::new();
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
/// 获取版本信息
|
||||
pub fn version_str() -> String {
|
||||
format!(
|
||||
"shenbot-rs v{}{}-[{}] ica v{}({}) tailchat v{}",
|
||||
VERSION,
|
||||
if STABLE { "" } else { "-开发版" },
|
||||
client_id(),
|
||||
ICA_VERSION,
|
||||
ica::ICA_PROTOCOL_VERSION,
|
||||
TAILCHAT_VERSION,
|
||||
)
|
||||
}
|
||||
|
||||
/// 是否为稳定版本
|
||||
/// 会在 release 的时候设置为 true
|
||||
pub const STABLE: bool = false;
|
||||
pub const ICA_VERSION: &str = "1.6.0";
|
||||
pub const TAILCHAT_VERSION: &str = "1.2.0";
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! async_callback_with_state {
|
||||
|
@ -104,32 +48,13 @@ macro_rules! async_any_callback_with_state {
|
|||
}};
|
||||
}
|
||||
|
||||
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?");
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// -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(());
|
||||
}
|
||||
let level = {
|
||||
let args = std::env::args();
|
||||
let args = args.collect::<Vec<String>>();
|
||||
if args.contains(&"-d".to_string()) {
|
||||
Level::DEBUG
|
||||
} else if args.contains(&"-t".to_string()) {
|
||||
|
@ -140,101 +65,63 @@ fn main() -> anyhow::Result<()> {
|
|||
};
|
||||
|
||||
tracing_subscriber::fmt().with_max_level(level).init();
|
||||
|
||||
let rt = tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.thread_name("shenbot-rs")
|
||||
.worker_threads(10)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let result = rt.block_on(inner_main());
|
||||
|
||||
event!(Level::INFO, "shenbot-rs v{} exiting", VERSION);
|
||||
|
||||
match result {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
if let Some(PyPluginError::PluginNotStopped) = e.downcast_ref::<PyPluginError>() {
|
||||
event!(Level::WARN, "Python 插件停不下来, 3s 后终止 tokio rt");
|
||||
rt.shutdown_timeout(Duration::from_secs(3));
|
||||
} else {
|
||||
event!(Level::ERROR, "shenbot-rs v{} exiting with error: {}", VERSION, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn inner_main() -> anyhow::Result<()> {
|
||||
let span = span!(Level::INFO, "bot-main");
|
||||
let span = span!(Level::INFO, "Shenbot Main");
|
||||
let _enter = span.enter();
|
||||
|
||||
event!(Level::INFO, "shenbot-rs v{} starting", VERSION);
|
||||
if !STABLE {
|
||||
event!(Level::WARN, "这是一个开发版本, 有问题记得找 shenjack");
|
||||
}
|
||||
event!(Level::INFO, "shenbot-async-rs v{} starting", VERSION);
|
||||
|
||||
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();
|
||||
}
|
||||
py::init_py();
|
||||
|
||||
// 准备一个用于停止 socket 的变量
|
||||
event!(Level::INFO, "启动 ICA");
|
||||
let (ica_send, ica_recv) = tokio::sync::oneshot::channel::<()>();
|
||||
|
||||
if bot_config.check_ica() {
|
||||
event!(Level::INFO, "{}", "开始启动 ICA".green());
|
||||
event!(Level::INFO, "启动 ica");
|
||||
let config = bot_config.ica();
|
||||
tokio::spawn(async move {
|
||||
ica::start_ica(&config, ica_recv).await.unwrap();
|
||||
});
|
||||
} else {
|
||||
event!(Level::INFO, "{}", "ica 未启用, 不管他".cyan());
|
||||
event!(Level::INFO, "未启用 ica");
|
||||
}
|
||||
|
||||
let (tailchat_send, tailchat_recv) = tokio::sync::oneshot::channel::<()>();
|
||||
|
||||
if bot_config.check_tailchat() {
|
||||
event!(Level::INFO, "{}", "开始启动 tailchat".green());
|
||||
event!(Level::INFO, "启动 Tailchat");
|
||||
let config = bot_config.tailchat();
|
||||
tokio::spawn(async move {
|
||||
tailchat::start_tailchat(config, tailchat_recv).await.unwrap();
|
||||
});
|
||||
} else {
|
||||
event!(Level::INFO, "{}", "tailchat 未启用, 不管他".bright_magenta());
|
||||
event!(Level::INFO, "未启用 Tailchat");
|
||||
}
|
||||
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
// 等待一个输入
|
||||
event!(Level::INFO, "Press ctrl+c to exit, second ctrl+c to force exit");
|
||||
tokio::signal::ctrl_c().await.ok();
|
||||
event!(Level::INFO, "Press any key to exit");
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input).unwrap();
|
||||
|
||||
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};
|
||||
use rust_socketio::Payload;
|
||||
|
||||
/// 一个简单的例子
|
||||
#[derive(Clone)]
|
||||
|
|
|
@ -1,84 +1,13 @@
|
|||
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 tracing::{event, info, warn, Level};
|
||||
|
||||
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(),
|
||||
})
|
||||
});
|
||||
use crate::py::{class, PyPlugin, PyStatus};
|
||||
use crate::MainStatus;
|
||||
|
||||
pub fn get_func<'py>(
|
||||
py_module: &Bound<'py, PyAny>,
|
||||
|
@ -131,22 +60,14 @@ pub fn get_func<'py>(
|
|||
}
|
||||
}
|
||||
|
||||
pub fn verify_and_reload_plugins() {
|
||||
pub fn verify_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) {
|
||||
if ext == "py" && !PyStatus::verify_file(&path) {
|
||||
need_reload_files.push(path);
|
||||
}
|
||||
}
|
||||
|
@ -155,54 +76,37 @@ pub fn verify_and_reload_plugins() {
|
|||
if need_reload_files.is_empty() {
|
||||
return;
|
||||
}
|
||||
event!(Level::INFO, "更改列表: {:?}", need_reload_files);
|
||||
let plugins = PyStatus::get_mut();
|
||||
info!("file change list: {:?}", need_reload_files);
|
||||
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);
|
||||
}
|
||||
match PyPlugin::new_from_path(&reload_file) {
|
||||
Some(plugin) => {
|
||||
PyStatus::add_file(reload_file.clone(), plugin);
|
||||
info!("重载 Python 插件: {:?}", reload_file);
|
||||
}
|
||||
None => {
|
||||
warn!("重载 Python 插件: {:?} 失败", reload_file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const ICA_NEW_MESSAGE_FUNC: &str = "on_ica_message";
|
||||
pub const ICA_DELETE_MESSAGE_FUNC: &str = "on_ica_delete_message";
|
||||
|
||||
pub const TAILCHAT_NEW_MESSAGE_FUNC: &str = "on_tailchat_message";
|
||||
|
||||
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) {
|
||||
if let Err(e) = py_func.call1($args) {
|
||||
let e = PyPluginError::FuncCallError(
|
||||
py_err,
|
||||
e,
|
||||
$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!(),
|
||||
}
|
||||
);
|
||||
event!(Level::WARN, "failed to call function<{}>: {:?}", $func_name, e);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -213,28 +117,27 @@ macro_rules! call_py_func {
|
|||
/// 执行 new message 的 python 插件
|
||||
pub async fn ica_new_message_py(message: &ica::messages::NewMessage, client: &Client) {
|
||||
// 验证插件是否改变
|
||||
verify_and_reload_plugins();
|
||||
verify_plugins();
|
||||
|
||||
let plugins = PyStatus::get();
|
||||
for (path, plugin) in plugins.files.iter().filter(|(_, plugin)| plugin.enabled) {
|
||||
let plugins = PyStatus::get_files();
|
||||
for (path, plugin) in plugins.iter() {
|
||||
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);
|
||||
// 甚至实际上压根不需要await这个spawn, 直接让他自己跑就好了(离谱)
|
||||
call_py_func!(args, plugin, path, ICA_NEW_MESSAGE_FUNC, client);
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn ica_delete_message_py(msg_id: ica::MessageId, client: &Client) {
|
||||
verify_and_reload_plugins();
|
||||
verify_plugins();
|
||||
|
||||
let plugins = PyStatus::get();
|
||||
for (path, plugin) in plugins.files.iter().filter(|(_, plugin)| plugin.enabled) {
|
||||
let plugins = PyStatus::get_files();
|
||||
for (path, plugin) in plugins.iter() {
|
||||
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);
|
||||
call_py_func!(args, plugin, path, ICA_DELETE_MESSAGE_FUNC, client);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -242,14 +145,13 @@ pub async fn tailchat_new_message_py(
|
|||
message: &tailchat::messages::ReceiveMessage,
|
||||
client: &Client,
|
||||
) {
|
||||
verify_and_reload_plugins();
|
||||
verify_plugins();
|
||||
|
||||
let plugins = PyStatus::get();
|
||||
for (path, plugin) in plugins.files.iter().filter(|(_, plugin)| plugin.enabled) {
|
||||
let plugins = PyStatus::get_files();
|
||||
for (path, plugin) in plugins.iter() {
|
||||
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);
|
||||
call_py_func!(args, plugin, path, TAILCHAT_NEW_MESSAGE_FUNC, client);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,23 @@
|
|||
pub mod commander;
|
||||
pub mod config;
|
||||
pub mod ica;
|
||||
pub mod schdule;
|
||||
pub mod tailchat;
|
||||
|
||||
use pyo3::{
|
||||
Bound, IntoPyObject, PyAny, PyRef, PyResult, pyclass, pymethods, pymodule,
|
||||
types::{PyBool, PyModule, PyModuleMethods, PyString},
|
||||
};
|
||||
use pyo3::prelude::*;
|
||||
use toml::Value as TomlValue;
|
||||
use tracing::{Level, event};
|
||||
|
||||
// #[derive(Clone)]
|
||||
#[derive(Clone)]
|
||||
#[pyclass]
|
||||
#[pyo3(name = "ConfigRequest")]
|
||||
pub struct ConfigRequestPy {
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl ConfigRequestPy {
|
||||
#[new]
|
||||
pub fn py_new(path: String) -> Self { Self { path } }
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[pyclass]
|
||||
#[pyo3(name = "ConfigData")]
|
||||
pub struct ConfigDataPy {
|
||||
|
@ -20,24 +26,21 @@ pub struct ConfigDataPy {
|
|||
|
||||
#[pymethods]
|
||||
impl ConfigDataPy {
|
||||
pub fn __getitem__(self_: PyRef<'_, Self>, key: String) -> Option<Bound<PyAny>> {
|
||||
pub fn __getitem__(self_: PyRef<'_, Self>, key: String) -> Option<Py<PyAny>> {
|
||||
match self_.data.get(&key) {
|
||||
Some(value) => match value {
|
||||
TomlValue::String(s) => Some(PyString::new(self_.py(), s).into_any()),
|
||||
TomlValue::Integer(i) => Some(i.into_pyobject(self_.py()).unwrap().into_any()),
|
||||
TomlValue::Float(f) => Some(f.into_pyobject(self_.py()).unwrap().into_any()),
|
||||
TomlValue::Boolean(b) => {
|
||||
let py_value = PyBool::new(self_.py(), *b);
|
||||
Some(py_value.as_any().clone())
|
||||
}
|
||||
TomlValue::String(s) => Some(s.into_py(self_.py())),
|
||||
TomlValue::Integer(i) => Some(i.into_py(self_.py())),
|
||||
TomlValue::Float(f) => Some(f.into_py(self_.py())),
|
||||
TomlValue::Boolean(b) => Some(b.into_py(self_.py())),
|
||||
TomlValue::Array(a) => {
|
||||
let new_self = Self::new(TomlValue::Array(a.clone()));
|
||||
let py_value = new_self.into_pyobject(self_.py()).unwrap().into_any();
|
||||
let py_value = new_self.into_py(self_.py());
|
||||
Some(py_value)
|
||||
}
|
||||
TomlValue::Table(t) => {
|
||||
let new_self = Self::new(TomlValue::Table(t.clone()));
|
||||
let py_value = new_self.into_pyobject(self_.py()).unwrap().into_any();
|
||||
let py_value = new_self.into_py(self_.py());
|
||||
Some(py_value)
|
||||
}
|
||||
_ => None,
|
||||
|
@ -51,35 +54,3 @@ impl ConfigDataPy {
|
|||
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 +0,0 @@
|
|||
|
|
@ -1,343 +0,0 @@
|
|||
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 }
|
||||
}
|
|
@ -1,17 +1,14 @@
|
|||
use std::time::SystemTime;
|
||||
|
||||
use pyo3::{pyclass, pymethods};
|
||||
use pyo3::prelude::*;
|
||||
use rust_socketio::asynchronous::Client;
|
||||
use tokio::runtime::Runtime;
|
||||
use tracing::{Level, event};
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
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;
|
||||
use crate::data_struct::ica::{MessageId, RoomId, RoomIdTrait};
|
||||
use crate::ica::client::{delete_message, send_message};
|
||||
use crate::MainStatus;
|
||||
|
||||
#[pyclass]
|
||||
#[pyo3(name = "IcaStatus")]
|
||||
|
@ -63,27 +60,6 @@ impl IcaStatusPy {
|
|||
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 {
|
||||
|
@ -94,47 +70,6 @@ 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")]
|
||||
|
@ -156,8 +91,6 @@ impl NewMessagePy {
|
|||
#[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() }
|
||||
|
@ -208,18 +141,10 @@ impl SendMessagePy {
|
|||
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 {
|
||||
|
@ -251,26 +176,6 @@ pub struct IcaClientPy {
|
|||
|
||||
#[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();
|
||||
|
@ -279,7 +184,7 @@ impl IcaClientPy {
|
|||
}
|
||||
|
||||
pub fn send_and_warn(&self, message: SendMessagePy) -> bool {
|
||||
event!(Level::WARN, message.msg.content);
|
||||
warn!(message.msg.content);
|
||||
self.send_message(message)
|
||||
}
|
||||
|
||||
|
@ -309,54 +214,16 @@ impl IcaClientPy {
|
|||
#[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);
|
||||
debug!("{}", content);
|
||||
}
|
||||
pub fn info(&self, content: String) {
|
||||
event!(Level::INFO, "{}", content);
|
||||
info!("{}", content);
|
||||
}
|
||||
pub fn warn(&self, content: String) {
|
||||
event!(Level::WARN, "{}", content);
|
||||
warn!("{}", content);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,59 +0,0 @@
|
|||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,14 +1,10 @@
|
|||
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]
|
||||
|
@ -67,57 +63,7 @@ impl TailchatClientPy {
|
|||
#[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);
|
||||
}
|
||||
|
@ -178,10 +124,6 @@ impl TailchatSendingMessagePy {
|
|||
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,
|
||||
|
|
|
@ -1,192 +0,0 @@
|
|||
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(())
|
||||
}
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
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,116 +1,27 @@
|
|||
pub mod call;
|
||||
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::{collections::HashMap, path::PathBuf};
|
||||
|
||||
use colored::Colorize;
|
||||
use pyo3::{
|
||||
Bound, Py, PyErr, PyResult, Python,
|
||||
exceptions::PyTypeError,
|
||||
intern,
|
||||
types::{PyAnyMethods, PyModule, PyTracebackMethods, PyTuple},
|
||||
};
|
||||
use tracing::{Level, event, span, warn};
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::types::PyTuple;
|
||||
use tracing::{debug, info, span, warn, Level};
|
||||
|
||||
use crate::MainStatus;
|
||||
use crate::error::PyPluginError;
|
||||
|
||||
use consts::config_func;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PyStatus {
|
||||
pub files: PyPlugins,
|
||||
pub config: config::PluginConfigFile,
|
||||
pub files: Option<HashMap<PathBuf, PyPlugin>>,
|
||||
}
|
||||
|
||||
pub type PyPlugins = HashMap<PathBuf, PyPlugin>;
|
||||
pub type PyPluginData = 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 {
|
||||
pub fn init() {
|
||||
let config =
|
||||
config::PluginConfigFile::default_init().expect("初始化 Python 插件配置文件失败");
|
||||
let status = PyStatus {
|
||||
files: HashMap::new(),
|
||||
config,
|
||||
};
|
||||
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 delete_file(&mut self, path: &PathBuf) -> Option<PyPlugin> { self.files.remove(path) }
|
||||
|
||||
pub fn get_status(&self, pluging_id: &str) -> Option<bool> {
|
||||
self.files.iter().find_map(|(_, plugin)| {
|
||||
if plugin.get_id() == pluging_id {
|
||||
return Some(plugin.enabled);
|
||||
}
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_status(&mut self, pluging_id: &str, status: bool) {
|
||||
self.files.iter_mut().for_each(|(_, plugin)| {
|
||||
if plugin.get_id() == pluging_id {
|
||||
plugin.enabled = status;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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 fn get_py_err_traceback(py_err: &PyErr) -> String {
|
||||
Python::with_gil(|py| match py_err.traceback(py) {
|
||||
Python::with_gil(|py| match py_err.traceback_bound(py) {
|
||||
Some(traceback) => match traceback.format() {
|
||||
Ok(trace) => trace,
|
||||
Err(e) => format!("{:?}", e),
|
||||
|
@ -121,25 +32,14 @@ pub fn get_py_err_traceback(py_err: &PyErr) -> String {
|
|||
.to_string()
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PyPlugin {
|
||||
pub file_path: PathBuf,
|
||||
pub modify_time: Option<SystemTime>,
|
||||
pub py_module: Py<PyModule>,
|
||||
pub enabled: bool,
|
||||
pub changed_time: Option<SystemTime>,
|
||||
pub py_module: Py<PyAny>,
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@ -161,42 +61,11 @@ impl PyPlugin {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 从文件更新
|
||||
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 {
|
||||
if let Some(changed_time) = self.changed_time {
|
||||
time.eq(&changed_time)
|
||||
} else {
|
||||
true
|
||||
|
@ -204,159 +73,13 @@ impl PyPlugin {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
let (path, changed_time, content) = value;
|
||||
let py_module = match py_module_from_code(&content, &path) {
|
||||
Ok(module) => module,
|
||||
Err(e) => {
|
||||
warn!("加载 Python 插件: {:?} 失败", e);
|
||||
|
@ -365,37 +88,68 @@ impl TryFrom<RawPyPlugin> for PyPlugin {
|
|||
};
|
||||
Python::with_gil(|py| {
|
||||
let module = py_module.bind(py);
|
||||
if let Ok(config_func) = call::get_func(module, config_func::REQUIRE_CONFIG) {
|
||||
if let Ok(config_func) = call::get_func(module, "on_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(),
|
||||
));
|
||||
let (config, default) = config.extract::<(String, String)>().unwrap();
|
||||
let base_path = MainStatus::global_config().py().config_path;
|
||||
|
||||
let mut base_path: PathBuf = PathBuf::from(base_path);
|
||||
|
||||
if !base_path.exists() {
|
||||
warn!("python 插件路径不存在, 创建: {:?}", base_path);
|
||||
std::fs::create_dir_all(&base_path)?;
|
||||
}
|
||||
base_path.push(&config);
|
||||
|
||||
let config_value = if base_path.exists() {
|
||||
info!("加载 {:?} 的配置文件 {:?} 中", path, base_path);
|
||||
let content = std::fs::read_to_string(&base_path)?;
|
||||
toml::from_str(&content)
|
||||
} else {
|
||||
warn!("配置文件 {:?} 不存在, 创建默认配置", base_path);
|
||||
// 写入默认配置
|
||||
std::fs::write(base_path, &default)?;
|
||||
toml::from_str(&default)
|
||||
};
|
||||
match config_value {
|
||||
Ok(config) => {
|
||||
let py_config =
|
||||
Bound::new(py, class::ConfigDataPy::new(config)).unwrap();
|
||||
module.setattr("CONFIG_DATA", py_config).unwrap();
|
||||
Ok(PyPlugin {
|
||||
file_path: path,
|
||||
changed_time,
|
||||
py_module: module.into_py(py),
|
||||
})
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"加载 Python 插件 {:?} 的配置文件信息时失败:{:?}",
|
||||
path, e
|
||||
);
|
||||
Err(PyErr::new::<pyo3::exceptions::PyTypeError, _>(format!(
|
||||
"加载 Python 插件 {:?} 的配置文件信息时失败:{:?}",
|
||||
path, e
|
||||
)))
|
||||
}
|
||||
}
|
||||
Ok(PyPlugin::new(path, modify_time, module.clone().unbind()))
|
||||
} else if config.is_none() {
|
||||
// 没有配置文件
|
||||
Ok(PyPlugin::new(path, modify_time, module.clone().unbind()))
|
||||
Ok(PyPlugin {
|
||||
file_path: path,
|
||||
changed_time,
|
||||
py_module: module.into_py(py),
|
||||
})
|
||||
} else {
|
||||
warn!(
|
||||
"加载 Python 插件 {:?} 的配置文件信息时失败:返回的不是 [str, str]",
|
||||
path
|
||||
);
|
||||
Err(PyTypeError::new_err("返回的不是 [str, str]".to_string()))
|
||||
Err(PyErr::new::<pyo3::exceptions::PyTypeError, _>(
|
||||
"返回的不是 [str, str]".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
|
@ -404,29 +158,66 @@ impl TryFrom<RawPyPlugin> for PyPlugin {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
Ok(PyPlugin::new(path, modify_time, module.clone().unbind()))
|
||||
Ok(PyPlugin {
|
||||
file_path: path,
|
||||
changed_time,
|
||||
py_module: module.into_py(py),
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// 插件路径转换为 id
|
||||
pub fn plugin_path_as_id(path: &Path) -> String {
|
||||
path.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_str()
|
||||
.unwrap_or("decode-failed")
|
||||
.to_string()
|
||||
impl PyStatus {
|
||||
pub fn get_files() -> &'static PyPluginData {
|
||||
unsafe {
|
||||
match PYSTATUS.files.as_ref() {
|
||||
Some(files) => files,
|
||||
None => {
|
||||
PYSTATUS.files = Some(HashMap::new());
|
||||
PYSTATUS.files.as_ref().unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_file(path: PathBuf, plugin: PyPlugin) {
|
||||
unsafe {
|
||||
match PYSTATUS.files.as_mut() {
|
||||
Some(files) => {
|
||||
files.insert(path, plugin);
|
||||
}
|
||||
None => {
|
||||
let mut files: PyPluginData = HashMap::new();
|
||||
files.insert(path, plugin);
|
||||
PYSTATUS.files = Some(files);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verify_file(path: &PathBuf) -> bool {
|
||||
unsafe {
|
||||
match PYSTATUS.files.as_ref() {
|
||||
Some(files) => match files.get(path) {
|
||||
Some(plugin) => plugin.verifiy(),
|
||||
None => false,
|
||||
},
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub static mut PYSTATUS: PyStatus = PyStatus { files: None };
|
||||
|
||||
pub fn load_py_plugins(path: &PathBuf) {
|
||||
let plugins = PyStatus::get_mut();
|
||||
if path.exists() {
|
||||
event!(Level::INFO, "找到位于 {:?} 的插件", path);
|
||||
info!("finding plugins in: {:?}", path);
|
||||
// 搜索所有的 py 文件 和 文件夹单层下面的 py 文件
|
||||
match path.read_dir() {
|
||||
Err(e) => {
|
||||
event!(Level::WARN, "读取插件路径失败 {:?}", e);
|
||||
warn!("failed to read plugin path: {:?}", e);
|
||||
}
|
||||
Ok(dir) => {
|
||||
for entry in dir {
|
||||
|
@ -435,7 +226,7 @@ pub fn load_py_plugins(path: &PathBuf) {
|
|||
if let Some(ext) = path.extension() {
|
||||
if ext == "py" {
|
||||
if let Some(plugin) = PyPlugin::new_from_path(&path) {
|
||||
plugins.add_file(path, plugin);
|
||||
PyStatus::add_file(path, plugin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -443,32 +234,28 @@ pub fn load_py_plugins(path: &PathBuf) {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
event!(Level::WARN, "插件加载目录不存在: {:?}", path);
|
||||
warn!("plugin path not exists: {:?}", path);
|
||||
}
|
||||
plugins.config.read_status_from_default();
|
||||
plugins.config.sync_status_to_config();
|
||||
event!(
|
||||
Level::INFO,
|
||||
info!(
|
||||
"python 插件目录: {:?} 加载完成, 加载到 {} 个插件",
|
||||
path,
|
||||
plugins.files.len()
|
||||
PyStatus::get_files().len()
|
||||
);
|
||||
}
|
||||
|
||||
pub fn get_change_time(path: &Path) -> Option<SystemTime> { path.metadata().ok()?.modified().ok() }
|
||||
|
||||
pub fn py_module_from_code(content: &str, path: &Path) -> PyResult<Py<PyModule>> {
|
||||
Python::with_gil(|py| -> PyResult<Py<PyModule>> {
|
||||
let module = PyModule::from_code(
|
||||
pub fn py_module_from_code(content: &str, path: &Path) -> PyResult<Py<PyAny>> {
|
||||
Python::with_gil(|py| -> PyResult<Py<PyAny>> {
|
||||
let module: PyResult<Py<PyAny>> = PyModule::from_code_bound(
|
||||
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(),
|
||||
content,
|
||||
&path.to_string_lossy(),
|
||||
&path.file_name().unwrap().to_string_lossy(),
|
||||
// !!!! 请注意, 一定要给他一个名字, cpython 会自动把后面的重名模块覆盖掉前面的
|
||||
)?;
|
||||
Ok(module.unbind())
|
||||
)
|
||||
.map(|module| module.into());
|
||||
module
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -480,122 +267,20 @@ pub fn load_py_file(path: &PathBuf) -> std::io::Result<RawPyPlugin> {
|
|||
Ok((path.clone(), changed_time, content))
|
||||
}
|
||||
|
||||
fn init_py_with_env_path(path: &str) {
|
||||
unsafe {
|
||||
#[cfg(target_os = "windows")]
|
||||
use std::ffi::OsStr;
|
||||
#[cfg(target_os = "windows")]
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
|
||||
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);
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
let wide_path = path.as_bytes().iter().map(|i| *i as i32).collect::<Vec<i32>>();
|
||||
#[cfg(target_os = "windows")]
|
||||
let wide_path = OsStr::new(path).encode_wide().chain(Some(0)).collect::<Vec<u16>>();
|
||||
|
||||
// 设置 prefix 和 exec_prefix
|
||||
pyo3::ffi::PyConfig_SetString(config_ptr, &mut config.prefix as *mut _, wide_path.as_ptr());
|
||||
pyo3::ffi::PyConfig_SetString(
|
||||
config_ptr,
|
||||
&mut config.exec_prefix as *mut _,
|
||||
wide_path.as_ptr(),
|
||||
);
|
||||
|
||||
// 使用 Py_InitializeFromConfig 初始化 python
|
||||
let status = pyo3::ffi::Py_InitializeFromConfig(&config as *const _);
|
||||
pyo3::ffi::PyEval_SaveThread();
|
||||
// 清理配置
|
||||
pyo3::ffi::PyConfig_Clear(config_ptr);
|
||||
match status._type {
|
||||
pyo3::ffi::_PyStatus_TYPE::_PyStatus_TYPE_OK => {
|
||||
event!(Level::INFO, "根据配置初始化 python 完成");
|
||||
}
|
||||
pyo3::ffi::_PyStatus_TYPE::_PyStatus_TYPE_EXIT => {
|
||||
event!(Level::ERROR, "不对啊, 怎么刚刚初始化 Python 就 EXIT 了");
|
||||
}
|
||||
pyo3::ffi::_PyStatus_TYPE::_PyStatus_TYPE_ERROR => {
|
||||
event!(Level::ERROR, "初始化 python 时发生错误: ERROR");
|
||||
pyo3::ffi::Py_ExitStatusException(status);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Python 侧初始化
|
||||
pub fn init_py() {
|
||||
// 从 全局配置中获取 python 插件路径
|
||||
let span = span!(Level::INFO, "py init");
|
||||
let span = span!(Level::INFO, "Init Python Plugin");
|
||||
let _enter = span.enter();
|
||||
|
||||
event!(Level::INFO, "开始初始化 python");
|
||||
let global_config = MainStatus::global_config().py();
|
||||
|
||||
// 注册东西
|
||||
class::regist_class();
|
||||
debug!("initing python threads");
|
||||
pyo3::prepare_freethreaded_python();
|
||||
|
||||
let plugin_path = MainStatus::global_config().py().plugin_path;
|
||||
let path = PathBuf::from(global_config.plugin_path);
|
||||
load_py_plugins(&path);
|
||||
debug!("python 插件列表: {:#?}", PyStatus::get_files());
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
info!("python inited")
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use crate::MAIN_STATUS;
|
||||
use crate::config::BotConfig;
|
||||
use crate::MAIN_STATUS;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BotStatus {
|
||||
|
@ -38,37 +38,20 @@ impl BotStatus {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn global_config() -> &'static BotConfig {
|
||||
unsafe {
|
||||
let ptr = &raw const MAIN_STATUS.config;
|
||||
(*ptr).as_ref().unwrap()
|
||||
}
|
||||
}
|
||||
pub fn global_config() -> &'static BotConfig { unsafe { MAIN_STATUS.config.as_ref().unwrap() } }
|
||||
|
||||
pub fn global_ica_status() -> &'static ica::MainStatus {
|
||||
unsafe {
|
||||
let ptr = &raw const MAIN_STATUS.ica_status;
|
||||
(*ptr).as_ref().unwrap()
|
||||
}
|
||||
unsafe { MAIN_STATUS.ica_status.as_ref().unwrap() }
|
||||
}
|
||||
pub fn global_tailchat_status() -> &'static tailchat::MainStatus {
|
||||
unsafe {
|
||||
let ptr = &raw const MAIN_STATUS.tailchat_status;
|
||||
(*ptr).as_ref().unwrap()
|
||||
}
|
||||
unsafe { MAIN_STATUS.tailchat_status.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()
|
||||
}
|
||||
unsafe { MAIN_STATUS.ica_status.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()
|
||||
}
|
||||
unsafe { MAIN_STATUS.tailchat_status.as_mut().unwrap() }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,13 +9,13 @@ 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 serde_json::{json, Value};
|
||||
use tracing::{event, span, Level};
|
||||
|
||||
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};
|
||||
use crate::{async_any_callback_with_state, async_callback_with_state, StopGetter};
|
||||
|
||||
pub async fn start_tailchat(
|
||||
config: TailchatConfig,
|
||||
|
@ -37,7 +37,7 @@ pub async fn start_tailchat(
|
|||
|
||||
let client = reqwest_ClientBuilder::new().default_headers(header_map.clone()).build()?;
|
||||
let status = match client
|
||||
.post(format!("{}/api/openapi/bot/login", config.host))
|
||||
.post(&format!("{}/api/openapi/bot/login", config.host))
|
||||
.body(json! {{"appId": config.app_id, "token": token}}.to_string())
|
||||
.send()
|
||||
.await
|
||||
|
@ -95,23 +95,6 @@ pub async fn start_tailchat(
|
|||
|
||||
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 {
|
||||
|
|
|
@ -4,8 +4,8 @@ use crate::data_struct::tailchat::messages::SendingMessage;
|
|||
use colored::Colorize;
|
||||
use reqwest::multipart;
|
||||
use rust_socketio::asynchronous::Client;
|
||||
use serde_json::{Value, json};
|
||||
use tracing::{Level, event, span};
|
||||
use serde_json::{json, Value};
|
||||
use tracing::{event, span, Level};
|
||||
|
||||
pub async fn send_message(client: &Client, message: &SendingMessage) -> bool {
|
||||
let span = span!(Level::INFO, "tailchat send message");
|
||||
|
@ -71,8 +71,9 @@ pub async fn send_message(client: &Client, message: &SendingMessage) -> bool {
|
|||
return false;
|
||||
}
|
||||
};
|
||||
event!(Level::INFO, "file upload success with data:{}", format!("{:#?}", data).cyan());
|
||||
let content = format!(
|
||||
"{}{}",
|
||||
"{}\n{}",
|
||||
message.content,
|
||||
message.file.gen_markdown(data["url"].as_str().unwrap())
|
||||
);
|
||||
|
|
|
@ -3,14 +3,11 @@ use std::sync::Arc;
|
|||
use colored::Colorize;
|
||||
use rust_socketio::asynchronous::Client;
|
||||
use rust_socketio::{Event, Payload};
|
||||
use tracing::{Level, event, info};
|
||||
use tracing::{event, info, Level};
|
||||
|
||||
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>) {
|
||||
|
@ -64,6 +61,7 @@ pub async fn any_event(event: Event, payload: Payload, _client: Client, _status:
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::collapsible_if)]
|
||||
pub async fn on_message(payload: Payload, client: Client, _status: Arc<BotStatus>) {
|
||||
if let Payload::Text(values) = payload {
|
||||
if let Some(value) = values.first() {
|
||||
|
@ -79,69 +77,15 @@ pub async fn on_message(payload: Payload, client: Client, _status: Arc<BotStatus
|
|||
|
||||
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()
|
||||
}
|
||||
"shenbot v{}\ntailchat-async-rs pong v{}",
|
||||
crate::VERSION,
|
||||
crate::TAILCHAT_VERSION
|
||||
));
|
||||
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;
|
||||
crate::py::call::tailchat_new_message_py(&message, &client).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
|
208
news.md
208
news.md
|
@ -1,5 +1,209 @@
|
|||
# 更新日志
|
||||
|
||||
## [0.9](./news/0-9.md)
|
||||
## 0.6.8
|
||||
|
||||
## [0.2 ~ 0.8](./news/old.md)
|
||||
- 修复了一堆拼写错误
|
||||
- 太难绷了
|
||||
- `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
|
||||
|
||||
重构了一波整体代码
|
||||
|
|
38
news/0-9.md
38
news/0-9.md
|
@ -1,38 +0,0 @@
|
|||
# 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
383
news/olds.md
|
@ -1,383 +0,0 @@
|
|||
# 更新日志 (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
|
||||
|
||||
重构了一波整体代码
|
51
readme.md
51
readme.md
|
@ -1,33 +1,31 @@
|
|||
# icalingua bot
|
||||
|
||||
这是一个基于 icalingua-bridge 的 bot
|
||||
这是一个基于 icalingua docker 版的 bot
|
||||
|
||||
[插件市场(确信)](https://github.com/shenjackyuanjie/shenbot-plugins)
|
||||
> 出于某个企鹅, 和保护 作者 和 原项目 ( ica ) 的原因 \
|
||||
> 功能请自行理解
|
||||
|
||||
## 通用环境准备
|
||||
|
||||
- 安装 Python 3.8+
|
||||
|
||||
```powershell
|
||||
# 你可以使用你自己的方法安装 Python
|
||||
# 例如
|
||||
choco install python
|
||||
# 或者
|
||||
scoop install python
|
||||
# 又或者
|
||||
uv venv
|
||||
```
|
||||
|
||||
- 启动 icalingua 后端
|
||||
|
||||
```bash
|
||||
# 用你自己的方法启动你的 icalingua-bridge
|
||||
# 用你自己的方法启动你的 icalingua 后端
|
||||
# 例如
|
||||
docker start icalingua
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
## 使用方法 ( Rust 版 )
|
||||
|
||||
- 准备一个 Python 环境
|
||||
|
||||
|
@ -48,3 +46,42 @@ cargo build --release
|
|||
```powershell
|
||||
cargo run --release -- -c config.toml
|
||||
```
|
||||
|
||||
## 使用方法 ( Python 版 )
|
||||
|
||||
> [!WARNING]
|
||||
> 请注意 Python 的 bot 已经不再维护
|
||||
|
||||
- 安装依赖
|
||||
|
||||
```powershell
|
||||
python -m pip install -r requirements.txt
|
||||
```
|
||||
|
||||
> 如果你想使用虚拟环境 \
|
||||
> 可以使用 `python -m venv venv` 创建虚拟环境 \
|
||||
> 然后使用 `venv\Scripts\activate` 激活虚拟环境 \
|
||||
> 最后使用 `python -m pip install -r requirements.txt` 安装依赖
|
||||
|
||||
- 修改配置文件
|
||||
|
||||
```powershell
|
||||
Copy-Item config-temp.toml config.toml
|
||||
# 欸我就用 powershell
|
||||
```
|
||||
|
||||
- icalingua 启动!
|
||||
|
||||
```bash
|
||||
# 用你自己的方法启动你的 icalingua 后端
|
||||
# 例如
|
||||
docker start icalingua
|
||||
# 或者
|
||||
docker up -d
|
||||
```
|
||||
|
||||
- bot 启动!
|
||||
|
||||
```powershell
|
||||
python connect.py
|
||||
```
|
||||
|
|
1
rust-toolchain
Normal file
1
rust-toolchain
Normal file
|
@ -0,0 +1 @@
|
|||
nightly
|
|
@ -1,2 +0,0 @@
|
|||
[toolchain]
|
||||
channel = "stable"
|
Loading…
Reference in New Issue
Block a user