From dd685567c9df45adef1cac99acbe5aea08dbe1a1 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 ++++++ __pycache__/__init__.cpython-310.pyc | Bin 0 -> 887 bytes __pycache__/config.cpython-310.pyc | Bin 0 -> 1009 bytes __pycache__/lib.cpython-310.pyc | Bin 0 -> 3188 bytes __pycache__/sync.cpython-310.pyc | Bin 0 -> 1709 bytes config.py | 22 ++++++ lib.py | 102 +++++++++++++++++++++++++++ sync.py | 53 ++++++++++++++ 9 files changed, 204 insertions(+) create mode 100644 .gitignore create mode 100644 __init__.py create mode 100644 __pycache__/__init__.cpython-310.pyc create mode 100644 __pycache__/config.cpython-310.pyc create mode 100644 __pycache__/lib.cpython-310.pyc create mode 100644 __pycache__/sync.cpython-310.pyc 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..ac1ea27 --- /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/__pycache__/__init__.cpython-310.pyc b/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cc46d1f25dfdc893b68a9cc6adf9f0b6c11d0379 GIT binary patch literal 887 zcmZ8f&5qMB5VrH5G+maJ_z_2LJuF8K2q7dmS4gacWL2`#*xR~I;xKlW?RmLy;2qi% z;!${oTsid>I5AEO?Bd8X-;5`o`NpHiX3zqBN10o9kY1ANtM(oO9z}*S)H*A?;zXJj<8(j ztFg$$=rg&Y>_pJ_WRmYOFPTngAA|hZDUkh>MmKVh!O3Pw@tKL0jtPQY~B%RBGkT9h zr$%D)oXzruvdeDHtzOH9;Z>F|mTe!*wYI%)EI{D?@XJqvofQVp4L7zEO7j-`452%X z3nBPQ&ml$YJAntN2Jvh9_QmXjhP7$2rrDA4`QhEp+X9S)nSt`p3BN<|(1aQ1s!^8n zXYB^L+5&9Xo?-@*qlmnmMl`37@q_eh|EDz|ROP0K>wHw4&Zsd$}B_yYbmt&d3MjZGj F^fz@x?g;<@ literal 0 HcmV?d00001 diff --git a/__pycache__/config.cpython-310.pyc b/__pycache__/config.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0f8b9123a7b560580a957c45ccfc667516a8ed3f GIT binary patch literal 1009 zcmZ{iJ#X7E5Qa%gmSxpnNsXc(OIHucSfGVb1WAD|t(PDODi|OX@$4wJC6%O#*sHU2 z@7O<(zr?ju=Wd;PXT?p1mI7bg9fiB+jx0Kzh~fJB?bX$5!PpOS9vIQe4XE&bR>+lw0zKvVf6rW$bwfodEx^n(#wKZh1Xz&Gsb(N%lHRbwOY zkNa2$V;;u-W;ka#=A`cvd(Tu;g*Qxv;BUOR3j(?u9`~S&*;`cYlntYwNXUl5MKiEc z>Ff(=w?}rlsAgF%(;}Z{vmtk_dWN?^0T@Fz`tm?xR6N|jiUd115Z zYEn<~uw1#uc3?d!jjxCKV%@I0Ijkr7KTzGqzjX%%G3kxzE(sCVvWJ!ncUQAd@y96Z z-d+0L3C=H+lCDom7fHD+RJ9DbS=^m?#%kA-GRd)p#AZcqq}25EU5KP5XuHze zDg!EY&#|=#Mu#XwZHP2tA@6g+2V95&-=4W~G&*9oSKePC7gF4p(CaSMrko<$rg`>Ljw={9{1ZAJKd0W(wFL{8leKCms E07Z}K9RL6T literal 0 HcmV?d00001 diff --git a/__pycache__/lib.cpython-310.pyc b/__pycache__/lib.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c68f05ceee1e5d004821237c7a852182394d4001 GIT binary patch literal 3188 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 X+BBviB~7DAr-8%7_=a!#PSg1p*)G1~ literal 0 HcmV?d00001 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) + + +