Restrict VRC log output to user events
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import difflib
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
import threading
|
import threading
|
||||||
@@ -11,6 +12,7 @@ from pathlib import Path
|
|||||||
from common.config_loader import loadVrcLogConfig
|
from common.config_loader import loadVrcLogConfig
|
||||||
from common.config_loader import getSecretValue
|
from common.config_loader import getSecretValue
|
||||||
from common.runtime_log import appendRuntimeLog
|
from common.runtime_log import appendRuntimeLog
|
||||||
|
from common.runtime_log import appendJoinLeaveLog
|
||||||
from discord_control.actions import setDiscordMuted
|
from discord_control.actions import setDiscordMuted
|
||||||
|
|
||||||
_monitor_thread = None
|
_monitor_thread = None
|
||||||
@@ -20,6 +22,9 @@ _self_monitor_thread = None
|
|||||||
_self_monitor_stop_event = threading.Event()
|
_self_monitor_stop_event = threading.Event()
|
||||||
_last_self_state = None
|
_last_self_state = None
|
||||||
REALTIME_IDLE_SECONDS = 0.2
|
REALTIME_IDLE_SECONDS = 0.2
|
||||||
|
_MONITOR_PREVIEW_WINDOW_SECONDS = 1.0
|
||||||
|
_last_monitor_preview = ""
|
||||||
|
_last_monitor_preview_at = None
|
||||||
|
|
||||||
JOIN_PATTERN = re.compile(
|
JOIN_PATTERN = re.compile(
|
||||||
r"OnPlayerJoined\s+(.+?)\s+\(usr_[0-9a-fA-F-]+\)"
|
r"OnPlayerJoined\s+(.+?)\s+\(usr_[0-9a-fA-F-]+\)"
|
||||||
@@ -39,11 +44,6 @@ WORLD_NAME_PATTERN = re.compile(
|
|||||||
TIME_PATTERN = re.compile(
|
TIME_PATTERN = re.compile(
|
||||||
r"(\d{4}\.\d{2}\.\d{2}\s+\d{2}:\d{2}:\d{2})"
|
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):
|
def log(level, message):
|
||||||
now = datetime.now().strftime("%H:%M:%S")
|
now = datetime.now().strftime("%H:%M:%S")
|
||||||
print(f"[{level}] {now} {message}", flush=True)
|
print(f"[{level}] {now} {message}", flush=True)
|
||||||
@@ -282,6 +282,45 @@ def debug(message):
|
|||||||
log("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):
|
def formatJoinLeaveLine(line_time, action, name, state, guest_names):
|
||||||
staff_count = len(state["staff_present_set"])
|
staff_count = len(state["staff_present_set"])
|
||||||
member_count = len(state["member_present_set"])
|
member_count = len(state["member_present_set"])
|
||||||
@@ -295,6 +334,7 @@ def parseVrchatEvents(text, config):
|
|||||||
staff_names = set(config["staff_names"])
|
staff_names = set(config["staff_names"])
|
||||||
self_name = config.get("self_name", "")
|
self_name = config.get("self_name", "")
|
||||||
world_name_map = extractWorldNameMap(text)
|
world_name_map = extractWorldNameMap(text)
|
||||||
|
compiled_patterns = compileLogPatterns(config.get("log_patterns", []))
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
"location": "",
|
"location": "",
|
||||||
@@ -317,8 +357,14 @@ def parseVrchatEvents(text, config):
|
|||||||
if not line_time or line_time < start_time:
|
if not line_time or line_time < start_time:
|
||||||
continue
|
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)
|
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()
|
location = world_match.group(1).strip()
|
||||||
world_id, instance_id = splitWorldLocation(location)
|
world_id, instance_id = splitWorldLocation(location)
|
||||||
|
|
||||||
@@ -331,18 +377,16 @@ def parseVrchatEvents(text, config):
|
|||||||
state["member_present_set"].clear()
|
state["member_present_set"].clear()
|
||||||
state["last_notice_key"] = None
|
state["last_notice_key"] = None
|
||||||
output_lines = []
|
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)
|
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()
|
name = join_match.group(1).strip()
|
||||||
|
join_leave_line = None
|
||||||
if isSelf(name, self_name):
|
if isSelf(name, self_name):
|
||||||
self_state = "joined"
|
self_state = "joined"
|
||||||
state["member_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)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
role = classifyUser(name, state["guest_name_set"], state["staff_names"])
|
role = classifyUser(name, state["guest_name_set"], state["staff_names"])
|
||||||
@@ -350,23 +394,28 @@ def parseVrchatEvents(text, config):
|
|||||||
if role == "guest":
|
if role == "guest":
|
||||||
state["guest_present_set"].add(name)
|
state["guest_present_set"].add(name)
|
||||||
state["member_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)
|
appendNoticeIfNeeded(output_lines, state)
|
||||||
elif role == "staff":
|
elif role == "staff":
|
||||||
state["staff_present_set"].add(name)
|
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:
|
else:
|
||||||
state["member_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)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
left_match = LEFT_PATTERN.search(raw_line)
|
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()
|
name = left_match.group(1).strip()
|
||||||
|
join_leave_line = None
|
||||||
if isSelf(name, self_name):
|
if isSelf(name, self_name):
|
||||||
self_state = "left"
|
self_state = "left"
|
||||||
state["member_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)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
role = classifyUser(name, state["guest_name_set"], state["staff_names"])
|
role = classifyUser(name, state["guest_name_set"], state["staff_names"])
|
||||||
@@ -374,14 +423,18 @@ def parseVrchatEvents(text, config):
|
|||||||
if role == "guest":
|
if role == "guest":
|
||||||
state["guest_present_set"].discard(name)
|
state["guest_present_set"].discard(name)
|
||||||
state["member_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)
|
appendNoticeIfNeeded(output_lines, state)
|
||||||
elif role == "staff":
|
elif role == "staff":
|
||||||
state["staff_present_set"].discard(name)
|
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:
|
else:
|
||||||
state["member_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)
|
||||||
|
continue
|
||||||
|
|
||||||
return output_lines, self_state
|
return output_lines, self_state
|
||||||
|
|
||||||
@@ -412,39 +465,35 @@ def grepVrchatLog(pattern=None):
|
|||||||
def collectVrchatLog(pattern=None, notify_changed=True):
|
def collectVrchatLog(pattern=None, notify_changed=True):
|
||||||
config = loadVrcLogConfig()
|
config = loadVrcLogConfig()
|
||||||
latest_log = findLatestVrchatLog()
|
latest_log = findLatestVrchatLog()
|
||||||
log("ACTION", f"parseVrchatLog file={latest_log}")
|
|
||||||
log("INFO", f'event_file={config["event_file"]}')
|
|
||||||
|
|
||||||
text = readTextSafe(latest_log)
|
text = readTextSafe(latest_log)
|
||||||
lines, self_state = parseVrchatEvents(text, config)
|
lines, self_state = parseVrchatEvents(text, config)
|
||||||
|
|
||||||
has_join_leave = any("[join]" in line or "[leave]" in line for line in lines)
|
output_path = None
|
||||||
if has_join_leave:
|
if lines:
|
||||||
lines = ["[DEBUG] 入退出イベント検出"] + lines
|
output_text = "\n".join(lines)
|
||||||
|
|
||||||
output_text = "\n".join(lines) if lines else "[INFO] 対象イベントなし"
|
|
||||||
output_path = appendRuntimeLog("VRC LOG", output_text)
|
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:
|
if notify_changed:
|
||||||
global _last_sent_output
|
global _last_sent_output
|
||||||
should_send = output_text != _last_sent_output or has_join_leave
|
diff_text = buildDiffMessage(_last_sent_output, output_text)
|
||||||
if should_send:
|
if diff_text:
|
||||||
try:
|
try:
|
||||||
ok = sendWebhook(output_text)
|
ok = sendWebhook(diff_text)
|
||||||
if ok:
|
if ok:
|
||||||
log("INFO", "Discord webhook sent")
|
log("INFO", "Discord webhook diff sent")
|
||||||
_last_sent_output = output_text
|
_last_sent_output = output_text
|
||||||
else:
|
else:
|
||||||
log("ERROR", "Discord webhook send failed")
|
log("ERROR", "Discord webhook diff send failed")
|
||||||
except Exception as e:
|
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:
|
if lines:
|
||||||
for line in lines:
|
for line in lines:
|
||||||
print(line, flush=True)
|
print(line, flush=True)
|
||||||
else:
|
|
||||||
debug("対象イベントなし")
|
|
||||||
|
|
||||||
return output_path
|
return output_path
|
||||||
|
|
||||||
@@ -499,9 +548,9 @@ def _watchVrchatLogChanges(stop_event, label, on_relevant_change):
|
|||||||
|
|
||||||
def _monitor_loop():
|
def _monitor_loop():
|
||||||
def on_relevant_change(text):
|
def on_relevant_change(text):
|
||||||
preview = " ".join(text.splitlines()[:3]).strip()
|
if not any(token in text for token in ("OnPlayerJoined", "OnPlayerLeft", "Entering Room", "Joining or Creating Room", "worldId=", "wrld_")):
|
||||||
if preview:
|
return
|
||||||
debug(f"VRC log monitor change preview={preview[:180]}")
|
|
||||||
collectVrchatLog(notify_changed=True)
|
collectVrchatLog(notify_changed=True)
|
||||||
|
|
||||||
_watchVrchatLogChanges(
|
_watchVrchatLogChanges(
|
||||||
|
|||||||
Reference in New Issue
Block a user