From 61b73d0bef94336d0ff9533288f9689c150a43fc Mon Sep 17 00:00:00 2001 From: Bluemangoo Date: Sat, 20 Jun 2026 23:05:07 +0800 Subject: [PATCH] Initial commit --- .gitignore | 4 +++ __init__.py | 23 ++++++++++++ config.py | 22 ++++++++++++ lib.py | 102 ++++++++++++++++++++++++++++++++++++++++++++++++++++ sync.py | 53 +++++++++++++++++++++++++++ 5 files changed, 204 insertions(+) create mode 100644 .gitignore create mode 100644 __init__.py create mode 100644 config.py create mode 100644 lib.py create mode 100644 sync.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8c41e77 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +__pycache__ +*.dll +*.so +*.dylib \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e1aa7a0 --- /dev/null +++ b/__init__.py @@ -0,0 +1,23 @@ +import nonebot +from nonebot import require + +require("nonebot_plugin_apscheduler") + +from pathlib import Path +from nonebot_plugin_apscheduler import scheduler + +from .config import pconfig +from .lib import rlib +from .sync import sync + +driver = nonebot.get_driver() + + +@driver.on_startup +def startup(): + if pconfig.proxy: + rlib.set_proxy(pconfig.proxy) + if pconfig.github_token: + rlib.set_ghp(pconfig.github_token) + rlib.boot(str(Path.cwd()), "nonebot_plugin_sekai_update_notify", pconfig.assets_studio_path) + scheduler.add_job(sync, "interval", seconds=10, id="sekai_update_sync", replace_existing=True) diff --git a/config.py b/config.py new file mode 100644 index 0000000..5a0e8a6 --- /dev/null +++ b/config.py @@ -0,0 +1,22 @@ +from nonebot import get_driver, get_plugin_config +from pydantic import BaseModel + +_nickname: str = "" +_proxy: str | None = None +try: + _proxy = next(iter(get_driver().config.proxy), None) + _nickname: str = next(iter(get_driver().config.nickname), "") +except: + pass + +class Config(BaseModel): + assets_studio_path: str + github_token: str | None = None + @property + def proxy(self) -> str | None: + return _proxy + @property + def nickname(self) -> str: + return _nickname + +pconfig: Config = get_plugin_config(Config) \ No newline at end of file diff --git a/lib.py b/lib.py new file mode 100644 index 0000000..4fcbf89 --- /dev/null +++ b/lib.py @@ -0,0 +1,102 @@ +import ctypes +import json +import os +import sys +from typing import Optional, Dict, Any, List + + +def get_lib_filename(base_name): + if sys.platform == "win32": + return f"{base_name}.dll" + elif sys.platform == "darwin": + return f"lib{base_name}.dylib" + else: + return f"lib{base_name}.so" + + +class FetchData: + event_id: int + card_paths: List[str] + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> 'FetchData': + instance = cls() + instance.event_id = data.get("event_id", 0) + instance.card_paths = data.get("card_paths", []) + return instance + +class SekaiSyncLib: + def __init__(self): + self.lib = ctypes.CDLL(os.path.join(os.path.dirname(__file__), get_lib_filename("sekai_sync_lib"))) + self._setup_signatures() + + def _setup_signatures(self): + # proxy(x: *const c_char) + self.lib.proxy.argtypes = [ctypes.c_char_p] + self.lib.proxy.restype = None + + # ghp(x: *const c_char) + self.lib.ghp.argtypes = [ctypes.c_char_p] + self.lib.ghp.restype = None + + # boot(cwd: *const c_char, name: *const c_char, as_path: *const c_char) -> i32 + self.lib.boot.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_char_p] + self.lib.boot.restype = ctypes.c_int32 + + # fetch_data() -> *mut c_char + # ⚠️ 这里必须用 c_void_p,不能用 c_char_p,否则会丢失指针地址导致内存泄漏 + self.lib.fetch_data.argtypes = [] + self.lib.fetch_data.restype = ctypes.c_void_p + + # free_string(s: *mut c_char) + self.lib.free_string.argtypes = [ctypes.c_void_p] + self.lib.free_string.restype = None + + def _str_to_bytes(self, s: Optional[str]) -> bytes: + """辅助函数:将 Python 字符串安全转换为 C 字符串字节""" + if s is None: + # 根据 Rust 端解析逻辑,传入空字符串将被视为 None + return b"" + return s.encode('utf-8') + + def set_proxy(self, proxy_url: Optional[str]): + """设置代理,传 None 或空字符串可清除""" + self.lib.proxy(self._str_to_bytes(proxy_url)) + + def set_ghp(self, token: Optional[str]): + """设置 GitHub Token,传 None 或空字符串可清除""" + self.lib.ghp(self._str_to_bytes(token)) + + def boot(self, cwd: str, name: str, as_path: str) -> int: + """ + 启动主循环与初始化状态 + """ + return self.lib.boot( + self._str_to_bytes(cwd), + self._str_to_bytes(name), + self._str_to_bytes(as_path) + ) + + def fetch_data(self) -> Optional[FetchData]: + # 1. 获取裸指针 + ptr = self.lib.fetch_data() + + # 指针为空 (std::ptr::null_mut) + if not ptr: + return None + + try: + # 2. 将 void_p 转换回 c_char_p 并读取里面的 bytes + json_bytes = ctypes.cast(ptr, ctypes.c_char_p).value + if not json_bytes: + return None + + # 3. 解码并反序列化 JSON + json_str = json_bytes.decode('utf-8') + return FetchData.from_dict(json.loads(json_str)) + + finally: + # 4. 无论是否发生 JSON 解析异常,绝对保证释放 Rust 内存! + self.lib.free_string(ptr) + +rlib = SekaiSyncLib() \ No newline at end of file diff --git a/sync.py b/sync.py new file mode 100644 index 0000000..15cb92c --- /dev/null +++ b/sync.py @@ -0,0 +1,53 @@ +import json +from pathlib import Path + +from nonebot import get_bot +from nonebot.adapters.onebot.v11 import Message, MessageSegment +from nonebot_plugin_apscheduler import scheduler + +from .config import pconfig +from .lib import rlib + +group_file = Path("config") / "nonebot_sekai_update_notify" / "group.json" +group_file.parent.mkdir(parents=True, exist_ok=True) +if not group_file.exists(): + group_file.write_text("[]") + +enabled_groups = json.loads(group_file.read_text()) + + +def add_group(group_id: int): + if group_id not in enabled_groups: + enabled_groups.append(group_id) + group_file.write_text(json.dumps(enabled_groups)) + + +def remove_group(group_id: int): + if group_id in enabled_groups: + enabled_groups.remove(group_id) + group_file.write_text(json.dumps(enabled_groups)) + + +async def sync(): + data = rlib.fetch_data() + if data is None: + return + message = Message() + inner = [MessageSegment.text(f"活动 {data.event_id} 的卡牌更新了!\n")] + for item in data.card_paths: + p = Path(item) + n = MessageSegment.image(file=item) + n.data["summary"] = f"[{p.name}]" + inner.append(n) + for node in inner: + message.append(MessageSegment.node_custom( + user_id=int(get_bot().self_id), + nickname=pconfig.nickname, + content=Message([node]) + )) + + for group in enabled_groups: + await get_bot().send_group_msg(message=message, group_id=group) + + +