diff --git a/src/vrc_log/log_actions.py b/src/vrc_log/log_actions.py index 2ea7047..7a00305 100644 --- a/src/vrc_log/log_actions.py +++ b/src/vrc_log/log_actions.py @@ -1,5 +1,6 @@ import os import re +import difflib import subprocess import tempfile import threading @@ -11,6 +12,7 @@ from pathlib import Path from common.config_loader import loadVrcLogConfig from common.config_loader import getSecretValue from common.runtime_log import appendRuntimeLog +from common.runtime_log import appendJoinLeaveLog from discord_control.actions import setDiscordMuted _monitor_thread = None @@ -20,6 +22,9 @@ _self_monitor_thread = None _self_monitor_stop_event = threading.Event() _last_self_state = None REALTIME_IDLE_SECONDS = 0.2 +_MONITOR_PREVIEW_WINDOW_SECONDS = 1.0 +_last_monitor_preview = "" +_last_monitor_preview_at = None JOIN_PATTERN = re.compile( r"OnPlayerJoined\s+(.+?)\s+\(usr_[0-9a-fA-F-]+\)" @@ -39,11 +44,6 @@ WORLD_NAME_PATTERN = re.compile( TIME_PATTERN = re.compile( r"(\d{4}\.\d{2}\.\d{2}\s+\d{2}:\d{2}:\d{2})" ) -RELEVANT_LOG_PATTERN = re.compile( - r"OnPlayerJoined|OnPlayerLeft|Entering Room|Joining or Creating Room|worldId=|wrld_" -) - - def log(level, message): now = datetime.now().strftime("%H:%M:%S") print(f"[{level}] {now} {message}", flush=True) @@ -282,6 +282,45 @@ def debug(message): log("DEBUG", message) +def buildDiffMessage(previous_text, current_text): + previous_lines = (previous_text or "").splitlines() + current_lines = (current_text or "").splitlines() + + if previous_text == current_text: + return "" + + diff_lines = list( + difflib.unified_diff( + previous_lines, + current_lines, + fromfile="before", + tofile="after", + lineterm="", + ) + ) + + if not diff_lines: + return "" + + return "\n".join(diff_lines) + + +def compileLogPatterns(patterns): + compiled = [] + for pattern in patterns or []: + try: + compiled.append(re.compile(pattern)) + except re.error as e: + log("ERROR", f"invalid vrc_log pattern={pattern} detail={e}") + return compiled + + +def matchesAnyPattern(text, compiled_patterns): + if not compiled_patterns: + return True + return any(pattern.search(text) for pattern in compiled_patterns) + + def formatJoinLeaveLine(line_time, action, name, state, guest_names): staff_count = len(state["staff_present_set"]) member_count = len(state["member_present_set"]) @@ -295,6 +334,7 @@ def parseVrchatEvents(text, config): staff_names = set(config["staff_names"]) self_name = config.get("self_name", "") world_name_map = extractWorldNameMap(text) + compiled_patterns = compileLogPatterns(config.get("log_patterns", [])) state = { "location": "", @@ -317,8 +357,14 @@ def parseVrchatEvents(text, config): if not line_time or line_time < start_time: continue + line_matches = matchesAnyPattern(raw_line, compiled_patterns) + + room_match = ENTERING_ROOM_PATTERN.search(raw_line) + world_name_match = WORLD_NAME_PATTERN.search(raw_line) world_match = WORLD_PATTERN.search(raw_line) - if world_match: + should_update_world = bool(room_match or world_name_match) + + if should_update_world and world_match: location = world_match.group(1).strip() world_id, instance_id = splitWorldLocation(location) @@ -331,18 +377,16 @@ def parseVrchatEvents(text, config): state["member_present_set"].clear() state["last_notice_key"] = None output_lines = [] - world_label = formatWorldLabel(world_id, world_name_map) - if world_label != "不明": - output_lines.append(f"ワールド入室: {world_label}") - debug(f"world changed world_id={world_id} label={world_label}") join_match = JOIN_PATTERN.search(raw_line) - if join_match and state["world_id"]: + if join_match and state["world_id"] and line_matches: name = join_match.group(1).strip() + join_leave_line = None if isSelf(name, self_name): self_state = "joined" state["member_present_set"].add(name) - output_lines.append(formatJoinLeaveLine(line_time, "join", name, state, guest_names)) + join_leave_line = formatJoinLeaveLine(line_time, "join", name, state, guest_names) + output_lines.append(join_leave_line) continue role = classifyUser(name, state["guest_name_set"], state["staff_names"]) @@ -350,23 +394,28 @@ def parseVrchatEvents(text, config): if role == "guest": state["guest_present_set"].add(name) state["member_present_set"].add(name) - output_lines.append(formatJoinLeaveLine(line_time, "join", name, state, guest_names)) + join_leave_line = formatJoinLeaveLine(line_time, "join", name, state, guest_names) + output_lines.append(join_leave_line) appendNoticeIfNeeded(output_lines, state) elif role == "staff": state["staff_present_set"].add(name) - output_lines.append(formatJoinLeaveLine(line_time, "join", name, state, guest_names)) + join_leave_line = formatJoinLeaveLine(line_time, "join", name, state, guest_names) + output_lines.append(join_leave_line) else: state["member_present_set"].add(name) - output_lines.append(formatJoinLeaveLine(line_time, "join", name, state, guest_names)) + join_leave_line = formatJoinLeaveLine(line_time, "join", name, state, guest_names) + output_lines.append(join_leave_line) continue left_match = LEFT_PATTERN.search(raw_line) - if left_match and state["world_id"]: + if left_match and state["world_id"] and line_matches: name = left_match.group(1).strip() + join_leave_line = None if isSelf(name, self_name): self_state = "left" state["member_present_set"].discard(name) - output_lines.append(formatJoinLeaveLine(line_time, "leave", name, state, guest_names)) + join_leave_line = formatJoinLeaveLine(line_time, "leave", name, state, guest_names) + output_lines.append(join_leave_line) continue role = classifyUser(name, state["guest_name_set"], state["staff_names"]) @@ -374,14 +423,18 @@ def parseVrchatEvents(text, config): if role == "guest": state["guest_present_set"].discard(name) state["member_present_set"].discard(name) - output_lines.append(formatJoinLeaveLine(line_time, "leave", name, state, guest_names)) + join_leave_line = formatJoinLeaveLine(line_time, "leave", name, state, guest_names) + output_lines.append(join_leave_line) appendNoticeIfNeeded(output_lines, state) elif role == "staff": state["staff_present_set"].discard(name) - output_lines.append(formatJoinLeaveLine(line_time, "leave", name, state, guest_names)) + join_leave_line = formatJoinLeaveLine(line_time, "leave", name, state, guest_names) + output_lines.append(join_leave_line) else: state["member_present_set"].discard(name) - output_lines.append(formatJoinLeaveLine(line_time, "leave", name, state, guest_names)) + join_leave_line = formatJoinLeaveLine(line_time, "leave", name, state, guest_names) + output_lines.append(join_leave_line) + continue return output_lines, self_state @@ -412,39 +465,35 @@ def grepVrchatLog(pattern=None): def collectVrchatLog(pattern=None, notify_changed=True): config = loadVrcLogConfig() latest_log = findLatestVrchatLog() - log("ACTION", f"parseVrchatLog file={latest_log}") - log("INFO", f'event_file={config["event_file"]}') text = readTextSafe(latest_log) lines, self_state = parseVrchatEvents(text, config) - has_join_leave = any("[join]" in line or "[leave]" in line for line in lines) - if has_join_leave: - lines = ["[DEBUG] 入退出イベント検出"] + lines - - output_text = "\n".join(lines) if lines else "[INFO] 対象イベントなし" - output_path = appendRuntimeLog("VRC LOG", output_text) + output_path = None + if lines: + output_text = "\n".join(lines) + output_path = appendRuntimeLog("VRC LOG", output_text) + join_leave_lines = [line for line in lines if "[join]" in line or "[leave]" in line] + if join_leave_lines: + appendJoinLeaveLog("\n".join(join_leave_lines)) if notify_changed: global _last_sent_output - should_send = output_text != _last_sent_output or has_join_leave - if should_send: + diff_text = buildDiffMessage(_last_sent_output, output_text) + if diff_text: try: - ok = sendWebhook(output_text) + ok = sendWebhook(diff_text) if ok: - log("INFO", "Discord webhook sent") + log("INFO", "Discord webhook diff sent") _last_sent_output = output_text else: - log("ERROR", "Discord webhook send failed") + log("ERROR", "Discord webhook diff send failed") except Exception as e: - log("ERROR", f"Discord webhook failed detail={e}") + log("ERROR", f"Discord webhook diff failed detail={e}") - log("INFO", f"result appended={output_path}") if lines: for line in lines: print(line, flush=True) - else: - debug("対象イベントなし") return output_path @@ -499,9 +548,9 @@ def _watchVrchatLogChanges(stop_event, label, on_relevant_change): def _monitor_loop(): def on_relevant_change(text): - preview = " ".join(text.splitlines()[:3]).strip() - if preview: - debug(f"VRC log monitor change preview={preview[:180]}") + if not any(token in text for token in ("OnPlayerJoined", "OnPlayerLeft", "Entering Room", "Joining or Creating Room", "worldId=", "wrld_")): + return + collectVrchatLog(notify_changed=True) _watchVrchatLogChanges(