From eabd2f0f7848b4d2a7afc47fb5c70c0c1167dfa7 Mon Sep 17 00:00:00 2001 From: norohind <60548839+norohind@users.noreply.github.com> Date: Sun, 25 May 2025 17:51:29 +0000 Subject: [PATCH] Implement filtering --- config.py | 2 - discord_subscriber.py | 19 ++++++--- monitor.py | 89 ++++++++++++++++++++-------------------- ui.py | 94 ++++++++++++++++++++++--------------------- 4 files changed, 104 insertions(+), 100 deletions(-) diff --git a/config.py b/config.py index 3933335..09aaaa9 100644 --- a/config.py +++ b/config.py @@ -5,8 +5,6 @@ import sqlite3 APP_NAME = "SC KF" -DEBUG = os.getenv("DEBUG") is not None - class PersistentConfig: def __init__(self, db_path: Path): diff --git a/discord_subscriber.py b/discord_subscriber.py index c8feb5c..626af9c 100644 --- a/discord_subscriber.py +++ b/discord_subscriber.py @@ -1,10 +1,12 @@ import requests from monitor import Event, SubscriberABC from loguru import logger +from monitor import Filter class DiscordSubscriber(SubscriberABC): - def __init__(self): + def __init__(self, event_filter: Filter): + super().__init__(event_filter) self.webhook_url: str | None = None self.enabled: bool = True @@ -18,10 +20,15 @@ class DiscordSubscriber(SubscriberABC): if not self.enabled: return - try: - requests.post( - self.webhook_url, json={"content": event.to_str(include_ts=False)} - ) + if not self.filter.filter_event(event): + return - except Exception as e: + try: + r = requests.post( + self.webhook_url, + json={"content": event.to_str(include_ts=False, rich_text=True)}, + ) + r.raise_for_status() + + except Exception: logger.opt(exception=True).warning(f"Error sending to Discord: {event}") diff --git a/monitor.py b/monitor.py index fab0717..00f2954 100644 --- a/monitor.py +++ b/monitor.py @@ -4,8 +4,6 @@ from pathlib import Path import time from dataclasses import dataclass from typing import Generator, Self - -import config from loguru import logger from abc import ABC, abstractmethod @@ -40,16 +38,10 @@ class Line: class Event: timestamp: datetime.datetime | None - def to_str(self, include_ts: bool = False) -> str: + def to_str(self, include_ts: bool = False, rich_text: bool = False) -> str: return str(self) -class SubscriberABC(ABC): - @abstractmethod - def consume_event(self, event: Event) -> None: - raise NotImplementedError - - @dataclass(frozen=True) class Kill(Event): killer: str @@ -71,13 +63,51 @@ class Kill(Event): damage_type=splited[21], ) - def to_str(self, include_ts: bool = False) -> str: + def to_str(self, include_ts: bool = False, rich_text: bool = True) -> str: ts = (self.timestamp.strftime("%Y-%m-%d %H:%M:%S") + ": ") * include_ts if self.victim == self.killer: - return f"{ts}**{self.victim}** РКН 🤦" + return f"{ts}{'**' * rich_text}{self.victim}{'**' * rich_text} РКН{' 🤦' * rich_text}" - emoji = damage_type_to_emoji.get(self.damage_type, "") - return f"{ts}**{self.victim}** killed by 🔫 **{self.killer}** {emoji}" + emoji = " " + damage_type_to_emoji.get(self.damage_type, "") + return f"{ts}{'**' * rich_text}{self.victim}{'**' * rich_text} killed by {'🔫 ' * rich_text}{'**' * rich_text}{self.killer}{'**' * rich_text}{emoji * rich_text}" + + +@dataclass +class Filter: + only_personal_feed: bool + include_npc: bool + nickname: str | None = None + + def filter_event(self, event: Event) -> bool: + if isinstance(event, Kill): + if "_NPC_" in event.killer + event.victim and not self.include_npc: + return False + + if all( + [ + self.nickname not in (event.killer, event.victim), + self.only_personal_feed is True, + self.nickname is not None, + ] + ): + return False + + return True + + else: + logger.warning( + f"Don't know how to filter event {type(event)}, letting through" + ) + return True + + +class SubscriberABC(ABC): + def __init__(self, event_filter: Filter): + self.filter = event_filter + + @abstractmethod + def consume_event(self, event: Event) -> None: + raise NotImplementedError @dataclass @@ -126,16 +156,6 @@ class Monitor: and process_important_realtime_events ): kill = Kill.from_line(line.line) - - # if "_NPC_" in kill.killer + kill.victim and not self.include_npc: # TODO: - # continue - # - # if ( - # self.nickname not in (kill.killer, kill.victim) - # and self.only_personal_feed is True - # ): - # continue - self.call_subscribers(kill) def monitor_log_file(self) -> Generator[Line, None, None]: @@ -152,31 +172,8 @@ class Monitor: is_realtime = True time.sleep(0.15) file.seek(current_position) - if config.DEBUG is True: - return line = line.removesuffix("\n") yield Line(line, is_realtime) logger.info("Exiting reading loop") - - -# def main(): -# try: -# if config.DEBUG is True: -# app = Monitor( -# log_file_path=Path("./Game.log"), -# do_backread=True, -# ) -# -# else: -# app = Monitor(config.LOG_FILE_PATH, config.DISCORD_WEBHOOK_URL) -# -# app.process_lines() -# -# except KeyboardInterrupt: -# print("\nMonitoring stopped") -# -# -# if __name__ == "__main__": -# main() diff --git a/ui.py b/ui.py index c79754c..c411fd5 100644 --- a/ui.py +++ b/ui.py @@ -1,11 +1,9 @@ +from contextlib import suppress from functools import partial from collections import deque import pystray from pystray import Menu, MenuItem from PIL import Image, ImageDraw -import config -import threading -from loguru import logger import sv_ttk from pathlib import Path import threading @@ -13,14 +11,17 @@ import tkinter as tk from tkinter import ttk, filedialog, scrolledtext import config from discord_subscriber import DiscordSubscriber -from monitor import Monitor, Event, SubscriberABC +from monitor import Monitor, Event, SubscriberABC, Filter from loguru import logger from typing import Callable class Feed(SubscriberABC): - def __init__(self, callback_data_changed: Callable | None = None): - self.ring_buffer: deque[Event] = deque(maxlen=150) + def __init__( + self, event_filter: Filter, callback_data_changed: Callable | None = None + ): + super().__init__(event_filter) + self.ring_buffer: deque[Event] = deque(maxlen=512) self.ring_buffer_lock = threading.Lock() self.callback = callback_data_changed @@ -38,8 +39,9 @@ class Feed(SubscriberABC): def render(self) -> str: with self.ring_buffer_lock: return "\n".join( - event.to_str(include_ts=self.include_ts.get()) + event.to_str(include_ts=self.include_ts.get(), rich_text=False) for event in self.ring_buffer + if self.filter.filter_event(event) ) def clear(self) -> None: @@ -68,9 +70,10 @@ class DeathLogGUI: def __init__(self, root: tk.Tk): self.root = root self.root.title(config.APP_NAME) - self.root.geometry("850x600") + self.root.geometry(config.db.get_str("GEOMETRY", "850x600")) self.root.minsize(850, 600) self.root.protocol("WM_DELETE_WINDOW", self.handle_close_window) + theme_mapping = {True: "dark", False: "light"} sv_ttk.set_theme(theme_mapping.get(config.db.get_bool("IS_DARK_MODE", False))) @@ -79,7 +82,23 @@ class DeathLogGUI: ) self.do_backread = config.db.get_tk_boolean("DO_BACKREAD", False) + self.include_npc_kills = config.db.get_tk_boolean("INCLUDE_NPC_KILLS", True) + self.personal_feed = config.db.get_tk_boolean("PERSONAL_FEED", False) + + self.filter = Filter( + only_personal_feed=self.personal_feed.get(), + include_npc=self.include_npc_kills.get(), + ) + + def filter_changed(a, b, c) -> None: + self.filter.include_npc = self.include_npc_kills.get() + self.filter.only_personal_feed = self.personal_feed.get() + logger.debug("Filter props changed") + self.feed.callback() + + self.include_npc_kills.trace_add("write", filter_changed) + self.personal_feed.trace_add("write", filter_changed) self.is_dark_mode = config.db.get_tk_boolean("IS_DARK_MODE", False) self.is_dark_mode.trace_add( @@ -90,13 +109,14 @@ class DeathLogGUI: ) self.feed = Feed( - callback_data_changed=partial(self.root.after, 0, self.update_feed_display) + callback_data_changed=partial(self.root.after, 0, self.update_feed_display), + event_filter=self.filter, ) self.enable_discord = config.db.get_tk_boolean("ENABLE_DISCORD", False) self.webhook_url = config.db.get_tk_str( "DISCORD_WEBHOOK_URL", "https://discord.com/..." ) - self.discord = DiscordSubscriber() + self.discord = DiscordSubscriber(event_filter=self.filter) self.discord.enabled = self.enable_discord.get() self.discord.webhook_url = self.webhook_url.get() @@ -105,7 +125,7 @@ class DeathLogGUI: ) self.enable_discord.trace_add( - "read", lambda a, b, c: self.discord.set_enabled(self.enable_discord.get()) + "write", lambda a, b, c: self.discord.set_enabled(self.enable_discord.get()) ) self.monitor: Monitor | None = None @@ -132,7 +152,10 @@ class DeathLogGUI: def shutdown(self): logger.info("Beginning GUI shutdown") self.stop_monitoring() + logger.info(self.root.winfo_geometry()) + config.db.set_str("GEOMETRY", self.root.winfo_geometry()) self.tray.stop() + config.db.db.close() self.root.quit() def handle_close_window(self) -> None: @@ -181,22 +204,29 @@ class DeathLogGUI: text="Do send events to discord", variable=self.enable_discord, ).grid(row=3, column=0, columnspan=2, sticky=tk.W, pady=2) + + ttk.Checkbutton( + config_frame, + text="Only personal events", + variable=self.personal_feed, + ).grid(row=4, column=0, columnspan=2, sticky=tk.W, pady=2) + ttk.Checkbutton( config_frame, text="Hide NPC kills (_NPC_ in name)", variable=self.include_npc_kills, - ).grid(row=4, column=0, columnspan=2, sticky=tk.W, pady=2) + ).grid(row=5, column=0, columnspan=2, sticky=tk.W, pady=2) ttk.Checkbutton( config_frame, text="Include timestamps", variable=self.feed.include_ts, - ).grid(row=5, column=0, sticky=tk.W, pady=2) + ).grid(row=6, column=0, sticky=tk.W, pady=2) ttk.Checkbutton( config_frame, text="Enable dark mode", variable=self.is_dark_mode, - ).grid(row=6, column=0, sticky=tk.W, pady=2) + ).grid(row=7, column=0, sticky=tk.W, pady=2) # Control buttons control_frame = ttk.Frame(main_frame) @@ -252,7 +282,7 @@ class DeathLogGUI: self.monitor = Monitor( log_file_path=Path(self.log_path.get()), do_backread=self.do_backread.get(), - subscribers={self.feed}, + subscribers={self.feed, self.discord}, ) self.monitor_thread = threading.Thread( @@ -282,39 +312,11 @@ class DeathLogGUI: self.stop_button.config(state=tk.DISABLED) def update_feed_display(self): + with suppress(AttributeError): + self.filter.nickname = self.monitor.nickname + self.feed_display.config(state=tk.NORMAL) self.feed_display.delete("1.0", tk.END) self.feed_display.insert("1.0", self.feed.render()) self.feed_display.config(state=tk.DISABLED) self.feed_display.see(tk.END) - - # try: - # # Check if we should run in GUI mode or CLI mode - # import sys - # - # # CLI mode - # if config.DEBUG is True: - # app = App( - # Path("./Game.log"), - # DEFAULT_DISCORD_WEBHOOK_URL, - # do_mock_discord=True, - # do_backread=True, - # hide_npc_kills=False, - # ) - # - # else: - # app = App( - # DEFAULT_LOG_FILE_PATH, - # DEFAULT_DISCORD_WEBHOOK_URL, - # hide_npc_kills=False, - # ) - # app.process_lines() - # - # else: - # # GUI mode - # root = tk.Tk() - # app = DeathLogGUI(root) - # root.mainloop() - # - # except KeyboardInterrupt: - # print("\nMonitoring stopped")