536 lines
9.5 KiB
Python
536 lines
9.5 KiB
Python
# v6
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
APP_NAME = "Discord Client"
|
|
APP_VERSION = "1.0.0"
|
|
|
|
START_URL = "https://discord.com/app"
|
|
|
|
WINDOW_WIDTH = 1400
|
|
WINDOW_HEIGHT = 900
|
|
|
|
LOG_LEVEL = logging.INFO
|
|
|
|
BASE_DIR = Path.home() / ".discord-client"
|
|
|
|
DATABASE_PATH = BASE_DIR / "client.db"
|
|
SETTINGS_FILE = BASE_DIR / "settings.json"
|
|
|
|
PROFILES_DIR = BASE_DIR / "profiles"
|
|
DOWNLOADS_DIR = BASE_DIR / "downloads"
|
|
LOGS_DIR = BASE_DIR / "logs"
|
|
|
|
THEMES_DIR = BASE_DIR / "themes"
|
|
EXTENSIONS_DIR = BASE_DIR / "extensions"
|
|
|
|
EXPORTS_DIR = BASE_DIR / "exports"
|
|
HAR_DIR = EXPORTS_DIR / "har"
|
|
|
|
DEFAULT_PROFILE = "default"
|
|
|
|
NETWORK_RETENTION = 100_000
|
|
NETWORK_BATCH_SIZE = 100
|
|
NETWORK_FLUSH_INTERVAL = 1.0
|
|
|
|
|
|
DISCORD_TELEMETRY_BLOCKS = [
|
|
"sentry.io",
|
|
"www.sentry.io",
|
|
"crash.discord.com",
|
|
"telemetry.discord.com",
|
|
"api.statsig.com",
|
|
"segment.io",
|
|
"cdn.segment.com",
|
|
]
|
|
|
|
DEFAULT_PROXY = {
|
|
"enabled": False,
|
|
"proxy_type": "http",
|
|
"host": "",
|
|
"port": 0,
|
|
"username": "",
|
|
"password": "",
|
|
}
|
|
|
|
DEFAULT_TRAY = {
|
|
"enabled": True,
|
|
"minimize_to_tray": True,
|
|
"close_to_tray": True,
|
|
}
|
|
|
|
DEFAULT_DEVTOOLS = {
|
|
"enabled": True,
|
|
"capture_requests": True,
|
|
"capture_responses": True,
|
|
"capture_bodies": False,
|
|
"auto_open": False,
|
|
}
|
|
|
|
DEFAULT_PERMISSIONS = {
|
|
"microphone": False,
|
|
"camera": False,
|
|
"notifications": False,
|
|
"clipboard": False,
|
|
}
|
|
|
|
DEFAULT_SETTINGS: dict[str, Any] = {
|
|
"active_profile": DEFAULT_PROFILE,
|
|
"window_width": WINDOW_WIDTH,
|
|
"window_height": WINDOW_HEIGHT,
|
|
"download_directory": str(DOWNLOADS_DIR),
|
|
"blocked_hosts": list(
|
|
DISCORD_TELEMETRY_BLOCKS
|
|
),
|
|
"network_logging": True,
|
|
"enable_cdp": False,
|
|
"enable_vencord": False,
|
|
"permissions": DEFAULT_PERMISSIONS,
|
|
"proxy": DEFAULT_PROXY,
|
|
"tray": DEFAULT_TRAY,
|
|
"devtools": DEFAULT_DEVTOOLS,
|
|
"blocklist_mode": "pro",
|
|
"disabled_blocklists": [],
|
|
}
|
|
|
|
FEATURE_PERMISSION_MAP = {
|
|
"MediaAudioCapture": "microphone",
|
|
"MediaVideoCapture": "camera",
|
|
"MediaAudioVideoCapture": "camera",
|
|
"Notifications": "notifications",
|
|
"ClipboardReadWrite": "clipboard",
|
|
"ClipboardSanitizedWrite": "clipboard",
|
|
}
|
|
|
|
PLUGIN_FILE_PATTERN = "*.plugin.js"
|
|
|
|
PLUGIN_MAX_SIZE = 5 * 1024 * 1024
|
|
|
|
PLUGIN_CRASH_LIMIT = 5
|
|
|
|
PLUGIN_AUTOLOAD = True
|
|
|
|
PLUGIN_ALLOWED_EXTENSIONS = {
|
|
".plugin.js",
|
|
}
|
|
|
|
PLUGIN_ALLOWED_SCHEMES = {
|
|
"https",
|
|
}
|
|
|
|
PLUGIN_BLOCKED_PATTERNS = {
|
|
"child_process.exec(",
|
|
"child_process.spawn(",
|
|
"powershell.exe",
|
|
"cmd.exe /c",
|
|
"process.exec(",
|
|
}
|
|
PLUGIN_PERMISSIONS = {
|
|
"storage",
|
|
"dom",
|
|
"network",
|
|
"websocket",
|
|
"notifications",
|
|
"clipboard",
|
|
}
|
|
PROFILE_EXPORT_VERSION = 1
|
|
|
|
PROFILE_EXPORT_NAME = (
|
|
"profilebundle"
|
|
)
|
|
|
|
# v1
|
|
BLOCKLISTS_DIR = (
|
|
BASE_DIR
|
|
/ "blocklists"
|
|
)
|
|
|
|
BLOCKLIST_CACHE_DIR = (
|
|
BLOCKLISTS_DIR
|
|
/ "cache"
|
|
)
|
|
def blocklists_dir(
|
|
) -> Path:
|
|
BLOCKLISTS_DIR.mkdir(
|
|
parents=True,
|
|
exist_ok=True,
|
|
)
|
|
return BLOCKLISTS_DIR
|
|
|
|
|
|
# v1
|
|
def blocklist_cache_dir(
|
|
) -> Path:
|
|
BLOCKLIST_CACHE_DIR.mkdir(
|
|
parents=True,
|
|
exist_ok=True,
|
|
)
|
|
return BLOCKLIST_CACHE_DIR
|
|
|
|
def ensure_directories() -> None:
|
|
for directory in (
|
|
BASE_DIR,
|
|
PROFILES_DIR,
|
|
DOWNLOADS_DIR,
|
|
LOGS_DIR,
|
|
THEMES_DIR,
|
|
EXTENSIONS_DIR,
|
|
EXPORTS_DIR,
|
|
HAR_DIR,
|
|
BLOCKLISTS_DIR,
|
|
BLOCKLIST_CACHE_DIR,
|
|
):
|
|
directory.mkdir(
|
|
parents=True,
|
|
exist_ok=True,
|
|
)
|
|
|
|
def merge_dict(
|
|
defaults: dict,
|
|
current: dict,
|
|
) -> dict:
|
|
merged = dict(defaults)
|
|
|
|
for key, value in current.items():
|
|
if (
|
|
key in merged
|
|
and isinstance(
|
|
merged[key],
|
|
dict,
|
|
)
|
|
and isinstance(
|
|
value,
|
|
dict,
|
|
)
|
|
):
|
|
merged[key] = merge_dict(
|
|
merged[key],
|
|
value,
|
|
)
|
|
else:
|
|
merged[key] = value
|
|
|
|
return merged
|
|
|
|
|
|
def _normalize_proxy(
|
|
settings: dict[str, Any],
|
|
) -> None:
|
|
proxy = settings.get(
|
|
"proxy",
|
|
{},
|
|
)
|
|
|
|
if not isinstance(
|
|
proxy,
|
|
dict,
|
|
):
|
|
settings["proxy"] = dict(
|
|
DEFAULT_PROXY
|
|
)
|
|
return
|
|
|
|
if (
|
|
"type" in proxy
|
|
and "proxy_type"
|
|
not in proxy
|
|
):
|
|
proxy["proxy_type"] = proxy.pop(
|
|
"type"
|
|
)
|
|
|
|
settings["proxy"] = merge_dict(
|
|
DEFAULT_PROXY,
|
|
proxy,
|
|
)
|
|
|
|
|
|
def _normalize_permissions(
|
|
settings: dict[str, Any],
|
|
) -> None:
|
|
permissions = settings.get(
|
|
"permissions",
|
|
{},
|
|
)
|
|
|
|
if not isinstance(
|
|
permissions,
|
|
dict,
|
|
):
|
|
permissions = {}
|
|
|
|
settings["permissions"] = merge_dict(
|
|
DEFAULT_PERMISSIONS,
|
|
permissions,
|
|
)
|
|
|
|
|
|
def _normalize_blocked_hosts(
|
|
settings: dict[str, Any],
|
|
) -> None:
|
|
hosts = {
|
|
str(host)
|
|
.strip()
|
|
.lower()
|
|
for host in settings.get(
|
|
"blocked_hosts",
|
|
[],
|
|
)
|
|
if str(host).strip()
|
|
}
|
|
|
|
hosts.update(
|
|
DISCORD_TELEMETRY_BLOCKS
|
|
)
|
|
|
|
settings["blocked_hosts"] = sorted(
|
|
hosts
|
|
)
|
|
|
|
|
|
def load_settings() -> dict[str, Any]:
|
|
ensure_directories()
|
|
|
|
if not SETTINGS_FILE.exists():
|
|
settings = merge_dict(
|
|
{},
|
|
DEFAULT_SETTINGS,
|
|
)
|
|
|
|
save_settings(
|
|
settings
|
|
)
|
|
|
|
return settings
|
|
|
|
try:
|
|
loaded = json.loads(
|
|
SETTINGS_FILE.read_text(
|
|
encoding="utf-8"
|
|
)
|
|
)
|
|
|
|
if not isinstance(
|
|
loaded,
|
|
dict,
|
|
):
|
|
raise ValueError
|
|
|
|
if (
|
|
"active_profile"
|
|
not in loaded
|
|
and "active_account"
|
|
in loaded
|
|
):
|
|
loaded[
|
|
"active_profile"
|
|
] = loaded[
|
|
"active_account"
|
|
]
|
|
|
|
settings = merge_dict(
|
|
DEFAULT_SETTINGS,
|
|
loaded,
|
|
)
|
|
|
|
if not settings.get(
|
|
"active_profile"
|
|
):
|
|
settings[
|
|
"active_profile"
|
|
] = DEFAULT_PROFILE
|
|
|
|
settings[
|
|
"download_directory"
|
|
] = str(
|
|
settings.get(
|
|
"download_directory"
|
|
)
|
|
or DOWNLOADS_DIR
|
|
)
|
|
|
|
_normalize_proxy(
|
|
settings
|
|
)
|
|
|
|
_normalize_permissions(
|
|
settings
|
|
)
|
|
|
|
_normalize_blocked_hosts(
|
|
settings
|
|
)
|
|
|
|
return settings
|
|
|
|
except Exception:
|
|
settings = merge_dict(
|
|
{},
|
|
DEFAULT_SETTINGS,
|
|
)
|
|
|
|
save_settings(
|
|
settings
|
|
)
|
|
|
|
return settings
|
|
|
|
|
|
def save_settings(
|
|
settings: dict[str, Any],
|
|
) -> None:
|
|
ensure_directories()
|
|
|
|
_normalize_proxy(
|
|
settings
|
|
)
|
|
|
|
_normalize_permissions(
|
|
settings
|
|
)
|
|
|
|
_normalize_blocked_hosts(
|
|
settings
|
|
)
|
|
|
|
SETTINGS_FILE.write_text(
|
|
json.dumps(
|
|
settings,
|
|
indent=2,
|
|
sort_keys=True,
|
|
ensure_ascii=False,
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
|
|
def profile_root(
|
|
profile_name: str,
|
|
) -> Path:
|
|
profile_name = (
|
|
str(profile_name).strip()
|
|
or DEFAULT_PROFILE
|
|
)
|
|
|
|
root = (
|
|
PROFILES_DIR
|
|
/ profile_name
|
|
)
|
|
|
|
root.mkdir(
|
|
parents=True,
|
|
exist_ok=True,
|
|
)
|
|
|
|
return root
|
|
|
|
|
|
def profile_paths(
|
|
profile_name: str,
|
|
) -> tuple[Path, Path]:
|
|
root = profile_root(
|
|
profile_name
|
|
)
|
|
|
|
storage = root / "storage"
|
|
cache = root / "cache"
|
|
|
|
storage.mkdir(
|
|
parents=True,
|
|
exist_ok=True,
|
|
)
|
|
|
|
cache.mkdir(
|
|
parents=True,
|
|
exist_ok=True,
|
|
)
|
|
|
|
return (
|
|
storage,
|
|
cache,
|
|
)
|
|
|
|
|
|
def profile_theme_dir(
|
|
profile_name: str,
|
|
) -> Path:
|
|
path = (
|
|
THEMES_DIR
|
|
/ profile_name
|
|
)
|
|
|
|
path.mkdir(
|
|
parents=True,
|
|
exist_ok=True,
|
|
)
|
|
|
|
return path
|
|
|
|
|
|
def profile_extension_dir(
|
|
profile_name: str,
|
|
) -> Path:
|
|
path = (
|
|
EXTENSIONS_DIR
|
|
/ profile_name
|
|
)
|
|
|
|
path.mkdir(
|
|
parents=True,
|
|
exist_ok=True,
|
|
)
|
|
|
|
return path
|
|
|
|
|
|
def profile_vencord_dir(
|
|
profile_name: str,
|
|
) -> Path:
|
|
path = (
|
|
profile_root(
|
|
profile_name
|
|
)
|
|
/ "vencord"
|
|
)
|
|
|
|
path.mkdir(
|
|
parents=True,
|
|
exist_ok=True,
|
|
)
|
|
|
|
for child in (
|
|
"plugins",
|
|
"themes",
|
|
"logs",
|
|
):
|
|
(
|
|
path / child
|
|
).mkdir(
|
|
parents=True,
|
|
exist_ok=True,
|
|
)
|
|
|
|
return path
|
|
|
|
|
|
def profile_plugin_dir(
|
|
profile_name: str,
|
|
) -> Path:
|
|
root = (
|
|
profile_vencord_dir(
|
|
profile_name
|
|
)
|
|
/ "plugins"
|
|
)
|
|
|
|
root.mkdir(
|
|
parents=True,
|
|
exist_ok=True,
|
|
)
|
|
|
|
return root |