mirror of
https://git.quad4.io/RNS-Things/MeshChatX.git
synced 2026-04-25 22:02:16 +00:00
refactor(code): clean up import statements, improve docstrings, and update error handling across various modules
This commit is contained in:
@@ -177,3 +177,21 @@ class AnnounceManager:
|
||||
|
||||
result = self.db.provider.fetchone(sql, params)
|
||||
return result["count"] if result else 0
|
||||
|
||||
|
||||
def filter_announced_dicts_by_search_query(
|
||||
items: list[dict],
|
||||
search_query: str,
|
||||
) -> list[dict]:
|
||||
"""Case-insensitive substring match on display name, hashes, and custom display name."""
|
||||
q = search_query.lower()
|
||||
return [
|
||||
a
|
||||
for a in items
|
||||
if (
|
||||
(a.get("display_name") and q in a["display_name"].lower())
|
||||
or (a.get("destination_hash") and q in a["destination_hash"].lower())
|
||||
or (a.get("identity_hash") and q in a["identity_hash"].lower())
|
||||
or (a.get("custom_display_name") and q in a["custom_display_name"].lower())
|
||||
)
|
||||
]
|
||||
|
||||
@@ -13,9 +13,10 @@ class AsyncUtils:
|
||||
|
||||
@staticmethod
|
||||
def apply_asyncio_313_patch():
|
||||
"""Apply a patch for asyncio on Python 3.13 to avoid a bug in sendfile with SSL.
|
||||
See: https://github.com/python/cpython/issues/124448
|
||||
And: https://github.com/aio-libs/aiohttp/issues/8863
|
||||
"""Patch asyncio on Python 3.13 to avoid sendfile + SSL failures.
|
||||
|
||||
See https://github.com/python/cpython/issues/124448 and
|
||||
https://github.com/aio-libs/aiohttp/issues/8863.
|
||||
"""
|
||||
if sys.version_info >= (3, 13):
|
||||
import asyncio.base_events
|
||||
|
||||
@@ -163,9 +163,9 @@ class Database:
|
||||
return False
|
||||
|
||||
def check_db_health_at_open(self, storage_path):
|
||||
"""
|
||||
Run integrity and baseline checks after opening the database.
|
||||
Returns a list of human-readable issue strings; empty if healthy.
|
||||
"""Run integrity and baseline checks after opening the database.
|
||||
|
||||
Returns human-readable issue strings; empty if healthy.
|
||||
"""
|
||||
issues = []
|
||||
try:
|
||||
@@ -210,9 +210,9 @@ class Database:
|
||||
return issues
|
||||
|
||||
def check_db_health_at_close(self, storage_path):
|
||||
"""
|
||||
Run health checks before closing the database (for logging only).
|
||||
Returns a list of issue strings; empty if healthy.
|
||||
"""Run health checks before closing the database (for logging only).
|
||||
|
||||
Returns issue strings; empty if healthy.
|
||||
"""
|
||||
issues = []
|
||||
try:
|
||||
@@ -234,7 +234,7 @@ class Database:
|
||||
baseline = self._read_backup_baseline(storage_path)
|
||||
if self._is_backup_suspicious(current, baseline):
|
||||
issues.append(
|
||||
"Database content anomaly detected at close. Consider restoring from backup."
|
||||
"Database content anomaly detected at close. Consider restoring from backup.",
|
||||
)
|
||||
_log.warning("DB close health check: content anomaly")
|
||||
else:
|
||||
@@ -392,7 +392,8 @@ class Database:
|
||||
if backup_path is None:
|
||||
if suspicious:
|
||||
backup_path = os.path.join(
|
||||
default_dir, f"backup-SUSPICIOUS-{timestamp}.zip"
|
||||
default_dir,
|
||||
f"backup-SUSPICIOUS-{timestamp}.zip",
|
||||
)
|
||||
else:
|
||||
backup_path = os.path.join(default_dir, f"backup-{timestamp}.zip")
|
||||
@@ -412,7 +413,7 @@ class Database:
|
||||
"Backup data-loss guard: current database looks wrong "
|
||||
f"(was {baseline.get('message_count')} messages / {baseline.get('total_bytes')} bytes, "
|
||||
f"now {current_stats.get('message_count')} / {current_stats.get('total_bytes')}). "
|
||||
"Wrote backup-SUSPICIOUS-*.zip; skipping rotation and baseline update. Check disk and DB."
|
||||
"Wrote backup-SUSPICIOUS-*.zip; skipping rotation and baseline update. Check disk and DB.",
|
||||
)
|
||||
result["suspicious"] = True
|
||||
result["baseline"] = baseline
|
||||
|
||||
@@ -114,7 +114,10 @@ class AccessAttemptsDAO:
|
||||
)
|
||||
|
||||
def count_login_attempts_ip(
|
||||
self, client_ip: str, path: str, since_ts: float
|
||||
self,
|
||||
client_ip: str,
|
||||
path: str,
|
||||
since_ts: float,
|
||||
) -> int:
|
||||
row = self.provider.fetchone(
|
||||
"""
|
||||
@@ -142,7 +145,10 @@ class AccessAttemptsDAO:
|
||||
return int(row["c"]) if row else 0
|
||||
|
||||
def count_lockout_failures(
|
||||
self, identity_hash: str, client_ip: str, since_ts: float
|
||||
self,
|
||||
identity_hash: str,
|
||||
client_ip: str,
|
||||
since_ts: float,
|
||||
) -> int:
|
||||
row = self.provider.fetchone(
|
||||
"""
|
||||
@@ -190,7 +196,9 @@ class AccessAttemptsDAO:
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
def count_attempts(
|
||||
self, search: str | None = None, outcome: str | None = None
|
||||
self,
|
||||
search: str | None = None,
|
||||
outcome: str | None = None,
|
||||
) -> int:
|
||||
sql = "SELECT COUNT(*) AS c FROM access_attempts WHERE 1=1"
|
||||
params: list[Any] = []
|
||||
|
||||
@@ -44,8 +44,9 @@ class LegacyMigrator:
|
||||
return None
|
||||
|
||||
def should_migrate(self):
|
||||
"""Check if migration should be performed.
|
||||
Only migrates if the current database is empty and a legacy database exists.
|
||||
"""Return whether migration should run.
|
||||
|
||||
Only migrates when the current database is empty and a legacy DB exists.
|
||||
"""
|
||||
legacy_path = self.get_legacy_db_path()
|
||||
if not legacy_path:
|
||||
@@ -77,7 +78,7 @@ class LegacyMigrator:
|
||||
# We use a randomized alias to avoid collisions
|
||||
alias = f"legacy_{os.urandom(4).hex()}"
|
||||
safe_path = legacy_path.replace("'", "''")
|
||||
self.provider.execute(f"ATTACH DATABASE '{safe_path}' AS {alias}") # noqa: S608
|
||||
self.provider.execute(f"ATTACH DATABASE '{safe_path}' AS {alias}")
|
||||
|
||||
# Tables that existed in the legacy Peewee version
|
||||
tables_to_migrate = [
|
||||
|
||||
@@ -44,7 +44,7 @@ class DatabaseSchema:
|
||||
|
||||
cursor = self.provider.connection.cursor()
|
||||
try:
|
||||
cursor.execute(f"PRAGMA table_info({table_name})") # noqa: S608
|
||||
cursor.execute(f"PRAGMA table_info({table_name})")
|
||||
columns = [row[1] for row in cursor.fetchall()]
|
||||
finally:
|
||||
cursor.close()
|
||||
@@ -67,7 +67,7 @@ class DatabaseSchema:
|
||||
).strip()
|
||||
|
||||
res = self._safe_execute(
|
||||
f"ALTER TABLE {table_name} ADD COLUMN {column_name} {stmt_type}", # noqa: S608
|
||||
f"ALTER TABLE {table_name} ADD COLUMN {column_name} {stmt_type}",
|
||||
)
|
||||
return res is not None
|
||||
except Exception as e:
|
||||
@@ -77,13 +77,14 @@ class DatabaseSchema:
|
||||
)
|
||||
return False
|
||||
return True
|
||||
return True
|
||||
|
||||
def _sync_table_columns(self, table_name, create_sql):
|
||||
"""Parses a CREATE TABLE statement and ensures all columns exist in the actual table.
|
||||
This is a robust way to handle legacy tables that are missing columns.
|
||||
"""Parse CREATE TABLE and add any missing columns to match the declaration.
|
||||
|
||||
Finds the column list between the first ``(`` and last ``)``, splits on
|
||||
commas outside nested parentheses (e.g. ``DECIMAL(10,2)``), then ensures
|
||||
each column exists on the actual table.
|
||||
"""
|
||||
# Find the first '(' and the last ')'
|
||||
start_idx = create_sql.find("(")
|
||||
end_idx = create_sql.rfind(")")
|
||||
|
||||
@@ -92,7 +93,6 @@ class DatabaseSchema:
|
||||
|
||||
inner_content = create_sql[start_idx + 1 : end_idx]
|
||||
|
||||
# Split by comma but ignore commas inside parentheses (e.g. DECIMAL(10,2))
|
||||
definitions = []
|
||||
depth = 0
|
||||
current = ""
|
||||
|
||||
@@ -54,7 +54,10 @@ class UserStickersDAO:
|
||||
return cur.rowcount
|
||||
|
||||
def update_name(
|
||||
self, sticker_id: int, identity_hash: str, name: str | None
|
||||
self,
|
||||
sticker_id: int,
|
||||
identity_hash: str,
|
||||
name: str | None,
|
||||
) -> bool:
|
||||
now = time.time()
|
||||
cur = self.provider.execute(
|
||||
@@ -75,9 +78,7 @@ class UserStickersDAO:
|
||||
image_bytes: bytes,
|
||||
source_message_hash: str | None = None,
|
||||
) -> dict | None:
|
||||
"""
|
||||
Insert a sticker. Returns summary dict or None if duplicate (same content_hash).
|
||||
"""
|
||||
"""Insert a sticker. Returns summary dict or None if duplicate (same content_hash)."""
|
||||
if (
|
||||
self.count_for_identity(identity_hash)
|
||||
>= sticker_utils.MAX_STICKERS_PER_IDENTITY
|
||||
|
||||
@@ -159,11 +159,11 @@ class IntegrityManager:
|
||||
and abs(actual_entropy - saved_entropy) > 1.0
|
||||
):
|
||||
issues.append(
|
||||
f"Database structural anomaly (Entropy Δ: {abs(actual_entropy - saved_entropy):.2f})"
|
||||
f"Database structural anomaly (Entropy Δ: {abs(actual_entropy - saved_entropy):.2f})",
|
||||
)
|
||||
else:
|
||||
issues.append(
|
||||
f"Database binary signature mismatch: {db_rel}"
|
||||
f"Database binary signature mismatch: {db_rel}",
|
||||
)
|
||||
|
||||
# Check other critical files in storage_dir
|
||||
@@ -185,10 +185,11 @@ class IntegrityManager:
|
||||
if actual_hash != manifest_files[rel_path]:
|
||||
actual_entropy = self._calculate_entropy(full_path)
|
||||
saved_entropy = manifest_metadata.get(rel_path, {}).get(
|
||||
"entropy"
|
||||
"entropy",
|
||||
)
|
||||
saved_size = manifest_metadata.get(rel_path, {}).get(
|
||||
"size", 0
|
||||
"size",
|
||||
0,
|
||||
)
|
||||
actual_size = full_path.stat().st_size
|
||||
|
||||
@@ -198,18 +199,18 @@ class IntegrityManager:
|
||||
|
||||
if is_critical:
|
||||
issues.append(
|
||||
f"Critical security component integrity compromised: {rel_path}"
|
||||
f"Critical security component integrity compromised: {rel_path}",
|
||||
)
|
||||
elif (
|
||||
saved_entropy is not None
|
||||
and abs(actual_entropy - saved_entropy) > 1.5
|
||||
):
|
||||
issues.append(
|
||||
f"Non-linear content shift detected in {rel_path} (Entropy Δ: {abs(actual_entropy - saved_entropy):.2f})"
|
||||
f"Non-linear content shift detected in {rel_path} (Entropy Δ: {abs(actual_entropy - saved_entropy):.2f})",
|
||||
)
|
||||
elif saved_size and actual_size != saved_size:
|
||||
issues.append(
|
||||
f"File size divergence: {rel_path} ({saved_size} -> {actual_size} bytes)"
|
||||
f"File size divergence: {rel_path} ({saved_size} -> {actual_size} bytes)",
|
||||
)
|
||||
else:
|
||||
issues.append(f"File signature mismatch: {rel_path}")
|
||||
|
||||
@@ -401,12 +401,11 @@ def convert_db_lxmf_message_to_dict(
|
||||
|
||||
|
||||
def compute_lxmf_conversation_unread_from_latest_row(row):
|
||||
"""
|
||||
Whether the conversation list should show unread for this latest-message row,
|
||||
using lxmf_conversation_read_state.last_read_at only.
|
||||
"""Return whether the conversation row should appear as unread.
|
||||
|
||||
Latest message must be incoming to be unread; if the last message is ours,
|
||||
the thread is not unread (matches filter_unread SQL in MessageHandler.get_conversations).
|
||||
Uses ``lxmf_conversation_read_state.last_read_at`` only. The latest message
|
||||
must be incoming; outbound-only threads are not unread (matches
|
||||
``filter_unread`` in ``MessageHandler.get_conversations``).
|
||||
"""
|
||||
from datetime import UTC, datetime
|
||||
|
||||
|
||||
@@ -10,15 +10,15 @@ from LXMF import LXMRouter
|
||||
|
||||
|
||||
def create_lxmf_router(identity, storagepath, propagation_cost=None):
|
||||
"""Creates an LXMF.LXMRouter instance safely, avoiding signal handler crashes
|
||||
when called from non-main threads.
|
||||
"""Construct an ``LXMF.LXMRouter`` without signal-handler crashes off the main thread.
|
||||
|
||||
``signal.signal`` only works on the main thread; on workers it is temporarily
|
||||
replaced with a no-op while the router is created.
|
||||
"""
|
||||
if propagation_cost is None:
|
||||
propagation_cost = 0
|
||||
|
||||
if threading.current_thread() != threading.main_thread():
|
||||
# signal.signal can only be called from the main thread in Python
|
||||
# We monkeypatch it temporarily to avoid the ValueError
|
||||
original_signal = signal.signal
|
||||
try:
|
||||
signal.signal = lambda s, h: None
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"""
|
||||
PageNode: Serves Micron pages and files over RNS.
|
||||
"""PageNode: Serves Micron pages and files over RNS.
|
||||
|
||||
Each PageNode owns an RNS Destination (SINGLE, IN) with the aspect
|
||||
nomadnetwork.node and registers per-page request handlers at
|
||||
@@ -19,7 +18,6 @@ import time
|
||||
|
||||
import RNS
|
||||
|
||||
|
||||
APP_NAME = "nomadnetwork"
|
||||
ASPECT = "node"
|
||||
DEFAULT_INDEX = "index.mu"
|
||||
@@ -58,6 +56,19 @@ def _safe_mesh_file_basename(name: str) -> str:
|
||||
return base
|
||||
|
||||
|
||||
def _reject_name_component_too_long(parent_dir: str, component: str) -> None:
|
||||
"""Raise ValueError if basename exceeds this directory's filename length limit."""
|
||||
try:
|
||||
if parent_dir and os.path.isdir(parent_dir):
|
||||
max_bytes = int(os.pathconf(parent_dir, "PC_NAME_MAX"))
|
||||
else:
|
||||
max_bytes = 255
|
||||
except (OSError, ValueError, TypeError, OverflowError):
|
||||
max_bytes = 255
|
||||
if len(os.fsencode(component)) > max_bytes:
|
||||
raise ValueError("name too long")
|
||||
|
||||
|
||||
class PageNode:
|
||||
"""A single page-serving node on the Reticulum mesh."""
|
||||
|
||||
@@ -163,9 +174,9 @@ class PageNode:
|
||||
self.active_links.remove(link)
|
||||
|
||||
def _ensure_local_path(self):
|
||||
"""
|
||||
Register the destination's identity in RNS.Identity.known_destinations
|
||||
so that Identity.recall() can resolve it for local link establishment.
|
||||
"""Register this identity in ``RNS.Identity.known_destinations``.
|
||||
|
||||
Lets ``Identity.recall()`` resolve the destination for local link setup.
|
||||
"""
|
||||
if not self.destination:
|
||||
return
|
||||
@@ -292,6 +303,7 @@ class PageNode:
|
||||
def add_page(self, name, content):
|
||||
"""Write a page file and register its request handler."""
|
||||
name = normalize_page_filename(name)
|
||||
_reject_name_component_too_long(self.pages_dir, name)
|
||||
page_path = os.path.join(self.pages_dir, name)
|
||||
if isinstance(content, str):
|
||||
content = content.encode("utf-8")
|
||||
@@ -305,6 +317,7 @@ class PageNode:
|
||||
"""Remove a page and deregister its request handler."""
|
||||
try:
|
||||
name = normalize_page_filename(name)
|
||||
_reject_name_component_too_long(self.pages_dir, name)
|
||||
except ValueError:
|
||||
return False
|
||||
page_path = os.path.join(self.pages_dir, name)
|
||||
@@ -329,17 +342,19 @@ class PageNode:
|
||||
"""Read and return a page's content."""
|
||||
try:
|
||||
name = normalize_page_filename(name)
|
||||
_reject_name_component_too_long(self.pages_dir, name)
|
||||
except ValueError:
|
||||
return None
|
||||
page_path = os.path.join(self.pages_dir, name)
|
||||
if not os.path.isfile(page_path):
|
||||
return None
|
||||
with open(page_path, "r", encoding="utf-8") as f:
|
||||
with open(page_path, encoding="utf-8") as f:
|
||||
return f.read()
|
||||
|
||||
def add_file(self, name, data):
|
||||
"""Write a file and register its request handler."""
|
||||
name = _safe_mesh_file_basename(name)
|
||||
_reject_name_component_too_long(self.files_dir, name)
|
||||
file_path = os.path.join(self.files_dir, name)
|
||||
mode = "wb" if isinstance(data, bytes) else "w"
|
||||
with open(file_path, mode) as f:
|
||||
@@ -352,6 +367,7 @@ class PageNode:
|
||||
"""Remove a file and deregister its request handler."""
|
||||
try:
|
||||
name = _safe_mesh_file_basename(name)
|
||||
_reject_name_component_too_long(self.files_dir, name)
|
||||
except ValueError:
|
||||
return False
|
||||
file_path = os.path.join(self.files_dir, name)
|
||||
@@ -413,5 +429,5 @@ class PageNode:
|
||||
config_path = os.path.join(base_dir, "config.json")
|
||||
if not os.path.isfile(config_path):
|
||||
return None
|
||||
with open(config_path, "r") as f:
|
||||
with open(config_path) as f:
|
||||
return json.load(f)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"""
|
||||
PageNodeManager: Manages the lifecycle of multiple PageNode instances.
|
||||
"""PageNodeManager: Manages the lifecycle of multiple PageNode instances.
|
||||
|
||||
Handles creation, deletion, persistence, start/stop, and announce
|
||||
scheduling for page nodes. Each node gets its own subdirectory under
|
||||
|
||||
@@ -250,7 +250,7 @@ class PersistentLogHandler(logging.Handler):
|
||||
"""Shannon entropy over log-level distribution in the last 60 seconds."""
|
||||
cutoff = time.monotonic() - 60.0
|
||||
with self.lock:
|
||||
counts = {lv: 0 for lv in _LOG_LEVELS}
|
||||
counts = dict.fromkeys(_LOG_LEVELS, 0)
|
||||
total = 0
|
||||
for ts, level in self._level_events:
|
||||
if ts >= cutoff:
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
"""CRASH RECOVERY & ADAPTIVE DIAGNOSTIC ENGINE
|
||||
--------------------------------------------------
|
||||
Diagnostic system for MeshChatX.
|
||||
"""Crash recovery and adaptive diagnostics for MeshChatX.
|
||||
|
||||
Uses Shannon Entropy, KL-Divergence, and Bayesian weight learning
|
||||
to diagnose application failures. Crash history is persisted and
|
||||
priors are refined over time using a conjugate Beta-Binomial model.
|
||||
Uses entropy, KL-divergence, and Bayesian weight learning. Crash history is
|
||||
persisted; priors refine over time (conjugate Beta-Binomial model).
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
@@ -36,9 +33,9 @@ _DEFAULT_PRIORS = {
|
||||
|
||||
|
||||
class CrashRecovery:
|
||||
"""A diagnostic utility that intercepts application crashes and provides
|
||||
meaningful error reports and system state analysis. Learns from crash
|
||||
history to refine root-cause probabilities over time.
|
||||
"""Intercept crashes and report diagnostics plus environment state.
|
||||
|
||||
Learns from crash history to refine root-cause probabilities over time.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -115,7 +112,13 @@ class CrashRecovery:
|
||||
return _DEFAULT_PRIORS.get(cause_key, 0.05)
|
||||
|
||||
def _persist_crash(
|
||||
self, error_type, error_msg, causes, symptoms, entropy, divergence
|
||||
self,
|
||||
error_type,
|
||||
error_msg,
|
||||
causes,
|
||||
symptoms,
|
||||
entropy,
|
||||
divergence,
|
||||
):
|
||||
"""Store crash event in crash_history for future learning."""
|
||||
if not self.database:
|
||||
@@ -271,9 +274,7 @@ class CrashRecovery:
|
||||
sys.exit(1)
|
||||
|
||||
def _analyze_cause(self, exc_type, exc_value, diagnosis):
|
||||
"""Uses heuristic pattern matching and Bayesian priors
|
||||
to determine the likely root cause of the application crash.
|
||||
"""
|
||||
"""Rank likely root causes using heuristics and Bayesian priors."""
|
||||
causes = []
|
||||
error_msg = str(exc_value).lower()
|
||||
error_type = exc_type.__name__.lower()
|
||||
@@ -395,7 +396,7 @@ class CrashRecovery:
|
||||
or (py_version.major == 3 and py_version.minor < 10),
|
||||
"legacy_kernel": "linux" in platform.system().lower()
|
||||
and (lambda m: m is not None and float(m.group(1)) < 4.0)(
|
||||
re.search(r"(\d+\.\d+)", platform.release())
|
||||
re.search(r"(\d+\.\d+)", platform.release()),
|
||||
),
|
||||
"attribute_error": "attributeerror" in error_type,
|
||||
}
|
||||
@@ -467,9 +468,7 @@ class CrashRecovery:
|
||||
return causes
|
||||
|
||||
def _calculate_system_entropy(self, diagnosis):
|
||||
"""Calculates a heuristic system state entropy and KL-Divergence.
|
||||
Provides a mathematical measure of both disorder and 'surprise' (Information Gain).
|
||||
"""
|
||||
"""Return heuristic system entropy and KL-divergence (information gain)."""
|
||||
import math
|
||||
|
||||
def h(p):
|
||||
|
||||
@@ -81,7 +81,7 @@ class HealthMonitor:
|
||||
"kind": "entropy_climbing",
|
||||
"message": f"Log entropy rising: {entropy:.2f} bits (threshold {self.ENTROPY_WARN_THRESHOLD})",
|
||||
"value": round(entropy, 4),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
if self._consecutive_above(self._error_rate_history, self.ERROR_RATE_WARN):
|
||||
@@ -90,7 +90,7 @@ class HealthMonitor:
|
||||
"kind": "error_rate_high",
|
||||
"message": f"Error rate elevated: {error_rate:.0%}",
|
||||
"value": round(error_rate, 4),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
if self._consecutive_below(self._mem_available_history, self.MEMORY_WARN_MB):
|
||||
@@ -99,7 +99,7 @@ class HealthMonitor:
|
||||
"kind": "memory_low",
|
||||
"message": f"Available memory low: {available_mb:.0f} MB",
|
||||
"value": round(available_mb, 1),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
for w in warnings:
|
||||
@@ -107,8 +107,7 @@ class HealthMonitor:
|
||||
self._broadcast(w)
|
||||
|
||||
def _detect_entropy_climb(self):
|
||||
"""True if 3+ consecutive entropy readings are strictly increasing
|
||||
AND the latest exceeds the warning threshold."""
|
||||
"""Return True when entropy climbs in 3+ steps above the warn threshold."""
|
||||
h = self._entropy_history
|
||||
if len(h) < 3:
|
||||
return False
|
||||
@@ -140,8 +139,8 @@ class HealthMonitor:
|
||||
|
||||
AsyncUtils.run_async(
|
||||
self.app.websocket_broadcast(
|
||||
json.dumps({"type": "health_warning", "data": warning_data})
|
||||
)
|
||||
json.dumps({"type": "health_warning", "data": warning_data}),
|
||||
),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -227,7 +227,7 @@ class RNCPHandler:
|
||||
if data.startswith(self.fetch_jail + "/"):
|
||||
data = data.replace(self.fetch_jail + "/", "")
|
||||
file_path = os.path.realpath(
|
||||
os.path.expanduser(f"{self.fetch_jail}/{data}")
|
||||
os.path.expanduser(f"{self.fetch_jail}/{data}"),
|
||||
)
|
||||
jail_real = os.path.realpath(self.fetch_jail)
|
||||
if not file_path.startswith(jail_real + "/"):
|
||||
|
||||
@@ -274,15 +274,15 @@ class RNStatusHandler:
|
||||
|
||||
if "incoming_announce_frequency" in ifstat:
|
||||
formatted_if["incoming_announce_frequency"] = fmt_per_second(
|
||||
ifstat["incoming_announce_frequency"]
|
||||
ifstat["incoming_announce_frequency"],
|
||||
)
|
||||
if "outgoing_announce_frequency" in ifstat:
|
||||
formatted_if["outgoing_announce_frequency"] = fmt_per_second(
|
||||
ifstat["outgoing_announce_frequency"]
|
||||
ifstat["outgoing_announce_frequency"],
|
||||
)
|
||||
if "held_announces" in ifstat:
|
||||
formatted_if["held_announces"] = fmt_packet_count(
|
||||
ifstat["held_announces"]
|
||||
ifstat["held_announces"],
|
||||
)
|
||||
|
||||
if "ifac_netname" in ifstat and ifstat["ifac_netname"] is not None:
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import base64
|
||||
import hashlib
|
||||
|
||||
MAX_STICKER_BYTES = 512 * 1024
|
||||
MAX_STICKERS_PER_IDENTITY = 2000
|
||||
@@ -35,9 +35,9 @@ def content_hash_hex(image_bytes: bytes) -> str:
|
||||
|
||||
|
||||
def detect_image_format_from_magic(image_bytes: bytes) -> str | None:
|
||||
"""
|
||||
Identify image format from file signature (magic bytes). Returns normalized
|
||||
type key (png, jpeg, gif, webp, bmp) or None if unknown / too short / not allowed.
|
||||
"""Detect image format from magic bytes.
|
||||
|
||||
Returns a normalized type key (png, jpeg, gif, webp, bmp), or None.
|
||||
"""
|
||||
if not isinstance(image_bytes, (bytes, bytearray)) or len(image_bytes) < 4:
|
||||
return None
|
||||
@@ -59,8 +59,7 @@ def validate_sticker_payload(
|
||||
image_bytes: bytes,
|
||||
image_type: str | None,
|
||||
) -> tuple[str, str]:
|
||||
"""
|
||||
Returns (normalized_image_type, content_hash_hex).
|
||||
"""Returns (normalized_image_type, content_hash_hex).
|
||||
|
||||
Declared image_type must match the format detected from magic bytes; stored
|
||||
type is the normalized detected format.
|
||||
@@ -99,10 +98,10 @@ _EXPORT_VERSION = 1
|
||||
|
||||
|
||||
def validate_export_document(data: object) -> list[dict]:
|
||||
"""
|
||||
Parse and lightly validate an import JSON document.
|
||||
Returns a list of sticker dicts with keys: name, image_type, image_bytes (str base64),
|
||||
source_message_hash (optional).
|
||||
"""Parse and validate a sticker export JSON document.
|
||||
|
||||
Each sticker dict has ``name``, ``image_type``, ``image_bytes`` (base64), and
|
||||
optional ``source_message_hash``.
|
||||
"""
|
||||
if not isinstance(data, dict):
|
||||
msg = "invalid_document"
|
||||
|
||||
@@ -249,7 +249,7 @@ class TelephoneManager:
|
||||
announce = self.db.announces.get_announce_by_hash(canonical)
|
||||
if not announce:
|
||||
# 3) By identity_hash field (if user entered identity hash but we missed recall, or other announce types)
|
||||
id_key = canonical if canonical else th
|
||||
id_key = canonical or th
|
||||
announces = self.db.announces.get_filtered_announces(
|
||||
identity_hash=id_key,
|
||||
)
|
||||
|
||||
@@ -60,7 +60,7 @@ class VoicemailManager:
|
||||
RNS.log("Voicemail: ffmpeg not found", RNS.LOG_ERROR)
|
||||
|
||||
def get_name_for_identity_hash(self, identity_hash):
|
||||
"""Default implementation, should be patched by ReticulumMeshChat"""
|
||||
"""Default implementation, should be patched by ReticulumMeshChat."""
|
||||
return
|
||||
|
||||
def _find_bundled_binary(self, name):
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Version string synced from package.json. Do not edit by hand.
|
||||
Run: pnpm run version:sync
|
||||
"""Version string synced from package.json.
|
||||
|
||||
Do not edit by hand. Run: ``pnpm run version:sync``.
|
||||
"""
|
||||
|
||||
__version__ = "4.4.0"
|
||||
|
||||
Reference in New Issue
Block a user