Compare commits

..

No commits in common. "main" and "0.6.9" have entirely different histories.
main ... 0.6.9

59 changed files with 25333 additions and 3444 deletions

View File

@ -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

View File

@ -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
View File

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

1408
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -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
View 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
View 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
View 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
View 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")

View 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
View File

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

62
ica-py/router.py Normal file
View 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)

View File

@ -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
View 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
View 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
View 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
View File

@ -0,0 +1,4 @@
input.txt
node_modules
package.json
package-lock.json

View 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

View 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]砍下了自己的右手"
}

View 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();

View 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

File diff suppressed because one or more lines are too long

View 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
View 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

View 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)

View File

@ -21,5 +21,3 @@ use_field_init_shorthand = true
color = "Always"
edition = "2021"
# 这样不用 nightly 也可以使用 unstable 特性
unstable_features = true

View File

@ -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 {

View File

@ -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;

View File

@ -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>,
}

View File

@ -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));
}

View File

@ -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
)
}
}

View File

@ -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};

View File

@ -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 } }

View File

@ -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,
}
}
}

View File

@ -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),
))

View File

@ -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");
}

View File

@ -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 => (),
}
}
}

View File

@ -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)]

View File

@ -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);
}
}

View File

@ -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, "注册完成");
}

View File

@ -1 +0,0 @@

View File

@ -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 }
}

View File

@ -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);
}
}

View File

@ -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);
}
});
});
}
}

View File

@ -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,

View 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(())
}
}

View File

@ -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";
}

View File

@ -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")
}

View File

@ -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() }
}
}

View File

@ -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 {

View File

@ -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())
);

View File

@ -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;
}
}
}

View File

@ -1 +0,0 @@

208
news.md
View File

@ -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
重构了一波整体代码

View File

@ -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]` 方法
- 用于获取当前所有的被屏蔽的人

View File

@ -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
重构了一波整体代码

View File

@ -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
View File

@ -0,0 +1 @@
nightly

View File

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