v1
This commit is contained in:
691
main.py
Normal file
691
main.py
Normal file
@@ -0,0 +1,691 @@
|
||||
# v6
|
||||
|
||||
from __future__ import annotations
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from logging.handlers import RotatingFileHandler
|
||||
import traceback
|
||||
|
||||
from theme import (
|
||||
apply_dark_palette,
|
||||
apply_stylesheet,
|
||||
)
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QIcon
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication,
|
||||
QLabel,
|
||||
QMainWindow,
|
||||
QMessageBox,
|
||||
QStatusBar,
|
||||
|
||||
)
|
||||
# whats needed
|
||||
from browser import BrowserView
|
||||
from config import (
|
||||
APP_NAME,
|
||||
APP_VERSION,
|
||||
DATABASE_PATH,
|
||||
LOG_LEVEL,
|
||||
LOGS_DIR,
|
||||
START_URL,
|
||||
WINDOW_HEIGHT,
|
||||
WINDOW_WIDTH,
|
||||
ensure_directories,
|
||||
load_settings,
|
||||
save_settings,
|
||||
)
|
||||
from db import Database
|
||||
|
||||
from managers import (
|
||||
AddonManagerWindow,
|
||||
NetworkInspector,
|
||||
PluginManagerWindow,
|
||||
ProfileManager,
|
||||
SettingsWindow,
|
||||
TrayManager,
|
||||
ContextManager,
|
||||
)
|
||||
|
||||
# v2
|
||||
os.environ.setdefault(
|
||||
"QTWEBENGINE_CHROMIUM_FLAGS",
|
||||
(
|
||||
"--disable-features="
|
||||
"Translate,"
|
||||
"MediaRouter "
|
||||
"--enable-features="
|
||||
"DnsOverHttps "
|
||||
"--dns-over-https-mode=secure "
|
||||
"--dns-over-https-templates="
|
||||
"https://dns.quad9.net/dns-query "
|
||||
"--disable-background-networking "
|
||||
"--disable-component-update "
|
||||
"--disable-domain-reliability "
|
||||
"--disable-breakpad "
|
||||
"--no-pings "
|
||||
"--enable-gpu-rasterization "
|
||||
"--enable-zero-copy "
|
||||
"--disable-sync "
|
||||
"--disable-translate "
|
||||
"--disable-features=AutofillServerCommunication "
|
||||
"--enable-native-gpu-memory-buffers "
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
LOG_FILE = LOGS_DIR / "client.log"
|
||||
|
||||
|
||||
def configure_logging() -> None:
|
||||
LOG_FILE.parent.mkdir(
|
||||
parents=True,
|
||||
exist_ok=True,
|
||||
)
|
||||
|
||||
root = logging.getLogger()
|
||||
|
||||
root.setLevel(
|
||||
LOG_LEVEL
|
||||
)
|
||||
|
||||
root.handlers.clear()
|
||||
|
||||
formatter = logging.Formatter(
|
||||
"%(asctime)s %(levelname)s %(name)s: %(message)s"
|
||||
)
|
||||
|
||||
file_handler = (
|
||||
RotatingFileHandler(
|
||||
LOG_FILE,
|
||||
maxBytes=10 * 1024 * 1024,
|
||||
backupCount=5,
|
||||
encoding="utf-8",
|
||||
)
|
||||
)
|
||||
|
||||
file_handler.setFormatter(
|
||||
formatter
|
||||
)
|
||||
|
||||
console_handler = (
|
||||
logging.StreamHandler()
|
||||
)
|
||||
|
||||
console_handler.setFormatter(
|
||||
formatter
|
||||
)
|
||||
|
||||
root.addHandler(
|
||||
file_handler
|
||||
)
|
||||
|
||||
root.addHandler(
|
||||
console_handler
|
||||
)
|
||||
|
||||
|
||||
configure_logging()
|
||||
|
||||
log = logging.getLogger(
|
||||
__name__
|
||||
)
|
||||
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
def __init__(
|
||||
self,
|
||||
db: Database,
|
||||
settings: dict,
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
self.db = db
|
||||
self.settings_data = settings
|
||||
|
||||
self.contexts = ContextManager(
|
||||
self.db,
|
||||
self.settings_data,
|
||||
)
|
||||
|
||||
self.plugins_window = None
|
||||
self.settings_window = None
|
||||
self.profile_window = None
|
||||
self.addons_window = None
|
||||
self.network_window = None
|
||||
self.tray_manager = None
|
||||
|
||||
self.setWindowTitle(
|
||||
f"{APP_NAME}---{APP_VERSION}"
|
||||
)
|
||||
|
||||
self.resize(
|
||||
max(
|
||||
800,
|
||||
int(
|
||||
settings.get(
|
||||
"window_width",
|
||||
WINDOW_WIDTH,
|
||||
)
|
||||
),
|
||||
),
|
||||
max(
|
||||
600,
|
||||
int(
|
||||
settings.get(
|
||||
"window_height",
|
||||
WINDOW_HEIGHT,
|
||||
)
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
self.browser = BrowserView(
|
||||
db=self.db,
|
||||
context=self.context,
|
||||
settings=self.settings_data,
|
||||
parent=self,
|
||||
)
|
||||
|
||||
self.setCentralWidget(
|
||||
self.browser
|
||||
)
|
||||
|
||||
self._build_status_bar()
|
||||
self._connect_signals()
|
||||
self._build_tray()
|
||||
|
||||
self.browser.open(
|
||||
START_URL
|
||||
)
|
||||
|
||||
@property
|
||||
def context(self):
|
||||
return self.contexts.current()
|
||||
|
||||
def _build_status_bar(
|
||||
self,
|
||||
) -> None:
|
||||
self.status_bar = (
|
||||
QStatusBar(
|
||||
self
|
||||
)
|
||||
)
|
||||
|
||||
self.setStatusBar(
|
||||
self.status_bar
|
||||
)
|
||||
|
||||
self.download_label = QLabel(
|
||||
"Ready",
|
||||
self,
|
||||
)
|
||||
|
||||
self.status_bar.addPermanentWidget(
|
||||
self.download_label
|
||||
)
|
||||
self.status_bar.addPermanentWidget(
|
||||
QLabel(
|
||||
f"Blocked: "
|
||||
f"{self.browser.network.blocklist_engine.count():,}"
|
||||
)
|
||||
)
|
||||
|
||||
def _connect_signals(
|
||||
self,
|
||||
) -> None:
|
||||
self.browser.downloadProgress.connect(
|
||||
self._download_progress
|
||||
)
|
||||
|
||||
self.browser.page_.consoleMessage.connect(
|
||||
self._console_message
|
||||
)
|
||||
|
||||
self.browser.bridge.messageReceived.connect(
|
||||
self._bridge_message
|
||||
)
|
||||
|
||||
def _build_tray(
|
||||
self,
|
||||
) -> None:
|
||||
tray_cfg = self.settings_data.get(
|
||||
"tray",
|
||||
{},
|
||||
)
|
||||
|
||||
if not tray_cfg.get(
|
||||
"enabled",
|
||||
True,
|
||||
):
|
||||
return
|
||||
|
||||
self.tray_manager = (
|
||||
TrayManager(
|
||||
self
|
||||
)
|
||||
)
|
||||
|
||||
self.tray_manager.open_action.triggered.connect(
|
||||
self._show_window
|
||||
)
|
||||
|
||||
self.tray_manager.tray.show()
|
||||
|
||||
def _show_window(
|
||||
self,
|
||||
) -> None:
|
||||
self.showNormal()
|
||||
self.raise_()
|
||||
self.activateWindow()
|
||||
|
||||
def _console_message(
|
||||
self,
|
||||
message: str,
|
||||
) -> None:
|
||||
ignored = {
|
||||
"setMemoryInformation not available",
|
||||
"[ProcessUtilsElectron]",
|
||||
}
|
||||
|
||||
if any(
|
||||
token in message
|
||||
for token in ignored
|
||||
):
|
||||
return
|
||||
|
||||
log.info(
|
||||
"console=%s",
|
||||
message,
|
||||
)
|
||||
|
||||
def _bridge_message(
|
||||
self,
|
||||
message: str,
|
||||
) -> None:
|
||||
log.info(
|
||||
"bridge=%s",
|
||||
message,
|
||||
)
|
||||
|
||||
def _download_progress(
|
||||
self,
|
||||
filename: str,
|
||||
received: int,
|
||||
total: int,
|
||||
) -> None:
|
||||
if total <= 0:
|
||||
self.download_label.setText(
|
||||
f"{filename}: {received:,} B"
|
||||
)
|
||||
return
|
||||
|
||||
percent = int(
|
||||
(
|
||||
received
|
||||
/ max(
|
||||
total,
|
||||
1,
|
||||
)
|
||||
)
|
||||
* 100
|
||||
)
|
||||
|
||||
self.download_label.setText(
|
||||
(
|
||||
f"{filename}: "
|
||||
f"{percent}% "
|
||||
f"({received:,}/{total:,})"
|
||||
)
|
||||
)
|
||||
|
||||
def open_settings(
|
||||
self,
|
||||
) -> None:
|
||||
window = SettingsWindow(
|
||||
self.settings_data,
|
||||
self,
|
||||
)
|
||||
|
||||
window.settingsChanged.connect(
|
||||
self._settings_changed
|
||||
)
|
||||
|
||||
self.settings_window = window
|
||||
|
||||
window.exec()
|
||||
|
||||
def open_plugins(
|
||||
self,
|
||||
) -> None:
|
||||
self.plugins_window = (
|
||||
PluginManagerWindow(
|
||||
self.browser,
|
||||
self.db,
|
||||
self.context.name,
|
||||
self,
|
||||
)
|
||||
)
|
||||
|
||||
self.plugins_window.show()
|
||||
|
||||
def open_profiles(
|
||||
self,
|
||||
) -> None:
|
||||
window = ProfileManager(
|
||||
self.db.list_profiles(),
|
||||
self.settings_data.get(
|
||||
"active_profile",
|
||||
"default",
|
||||
),
|
||||
self,
|
||||
)
|
||||
|
||||
window.profileSelected.connect(
|
||||
self._profile_selected
|
||||
)
|
||||
|
||||
self.profile_window = window
|
||||
|
||||
window.exec()
|
||||
|
||||
def open_addons(
|
||||
self,
|
||||
) -> None:
|
||||
self.addons_window = (
|
||||
AddonManagerWindow(
|
||||
self.context,
|
||||
self,
|
||||
)
|
||||
)
|
||||
|
||||
self.addons_window.show()
|
||||
|
||||
def open_network(
|
||||
self,
|
||||
) -> None:
|
||||
self.network_window = (
|
||||
NetworkInspector(
|
||||
self.browser.network,
|
||||
self,
|
||||
)
|
||||
)
|
||||
|
||||
self.network_window.show()
|
||||
|
||||
def open_devtools(
|
||||
self,
|
||||
) -> None:
|
||||
self.browser.open_devtools()
|
||||
|
||||
def _settings_changed(
|
||||
self,
|
||||
) -> None:
|
||||
save_settings(
|
||||
self.settings_data
|
||||
)
|
||||
|
||||
def _profile_selected(
|
||||
self,
|
||||
profile_name: str,
|
||||
) -> None:
|
||||
current = (
|
||||
self.settings_data.get(
|
||||
"active_profile"
|
||||
)
|
||||
)
|
||||
|
||||
if profile_name == current:
|
||||
return
|
||||
|
||||
self.settings_data[
|
||||
"active_profile"
|
||||
] = profile_name
|
||||
|
||||
save_settings(
|
||||
self.settings_data
|
||||
)
|
||||
|
||||
QMessageBox.information(
|
||||
self,
|
||||
APP_NAME,
|
||||
(
|
||||
"Profile updated.\n\n"
|
||||
"Restart required."
|
||||
),
|
||||
)
|
||||
|
||||
def _persist_window_state(
|
||||
self,
|
||||
) -> None:
|
||||
size = self.size()
|
||||
|
||||
self.settings_data[
|
||||
"window_width"
|
||||
] = size.width()
|
||||
|
||||
self.settings_data[
|
||||
"window_height"
|
||||
] = size.height()
|
||||
|
||||
save_settings(
|
||||
self.settings_data
|
||||
)
|
||||
|
||||
def _shutdown(
|
||||
self,
|
||||
) -> None:
|
||||
try:
|
||||
self.browser.shutdown()
|
||||
except Exception:
|
||||
log.exception(
|
||||
"browser shutdown failed"
|
||||
)
|
||||
|
||||
try:
|
||||
self.db.shutdown()
|
||||
except Exception:
|
||||
log.exception(
|
||||
"database shutdown failed"
|
||||
)
|
||||
|
||||
# v2
|
||||
def closeEvent(
|
||||
self,
|
||||
event,
|
||||
):
|
||||
if getattr(
|
||||
self,
|
||||
"_closing",
|
||||
False,
|
||||
):
|
||||
event.accept()
|
||||
return
|
||||
|
||||
tray_cfg = self.settings_data.get(
|
||||
"tray",
|
||||
{},
|
||||
)
|
||||
|
||||
if (
|
||||
tray_cfg.get(
|
||||
"close_to_tray",
|
||||
True,
|
||||
)
|
||||
and self.tray_manager
|
||||
):
|
||||
event.ignore()
|
||||
self.hide()
|
||||
return
|
||||
|
||||
self._closing = True
|
||||
|
||||
try:
|
||||
self._persist_window_state()
|
||||
|
||||
for widget in (
|
||||
self.plugins_window,
|
||||
self.settings_window,
|
||||
self.profile_window,
|
||||
self.addons_window,
|
||||
self.network_window,
|
||||
):
|
||||
if widget:
|
||||
try:
|
||||
widget.close()
|
||||
except Exception:
|
||||
log.exception(
|
||||
"child window close failed"
|
||||
)
|
||||
|
||||
self._shutdown()
|
||||
|
||||
except Exception:
|
||||
log.exception(
|
||||
"shutdown failed"
|
||||
)
|
||||
|
||||
event.accept()
|
||||
|
||||
super().closeEvent(
|
||||
event
|
||||
)
|
||||
|
||||
|
||||
|
||||
def build_database() -> Database:
|
||||
return Database(
|
||||
DATABASE_PATH
|
||||
)
|
||||
|
||||
# v3
|
||||
def build_app() -> QApplication:
|
||||
os.environ.setdefault(
|
||||
"QTWEBENGINE_REMOTE_DEBUGGING",
|
||||
"0",
|
||||
)
|
||||
|
||||
os.environ.setdefault(
|
||||
"CHROME_CRASHPAD_PIPE_NAME",
|
||||
"",
|
||||
)
|
||||
|
||||
QApplication.setAttribute(
|
||||
Qt.ApplicationAttribute.AA_ShareOpenGLContexts
|
||||
)
|
||||
|
||||
return QApplication(
|
||||
sys.argv
|
||||
)
|
||||
|
||||
|
||||
def fatal_error(
|
||||
text: str,
|
||||
) -> None:
|
||||
try:
|
||||
app = QApplication.instance()
|
||||
|
||||
if app is None:
|
||||
app = QApplication(
|
||||
sys.argv
|
||||
)
|
||||
|
||||
QMessageBox.critical(
|
||||
None,
|
||||
APP_NAME,
|
||||
text,
|
||||
)
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
# v3
|
||||
def main() -> int:
|
||||
try:
|
||||
ensure_directories()
|
||||
|
||||
settings = load_settings()
|
||||
|
||||
db = build_database()
|
||||
|
||||
QApplication.setDesktopSettingsAware(
|
||||
True
|
||||
)
|
||||
|
||||
app = build_app()
|
||||
|
||||
icon = QIcon(
|
||||
"app.ico"
|
||||
)
|
||||
|
||||
if icon.isNull():
|
||||
|
||||
for candidate in (
|
||||
"ui/app.ico",
|
||||
"assets/app.ico",
|
||||
"icon.ico",
|
||||
):
|
||||
test = QIcon(
|
||||
candidate
|
||||
)
|
||||
|
||||
if not test.isNull():
|
||||
icon = test
|
||||
break
|
||||
|
||||
if not icon.isNull():
|
||||
app.setWindowIcon(
|
||||
icon
|
||||
)
|
||||
|
||||
app.setApplicationName(
|
||||
APP_NAME
|
||||
)
|
||||
|
||||
app.setApplicationVersion(
|
||||
APP_VERSION
|
||||
)
|
||||
|
||||
app.setStyle(
|
||||
"Fusion"
|
||||
)
|
||||
|
||||
apply_dark_palette(
|
||||
app
|
||||
)
|
||||
|
||||
app.setStyleSheet(
|
||||
apply_stylesheet()
|
||||
)
|
||||
|
||||
window = MainWindow(
|
||||
db=db,
|
||||
settings=settings,
|
||||
)
|
||||
|
||||
window.show()
|
||||
|
||||
return app.exec()
|
||||
|
||||
except KeyboardInterrupt:
|
||||
return 0
|
||||
|
||||
except SystemExit:
|
||||
return 0
|
||||
|
||||
except Exception as exc:
|
||||
log.exception(
|
||||
"fatal startup error"
|
||||
)
|
||||
|
||||
fatal_error(
|
||||
str(exc)
|
||||
)
|
||||
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(
|
||||
main()
|
||||
)
|
||||
1609
managers.py
Normal file
1609
managers.py
Normal file
File diff suppressed because it is too large
Load Diff
768
models.py
Normal file
768
models.py
Normal file
@@ -0,0 +1,768 @@
|
||||
# v7
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from PySide6.QtCore import (
|
||||
QAbstractTableModel,
|
||||
QModelIndex,
|
||||
Qt,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ProfileContext:
|
||||
name: str
|
||||
|
||||
root: Path
|
||||
|
||||
storage_path: Path
|
||||
cache_path: Path
|
||||
downloads_path: Path
|
||||
|
||||
plugins_path: Path
|
||||
themes_path: Path
|
||||
extensions_path: Path
|
||||
|
||||
profile_db: Path
|
||||
permissions_db: Path
|
||||
network_db: Path
|
||||
|
||||
class NetworkTableModel(
|
||||
QAbstractTableModel
|
||||
):
|
||||
HEADERS = (
|
||||
"Method",
|
||||
"Status",
|
||||
"Host",
|
||||
"URL",
|
||||
"Type",
|
||||
"Duration",
|
||||
"Timestamp",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent=None,
|
||||
):
|
||||
super().__init__(
|
||||
parent
|
||||
)
|
||||
|
||||
self._rows: list[
|
||||
dict[str, Any]
|
||||
] = []
|
||||
|
||||
def set_rows(
|
||||
self,
|
||||
rows,
|
||||
) -> None:
|
||||
self.beginResetModel()
|
||||
|
||||
self._rows = [
|
||||
dict(row)
|
||||
for row in rows
|
||||
]
|
||||
|
||||
self.endResetModel()
|
||||
|
||||
def append_row(
|
||||
self,
|
||||
row: dict,
|
||||
) -> None:
|
||||
position = len(
|
||||
self._rows
|
||||
)
|
||||
|
||||
self.beginInsertRows(
|
||||
QModelIndex(),
|
||||
position,
|
||||
position,
|
||||
)
|
||||
|
||||
self._rows.append(
|
||||
row
|
||||
)
|
||||
|
||||
self.endInsertRows()
|
||||
|
||||
def rowCount(
|
||||
self,
|
||||
parent=QModelIndex(),
|
||||
) -> int:
|
||||
return 0 if parent.isValid() else len(
|
||||
self._rows
|
||||
)
|
||||
|
||||
def columnCount(
|
||||
self,
|
||||
parent=QModelIndex(),
|
||||
) -> int:
|
||||
return 0 if parent.isValid() else len(
|
||||
self.HEADERS
|
||||
)
|
||||
|
||||
def headerData(
|
||||
self,
|
||||
section,
|
||||
orientation,
|
||||
role,
|
||||
):
|
||||
if (
|
||||
role
|
||||
!= Qt.ItemDataRole.DisplayRole
|
||||
):
|
||||
return None
|
||||
|
||||
if (
|
||||
orientation
|
||||
== Qt.Orientation.Horizontal
|
||||
):
|
||||
if (
|
||||
0
|
||||
<= section
|
||||
< len(
|
||||
self.HEADERS
|
||||
)
|
||||
):
|
||||
return self.HEADERS[
|
||||
section
|
||||
]
|
||||
|
||||
return str(
|
||||
section + 1
|
||||
)
|
||||
|
||||
def data(
|
||||
self,
|
||||
index,
|
||||
role,
|
||||
):
|
||||
if (
|
||||
not index.isValid()
|
||||
or role
|
||||
!= Qt.ItemDataRole.DisplayRole
|
||||
):
|
||||
return None
|
||||
|
||||
row = self._rows[
|
||||
index.row()
|
||||
]
|
||||
|
||||
match index.column():
|
||||
case 0:
|
||||
return row.get(
|
||||
"method",
|
||||
"",
|
||||
)
|
||||
|
||||
case 1:
|
||||
return row.get(
|
||||
"status_code",
|
||||
row.get(
|
||||
"status",
|
||||
"",
|
||||
),
|
||||
)
|
||||
|
||||
case 2:
|
||||
return row.get(
|
||||
"host",
|
||||
"",
|
||||
)
|
||||
|
||||
case 3:
|
||||
return row.get(
|
||||
"url",
|
||||
"",
|
||||
)
|
||||
|
||||
case 4:
|
||||
return row.get(
|
||||
"resource_type",
|
||||
"",
|
||||
)
|
||||
|
||||
case 5:
|
||||
duration = row.get(
|
||||
"duration_ms"
|
||||
)
|
||||
|
||||
if duration is None:
|
||||
return ""
|
||||
|
||||
try:
|
||||
return (
|
||||
f"{float(duration):.2f}"
|
||||
)
|
||||
except (
|
||||
TypeError,
|
||||
ValueError,
|
||||
):
|
||||
return ""
|
||||
|
||||
case 6:
|
||||
return row.get(
|
||||
"timestamp",
|
||||
"",
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
def row(
|
||||
self,
|
||||
position: int,
|
||||
) -> dict:
|
||||
return self._rows[
|
||||
position
|
||||
]
|
||||
|
||||
|
||||
class ProfileModel(
|
||||
QAbstractTableModel
|
||||
):
|
||||
HEADERS = (
|
||||
"Profile",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent=None,
|
||||
):
|
||||
super().__init__(
|
||||
parent
|
||||
)
|
||||
|
||||
self._profiles: list[
|
||||
str
|
||||
] = []
|
||||
|
||||
def set_profiles(
|
||||
self,
|
||||
profiles: list[str],
|
||||
):
|
||||
self.beginResetModel()
|
||||
|
||||
self._profiles = sorted(
|
||||
profiles
|
||||
)
|
||||
|
||||
self.endResetModel()
|
||||
|
||||
def rowCount(
|
||||
self,
|
||||
parent=QModelIndex(),
|
||||
):
|
||||
return 0 if parent.isValid() else len(
|
||||
self._profiles
|
||||
)
|
||||
|
||||
def columnCount(
|
||||
self,
|
||||
parent=QModelIndex(),
|
||||
):
|
||||
return 0 if parent.isValid() else 1
|
||||
|
||||
def headerData(
|
||||
self,
|
||||
section,
|
||||
orientation,
|
||||
role,
|
||||
):
|
||||
if (
|
||||
role
|
||||
!= Qt.ItemDataRole.DisplayRole
|
||||
):
|
||||
return None
|
||||
|
||||
if (
|
||||
orientation
|
||||
== Qt.Orientation.Horizontal
|
||||
):
|
||||
return self.HEADERS[
|
||||
section
|
||||
]
|
||||
|
||||
return None
|
||||
|
||||
def data(
|
||||
self,
|
||||
index,
|
||||
role,
|
||||
):
|
||||
if (
|
||||
not index.isValid()
|
||||
or role
|
||||
!= Qt.ItemDataRole.DisplayRole
|
||||
):
|
||||
return None
|
||||
|
||||
return self._profiles[
|
||||
index.row()
|
||||
]
|
||||
|
||||
|
||||
class AddonModel(
|
||||
QAbstractTableModel
|
||||
):
|
||||
HEADERS = (
|
||||
"Name",
|
||||
"Enabled",
|
||||
"Type",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent=None,
|
||||
):
|
||||
super().__init__(
|
||||
parent
|
||||
)
|
||||
|
||||
self._addons: list[
|
||||
dict
|
||||
] = []
|
||||
|
||||
def set_addons(
|
||||
self,
|
||||
addons: list[dict],
|
||||
):
|
||||
self.beginResetModel()
|
||||
|
||||
self._addons = list(
|
||||
addons
|
||||
)
|
||||
|
||||
self.endResetModel()
|
||||
|
||||
def rowCount(
|
||||
self,
|
||||
parent=QModelIndex(),
|
||||
):
|
||||
return 0 if parent.isValid() else len(
|
||||
self._addons
|
||||
)
|
||||
|
||||
def columnCount(
|
||||
self,
|
||||
parent=QModelIndex(),
|
||||
):
|
||||
return 0 if parent.isValid() else 3
|
||||
|
||||
def headerData(
|
||||
self,
|
||||
section,
|
||||
orientation,
|
||||
role,
|
||||
):
|
||||
if (
|
||||
role
|
||||
!= Qt.ItemDataRole.DisplayRole
|
||||
):
|
||||
return None
|
||||
|
||||
if (
|
||||
orientation
|
||||
== Qt.Orientation.Horizontal
|
||||
):
|
||||
return self.HEADERS[
|
||||
section
|
||||
]
|
||||
|
||||
return None
|
||||
|
||||
def data(
|
||||
self,
|
||||
index,
|
||||
role,
|
||||
):
|
||||
if (
|
||||
not index.isValid()
|
||||
or role
|
||||
!= Qt.ItemDataRole.DisplayRole
|
||||
):
|
||||
return None
|
||||
|
||||
addon = self._addons[
|
||||
index.row()
|
||||
]
|
||||
|
||||
match index.column():
|
||||
case 0:
|
||||
return addon.get(
|
||||
"name",
|
||||
"",
|
||||
)
|
||||
|
||||
case 1:
|
||||
return (
|
||||
"Yes"
|
||||
if addon.get(
|
||||
"enabled",
|
||||
True,
|
||||
)
|
||||
else "No"
|
||||
)
|
||||
|
||||
case 2:
|
||||
return addon.get(
|
||||
"type",
|
||||
"",
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class PluginTableModel(
|
||||
QAbstractTableModel
|
||||
):
|
||||
HEADERS = (
|
||||
"Enabled",
|
||||
"Name",
|
||||
"Version",
|
||||
"Author",
|
||||
"Status",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent=None,
|
||||
):
|
||||
super().__init__(
|
||||
parent
|
||||
)
|
||||
|
||||
self._plugins = []
|
||||
|
||||
def set_plugins(
|
||||
self,
|
||||
plugins,
|
||||
):
|
||||
self.beginResetModel()
|
||||
|
||||
self._plugins = [
|
||||
dict(plugin)
|
||||
for plugin in plugins
|
||||
]
|
||||
|
||||
self.endResetModel()
|
||||
|
||||
def rowCount(
|
||||
self,
|
||||
parent=QModelIndex(),
|
||||
):
|
||||
return 0 if parent.isValid() else len(
|
||||
self._plugins
|
||||
)
|
||||
|
||||
def columnCount(
|
||||
self,
|
||||
parent=QModelIndex(),
|
||||
):
|
||||
return 0 if parent.isValid() else 5
|
||||
|
||||
def headerData(
|
||||
self,
|
||||
section,
|
||||
orientation,
|
||||
role,
|
||||
):
|
||||
if (
|
||||
role
|
||||
!= Qt.ItemDataRole.DisplayRole
|
||||
):
|
||||
return None
|
||||
|
||||
if (
|
||||
orientation
|
||||
== Qt.Orientation.Horizontal
|
||||
):
|
||||
return self.HEADERS[
|
||||
section
|
||||
]
|
||||
|
||||
return None
|
||||
|
||||
def data(
|
||||
self,
|
||||
index,
|
||||
role,
|
||||
):
|
||||
if (
|
||||
not index.isValid()
|
||||
or role
|
||||
!= Qt.ItemDataRole.DisplayRole
|
||||
):
|
||||
return None
|
||||
|
||||
plugin = self._plugins[
|
||||
index.row()
|
||||
]
|
||||
|
||||
match index.column():
|
||||
case 0:
|
||||
return (
|
||||
"Yes"
|
||||
if plugin.get(
|
||||
"enabled",
|
||||
True,
|
||||
)
|
||||
else "No"
|
||||
)
|
||||
|
||||
case 1:
|
||||
return plugin.get(
|
||||
"name",
|
||||
"",
|
||||
)
|
||||
|
||||
case 2:
|
||||
return plugin.get(
|
||||
"version",
|
||||
"",
|
||||
)
|
||||
|
||||
case 3:
|
||||
return plugin.get(
|
||||
"author",
|
||||
"",
|
||||
)
|
||||
|
||||
case 4:
|
||||
return plugin.get(
|
||||
"status",
|
||||
"Loaded",
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
def row(
|
||||
self,
|
||||
position: int,
|
||||
) -> dict:
|
||||
return self._plugins[
|
||||
position
|
||||
]
|
||||
|
||||
|
||||
class PluginLogTableModel(
|
||||
QAbstractTableModel
|
||||
):
|
||||
HEADERS = (
|
||||
"Time",
|
||||
"Plugin",
|
||||
"Level",
|
||||
"Message",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent=None,
|
||||
):
|
||||
super().__init__(
|
||||
parent
|
||||
)
|
||||
|
||||
self._rows = []
|
||||
|
||||
def set_rows(
|
||||
self,
|
||||
rows,
|
||||
):
|
||||
self.beginResetModel()
|
||||
|
||||
self._rows = [
|
||||
dict(row)
|
||||
for row in rows
|
||||
]
|
||||
|
||||
self.endResetModel()
|
||||
|
||||
def rowCount(
|
||||
self,
|
||||
parent=QModelIndex(),
|
||||
):
|
||||
return 0 if parent.isValid() else len(
|
||||
self._rows
|
||||
)
|
||||
|
||||
def columnCount(
|
||||
self,
|
||||
parent=QModelIndex(),
|
||||
):
|
||||
return 0 if parent.isValid() else 4
|
||||
|
||||
def headerData(
|
||||
self,
|
||||
section,
|
||||
orientation,
|
||||
role,
|
||||
):
|
||||
if (
|
||||
role
|
||||
!= Qt.ItemDataRole.DisplayRole
|
||||
):
|
||||
return None
|
||||
|
||||
if (
|
||||
orientation
|
||||
== Qt.Orientation.Horizontal
|
||||
):
|
||||
return self.HEADERS[
|
||||
section
|
||||
]
|
||||
|
||||
return None
|
||||
|
||||
def data(
|
||||
self,
|
||||
index,
|
||||
role,
|
||||
):
|
||||
if (
|
||||
not index.isValid()
|
||||
or role
|
||||
!= Qt.ItemDataRole.DisplayRole
|
||||
):
|
||||
return None
|
||||
|
||||
row = self._rows[
|
||||
index.row()
|
||||
]
|
||||
|
||||
match index.column():
|
||||
case 0:
|
||||
return row.get(
|
||||
"created_at",
|
||||
"",
|
||||
)
|
||||
|
||||
case 1:
|
||||
return row.get(
|
||||
"plugin_name",
|
||||
"",
|
||||
)
|
||||
|
||||
case 2:
|
||||
return row.get(
|
||||
"level",
|
||||
"",
|
||||
)
|
||||
|
||||
case 3:
|
||||
return row.get(
|
||||
"message",
|
||||
"",
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class PluginCrashTableModel(
|
||||
QAbstractTableModel
|
||||
):
|
||||
HEADERS = (
|
||||
"Time",
|
||||
"Plugin",
|
||||
"Error",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent=None,
|
||||
):
|
||||
super().__init__(
|
||||
parent
|
||||
)
|
||||
|
||||
self._rows = []
|
||||
|
||||
def set_rows(
|
||||
self,
|
||||
rows,
|
||||
):
|
||||
self.beginResetModel()
|
||||
|
||||
self._rows = [
|
||||
dict(row)
|
||||
for row in rows
|
||||
]
|
||||
|
||||
self.endResetModel()
|
||||
|
||||
def rowCount(
|
||||
self,
|
||||
parent=QModelIndex(),
|
||||
):
|
||||
return 0 if parent.isValid() else len(
|
||||
self._rows
|
||||
)
|
||||
|
||||
def columnCount(
|
||||
self,
|
||||
parent=QModelIndex(),
|
||||
):
|
||||
return 0 if parent.isValid() else 3
|
||||
|
||||
def headerData(
|
||||
self,
|
||||
section,
|
||||
orientation,
|
||||
role,
|
||||
):
|
||||
if (
|
||||
role
|
||||
!= Qt.ItemDataRole.DisplayRole
|
||||
):
|
||||
return None
|
||||
|
||||
if (
|
||||
orientation
|
||||
== Qt.Orientation.Horizontal
|
||||
):
|
||||
return self.HEADERS[
|
||||
section
|
||||
]
|
||||
|
||||
return None
|
||||
|
||||
def data(
|
||||
self,
|
||||
index,
|
||||
role,
|
||||
):
|
||||
if (
|
||||
not index.isValid()
|
||||
or role
|
||||
!= Qt.ItemDataRole.DisplayRole
|
||||
):
|
||||
return None
|
||||
|
||||
row = self._rows[
|
||||
index.row()
|
||||
]
|
||||
|
||||
match index.column():
|
||||
case 0:
|
||||
return row.get(
|
||||
"created_at",
|
||||
"",
|
||||
)
|
||||
|
||||
case 1:
|
||||
return row.get(
|
||||
"plugin_name",
|
||||
"",
|
||||
)
|
||||
|
||||
case 2:
|
||||
return row.get(
|
||||
"error",
|
||||
"",
|
||||
)
|
||||
|
||||
return None
|
||||
851
network.py
Normal file
851
network.py
Normal file
@@ -0,0 +1,851 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import socket
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from urllib.parse import urlparse
|
||||
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from PySide6.QtCore import QObject, Signal
|
||||
|
||||
from config import DISCORD_TELEMETRY_BLOCKS
|
||||
|
||||
from blocklists import (
|
||||
BlocklistEngine,
|
||||
)
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
REDACT_HEADERS = {
|
||||
"authorization",
|
||||
"cookie",
|
||||
"set-cookie",
|
||||
"x-super-properties",
|
||||
"x-fingerprint",
|
||||
"token",
|
||||
"access_token",
|
||||
"refresh_token",
|
||||
}
|
||||
|
||||
DISCORD_GATEWAY_HOSTS = {
|
||||
"gateway.discord.gg",
|
||||
"remote-auth-gateway.discord.gg",
|
||||
}
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ProxyConfig:
|
||||
enabled: bool = False
|
||||
proxy_type: str = "http"
|
||||
host: str = ""
|
||||
port: int = 0
|
||||
username: str = ""
|
||||
password: str = ""
|
||||
|
||||
@property
|
||||
def valid(self) -> bool:
|
||||
return (
|
||||
self.enabled
|
||||
and bool(self.host)
|
||||
and self.port > 0
|
||||
)
|
||||
|
||||
def to_dict(
|
||||
self,
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"enabled": self.enabled,
|
||||
"proxy_type": self.proxy_type,
|
||||
"host": self.host,
|
||||
"port": self.port,
|
||||
"username": self.username,
|
||||
"password": self.password,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(
|
||||
cls,
|
||||
data: dict[str, Any] | None,
|
||||
) -> "ProxyConfig":
|
||||
data = data or {}
|
||||
|
||||
return cls(
|
||||
enabled=bool(
|
||||
data.get(
|
||||
"enabled",
|
||||
False,
|
||||
)
|
||||
),
|
||||
proxy_type=str(
|
||||
data.get(
|
||||
"proxy_type",
|
||||
"http",
|
||||
)
|
||||
),
|
||||
host=str(
|
||||
data.get(
|
||||
"host",
|
||||
"",
|
||||
)
|
||||
),
|
||||
port=int(
|
||||
data.get(
|
||||
"port",
|
||||
0,
|
||||
)
|
||||
),
|
||||
username=str(
|
||||
data.get(
|
||||
"username",
|
||||
"",
|
||||
)
|
||||
),
|
||||
password=str(
|
||||
data.get(
|
||||
"password",
|
||||
"",
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class ProxyManager(QObject):
|
||||
proxyChanged = Signal(dict)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
db,
|
||||
):
|
||||
super().__init__()
|
||||
self.db = db
|
||||
|
||||
def get_proxy(
|
||||
self,
|
||||
profile_name: str,
|
||||
) -> ProxyConfig:
|
||||
return ProxyConfig.from_dict(
|
||||
self.db.get_setting(
|
||||
f"proxy:{profile_name}",
|
||||
{},
|
||||
)
|
||||
)
|
||||
|
||||
def save_proxy(
|
||||
self,
|
||||
profile_name: str,
|
||||
proxy: ProxyConfig,
|
||||
) -> None:
|
||||
self.db.set_setting(
|
||||
f"proxy:{profile_name}",
|
||||
proxy.to_dict(),
|
||||
)
|
||||
|
||||
self.proxyChanged.emit(
|
||||
proxy.to_dict()
|
||||
)
|
||||
|
||||
def clear_proxy(
|
||||
self,
|
||||
profile_name: str,
|
||||
) -> None:
|
||||
self.save_proxy(
|
||||
profile_name,
|
||||
ProxyConfig(),
|
||||
)
|
||||
|
||||
def test_proxy(
|
||||
self,
|
||||
proxy: ProxyConfig,
|
||||
timeout: float = 5.0,
|
||||
) -> bool:
|
||||
if not proxy.valid:
|
||||
return False
|
||||
|
||||
try:
|
||||
with socket.create_connection(
|
||||
(
|
||||
proxy.host,
|
||||
proxy.port,
|
||||
),
|
||||
timeout=timeout,
|
||||
):
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
# v2
|
||||
class DomainBlocker:
|
||||
|
||||
DEFAULT_BLOCKS = set(
|
||||
DISCORD_TELEMETRY_BLOCKS
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
db,
|
||||
blocklist_engine=None,
|
||||
):
|
||||
self.db = db
|
||||
self.blocklist = (
|
||||
blocklist_engine
|
||||
)
|
||||
|
||||
def domains(
|
||||
self,
|
||||
) -> set[str]:
|
||||
domains = set(
|
||||
self.DEFAULT_BLOCKS
|
||||
)
|
||||
|
||||
try:
|
||||
domains.update(
|
||||
self.db.blocked_domains()
|
||||
)
|
||||
except Exception:
|
||||
log.exception(
|
||||
"blocked domain load failed"
|
||||
)
|
||||
|
||||
return {
|
||||
str(domain)
|
||||
.lower()
|
||||
.strip()
|
||||
for domain in domains
|
||||
if domain
|
||||
}
|
||||
|
||||
def add(
|
||||
self,
|
||||
domain: str,
|
||||
) -> None:
|
||||
domain = (
|
||||
domain
|
||||
.lower()
|
||||
.strip()
|
||||
)
|
||||
|
||||
if domain:
|
||||
self.db.add_blocked_domain(
|
||||
domain
|
||||
)
|
||||
|
||||
def remove(
|
||||
self,
|
||||
domain: str,
|
||||
) -> None:
|
||||
try:
|
||||
with self.db.connection() as conn:
|
||||
conn.execute(
|
||||
"""
|
||||
DELETE FROM blocked_domains
|
||||
WHERE domain=?
|
||||
""",
|
||||
(
|
||||
domain.lower()
|
||||
.strip(),
|
||||
),
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
|
||||
except Exception:
|
||||
log.exception(
|
||||
"domain removal failed"
|
||||
)
|
||||
|
||||
# v2
|
||||
def is_blocked(
|
||||
self,
|
||||
host: str,
|
||||
) -> bool:
|
||||
|
||||
host = (
|
||||
host.lower()
|
||||
.strip()
|
||||
)
|
||||
|
||||
if (
|
||||
self.blocklist
|
||||
and self.blocklist.contains(
|
||||
host
|
||||
)
|
||||
):
|
||||
return True
|
||||
|
||||
for domain in self.domains():
|
||||
|
||||
if (
|
||||
host == domain
|
||||
or host.endswith(
|
||||
f".{domain}"
|
||||
)
|
||||
):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
class HeaderRules:
|
||||
def __init__(
|
||||
self,
|
||||
db,
|
||||
):
|
||||
self.db = db
|
||||
|
||||
def all(
|
||||
self,
|
||||
) -> list[dict]:
|
||||
return self.db.get_setting(
|
||||
"header_rules",
|
||||
[],
|
||||
)
|
||||
|
||||
def save(
|
||||
self,
|
||||
rules: list[dict],
|
||||
) -> None:
|
||||
self.db.set_setting(
|
||||
"header_rules",
|
||||
rules,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def redact(
|
||||
headers: dict,
|
||||
) -> dict:
|
||||
result = {}
|
||||
|
||||
for key, value in (
|
||||
headers or {}
|
||||
).items():
|
||||
if (
|
||||
str(key)
|
||||
.lower()
|
||||
.strip()
|
||||
in REDACT_HEADERS
|
||||
):
|
||||
result[key] = (
|
||||
"[REDACTED]"
|
||||
)
|
||||
else:
|
||||
result[key] = value
|
||||
|
||||
return result
|
||||
|
||||
def apply(
|
||||
self,
|
||||
headers: dict,
|
||||
) -> dict:
|
||||
result = dict(
|
||||
headers or {}
|
||||
)
|
||||
|
||||
for rule in self.all():
|
||||
action = str(
|
||||
rule.get(
|
||||
"action",
|
||||
"",
|
||||
)
|
||||
).lower()
|
||||
|
||||
name = str(
|
||||
rule.get(
|
||||
"name",
|
||||
"",
|
||||
)
|
||||
)
|
||||
|
||||
value = rule.get(
|
||||
"value",
|
||||
"",
|
||||
)
|
||||
|
||||
if not name:
|
||||
continue
|
||||
|
||||
if action in {
|
||||
"add",
|
||||
"replace",
|
||||
}:
|
||||
result[name] = value
|
||||
|
||||
elif action == "remove":
|
||||
result.pop(
|
||||
name,
|
||||
None,
|
||||
)
|
||||
|
||||
return result
|
||||
# v5
|
||||
|
||||
|
||||
# v3
|
||||
class NetworkRules:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
settings: dict,
|
||||
):
|
||||
self.settings = settings
|
||||
|
||||
def load(
|
||||
self,
|
||||
profile_name: str | None = None,
|
||||
plugin_name: str | None = None,
|
||||
) -> list[dict]:
|
||||
|
||||
try:
|
||||
rules = self.settings.get(
|
||||
"network_rules",
|
||||
[],
|
||||
)
|
||||
|
||||
if not isinstance(
|
||||
rules,
|
||||
list,
|
||||
):
|
||||
return []
|
||||
|
||||
except Exception:
|
||||
log.exception(
|
||||
"network rule load failed"
|
||||
)
|
||||
return []
|
||||
|
||||
output: list[dict] = []
|
||||
|
||||
for rule in rules:
|
||||
|
||||
if not isinstance(
|
||||
rule,
|
||||
dict,
|
||||
):
|
||||
continue
|
||||
|
||||
if (
|
||||
profile_name
|
||||
and rule.get(
|
||||
"profile"
|
||||
)
|
||||
not in {
|
||||
None,
|
||||
profile_name,
|
||||
}
|
||||
):
|
||||
continue
|
||||
|
||||
if (
|
||||
plugin_name
|
||||
and rule.get(
|
||||
"plugin"
|
||||
)
|
||||
not in {
|
||||
None,
|
||||
plugin_name,
|
||||
}
|
||||
):
|
||||
continue
|
||||
|
||||
output.append(
|
||||
rule
|
||||
)
|
||||
|
||||
return output
|
||||
|
||||
def evaluate(
|
||||
self,
|
||||
host: str,
|
||||
profile_name: str | None = None,
|
||||
plugin_name: str | None = None,
|
||||
) -> tuple[bool, str | None]:
|
||||
|
||||
host = (
|
||||
host.lower()
|
||||
.strip()
|
||||
)
|
||||
|
||||
rules = sorted(
|
||||
self.load(
|
||||
profile_name,
|
||||
plugin_name,
|
||||
),
|
||||
key=lambda r: (
|
||||
0
|
||||
if r.get(
|
||||
"plugin"
|
||||
)
|
||||
else (
|
||||
1
|
||||
if r.get(
|
||||
"profile"
|
||||
)
|
||||
else 2
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
for rule in rules:
|
||||
|
||||
pattern = (
|
||||
str(
|
||||
rule.get(
|
||||
"pattern",
|
||||
"",
|
||||
)
|
||||
)
|
||||
.lower()
|
||||
.strip()
|
||||
)
|
||||
|
||||
if not pattern:
|
||||
continue
|
||||
|
||||
matched = (
|
||||
host == pattern
|
||||
or host.endswith(
|
||||
f".{pattern}"
|
||||
)
|
||||
)
|
||||
|
||||
if not matched:
|
||||
continue
|
||||
|
||||
action = (
|
||||
str(
|
||||
rule.get(
|
||||
"action",
|
||||
"",
|
||||
)
|
||||
)
|
||||
.lower()
|
||||
.strip()
|
||||
)
|
||||
|
||||
if action in {
|
||||
"block",
|
||||
"deny",
|
||||
}:
|
||||
return (
|
||||
False,
|
||||
None,
|
||||
)
|
||||
|
||||
if action == "allow":
|
||||
return (
|
||||
True,
|
||||
None,
|
||||
)
|
||||
|
||||
if action == "redirect":
|
||||
target = (
|
||||
str(
|
||||
rule.get(
|
||||
"target",
|
||||
"",
|
||||
)
|
||||
)
|
||||
.strip()
|
||||
)
|
||||
|
||||
return (
|
||||
True,
|
||||
target or None,
|
||||
)
|
||||
|
||||
return (
|
||||
True,
|
||||
None,
|
||||
)
|
||||
|
||||
class RequestReplay:
|
||||
def __init__(
|
||||
self,
|
||||
db,
|
||||
):
|
||||
self.db = db
|
||||
|
||||
def get_request(
|
||||
self,
|
||||
request_id: int,
|
||||
):
|
||||
with self.db.connection() as conn:
|
||||
return conn.execute(
|
||||
"""
|
||||
SELECT *
|
||||
FROM network_requests
|
||||
WHERE id=?
|
||||
""",
|
||||
(
|
||||
request_id,
|
||||
),
|
||||
).fetchone()
|
||||
|
||||
|
||||
class HARExporter:
|
||||
def __init__(
|
||||
self,
|
||||
db,
|
||||
):
|
||||
self.db = db
|
||||
|
||||
def export(
|
||||
self,
|
||||
output_file: str | Path,
|
||||
limit: int = 5000,
|
||||
) -> Path:
|
||||
output_file = Path(
|
||||
output_file
|
||||
)
|
||||
|
||||
entries = []
|
||||
|
||||
for row in self.db.recent_requests(
|
||||
limit
|
||||
):
|
||||
entries.append(
|
||||
{
|
||||
"startedDateTime": row[
|
||||
"timestamp"
|
||||
],
|
||||
"request": {
|
||||
"method": row[
|
||||
"method"
|
||||
],
|
||||
"url": row[
|
||||
"url"
|
||||
],
|
||||
},
|
||||
"response": {
|
||||
"status": row[
|
||||
"status_code"
|
||||
],
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
output_file.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"log": {
|
||||
"version": "1.2",
|
||||
"creator": {
|
||||
"name": "Discord Client",
|
||||
"version": "1.0",
|
||||
},
|
||||
"entries": entries,
|
||||
}
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
return output_file
|
||||
|
||||
|
||||
# v4# v5
|
||||
class NetworkBackend(QObject):
|
||||
requestAdded = Signal(dict)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
db,
|
||||
settings: dict,
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
self.db = db
|
||||
self.settings = settings
|
||||
|
||||
mode = self.db.get_setting(
|
||||
"blocklist_mode",
|
||||
"pro",
|
||||
)
|
||||
|
||||
blocklist_dir = Path(
|
||||
"config/blocklists"
|
||||
)
|
||||
|
||||
blocklists = sorted(
|
||||
path
|
||||
for path in blocklist_dir.glob(
|
||||
"*.txt"
|
||||
)
|
||||
if path.is_file()
|
||||
)
|
||||
|
||||
if mode == "pro":
|
||||
blocklists = [
|
||||
path
|
||||
for path in blocklists
|
||||
if "ultimate"
|
||||
not in path.name.lower()
|
||||
]
|
||||
|
||||
cache_file = (
|
||||
blocklist_dir
|
||||
/ f"{mode}.cache.bin"
|
||||
)
|
||||
|
||||
self.blocklist_engine = (
|
||||
BlocklistEngine(
|
||||
blocklists=blocklists,
|
||||
cache_file=cache_file,
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
self.blocklist_engine.load()
|
||||
except Exception:
|
||||
log.exception(
|
||||
"blocklist load failed"
|
||||
)
|
||||
|
||||
self.blocker = DomainBlocker(
|
||||
db,
|
||||
self.blocklist_engine,
|
||||
)
|
||||
|
||||
self.headers = HeaderRules(
|
||||
db
|
||||
)
|
||||
|
||||
self.rules = NetworkRules(
|
||||
settings
|
||||
)
|
||||
|
||||
self.proxy = ProxyManager(
|
||||
db
|
||||
)
|
||||
|
||||
self.har = HARExporter(
|
||||
db
|
||||
)
|
||||
|
||||
self.replay = RequestReplay(
|
||||
db
|
||||
)
|
||||
|
||||
def recent(
|
||||
self,
|
||||
limit: int = 1000,
|
||||
):
|
||||
return self.db.recent_requests(
|
||||
limit
|
||||
)
|
||||
|
||||
def search(
|
||||
self,
|
||||
query: str,
|
||||
limit: int = 1000,
|
||||
):
|
||||
return self.db.search_requests(
|
||||
query,
|
||||
limit,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def host_from_url(
|
||||
url: str,
|
||||
) -> str:
|
||||
try:
|
||||
return (
|
||||
urlparse(url)
|
||||
.hostname
|
||||
or ""
|
||||
).lower()
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
def process_request(
|
||||
self,
|
||||
request: dict,
|
||||
) -> tuple[dict, bool]:
|
||||
|
||||
url = str(
|
||||
request.get(
|
||||
"url",
|
||||
"",
|
||||
)
|
||||
)
|
||||
|
||||
host = self.host_from_url(
|
||||
url
|
||||
)
|
||||
|
||||
request["host"] = host
|
||||
|
||||
if host in DISCORD_GATEWAY_HOSTS:
|
||||
|
||||
self.requestAdded.emit(
|
||||
request
|
||||
)
|
||||
|
||||
return request, False
|
||||
|
||||
if (
|
||||
host
|
||||
and self.blocker.is_blocked(
|
||||
host
|
||||
)
|
||||
):
|
||||
|
||||
request["_blocked"] = True
|
||||
|
||||
self.requestAdded.emit(
|
||||
request
|
||||
)
|
||||
|
||||
return request, True
|
||||
|
||||
allowed, redirect_url = (
|
||||
self.rules.evaluate(
|
||||
host=host,
|
||||
profile_name=request.get(
|
||||
"profile"
|
||||
),
|
||||
plugin_name=request.get(
|
||||
"plugin"
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
if not allowed:
|
||||
|
||||
request["_blocked"] = True
|
||||
|
||||
self.requestAdded.emit(
|
||||
request
|
||||
)
|
||||
|
||||
return request, True
|
||||
|
||||
if redirect_url:
|
||||
|
||||
request[
|
||||
"redirect_url"
|
||||
] = redirect_url
|
||||
|
||||
headers = self.headers.apply(
|
||||
request.get(
|
||||
"headers",
|
||||
{},
|
||||
)
|
||||
)
|
||||
|
||||
request["headers"] = (
|
||||
self.headers.redact(
|
||||
headers
|
||||
)
|
||||
)
|
||||
|
||||
self.requestAdded.emit(
|
||||
request
|
||||
)
|
||||
|
||||
return request, False
|
||||
102
theme.py
Normal file
102
theme.py
Normal file
@@ -0,0 +1,102 @@
|
||||
# v1
|
||||
from PySide6.QtGui import QColor
|
||||
from PySide6.QtGui import QPalette
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
|
||||
def apply_dark_palette(
|
||||
app: QApplication,
|
||||
) -> None:
|
||||
|
||||
palette = QPalette()
|
||||
|
||||
palette.setColor(
|
||||
QPalette.ColorRole.Window,
|
||||
QColor(24, 24, 27),
|
||||
)
|
||||
|
||||
palette.setColor(
|
||||
QPalette.ColorRole.WindowText,
|
||||
QColor(250, 250, 250),
|
||||
)
|
||||
|
||||
palette.setColor(
|
||||
QPalette.ColorRole.Base,
|
||||
QColor(17, 17, 20),
|
||||
)
|
||||
|
||||
palette.setColor(
|
||||
QPalette.ColorRole.AlternateBase,
|
||||
QColor(30, 30, 34),
|
||||
)
|
||||
|
||||
palette.setColor(
|
||||
QPalette.ColorRole.Text,
|
||||
QColor(250, 250, 250),
|
||||
)
|
||||
|
||||
palette.setColor(
|
||||
QPalette.ColorRole.Button,
|
||||
QColor(39, 39, 42),
|
||||
)
|
||||
|
||||
palette.setColor(
|
||||
QPalette.ColorRole.ButtonText,
|
||||
QColor(250, 250, 250),
|
||||
)
|
||||
|
||||
palette.setColor(
|
||||
QPalette.ColorRole.Highlight,
|
||||
QColor(59, 130, 246),
|
||||
)
|
||||
|
||||
palette.setColor(
|
||||
QPalette.ColorRole.HighlightedText,
|
||||
QColor(255, 255, 255),
|
||||
)
|
||||
|
||||
palette.setColor(
|
||||
QPalette.ColorRole.ToolTipBase,
|
||||
QColor(24, 24, 27),
|
||||
)
|
||||
|
||||
palette.setColor(
|
||||
QPalette.ColorRole.ToolTipText,
|
||||
QColor(250, 250, 250),
|
||||
)
|
||||
|
||||
app.setPalette(
|
||||
palette
|
||||
)
|
||||
|
||||
|
||||
def apply_stylesheet(
|
||||
) -> str:
|
||||
|
||||
return """
|
||||
QPushButton {
|
||||
border:none;
|
||||
border-radius:6px;
|
||||
padding:6px 12px;
|
||||
}
|
||||
|
||||
QLineEdit,
|
||||
QTextEdit,
|
||||
QPlainTextEdit,
|
||||
QComboBox,
|
||||
QListWidget,
|
||||
QTreeWidget,
|
||||
QTableView {
|
||||
border:1px solid #3f3f46;
|
||||
border-radius:6px;
|
||||
padding:4px;
|
||||
}
|
||||
|
||||
QTabWidget::pane {
|
||||
border:none;
|
||||
}
|
||||
|
||||
QToolTip {
|
||||
border:1px solid #3f3f46;
|
||||
}
|
||||
"""
|
||||
Reference in New Issue
Block a user