- Drop 'like' event from rules, handler and web UI (Bilibili like messages are not reliably delivered as INTERACT_WORD_V2 msg_type=6) - Keep enter_room / follow / interact_other - event_rules.json rewritten without like key - build.py keeps BUILD_DIR = dist/portable
124 lines
4.3 KiB
Python
124 lines
4.3 KiB
Python
"""Event voice rules: load/save event_rules.json and render templates.
|
|
|
|
Each live event (danmaku, gift, blind box, enter room, follow, like,
|
|
interact, buy guard, guard toast, super chat) can be voiced using a
|
|
user-customizable template. Empty template = event is not spoken.
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
import pathlib
|
|
import re
|
|
import sys
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
DEFAULT_RULES = {
|
|
"enabled": {
|
|
"danmaku": True,
|
|
"gift": True,
|
|
"blind_box": True,
|
|
"enter_room": True,
|
|
"follow": False,
|
|
"interact_other": False,
|
|
"buy_guard": True,
|
|
"guard_toast": False,
|
|
"super_chat": True,
|
|
},
|
|
"templates": {
|
|
"danmaku": "{uname}\u8bf4\u3001 {msg}",
|
|
"gift": "\u611f\u8c22\u7531{viewer_name}\u6295\u5582\u7684{gift_name}",
|
|
"blind_box": "\u611f\u8c22\u7531{viewer_name}\u6295\u5582\u7684{gift_name}\u7206\u51fa\u4e86{blind_box_gift}",
|
|
"enter_room": "\u6b22\u8fce{viewer_name}\u8fdb\u5165\u76f4\u64ad\u95f4",
|
|
"follow": "",
|
|
"interact_other": "",
|
|
"buy_guard": "\u606d\u559c{ships_viewer_name}\u6210\u4e3a\u5c0a\u8d35\u7684{ships_level}",
|
|
"guard_toast": "",
|
|
"super_chat": "{viewer_name}\u7684\u9192\u76ee\u7559\u8a00\uff1a{super_chat_content}",
|
|
},
|
|
"skip_free_gift": True,
|
|
}
|
|
|
|
ALL_EVENTS = [
|
|
"danmaku", "gift", "blind_box", "enter_room", "follow",
|
|
"interact_other", "buy_guard", "guard_toast", "super_chat",
|
|
]
|
|
|
|
_TEMPLATE_RE = re.compile(r"\{(\w+)\}")
|
|
|
|
|
|
def _base_dir() -> pathlib.Path:
|
|
if getattr(sys, "frozen", False):
|
|
return pathlib.Path(sys.executable).parent.parent.parent
|
|
return pathlib.Path(__file__).parent
|
|
|
|
|
|
def rules_path() -> pathlib.Path:
|
|
return _base_dir() / "event_rules.json"
|
|
|
|
|
|
def load_rules(path: str | None = None) -> dict:
|
|
"""Load rules from file; create default file if missing."""
|
|
p = pathlib.Path(path) if path else rules_path()
|
|
if not p.exists():
|
|
try:
|
|
p.write_text(json.dumps(DEFAULT_RULES, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
logger.info("Created default event_rules.json at %s", p)
|
|
except OSError:
|
|
logger.warning("Cannot write default rules to %s", p)
|
|
return json.loads(json.dumps(DEFAULT_RULES))
|
|
|
|
try:
|
|
data = json.loads(p.read_text(encoding="utf-8"))
|
|
except (json.JSONDecodeError, OSError) as e:
|
|
logger.error("Failed to load rules from %s: %s", p, e)
|
|
return json.loads(json.dumps(DEFAULT_RULES))
|
|
|
|
# merge with defaults to fill missing keys
|
|
merged = json.loads(json.dumps(DEFAULT_RULES))
|
|
for section in ("enabled", "templates"):
|
|
if isinstance(data.get(section), dict):
|
|
merged[section].update({k: v for k, v in data[section].items() if k in merged[section]})
|
|
if isinstance(data.get("skip_free_gift"), bool):
|
|
merged["skip_free_gift"] = data["skip_free_gift"]
|
|
return merged
|
|
|
|
|
|
def save_rules(rules: dict, path: str | None = None) -> None:
|
|
p = pathlib.Path(path) if path else rules_path()
|
|
p.write_text(json.dumps(rules, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
logger.info("Saved event rules to %s", p)
|
|
|
|
|
|
def render_template(template: str, **vars_dict) -> str:
|
|
"""Render a template with given variables. Missing variables become empty.
|
|
Unknown variables are kept as-is (so users can see typos in the UI log)."""
|
|
if not template:
|
|
return ""
|
|
|
|
def _replace(m: re.Match) -> str:
|
|
name = m.group(1)
|
|
return str(vars_dict.get(name, "")) if name in vars_dict else m.group(0)
|
|
|
|
return _TEMPLATE_RE.sub(_replace, template)
|
|
|
|
|
|
def is_enabled(rules: dict, event: str) -> bool:
|
|
return bool(rules.get("enabled", {}).get(event, False))
|
|
|
|
|
|
def get_template(rules: dict, event: str) -> str:
|
|
return str(rules.get("templates", {}).get(event, ""))
|
|
|
|
|
|
def event_text(rules: dict, event: str, **vars_dict) -> str:
|
|
"""Build the speech text for an event, or '' if disabled/empty template."""
|
|
if not is_enabled(rules, event):
|
|
return ""
|
|
text = render_template(get_template(rules, event), **vars_dict)
|
|
return text.strip()
|
|
|
|
|
|
# ── guard level mapping ─────────────────────────────────────────────────
|
|
GUARD_LEVEL_NAMES = {1: "\u603b\u7763", 2: "\u63d0\u7763", 3: "\u8230\u957f"}
|