Compare commits
2 Commits
25da1593d1
...
d8bf7239f6
Author | SHA1 | Date | |
---|---|---|---|
d8bf7239f6 | |||
eabd2f0f78 |
@ -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):
|
||||
|
@ -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}")
|
||||
|
6
main.py
6
main.py
@ -1,8 +1,14 @@
|
||||
from ui import DeathLogGUI
|
||||
import tkinter as tk
|
||||
from contextlib import suppress
|
||||
|
||||
|
||||
def main():
|
||||
with suppress(Exception):
|
||||
from ctypes import windll
|
||||
|
||||
windll.shcore.SetProcessDpiAwareness(1)
|
||||
|
||||
root = tk.Tk()
|
||||
app = DeathLogGUI(root)
|
||||
root.mainloop()
|
||||
|
89
monitor.py
89
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()
|
||||
|
94
ui.py
94
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")
|
||||
|
Loading…
x
Reference in New Issue
Block a user