mirror of
https://github.com/Quad4-Software/MeshChatX.git
synced 2026-08-18 09:49:09 -04:00
refactor: cleanup comments
This commit is contained in:
parent
a9c6aa244d
commit
591b6efa40
92 changed files with 396 additions and 397 deletions
|
|
@ -1,7 +1,7 @@
|
|||
# SPDX-License-Identifier: MIT
|
||||
"""Minimal Android BLE stack for RNS RNodeInterface on Chaquopy.
|
||||
|
||||
API-compatible with the subset of ``able`` that Reticulum's Android
|
||||
API-compatible with the subset of able that Reticulum's Android
|
||||
RNodeInterface imports. Uses org.able.BLE (Java) plus Chaquopy proxies
|
||||
instead of Kivy / pyjnius.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# SPDX-License-Identifier: 0BSD
|
||||
"""PyJNIus-compatible facade over Chaquopy's ``java`` module.
|
||||
"""PyJNIus-compatible facade over Chaquopy's java module.
|
||||
|
||||
RNS, usb4a, and related Android serial/Bluetooth code import ``jnius``.
|
||||
RNS, usb4a, and related Android serial/Bluetooth code import jnius.
|
||||
Chaquopy does not ship pyjnius. Map the small surface those libraries need
|
||||
onto Chaquopy's native Java bridge so RNode USB and classic Bluetooth work.
|
||||
"""
|
||||
|
|
@ -18,12 +18,12 @@ except ImportError as exc: # pragma: no cover - desktop import path
|
|||
|
||||
|
||||
def autoclass(class_name: str):
|
||||
"""Return a Java class, matching pyjnius ``autoclass``."""
|
||||
"""Return a Java class, matching pyjnius autoclass."""
|
||||
return jclass(class_name)
|
||||
|
||||
|
||||
def cast(cls, obj):
|
||||
"""Cast ``obj`` to ``cls``, accepting a class name string like pyjnius."""
|
||||
"""Cast obj to cls, accepting a class name string like pyjnius."""
|
||||
if isinstance(cls, str):
|
||||
cls = jclass(cls)
|
||||
return _java_cast(cls, obj)
|
||||
|
|
@ -39,9 +39,9 @@ def java_method(_signature):
|
|||
|
||||
|
||||
class PythonJavaClass:
|
||||
"""Base that rebinds subclasses onto ``java.dynamic_proxy`` interfaces.
|
||||
"""Base that rebinds subclasses onto java.dynamic_proxy interfaces.
|
||||
|
||||
Subclasses set ``__javainterfaces__`` to a list of Java interface names
|
||||
Subclasses set __javainterfaces__ to a list of Java interface names
|
||||
(dot or slash form). Instantiation switches the instance class bases so
|
||||
method implementations are visible to Java callers.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# SPDX-License-Identifier: MIT
|
||||
"""USB helpers for Android (Chaquopy build of usb4a).
|
||||
|
||||
Upstream usb4a expects Kivy ``PythonActivity`` via pyjnius. MeshChatX injects
|
||||
Upstream usb4a expects Kivy PythonActivity via pyjnius. MeshChatX injects
|
||||
the Activity context at startup and uses the Chaquopy jnius shim instead.
|
||||
"""
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"""USB module for Android (Chaquopy / MeshChatX).
|
||||
|
||||
Based on usb4a 0.3.0 by Quan Lin. Context comes from MeshChatX instead of
|
||||
Kivy ``org.kivy.android.PythonActivity``.
|
||||
Kivy org.kivy.android.PythonActivity.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
|
|||
BIN
meshchatx.rsm
BIN
meshchatx.rsm
Binary file not shown.
|
|
@ -38,7 +38,7 @@ def _cdll_load(path_or_name: str):
|
|||
def _libcodec2_candidates() -> list[Path]:
|
||||
"""Return candidate paths for libcodec2.so without importing pycodec2.
|
||||
|
||||
``import pycodec2`` loads the extension which already needs libcodec2.so.
|
||||
import pycodec2 loads the extension which already needs libcodec2.so.
|
||||
Searching sys.path on disk avoids that chicken-and-egg failure.
|
||||
"""
|
||||
candidates: list[Path] = []
|
||||
|
|
@ -62,12 +62,12 @@ def _libcodec2_candidates() -> list[Path]:
|
|||
|
||||
|
||||
def ensure_codec2_native_library() -> bool:
|
||||
"""Preload ``libcodec2.so`` so ``import pycodec2`` works on Android.
|
||||
"""Preload libcodec2.so so import pycodec2 works on Android.
|
||||
|
||||
Chaquopy installs ``chaquopy-libcodec2`` separately from ``pycodec2``. The
|
||||
extension module only declares a NEEDED entry for ``libcodec2.so``. Without
|
||||
preloading or bundling the shared library next to ``pycodec2.so``, imports
|
||||
fail at runtime with ``dlopen`` errors.
|
||||
Chaquopy installs chaquopy-libcodec2 separately from pycodec2. The
|
||||
extension module only declares a NEEDED entry for libcodec2.so. Without
|
||||
preloading or bundling the shared library next to pycodec2.so, imports
|
||||
fail at runtime with dlopen errors.
|
||||
"""
|
||||
global _codec2_preload_done, _codec2_preload_error
|
||||
|
||||
|
|
|
|||
|
|
@ -304,15 +304,15 @@ def _resolve_rns_loglevel(cli_override: str | None) -> int | None:
|
|||
|
||||
|
||||
def _restore_rns_console_logging_after_reticulum_init(app) -> None:
|
||||
"""Undo shutdown side effects from ``RNS.Reticulum.exit_handler``.
|
||||
"""Undo shutdown side effects from RNS.Reticulum.exit_handler.
|
||||
|
||||
That handler sets ``RNS.loglevel`` to ``LOG_NONE`` and points ``sys.stdout`` /
|
||||
``sys.stderr`` at ``os.devnull``. Without this, hot reload appears to stop all
|
||||
That handler sets RNS.loglevel to LOG_NONE and points sys.stdout /
|
||||
sys.stderr at os.devnull. Without this, hot reload appears to stop all
|
||||
announce traffic logging even though interfaces are up.
|
||||
|
||||
When no CLI or ``MESHCHAT_RNS_LOG_LEVEL`` value applies and the level is still
|
||||
``LOG_NONE`` after reading config, fall back to ``LOG_WARNING`` so notices are
|
||||
visible. Explicit ``none`` in the environment remains respected.
|
||||
When no CLI or MESHCHAT_RNS_LOG_LEVEL value applies and the level is still
|
||||
LOG_NONE after reading config, fall back to LOG_WARNING so notices are
|
||||
visible. Explicit none in the environment remains respected.
|
||||
"""
|
||||
try:
|
||||
if hasattr(sys, "__stdout__"):
|
||||
|
|
@ -327,16 +327,16 @@ def _restore_rns_console_logging_after_reticulum_init(app) -> None:
|
|||
|
||||
|
||||
def _create_reticulum_instance(config_dir: str, loglevel: int | None = None):
|
||||
"""Construct ``RNS.Reticulum`` even when called off the main thread.
|
||||
"""Construct RNS.Reticulum even when called off the main thread.
|
||||
|
||||
Reticulum registers SIGINT/SIGTERM handlers in ``__init__``. Python only allows
|
||||
``signal.signal`` on the main thread, so deferred network setup must skip that
|
||||
Reticulum registers SIGINT/SIGTERM handlers in __init__. Python only allows
|
||||
signal.signal on the main thread, so deferred network setup must skip that
|
||||
registration when running in a background worker and install handlers later.
|
||||
|
||||
On failure, progressively disables risky interfaces (I2P, unsupported RNode,
|
||||
AutoInterface, etc.) and retries so Android/desktop can recover without
|
||||
wiping app data or the whole ``.reticulum`` tree. ``RNS.panic`` is contained
|
||||
so it cannot ``os._exit`` the MeshChatX process.
|
||||
wiping app data or the whole .reticulum tree. RNS.panic is contained
|
||||
so it cannot os._exit the MeshChatX process.
|
||||
"""
|
||||
kwargs = {}
|
||||
if loglevel is not None:
|
||||
|
|
@ -382,10 +382,10 @@ def list_host_network_interfaces():
|
|||
"""Enumerate kernel network interfaces on the host running MeshChat.
|
||||
|
||||
Uses psutil (Linux, macOS, Windows). Fails soft on restricted environments
|
||||
(e.g. some Android sandboxes) and returns ``([], error)``.
|
||||
(e.g. some Android sandboxes) and returns ([], error).
|
||||
|
||||
Reticulum's ``device`` field on server-style interfaces is a *single* interface
|
||||
name, or omitted when binding only via ``listen_ip``.
|
||||
Reticulum's device field on server-style interfaces is a *single* interface
|
||||
name, or omitted when binding only via listen_ip.
|
||||
"""
|
||||
try:
|
||||
raw = psutil.net_if_addrs()
|
||||
|
|
@ -873,7 +873,7 @@ class ReticulumMeshChat:
|
|||
"""Create, start, stop, and delete an Echo bot subprocess.
|
||||
|
||||
Uses an isolated identity + Reticulum config under storage so the check
|
||||
does not touch user bots or ``~/.reticulum``.
|
||||
does not touch user bots or ~/.reticulum.
|
||||
"""
|
||||
from meshchatx.src.backend.bot_handler import BotHandler
|
||||
|
||||
|
|
@ -1253,9 +1253,9 @@ class ReticulumMeshChat:
|
|||
|
||||
@staticmethod
|
||||
def _write_rns_reticulum_default_config_file(config_path: str) -> str:
|
||||
"""Write RNS stock default config to ``config_path``; return on-disk text.
|
||||
"""Write RNS stock default config to config_path; return on-disk text.
|
||||
|
||||
Uses the same template and ConfigObj path as ``Reticulum.__create_default_config``.
|
||||
Uses the same template and ConfigObj path as Reticulum.__create_default_config.
|
||||
"""
|
||||
from RNS.vendor.configobj import ConfigObj
|
||||
|
||||
|
|
@ -1339,13 +1339,13 @@ class ReticulumMeshChat:
|
|||
return disable_rnode_interfaces_in_config(config_path, is_android=True)
|
||||
|
||||
def _ensure_reticulum_config(self, materialize: bool = True):
|
||||
"""Normalize ``reticulum_config_dir`` and optionally ensure a ``config`` file exists.
|
||||
"""Normalize reticulum_config_dir and optionally ensure a config file exists.
|
||||
|
||||
When ``materialize`` is true (default), write RNS stock defaults if the file
|
||||
When materialize is true (default), write RNS stock defaults if the file
|
||||
is missing or lacks required sections so first Reticulum startup is reliable.
|
||||
|
||||
API handlers that must distinguish a missing file (e.g. raw config GET) pass
|
||||
``materialize=False`` to only normalize the directory path.
|
||||
materialize=False to only normalize the directory path.
|
||||
"""
|
||||
config_dir = self._normalize_reticulum_config_dir(self.reticulum_config_dir)
|
||||
self.reticulum_config_dir = config_dir
|
||||
|
|
@ -1897,8 +1897,8 @@ class ReticulumMeshChat:
|
|||
def _reset_transport_globals_for_reload() -> None:
|
||||
"""Clear RNS Transport globals so a new Reticulum can start cleanly.
|
||||
|
||||
``Reticulum.exit_handler`` sets ``Transport._should_run = False``. Upstream
|
||||
``Transport.start`` never flips it back, so hot reload must restore it or
|
||||
Reticulum.exit_handler sets Transport._should_run = False. Upstream
|
||||
Transport.start never flips it back, so hot reload must restore it or
|
||||
the new jobloop exits immediately and path/link tools stay dead while
|
||||
interface RX/TX counters still update.
|
||||
"""
|
||||
|
|
@ -1931,8 +1931,8 @@ class ReticulumMeshChat:
|
|||
def _looks_like_meshchat_hot_reload_tail(pid: int, epoch: int) -> bool:
|
||||
"""Limit repairs to suffixes :meth:`reload_reticulum` actually writes.
|
||||
|
||||
Hot reload uses ``-reload-{os.getpid()}-{int(time.time())}``. Names like
|
||||
``my-net-reload-peer`` must not be truncated.
|
||||
Hot reload uses -reload-{os.getpid()}-{int(time.time())}. Names like
|
||||
my-net-reload-peer must not be truncated.
|
||||
"""
|
||||
if pid < 1 or pid > ReticulumMeshChat._meshchat_reload_pid_max:
|
||||
return False
|
||||
|
|
@ -1984,7 +1984,7 @@ class ReticulumMeshChat:
|
|||
return cp.get("reticulum", "instance_name", fallback=None)
|
||||
|
||||
def _repair_reticulum_instance_name_corruption(self):
|
||||
"""Rewrite persisted ``instance_name`` if hot-reload suffixes were left on disk."""
|
||||
"""Rewrite persisted instance_name if hot-reload suffixes were left on disk."""
|
||||
raw = self._read_reticulum_instance_name()
|
||||
if not raw:
|
||||
return
|
||||
|
|
@ -2772,7 +2772,7 @@ class ReticulumMeshChat:
|
|||
"""Resolve an installed distribution version for About /app/info.
|
||||
|
||||
cx_Freeze and similar bundles often omit .dist-info; fall back to module
|
||||
attributes and known submodule layouts (e.g. ``websockets.version``).
|
||||
attributes and known submodule layouts (e.g. websockets.version).
|
||||
"""
|
||||
try:
|
||||
from packaging.utils import canonicalize_name as _canonicalize_name
|
||||
|
|
@ -3051,14 +3051,14 @@ class ReticulumMeshChat:
|
|||
"""Surface IFAC fields from discovery announces in a frontend-friendly shape.
|
||||
|
||||
RNS publishes IFAC values in discovered interface dicts as
|
||||
``ifac_netname`` and ``ifac_netkey`` (when the publishing interface
|
||||
sets ``publish_ifac = yes``). The Reticulum config file uses
|
||||
``network_name`` / ``passphrase`` instead. This helper keeps the raw
|
||||
ifac_netname and ifac_netkey (when the publishing interface
|
||||
sets publish_ifac = yes). The Reticulum config file uses
|
||||
network_name / passphrase instead. This helper keeps the raw
|
||||
RNS keys for backwards compatibility but also exposes the canonical
|
||||
config-style aliases (``network_name`` and ``passphrase``) and ensures
|
||||
the optional ``config_entry`` blob is always a string when present.
|
||||
config-style aliases (network_name and passphrase) and ensures
|
||||
the optional config_entry blob is always a string when present.
|
||||
|
||||
Returns the list with new keys added; missing values become ``None``
|
||||
Returns the list with new keys added; missing values become None
|
||||
so the frontend can render placeholders consistently.
|
||||
"""
|
||||
if not isinstance(interfaces, list):
|
||||
|
|
@ -9564,8 +9564,8 @@ class ReticulumMeshChat:
|
|||
async def reticulum_config_raw_put(request):
|
||||
"""Persist new raw text to the Reticulum config file.
|
||||
|
||||
The body must be JSON with a ``content`` string. Basic validation
|
||||
requires the ``[reticulum]`` and ``[interfaces]`` sections so we
|
||||
The body must be JSON with a content string. Basic validation
|
||||
requires the [reticulum] and [interfaces] sections so we
|
||||
do not write a config that would prevent RNS from starting on the
|
||||
next reload.
|
||||
"""
|
||||
|
|
@ -20463,9 +20463,9 @@ class ReticulumMeshChat:
|
|||
def _identity_from_public_key_bytes(public_key: bytes) -> RNS.Identity | None:
|
||||
"""Load an RNS Identity from raw public-key bytes.
|
||||
|
||||
``Identity.load_public_key`` is documented as returning True/False, but
|
||||
current RNS releases return ``None`` on both success and failure. Treat
|
||||
a non-None ``identity.pub`` (and a computed hash) as success.
|
||||
Identity.load_public_key is documented as returning True/False, but
|
||||
current RNS releases return None on both success and failure. Treat
|
||||
a non-None identity.pub (and a computed hash) as success.
|
||||
"""
|
||||
if not public_key:
|
||||
return None
|
||||
|
|
@ -20833,7 +20833,7 @@ class ReticulumMeshChat:
|
|||
"""Resolve a contact for an identity or destination hash.
|
||||
|
||||
Contacts are often saved with an LXMF destination hash as
|
||||
``remote_identity_hash`` (from chat UI). Incoming calls provide the
|
||||
remote_identity_hash (from chat UI). Incoming calls provide the
|
||||
caller's identity hash. Bridge those forms via announces and derived
|
||||
destination hashes so contacts-only call policy works.
|
||||
"""
|
||||
|
|
@ -20891,7 +20891,7 @@ class ReticulumMeshChat:
|
|||
return encoded
|
||||
|
||||
def is_destination_blocked(self, destination_hash: str, context=None) -> bool:
|
||||
"""Return whether ``destination_hash`` is in the block list.
|
||||
"""Return whether destination_hash is in the block list.
|
||||
|
||||
Accepts either a destination hash or an identity hash. When an identity
|
||||
hash is passed, any blocked destination belonging to that identity will
|
||||
|
|
@ -22860,7 +22860,7 @@ class ReticulumMeshChat:
|
|||
announce_packet_hash,
|
||||
context=None,
|
||||
):
|
||||
"""Handle Relay Chat ``rrc.hub`` announces for hub discovery."""
|
||||
"""Handle Relay Chat rrc.hub announces for hub discovery."""
|
||||
ctx = context or self.current_context
|
||||
if not ctx or not ctx.running or not ctx.announce_manager or not ctx.database:
|
||||
return
|
||||
|
|
@ -22997,7 +22997,7 @@ class ReticulumMeshChat:
|
|||
def _try_serve_local_page_node_file(self, destination_hash, file_path):
|
||||
"""Serve a file from disk when the hash matches a local page node.
|
||||
|
||||
Returns ``(file_name, file_bytes)``, or None.
|
||||
Returns (file_name, file_bytes), or None.
|
||||
"""
|
||||
for node in self.page_node_manager.nodes.values():
|
||||
if not node.running or not node.destination:
|
||||
|
|
@ -23112,9 +23112,9 @@ class ReticulumMeshChat:
|
|||
def _maybe_run_embedded_module():
|
||||
"""Re-enter a bundled Python module from a frozen MeshChatX executable.
|
||||
|
||||
Desktop builds set ``sys.executable`` to MeshChatX itself, so
|
||||
``python -m …`` cannot be used to start tools like rnsh. Callers pass
|
||||
``--meshchatx-run-module <dotted.name>`` followed by that module's argv.
|
||||
Desktop builds set sys.executable to MeshChatX itself, so
|
||||
python -m … cannot be used to start tools like rnsh. Callers pass
|
||||
--meshchatx-run-module <dotted.name> followed by that module's argv.
|
||||
"""
|
||||
marker = "--meshchatx-run-module"
|
||||
if len(sys.argv) < 3 or sys.argv[1] != marker:
|
||||
|
|
|
|||
|
|
@ -3,20 +3,20 @@
|
|||
|
||||
This module replaces the previous ffmpeg subprocess pipelines used for
|
||||
voicemail greetings, ringtones and browser-recorded voice messages. It
|
||||
exposes a small API that decodes user-supplied audio into ``float32`` PCM
|
||||
exposes a small API that decodes user-supplied audio into float32 PCM
|
||||
frames and encodes PCM frames into LXMF-compatible OGG/Opus files using
|
||||
``LXST.Sinks.OpusFileSink``.
|
||||
LXST.Sinks.OpusFileSink.
|
||||
|
||||
Decoders, in priority order:
|
||||
|
||||
1. ``wave`` (built-in) for ``RIFF/WAVE`` containers.
|
||||
1. wave (built-in) for RIFF/WAVE containers.
|
||||
2. `miniaudio <https://pypi.org/project/miniaudio/>`_ for WAV, MP3, FLAC
|
||||
and OGG/Vorbis. Bundled as a runtime dependency on every supported
|
||||
target (including Android via the Chaquopy recipe under
|
||||
``android/chaquopy-recipes/miniaudio-1.70``).
|
||||
3. LXST/pyogg ``OpusFile`` for OGG/Opus payloads.
|
||||
android/chaquopy-recipes/miniaudio-1.70).
|
||||
3. LXST/pyogg OpusFile for OGG/Opus payloads.
|
||||
|
||||
Encoder: ``LXST.Sinks.OpusFileSink`` configured with a voice-friendly
|
||||
Encoder: LXST.Sinks.OpusFileSink configured with a voice-friendly
|
||||
Opus profile so the output is a valid OGG/Opus file that Sideband and
|
||||
the rest of the LXMF ecosystem can play.
|
||||
"""
|
||||
|
|
@ -35,8 +35,8 @@ class DecodedAudio:
|
|||
"""A decoded audio buffer.
|
||||
|
||||
Attributes:
|
||||
samples: ``float32`` numpy array shaped ``(frames, channels)`` with
|
||||
values in ``[-1.0, 1.0]``.
|
||||
samples: float32 numpy array shaped (frames, channels) with
|
||||
values in [-1.0, 1.0].
|
||||
samplerate: PCM sample rate in Hz.
|
||||
channels: Channel count.
|
||||
|
||||
|
|
@ -48,7 +48,7 @@ class DecodedAudio:
|
|||
|
||||
|
||||
def _read_bytes(source) -> bytes:
|
||||
"""Return the bytes for ``source`` (path, file-like or bytes)."""
|
||||
"""Return the bytes for source (path, file-like or bytes)."""
|
||||
if isinstance(source, (bytes, bytearray, memoryview)):
|
||||
return bytes(source)
|
||||
if hasattr(source, "read"):
|
||||
|
|
@ -156,10 +156,10 @@ def _decode_with_lxst_opus(data: bytes):
|
|||
|
||||
|
||||
def decode_audio(source) -> DecodedAudio:
|
||||
"""Decode ``source`` into ``float32`` PCM frames.
|
||||
"""Decode source into float32 PCM frames.
|
||||
|
||||
``source`` may be a path (``str``/``os.PathLike``), a file-like object
|
||||
open in binary mode, or a ``bytes``/``bytearray`` payload.
|
||||
source may be a path (str/os.PathLike), a file-like object
|
||||
open in binary mode, or a bytes/bytearray payload.
|
||||
|
||||
Raises:
|
||||
ValueError: If the payload could not be decoded by any backend.
|
||||
|
|
@ -191,9 +191,9 @@ def _normalize_for_opus(
|
|||
target_rate: int = _OPUS_TARGET_RATE,
|
||||
target_channels: int = 1,
|
||||
):
|
||||
"""Resample/remix ``samples`` to a layout the chosen Opus profile accepts.
|
||||
"""Resample/remix samples to a layout the chosen Opus profile accepts.
|
||||
|
||||
Returns ``float32`` frames at ``target_rate`` with ``target_channels``
|
||||
Returns float32 frames at target_rate with target_channels
|
||||
channels. Multi-channel input is downmixed to mono by averaging,
|
||||
mono input is duplicated when the profile expects stereo, and the
|
||||
rate is resampled via LXST's helper to stay consistent with the
|
||||
|
|
@ -243,16 +243,16 @@ def encode_pcm_to_ogg_opus(
|
|||
profile=None,
|
||||
frame_ms: int = 60,
|
||||
) -> str:
|
||||
"""Encode ``samples`` (``float32`` ``(frames, channels)``) to OGG/Opus.
|
||||
"""Encode samples (float32 (frames, channels)) to OGG/Opus.
|
||||
|
||||
``profile`` defaults to ``LXST.Codecs.Opus.PROFILE_VOICE_HIGH`` (48 kHz
|
||||
profile defaults to LXST.Codecs.Opus.PROFILE_VOICE_HIGH (48 kHz
|
||||
mono voip, ~16 kbps ceiling) which keeps voice payloads small while
|
||||
remaining intelligible. Inputs at any sample rate or channel count are
|
||||
transparently resampled/remixed to the profile's expected layout
|
||||
before encoding.
|
||||
|
||||
Encoding is fully synchronous: PCM is fed straight into a
|
||||
``OpusBufferedEncoder`` wrapped by an ``OggOpusWriter`` and flushed
|
||||
OpusBufferedEncoder wrapped by an OggOpusWriter and flushed
|
||||
on close, so the encoded duration matches the input exactly with no
|
||||
frame loss and no trailing silence padding.
|
||||
"""
|
||||
|
|
@ -309,7 +309,7 @@ def encode_pcm_to_ogg_opus(
|
|||
|
||||
|
||||
def encode_audio_to_ogg_opus(source, output_path: str, profile=None) -> str:
|
||||
"""Decode any supported ``source`` and re-encode it as OGG/Opus.
|
||||
"""Decode any supported source and re-encode it as OGG/Opus.
|
||||
|
||||
Intended for converting user-uploaded greetings/ringtones (any format
|
||||
miniaudio can decode) into the OGG/Opus container required by the
|
||||
|
|
@ -332,7 +332,7 @@ def write_silence_ogg_opus(
|
|||
channels: int = 1,
|
||||
profile=None,
|
||||
) -> str:
|
||||
"""Write a silent OGG/Opus file of ``seconds`` seconds to ``output_path``."""
|
||||
"""Write a silent OGG/Opus file of seconds seconds to output_path."""
|
||||
import numpy as np
|
||||
|
||||
duration = max(0.05, float(seconds))
|
||||
|
|
@ -348,14 +348,14 @@ def write_silence_ogg_opus(
|
|||
|
||||
|
||||
def is_ogg_opus_bytes(data: bytes) -> bool:
|
||||
"""Return ``True`` if ``data`` looks like an OGG container payload."""
|
||||
"""Return True if data looks like an OGG container payload."""
|
||||
return len(data) >= 4 and data[:4] == b"OggS"
|
||||
|
||||
|
||||
def encode_audio_bytes_to_ogg_opus(data: bytes, profile=None) -> bytes | None:
|
||||
"""Decode ``data`` and return the corresponding OGG/Opus byte string.
|
||||
"""Decode data and return the corresponding OGG/Opus byte string.
|
||||
|
||||
Returns ``None`` if the payload could not be decoded. If the payload
|
||||
Returns None if the payload could not be decoded. If the payload
|
||||
is already an OGG container it is returned unchanged.
|
||||
"""
|
||||
if is_ogg_opus_bytes(data):
|
||||
|
|
|
|||
|
|
@ -47,9 +47,9 @@ def estimate_battery_usage(
|
|||
) -> dict[str, Any] | None:
|
||||
"""Build an estimated MeshChatX battery-usage payload.
|
||||
|
||||
``avg_cpu_percent`` is percent of one logical core (may exceed 100 on
|
||||
multi-threaded work). ``machine_share_percent`` normalizes by CPU count.
|
||||
``estimated_percent_per_hour`` is a rough battery pack drain rate.
|
||||
avg_cpu_percent is percent of one logical core (may exceed 100 on
|
||||
multi-threaded work). machine_share_percent normalizes by CPU count.
|
||||
estimated_percent_per_hour is a rough battery pack drain rate.
|
||||
"""
|
||||
if cpu_time_seconds is None or uptime_seconds is None:
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -51,9 +51,9 @@ class BotHandler:
|
|||
def _resolve_bot_launcher(self):
|
||||
"""Return argv prefix for launching bot_process.
|
||||
|
||||
Frozen desktop builds set ``sys.executable`` to MeshChatX itself. Passing
|
||||
a ``.py`` path as argv[1] starts another full app instance and hits the
|
||||
storage lock. Those builds re-enter via ``--meshchatx-run-module``.
|
||||
Frozen desktop builds set sys.executable to MeshChatX itself. Passing
|
||||
a .py path as argv[1] starts another full app instance and hits the
|
||||
storage lock. Those builds re-enter via --meshchatx-run-module.
|
||||
"""
|
||||
if self._is_frozen_executable():
|
||||
return [
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# SPDX-License-Identifier: 0BSD
|
||||
|
||||
"""Bug report collector and sender over aspect ``mcx-bugs-v1``."""
|
||||
"""Bug report collector and sender over aspect mcx-bugs-v1."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ _ALLOWED_WAL_CHECKPOINT_MODES = frozenset({"PASSIVE", "FULL", "RESTART", "TRUNCA
|
|||
|
||||
|
||||
def _sanitize_pragma_read_name(name: str | None) -> str | None:
|
||||
"""Allow only simple SQLite pragma tokens for dynamic ``PRAGMA name`` reads."""
|
||||
"""Allow only simple SQLite pragma tokens for dynamic PRAGMA name reads."""
|
||||
if not name or not isinstance(name, str):
|
||||
return None
|
||||
token = name.strip()
|
||||
|
|
@ -140,7 +140,7 @@ class Database:
|
|||
"""Shrink SQLite cache under low RAM.
|
||||
|
||||
FILE temp spills break complex conversation queries under Landlock
|
||||
(``unable to open database file``), even when TMPDIR is inside the
|
||||
(unable to open database file), even when TMPDIR is inside the
|
||||
allowed storage tree. Keep MEMORY temp while Landlock is active and
|
||||
only reduce cache/mmap. Without Landlock, FILE temp is still used.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ class AnnounceDAO:
|
|||
|
||||
Announces that correspond to a favourited destination or to a saved
|
||||
contact are considered protected and are never deleted by this trim,
|
||||
even if the total count exceeds ``max_rows``. This prevents purging
|
||||
even if the total count exceeds max_rows. This prevents purging
|
||||
of announces (and the path/identity context they provide) for
|
||||
favourited NomadNet nodes and for messaging contacts when storage
|
||||
limits are enforced.
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ class ContactsDAO:
|
|||
def get_contact_by_identity_hash(self, remote_identity_hash, related_hashes=None):
|
||||
"""Match a contact by identity, LXMF, or LXST hash.
|
||||
|
||||
``related_hashes`` may include derived destination hashes for the same
|
||||
related_hashes may include derived destination hashes for the same
|
||||
peer so callers that only know an identity hash still match contacts
|
||||
that were saved with an LXMF or LXST destination hash as the primary key.
|
||||
Matching is case-insensitive. Hex-only forms are also tried so UUID-style
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ from meshchatx.src.backend import gif_utils
|
|||
class UserGifsDAO:
|
||||
"""Per-identity library of user-uploaded GIFs.
|
||||
|
||||
Mirrors :class:`UserStickersDAO` but exposes a ``usage_count``/``last_used_at``
|
||||
Mirrors :class:`UserStickersDAO` but exposes a usage_count/last_used_at
|
||||
pair so the picker can order entries by most-used and the user can quickly
|
||||
reuse their favorite GIFs across chats.
|
||||
"""
|
||||
|
|
@ -82,9 +82,9 @@ class UserGifsDAO:
|
|||
return cur.rowcount > 0
|
||||
|
||||
def record_usage(self, gif_id: int, identity_hash: str) -> bool:
|
||||
"""Increment ``usage_count`` and refresh ``last_used_at`` for a GIF.
|
||||
"""Increment usage_count and refresh last_used_at for a GIF.
|
||||
|
||||
Returns ``True`` when a row was updated, ``False`` when the GIF does
|
||||
Returns True when a row was updated, False when the GIF does
|
||||
not belong to the given identity.
|
||||
"""
|
||||
now = time.time()
|
||||
|
|
@ -106,7 +106,7 @@ class UserGifsDAO:
|
|||
image_bytes: bytes,
|
||||
source_message_hash: str | None = None,
|
||||
) -> dict | None:
|
||||
"""Insert a GIF. Returns summary dict or ``None`` if duplicate (same content_hash)."""
|
||||
"""Insert a GIF. Returns summary dict or None if duplicate (same content_hash)."""
|
||||
if self.count_for_identity(identity_hash) >= gif_utils.MAX_GIFS_PER_IDENTITY:
|
||||
msg = "gif_limit_reached"
|
||||
raise ValueError(msg)
|
||||
|
|
|
|||
|
|
@ -279,7 +279,7 @@ class MessageDAO:
|
|||
"""Lightweight update for delivery-state changes only.
|
||||
|
||||
Avoids re-serializing the full message (including base64 attachment
|
||||
data) which the heavy ``upsert_lxmf_message`` path does.
|
||||
data) which the heavy upsert_lxmf_message path does.
|
||||
"""
|
||||
now = datetime.now(UTC).isoformat()
|
||||
if method is None:
|
||||
|
|
@ -446,14 +446,14 @@ class MessageDAO:
|
|||
)
|
||||
|
||||
def get_latest_user_facing_incoming_message(self, peer_hash, *, scan_limit=50):
|
||||
"""Return the most recent incoming user-facing message for ``peer_hash``.
|
||||
"""Return the most recent incoming user-facing message for peer_hash.
|
||||
|
||||
Walks recent incoming messages in timestamp-descending order and applies
|
||||
:func:`is_user_facing_lxmf_payload` in Python (the SQLite layer cannot
|
||||
cheaply parse the JSON ``fields`` blob). ``scan_limit`` bounds the walk
|
||||
cheaply parse the JSON fields blob). scan_limit bounds the walk
|
||||
so a long chain of reactions/telemetry won't degrade the bell endpoint.
|
||||
|
||||
Returns ``None`` if no user-facing incoming message exists in the
|
||||
Returns None if no user-facing incoming message exists in the
|
||||
scanned window.
|
||||
"""
|
||||
from meshchatx.src.backend.lxmf_utils import is_user_facing_lxmf_payload
|
||||
|
|
|
|||
|
|
@ -90,8 +90,8 @@ class DatabaseSchema:
|
|||
def _sync_table_columns(self, table_name, create_sql):
|
||||
"""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
|
||||
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.
|
||||
"""
|
||||
start_idx = create_sql.find("(")
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
Packs group multiple stickers under a single user-facing label so they can be
|
||||
exported, shared with peers over LXMF, or installed from a peer's pack
|
||||
attachment. Stickers belonging to a pack reference it via
|
||||
``user_stickers.pack_id``.
|
||||
user_stickers.pack_id.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -22,13 +22,13 @@ _PACK_COLUMNS = (
|
|||
|
||||
|
||||
class UserStickerPacksDAO:
|
||||
"""CRUD for ``user_sticker_packs``."""
|
||||
"""CRUD for user_sticker_packs."""
|
||||
|
||||
def __init__(self, provider):
|
||||
self.provider = provider
|
||||
|
||||
def count_for_identity(self, identity_hash: str) -> int:
|
||||
"""Return the number of packs stored for ``identity_hash``."""
|
||||
"""Return the number of packs stored for identity_hash."""
|
||||
row = self.provider.fetchone(
|
||||
"SELECT COUNT(*) AS c FROM user_sticker_packs WHERE identity_hash = ?",
|
||||
(identity_hash,),
|
||||
|
|
@ -36,7 +36,7 @@ class UserStickerPacksDAO:
|
|||
return int(row["c"]) if row else 0
|
||||
|
||||
def list_for_identity(self, identity_hash: str):
|
||||
"""List packs for ``identity_hash`` ordered by user-defined sort order."""
|
||||
"""List packs for identity_hash ordered by user-defined sort order."""
|
||||
return self.provider.fetchall(
|
||||
f"""
|
||||
SELECT {_PACK_COLUMNS}
|
||||
|
|
@ -48,7 +48,7 @@ class UserStickerPacksDAO:
|
|||
)
|
||||
|
||||
def get_row(self, pack_id: int, identity_hash: str):
|
||||
"""Fetch a single pack row scoped to ``identity_hash``."""
|
||||
"""Fetch a single pack row scoped to identity_hash."""
|
||||
return self.provider.fetchone(
|
||||
f"""
|
||||
SELECT {_PACK_COLUMNS}
|
||||
|
|
@ -59,7 +59,7 @@ class UserStickerPacksDAO:
|
|||
)
|
||||
|
||||
def get_by_short_name(self, identity_hash: str, short_name: str):
|
||||
"""Fetch a pack by its identity-scoped ``short_name`` slug."""
|
||||
"""Fetch a pack by its identity-scoped short_name slug."""
|
||||
return self.provider.fetchone(
|
||||
f"""
|
||||
SELECT {_PACK_COLUMNS}
|
||||
|
|
@ -80,7 +80,7 @@ class UserStickerPacksDAO:
|
|||
author: str | None = None,
|
||||
is_strict: bool = True,
|
||||
) -> dict:
|
||||
"""Create a new pack. Raises ``ValueError`` on quota or short_name clash."""
|
||||
"""Create a new pack. Raises ValueError on quota or short_name clash."""
|
||||
if (
|
||||
self.count_for_identity(identity_hash)
|
||||
>= sticker_utils.MAX_STICKER_PACKS_PER_IDENTITY
|
||||
|
|
@ -132,8 +132,8 @@ class UserStickerPacksDAO:
|
|||
) -> bool:
|
||||
"""Update mutable fields of an existing pack.
|
||||
|
||||
``cover_sticker_id`` uses a sentinel default so callers can clear the
|
||||
cover by passing ``None`` while leaving it untouched when omitted.
|
||||
cover_sticker_id uses a sentinel default so callers can clear the
|
||||
cover by passing None while leaving it untouched when omitted.
|
||||
"""
|
||||
existing = self.get_row(pack_id, identity_hash)
|
||||
if not existing:
|
||||
|
|
@ -198,7 +198,7 @@ class UserStickerPacksDAO:
|
|||
return updated
|
||||
|
||||
def delete(self, pack_id: int, identity_hash: str) -> bool:
|
||||
"""Delete a pack and detach its stickers (set ``pack_id`` to NULL)."""
|
||||
"""Delete a pack and detach its stickers (set pack_id to NULL)."""
|
||||
self.provider.execute(
|
||||
"""
|
||||
UPDATE user_stickers SET pack_id = NULL
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ class UserStickersDAO:
|
|||
self.provider = provider
|
||||
|
||||
def count_for_identity(self, identity_hash: str) -> int:
|
||||
"""Return the total number of stickers stored for ``identity_hash``."""
|
||||
"""Return the total number of stickers stored for identity_hash."""
|
||||
row = self.provider.fetchone(
|
||||
"SELECT COUNT(*) AS c FROM user_stickers WHERE identity_hash = ?",
|
||||
(identity_hash,),
|
||||
|
|
@ -35,7 +35,7 @@ class UserStickersDAO:
|
|||
return int(row["c"]) if row else 0
|
||||
|
||||
def count_for_pack(self, pack_id: int, identity_hash: str) -> int:
|
||||
"""Return the number of stickers belonging to ``pack_id``."""
|
||||
"""Return the number of stickers belonging to pack_id."""
|
||||
row = self.provider.fetchone(
|
||||
"SELECT COUNT(*) AS c FROM user_stickers WHERE pack_id = ? AND identity_hash = ?",
|
||||
(pack_id, identity_hash),
|
||||
|
|
@ -43,7 +43,7 @@ class UserStickersDAO:
|
|||
return int(row["c"]) if row else 0
|
||||
|
||||
def list_for_identity(self, identity_hash: str):
|
||||
"""List all sticker summaries for ``identity_hash``, newest first."""
|
||||
"""List all sticker summaries for identity_hash, newest first."""
|
||||
return self.provider.fetchall(
|
||||
f"""
|
||||
SELECT {_STICKER_SUMMARY_COLUMNS}
|
||||
|
|
@ -55,7 +55,7 @@ class UserStickersDAO:
|
|||
)
|
||||
|
||||
def list_for_pack(self, pack_id: int, identity_hash: str):
|
||||
"""List sticker summaries belonging to a pack, ordered by ``sort_order``."""
|
||||
"""List sticker summaries belonging to a pack, ordered by sort_order."""
|
||||
return self.provider.fetchall(
|
||||
f"""
|
||||
SELECT {_STICKER_SUMMARY_COLUMNS}
|
||||
|
|
@ -79,7 +79,7 @@ class UserStickersDAO:
|
|||
)
|
||||
|
||||
def get_row(self, sticker_id: int, identity_hash: str):
|
||||
"""Fetch the full row (including ``image_blob``) for a sticker."""
|
||||
"""Fetch the full row (including image_blob) for a sticker."""
|
||||
return self.provider.fetchone(
|
||||
f"""
|
||||
SELECT {_STICKER_FULL_COLUMNS}
|
||||
|
|
@ -90,7 +90,7 @@ class UserStickersDAO:
|
|||
)
|
||||
|
||||
def delete(self, sticker_id: int, identity_hash: str) -> bool:
|
||||
"""Delete a single sticker. Returns ``True`` when a row was removed."""
|
||||
"""Delete a single sticker. Returns True when a row was removed."""
|
||||
cur = self.provider.execute(
|
||||
"DELETE FROM user_stickers WHERE id = ? AND identity_hash = ?",
|
||||
(sticker_id, identity_hash),
|
||||
|
|
@ -106,7 +106,7 @@ class UserStickersDAO:
|
|||
return cur.rowcount
|
||||
|
||||
def delete_all_for_pack(self, pack_id: int, identity_hash: str) -> int:
|
||||
"""Delete every sticker that belongs to ``pack_id``."""
|
||||
"""Delete every sticker that belongs to pack_id."""
|
||||
cur = self.provider.execute(
|
||||
"DELETE FROM user_stickers WHERE pack_id = ? AND identity_hash = ?",
|
||||
(pack_id, identity_hash),
|
||||
|
|
@ -180,10 +180,10 @@ class UserStickersDAO:
|
|||
strict: bool = False,
|
||||
sort_order: int = 0,
|
||||
) -> dict | None:
|
||||
"""Insert a sticker. Returns ``None`` if a duplicate (by content hash).
|
||||
"""Insert a sticker. Returns None if a duplicate (by content hash).
|
||||
|
||||
Validates the payload against the legacy or strict Telegram rules
|
||||
depending on ``strict``. Extracts and stores width/height/fps/duration
|
||||
depending on strict. Extracts and stores width/height/fps/duration
|
||||
metadata so the picker can render the sticker correctly without
|
||||
re-parsing.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -128,9 +128,9 @@ class MemoryDiagnostics:
|
|||
linger longer between full collections. This class helps detect
|
||||
accumulating objects by:
|
||||
|
||||
* Taking periodic ``tracemalloc`` snapshots and diffing against a baseline.
|
||||
* Taking periodic tracemalloc snapshots and diffing against a baseline.
|
||||
* Tracking GC generation sizes (gen0/gen1/gen2 object counts).
|
||||
* Profiling ``gc.get_objects()`` by type to spot monotonic growth.
|
||||
* Profiling gc.get_objects() by type to spot monotonic growth.
|
||||
* Finding the top-N allocation sites (filename + line number) that
|
||||
contribute the most to memory growth.
|
||||
|
||||
|
|
@ -205,8 +205,8 @@ class MemoryDiagnostics:
|
|||
def _trim_history(self) -> None:
|
||||
"""Bound retained snapshots and GC records while preserving the baseline.
|
||||
|
||||
The baseline (index 0) is always kept so ``diff_snapshots`` and
|
||||
``gc_stats`` deltas remain anchored to application start; only the
|
||||
The baseline (index 0) is always kept so diff_snapshots and
|
||||
gc_stats deltas remain anchored to application start; only the
|
||||
intermediate readings are evicted once the cap is exceeded.
|
||||
"""
|
||||
if len(self._snapshots) > self._max_snapshots:
|
||||
|
|
@ -555,7 +555,7 @@ def get_diagnostics() -> MemoryDiagnostics:
|
|||
def take_heap_snapshot(include_tracemalloc: bool = True) -> dict[str, Any]:
|
||||
"""Convenience function: take a one-shot heap snapshot.
|
||||
|
||||
Useful for ``pdb`` / ``breakpoint()`` sessions::
|
||||
Useful for pdb / breakpoint() sessions::
|
||||
|
||||
from meshchatx.src.backend.diagnostics import take_heap_snapshot
|
||||
report = take_heap_snapshot()
|
||||
|
|
|
|||
|
|
@ -20,11 +20,11 @@ class DocsManager:
|
|||
"""Manages the bundled Reticulum manual and any user-uploaded overrides.
|
||||
|
||||
The Reticulum manual is shipped with the application under
|
||||
``<public_dir>/reticulum-docs-bundled/current``. Users may upload a
|
||||
replacement archive which is extracted into ``<storage_dir>/reticulum-docs``
|
||||
<public_dir>/reticulum-docs-bundled/current. Users may upload a
|
||||
replacement archive which is extracted into <storage_dir>/reticulum-docs
|
||||
and takes precedence at request time. Removing the user upload restores the
|
||||
bundled copy. There is no runtime download path. Fresh manuals are staged at
|
||||
build time with ``scripts/build/fetch_reticulum_manual.py`` (``pnpm run build-docs``).
|
||||
build time with scripts/build/fetch_reticulum_manual.py (pnpm run build-docs).
|
||||
"""
|
||||
|
||||
def __init__(self, config, public_dir, project_root=None, storage_dir=None):
|
||||
|
|
@ -221,7 +221,7 @@ class DocsManager:
|
|||
def _sync_docs_tree(self, src_docs, dest_dir):
|
||||
"""Copy manifest, markdown, and text files from src_docs into dest_dir.
|
||||
|
||||
Skips ``agents/`` (contributor and automated-agent guidance, not
|
||||
Skips agents/ (contributor and automated-agent guidance, not
|
||||
end-user documentation).
|
||||
"""
|
||||
for root, dirnames, files in os.walk(src_docs):
|
||||
|
|
@ -610,9 +610,9 @@ class DocsManager:
|
|||
def export_reticulum_docs(self, root_folder="reticulum_manual"):
|
||||
"""Build a ZIP of the active Reticulum manual in upload-compatible form.
|
||||
|
||||
The archive lays out files under ``<root_folder>/docs/`` so that another
|
||||
MeshChatX instance can re-import it via the ``/api/v1/docs/upload``
|
||||
endpoint without modification. Returns ``None`` when no Reticulum docs
|
||||
The archive lays out files under <root_folder>/docs/ so that another
|
||||
MeshChatX instance can re-import it via the /api/v1/docs/upload
|
||||
endpoint without modification. Returns None when no Reticulum docs
|
||||
are currently available (neither user-uploaded nor bundled).
|
||||
"""
|
||||
active_docs_dir = self._active_reticulum_docs_dir()
|
||||
|
|
@ -773,9 +773,9 @@ class DocsManager:
|
|||
return os.path.exists(os.path.join(self.bundled_docs_dir, "index.html"))
|
||||
|
||||
def find_docs_file(self, rel_path):
|
||||
"""Resolve ``rel_path`` against user docs first, then bundled docs.
|
||||
"""Resolve rel_path against user docs first, then bundled docs.
|
||||
|
||||
Returns the absolute on-disk path of the matching file, or ``None`` when
|
||||
Returns the absolute on-disk path of the matching file, or None when
|
||||
the path either escapes the docs roots or no file exists in either
|
||||
location. Path traversal attempts are rejected.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ UNKNOWN_FAVOURITE_NAMES = frozenset(
|
|||
|
||||
|
||||
def is_unknown_favourite_display_name(name) -> bool:
|
||||
"""Return True when ``name`` is empty or a known unknown-node placeholder."""
|
||||
"""Return True when name is empty or a known unknown-node placeholder."""
|
||||
if not isinstance(name, str):
|
||||
return True
|
||||
return name.strip() in UNKNOWN_FAVOURITE_NAMES
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ def _clip_str(value, max_len):
|
|||
|
||||
|
||||
def normalize_favourites_layout(layout):
|
||||
"""Return a sanitized layout dict, or ``None`` when the shape is invalid."""
|
||||
"""Return a sanitized layout dict, or None when the shape is invalid."""
|
||||
if not isinstance(layout, dict) or not isinstance(layout.get("sections"), list):
|
||||
return None
|
||||
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@
|
|||
|
||||
"""Validation, hashing, and export/import helpers for user GIF library entries.
|
||||
|
||||
GIFs are stored per identity in the ``user_gifs`` table. Compared to stickers,
|
||||
GIFs are stored per identity in the user_gifs table. Compared to stickers,
|
||||
the library is intended for animated content shared in chats, so:
|
||||
|
||||
* Only animated-friendly formats are accepted (``gif`` and ``webp``).
|
||||
* Only animated-friendly formats are accepted (gif and webp).
|
||||
* The per-file byte limit is larger.
|
||||
* A ``usage_count`` is tracked at the DAO level so the picker can surface
|
||||
* A usage_count is tracked at the DAO level so the picker can surface
|
||||
most-used GIFs first.
|
||||
"""
|
||||
|
||||
|
|
@ -43,7 +43,7 @@ def content_hash_hex(image_bytes: bytes) -> str:
|
|||
def detect_image_format_from_magic(image_bytes: bytes) -> str | None:
|
||||
"""Detect a GIF-library compatible image format from magic bytes.
|
||||
|
||||
Returns a normalized type key (``gif`` or ``webp``), or ``None``.
|
||||
Returns a normalized type key (gif or webp), or None.
|
||||
"""
|
||||
if not isinstance(image_bytes, (bytes, bytearray)) or len(image_bytes) < 4:
|
||||
return None
|
||||
|
|
@ -59,12 +59,12 @@ def validate_gif_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).
|
||||
|
||||
The declared ``image_type`` must match the format detected from magic
|
||||
The declared image_type must match the format detected from magic
|
||||
bytes; the stored type is the normalized detected format.
|
||||
|
||||
Raises ``ValueError`` with a short reason on invalid input.
|
||||
Raises ValueError with a short reason on invalid input.
|
||||
"""
|
||||
if not isinstance(image_bytes, (bytes, bytearray)):
|
||||
msg = "invalid_image_bytes"
|
||||
|
|
@ -100,8 +100,8 @@ _EXPORT_VERSION = 1
|
|||
def validate_export_document(data: object) -> list[dict]:
|
||||
"""Parse and validate a GIF library export JSON document.
|
||||
|
||||
Each entry has ``name``, ``image_type``, ``image_bytes`` (base64), and
|
||||
optional ``source_message_hash`` and ``usage_count``.
|
||||
Each entry has name, image_type, image_bytes (base64), and
|
||||
optional source_message_hash and usage_count.
|
||||
"""
|
||||
if not isinstance(data, dict):
|
||||
msg = "invalid_document"
|
||||
|
|
@ -159,8 +159,8 @@ def validate_export_document(data: object) -> list[dict]:
|
|||
def build_export_document(gifs: list[dict], exported_at_iso: str) -> dict:
|
||||
"""Build the GIF export document.
|
||||
|
||||
``gifs``: rows with ``name``, ``image_type``, ``image_bytes`` (base64 str),
|
||||
``source_message_hash``, and ``usage_count``.
|
||||
gifs: rows with name, image_type, image_bytes (base64 str),
|
||||
source_message_hash, and usage_count.
|
||||
"""
|
||||
return {
|
||||
"format": _EXPORT_FORMAT,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ class UnsafeOutboundUrlError(ValueError):
|
|||
def normalize_loopback_http_service_base(url: str) -> str:
|
||||
"""Return scheme://host:port with no path, query, or fragment.
|
||||
|
||||
Only ``http``/``https`` to loopback hosts (127.0.0.1, localhost, ::1) are allowed.
|
||||
Only http/https to loopback hosts (127.0.0.1, localhost, ::1) are allowed.
|
||||
Userinfo (embedded credentials) is rejected.
|
||||
"""
|
||||
if not url or not isinstance(url, str):
|
||||
|
|
@ -57,7 +57,7 @@ def normalize_libretranslate_http_service_base(url: str) -> str:
|
|||
Accepts any HTTP(S) hostname or IP reachable from this process (remote LibreTranslate or
|
||||
public API). Embedded credentials are rejected; non-http(s) schemes are rejected.
|
||||
|
||||
Literal IPv4 link-local targets (``169.254.0.0/16``) are rejected as a common SSRF/metadata
|
||||
Literal IPv4 link-local targets (169.254.0.0/16) are rejected as a common SSRF/metadata
|
||||
path. Other private or loopback addresses are allowed so local servers and overlays (e.g. VPN
|
||||
mesh) continue to work.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ class IntegrityManager:
|
|||
"""Determine if a file path is volatile RNS/LXMF state to skip.
|
||||
|
||||
Critical security components living directly under the identity storage
|
||||
directory (``identity``, ``config``, ``database.db``) are never ignored;
|
||||
directory (identity, config, database.db) are never ignored;
|
||||
only the continuously-rewritten LXMF router tree, ratchets, message
|
||||
store and known RNS state files are excluded.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -8,12 +8,12 @@ _IPV4_HOST_PORT = re.compile(r"^(\d{1,3}(?:\.\d{1,3}){3}):(\d{1,5})$")
|
|||
|
||||
|
||||
def normalize_rnode_tcp_port(port: str) -> str:
|
||||
"""Normalize RNodeInterface ``port`` when using ``tcp://``.
|
||||
"""Normalize RNodeInterface port when using tcp://.
|
||||
|
||||
Reticulum's ``TCPConnection`` (``RNS/Interfaces/RNodeInterface.py``) calls
|
||||
``socket.getaddrinfo(target_host, 7633)``. The first argument must be a hostname or IP **only**; an embedded ``:port``
|
||||
breaks resolution. Config may list legacy ``tcp://host:7633`` or ``tcp://host:``;
|
||||
strip those so storage matches ``tcp://<host>``.
|
||||
Reticulum's TCPConnection (RNS/Interfaces/RNodeInterface.py) calls
|
||||
socket.getaddrinfo(target_host, 7633). The first argument must be a hostname or IP **only**; an embedded :port
|
||||
breaks resolution. Config may list legacy tcp://host:7633 or tcp://host:;
|
||||
strip those so storage matches tcp://<host>.
|
||||
"""
|
||||
raw = str(port).strip()
|
||||
low = raw.lower()
|
||||
|
|
@ -45,7 +45,7 @@ def normalize_rnode_tcp_port(port: str) -> str:
|
|||
def coerce_rnode_frequency_hz(value):
|
||||
"""Return RNode carrier frequency as integer Hz for Reticulum config.
|
||||
|
||||
Reticulum reads ``frequency`` with ``int()``; MHz-style decimals (868.825)
|
||||
Reticulum reads frequency with int(); MHz-style decimals (868.825)
|
||||
must not be stored verbatim or they truncate to invalid values. Accepts
|
||||
Hz integers, bare MHz-style numbers below 1e6, and strings with optional
|
||||
ghz/mhz/khz/hz suffix (ASCII, case-insensitive).
|
||||
|
|
@ -76,7 +76,7 @@ RNODE_TXPOWER_MAX = 37
|
|||
|
||||
|
||||
def normalize_rnode_txpower(value):
|
||||
"""Return integer dBm for Reticulum ``RNodeInterface`` config."""
|
||||
"""Return integer dBm for Reticulum RNodeInterface config."""
|
||||
if value is None or value == "":
|
||||
return value
|
||||
return int(float(str(value).strip()))
|
||||
|
|
@ -108,7 +108,7 @@ class InterfaceEditor:
|
|||
|
||||
@staticmethod
|
||||
def sanitize_interface_section_name(name: str | None) -> str:
|
||||
"""Make a name safe for Reticulum/ConfigObj ``[[section]]`` headers.
|
||||
"""Make a name safe for Reticulum/ConfigObj [[section]] headers.
|
||||
|
||||
Square brackets break ConfigObj nesting and can leave the in-memory
|
||||
interfaces map dirty after a failed write, blocking later adds.
|
||||
|
|
@ -134,7 +134,7 @@ class InterfaceEditor:
|
|||
|
||||
@staticmethod
|
||||
def apply_fixed_mtu(interface_details: dict, data: dict) -> str | None:
|
||||
"""Persist ``fixed_mtu`` when valid; return an API error message otherwise."""
|
||||
"""Persist fixed_mtu when valid; return an API error message otherwise."""
|
||||
value = data.get("fixed_mtu")
|
||||
if value is None or value == "":
|
||||
interface_details.pop("fixed_mtu", None)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# SPDX-License-Identifier: 0BSD
|
||||
|
||||
"""Install custom Reticulum interface modules into ``configdir/interfaces``."""
|
||||
"""Install custom Reticulum interface modules into configdir/interfaces."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
@ -14,7 +14,7 @@ _MAX_MODULE_BYTES = 512 * 1024
|
|||
|
||||
|
||||
def interface_modules_dir(reticulum_config_dir: str | None) -> str:
|
||||
"""Return the RNS ``interfacepath`` directory for this MeshChatX instance."""
|
||||
"""Return the RNS interfacepath directory for this MeshChatX instance."""
|
||||
if not reticulum_config_dir:
|
||||
raise ValueError("Reticulum config directory is not configured")
|
||||
root = os.path.abspath(os.path.expanduser(str(reticulum_config_dir)))
|
||||
|
|
@ -57,7 +57,7 @@ def validate_interface_module_source(data: bytes) -> str | None:
|
|||
|
||||
|
||||
def list_interface_modules(reticulum_config_dir: str | None) -> dict:
|
||||
"""List installed ``*.py`` modules under interfacepath."""
|
||||
"""List installed *.py modules under interfacepath."""
|
||||
path = interface_modules_dir(reticulum_config_dir)
|
||||
modules: list[dict] = []
|
||||
if os.path.isdir(path):
|
||||
|
|
@ -90,8 +90,8 @@ def install_interface_module(
|
|||
) -> dict:
|
||||
"""Write a custom interface module into interfacepath.
|
||||
|
||||
Returns a dict with ``type``, ``filename``, ``path``, and ``interfacepath``.
|
||||
Raises ``ValueError`` on validation failures.
|
||||
Returns a dict with type, filename, path, and interfacepath.
|
||||
Raises ValueError on validation failures.
|
||||
"""
|
||||
err = validate_interface_module_source(data)
|
||||
if err:
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ _PORT_IN_USE_ERRNOS = {
|
|||
|
||||
|
||||
def _normalize_host(host: str | None) -> str:
|
||||
"""Return a host string that is safe to call ``getaddrinfo`` with."""
|
||||
"""Return a host string that is safe to call getaddrinfo with."""
|
||||
if host is None:
|
||||
return ""
|
||||
host = str(host).strip()
|
||||
|
|
@ -45,13 +45,13 @@ def _coerce_port(port) -> int | None:
|
|||
|
||||
|
||||
def is_port_in_use(host: str | None, port, *, kind: str = "tcp") -> bool:
|
||||
"""Return ``True`` when the given ``host``:``port`` is already bound.
|
||||
"""Return True when the given host:port is already bound.
|
||||
|
||||
``kind`` may be ``"tcp"`` or ``"udp"``. Unknown values are treated as TCP.
|
||||
kind may be "tcp" or "udp". Unknown values are treated as TCP.
|
||||
|
||||
The helper resolves the supplied host (falling back to ``INADDR_ANY``) and
|
||||
tries to bind a fresh socket. ``EADDRINUSE``/``EACCES``/``EADDRNOTAVAIL``
|
||||
are reported as "in use", any other exception bubbles up as ``False`` so
|
||||
The helper resolves the supplied host (falling back to INADDR_ANY) and
|
||||
tries to bind a fresh socket. EADDRINUSE/EACCES/EADDRNOTAVAIL
|
||||
are reported as "in use", any other exception bubbles up as False so
|
||||
that we never block save flows because of a transient resolution glitch.
|
||||
"""
|
||||
coerced_port = _coerce_port(port)
|
||||
|
|
|
|||
|
|
@ -266,7 +266,7 @@ def _author_from_package_json(data: dict[str, Any]) -> str:
|
|||
|
||||
|
||||
def _workspace_root_npm_identity(repo_root: Path) -> tuple[str | None, str | None]:
|
||||
"""Return ``(name_lower, version)`` from the repository root ``package.json``."""
|
||||
"""Return (name_lower, version) from the repository root package.json."""
|
||||
pj = repo_root / "package.json"
|
||||
if not pj.is_file():
|
||||
return None, None
|
||||
|
|
@ -306,7 +306,7 @@ def _filter_out_workspace_root_package(
|
|||
def collect_frontend_from_node_modules(repo_root: Path) -> list[dict[str, Any]]:
|
||||
"""Collect license rows by scanning node_modules/**/package.json.
|
||||
|
||||
Used when ``pnpm licenses list`` is unavailable or fails (e.g. pnpm lockfile
|
||||
Used when pnpm licenses list is unavailable or fails (e.g. pnpm lockfile
|
||||
bugs). Recursive glob follows symlinks so pnpm-linked layouts are included.
|
||||
"""
|
||||
nm = repo_root / "node_modules"
|
||||
|
|
|
|||
|
|
@ -141,11 +141,11 @@ def is_user_facing_lxmf_payload(fields, content, title) -> bool:
|
|||
- icon-only / appearance-only updates (no body, no attachment)
|
||||
- empty pings (no content, no title, no attachment)
|
||||
|
||||
Location shares (telemetry including ``location``), telemetry streams,
|
||||
and Sideband ``commands`` entries with key ``0x01`` (location request) ARE
|
||||
Location shares (telemetry including location), telemetry streams,
|
||||
and Sideband commands entries with key 0x01 (location request) ARE
|
||||
treated as user-facing so the bell and previews stay informative.
|
||||
|
||||
The helper is intentionally tolerant: ``fields`` may be the rich dict
|
||||
The helper is intentionally tolerant: fields may be the rich dict
|
||||
produced by :func:`convert_lxmf_message_to_dict` (string keys), the raw
|
||||
LXMF integer-keyed dict, or a JSON-string from the database.
|
||||
"""
|
||||
|
|
@ -279,7 +279,7 @@ def _b64_payload_size(b64_bytes) -> int:
|
|||
def lxmf_fields_without_attachment_bytes(fields) -> dict:
|
||||
"""Return fields with image/audio/file byte payloads removed.
|
||||
|
||||
Used for ``fields_meta`` storage and conversation-list/thread APIs so
|
||||
Used for fields_meta storage and conversation-list/thread APIs so
|
||||
multi-MB base64 blobs are never re-parsed on every load.
|
||||
"""
|
||||
if not isinstance(fields, dict):
|
||||
|
|
@ -423,9 +423,9 @@ def lxmf_sidebar_preview_for_conversation_latest_row(
|
|||
) -> str:
|
||||
"""Single-line preview for conversation list APIs (reactions and some media have empty body).
|
||||
|
||||
Conversation list rows may omit full ``fields`` (to avoid loading multi-MB
|
||||
attachment blobs). In that case SQL-derived flags such as ``has_image`` /
|
||||
``has_reaction`` are used instead.
|
||||
Conversation list rows may omit full fields (to avoid loading multi-MB
|
||||
attachment blobs). In that case SQL-derived flags such as has_image /
|
||||
has_reaction are used instead.
|
||||
"""
|
||||
content = row.get("content")
|
||||
if content is not None and str(content).strip():
|
||||
|
|
@ -976,11 +976,11 @@ def convert_db_lxmf_message_to_dict(
|
|||
def compute_lxmf_conversation_unread_from_latest_row(row, *, require_user_facing=False):
|
||||
"""Return whether the conversation row should appear as unread.
|
||||
|
||||
Uses ``lxmf_conversation_read_state.last_read_at`` only. The latest message
|
||||
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``).
|
||||
filter_unread in MessageHandler.get_conversations).
|
||||
|
||||
When ``require_user_facing`` is True, the row's latest message must also be
|
||||
When require_user_facing is True, the row's latest message must also be
|
||||
user-facing (i.e. not a bare reaction / telemetry / icon-only payload).
|
||||
Used by the notification bell so silent system messages do not raise the
|
||||
unread badge.
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
"""List and create Reticulum management identity files.
|
||||
|
||||
These live under ``<reticulum_config_dir>/storage/identities/`` and are used by
|
||||
rnstatus / rnpath remote queries (``-i``), rnx, and rnsh.
|
||||
These live under <reticulum_config_dir>/storage/identities/ and are used by
|
||||
rnstatus / rnpath remote queries (-i), rnx, and rnsh.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
|
|||
|
|
@ -77,8 +77,8 @@ class MarkdownRenderer:
|
|||
flags=re.DOTALL,
|
||||
)
|
||||
|
||||
# Inline code before emphasis so snake_case / ``rst`` spans are not
|
||||
# mangled by underscore italic (changelog uses both `code` and ``code``).
|
||||
# Inline code before emphasis so snake_case / rst spans are not
|
||||
# mangled by underscore italic (changelog uses both `code` and code).
|
||||
inline_codes: list[str] = []
|
||||
|
||||
def inline_code_placeholder(match):
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ MAX_MDI_ICON_NAME_LEN = 64
|
|||
|
||||
|
||||
def normalize_mdi_icon_name(value):
|
||||
"""Return a normalized icon name or ``None`` when unset/invalid."""
|
||||
"""Return a normalized icon name or None when unset/invalid."""
|
||||
if value is None:
|
||||
return None
|
||||
if not isinstance(value, str):
|
||||
|
|
|
|||
|
|
@ -12,9 +12,9 @@ from LXMF import LXMRouter
|
|||
|
||||
|
||||
def create_lxmf_router(identity, storagepath, propagation_cost=None):
|
||||
"""Construct an ``LXMF.LXMRouter`` without signal-handler crashes off the main thread.
|
||||
"""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
|
||||
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:
|
||||
|
|
@ -250,7 +250,7 @@ def normalize_hex_identifier(value: str | None) -> str:
|
|||
|
||||
|
||||
def hex_identifier_to_bytes(value: str | None) -> bytes | None:
|
||||
"""Parse a hex identity or hash string for ``bytes.fromhex`` (tolerates UUID-style separators)."""
|
||||
"""Parse a hex identity or hash string for bytes.fromhex (tolerates UUID-style separators)."""
|
||||
h = normalize_hex_identifier(value)
|
||||
if not h or len(h) % 2:
|
||||
return None
|
||||
|
|
@ -294,10 +294,10 @@ def find_lxm_by_content_hash_for_paper_uri(
|
|||
message_router,
|
||||
content_hash_bytes: bytes,
|
||||
):
|
||||
"""Return a live ``LXMessage`` from router outbound queues, or ``None``.
|
||||
"""Return a live LXMessage from router outbound queues, or None.
|
||||
|
||||
Paper URI generation needs packed bytes that only exist while the message is
|
||||
still in ``pending_outbound`` or ``pending_deferred_stamps``.
|
||||
still in pending_outbound or pending_deferred_stamps.
|
||||
"""
|
||||
if not message_router or not content_hash_bytes:
|
||||
return None
|
||||
|
|
@ -312,9 +312,9 @@ def find_lxm_by_content_hash_for_paper_uri(
|
|||
|
||||
|
||||
def lxmf_message_try_paper_uri_string(lxm) -> tuple[str | None, str | None]:
|
||||
"""Build an ``lxm://`` Paper URI from a live message without mutating it.
|
||||
"""Build an lxm:// Paper URI from a live message without mutating it.
|
||||
|
||||
Returns ``(uri, None)`` on success, or ``(None, detail)`` on failure.
|
||||
Returns (uri, None) on success, or (None, detail) on failure.
|
||||
"""
|
||||
if lxm is None:
|
||||
return None, "No message"
|
||||
|
|
@ -361,7 +361,7 @@ def interval_action_due(
|
|||
"""Return whether a periodic action should run now.
|
||||
|
||||
Used for auto-announce, propagation sync, and similar timers stored in config.
|
||||
If ``last_at`` is ahead of ``now`` (clock skew, restored DB, or bad values),
|
||||
If last_at is ahead of now (clock skew, restored DB, or bad values),
|
||||
the action is treated as due so scheduling does not stall until wall clock
|
||||
catches a corrupted future timestamp.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -174,7 +174,7 @@ def _import_display_names(database, display_names) -> int:
|
|||
def import_messages_export_bundle(database, payload) -> dict:
|
||||
"""Import messages plus optional contacts, names, and read state.
|
||||
|
||||
Accepts legacy ``{messages: [...]}`` / bare arrays and v2 bundles.
|
||||
Accepts legacy {messages: [...]} / bare arrays and v2 bundles.
|
||||
"""
|
||||
if isinstance(payload, list):
|
||||
messages = payload
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ class MessageHandler:
|
|||
params = [like_term, like_term, like_term, limit]
|
||||
return self.db.provider.fetchall(query, params)
|
||||
|
||||
# Keep conversation-list payloads small. Full ``fields`` often embeds
|
||||
# Keep conversation-list payloads small. Full fields often embeds
|
||||
# multi-MB base64 attachments and must never be loaded into the list API.
|
||||
# Prefer persisted has_* columns (schema v51+). Fall back to instr only
|
||||
# when flags were never backfilled (NULL) on filter paths.
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ request/response with specific path conventions).
|
|||
Clients link to the destination and call link.request("/page/name.mu")
|
||||
to fetch a page, or /file/name for files.
|
||||
|
||||
Supported page filename extensions are ``.mu``, ``.md``, ``.txt``, and ``.html``.
|
||||
Supported page filename extensions are .mu, .md, .txt, and .html.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
|
@ -177,9 +177,9 @@ class PageNode:
|
|||
self.active_links.remove(link)
|
||||
|
||||
def _ensure_local_path(self):
|
||||
"""Register this identity in ``RNS.Identity.known_destinations``.
|
||||
"""Register this identity in RNS.Identity.known_destinations.
|
||||
|
||||
Lets ``Identity.recall()`` resolve the destination for local link setup.
|
||||
Lets Identity.recall() resolve the destination for local link setup.
|
||||
"""
|
||||
if not self.destination:
|
||||
return
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
Handles creation, deletion, persistence, start/stop, and announce
|
||||
scheduling for page nodes. Each node gets its own subdirectory under
|
||||
``storage/page_nodes/<node_id>/``.
|
||||
storage/page_nodes/<node_id>/.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
|
|
|||
|
|
@ -71,8 +71,8 @@ class CrashRecovery:
|
|||
def install(self):
|
||||
"""Installs the crash recovery exception hook into the system.
|
||||
|
||||
Covers both the main thread (``sys.excepthook``) and background
|
||||
threads (``threading.excepthook``). A daemon worker dying silently is
|
||||
Covers both the main thread (sys.excepthook) and background
|
||||
threads (threading.excepthook). A daemon worker dying silently is
|
||||
a common source of hard-to-diagnose failures, so its exception is
|
||||
diagnosed and logged without tearing down the whole process.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
"""Remote Reticulum management client for status and path queries.
|
||||
|
||||
Uses the same ``rnstransport.remote.management`` destination and request paths
|
||||
as the bundled ``rnstatus`` / ``rnpath`` utilities (``-R`` / ``-i``).
|
||||
Uses the same rnstransport.remote.management destination and request paths
|
||||
as the bundled rnstatus / rnpath utilities (-R / -i).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ def bundled_pip_targets() -> tuple[str, ...]:
|
|||
|
||||
|
||||
def meshchat_bundle_project_root() -> Path | None:
|
||||
"""Directory containing ``pyproject.toml`` for this MeshChatX tree (repo layout helper)."""
|
||||
"""Directory containing pyproject.toml for this MeshChatX tree (repo layout helper)."""
|
||||
here = Path(__file__).resolve()
|
||||
for anc in here.parents:
|
||||
meta = anc / "pyproject.toml"
|
||||
|
|
@ -68,7 +68,7 @@ REPOSITORY_BUNDLED_PUBLIC_PARTS = ("repository-server-bundled", "bundled")
|
|||
|
||||
|
||||
def public_bundled_wheels_dir(public_dir: str) -> str:
|
||||
"""Directory under ``public_dir`` where build-staged wheels live (HTTP + optional pip fallback)."""
|
||||
"""Directory under public_dir where build-staged wheels live (HTTP + optional pip fallback)."""
|
||||
return os.path.join(public_dir, *REPOSITORY_BUNDLED_PUBLIC_PARTS)
|
||||
|
||||
|
||||
|
|
@ -180,9 +180,9 @@ def _download_wheel_via_pypi_index(
|
|||
|
||||
|
||||
def stage_local_meshchatx_wheel_into_bundled_dir(dest: Path) -> Path | None:
|
||||
"""If ``dist/reticulum_meshchatx-*.whl`` exists under the project root, copy the newest into ``dest``.
|
||||
"""If dist/reticulum_meshchatx-*.whl exists under the project root, copy the newest into dest.
|
||||
|
||||
Replaces any PyPI-downloaded ``reticulum_meshchatx-*.whl`` so APK/offline bundles ship this tree's wheel.
|
||||
Replaces any PyPI-downloaded reticulum_meshchatx-*.whl so APK/offline bundles ship this tree's wheel.
|
||||
"""
|
||||
root = meshchat_bundle_project_root()
|
||||
if root is None:
|
||||
|
|
@ -214,9 +214,9 @@ def download_bundled_wheels_to_directory(
|
|||
*,
|
||||
on_package: Callable[[int, int, str], None] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Populate ``dest`` with wheels for :func:`bundled_pip_targets`.
|
||||
"""Populate dest with wheels for :func:`bundled_pip_targets`.
|
||||
|
||||
Uses PyPI project metadata JSON and HTTPS downloads via ``urllib`` only.
|
||||
Uses PyPI project metadata JSON and HTTPS downloads via urllib only.
|
||||
"""
|
||||
dest.mkdir(parents=True, exist_ok=True)
|
||||
packages = list(bundled_pip_targets())
|
||||
|
|
@ -255,7 +255,7 @@ _FILE_LIST_MARKER = "<!--FILE_LISTS-->"
|
|||
|
||||
|
||||
def _repository_index_template_path(public_dir: str | None) -> Path | None:
|
||||
"""Resolve the repository index HTML (Vite public / built ``public`` / source tree)."""
|
||||
"""Resolve the repository index HTML (Vite public / built public / source tree)."""
|
||||
if public_dir:
|
||||
candidate = Path(public_dir) / _REPOSITORY_INDEX_HTML
|
||||
if candidate.is_file():
|
||||
|
|
@ -319,7 +319,7 @@ def build_repository_index_html(
|
|||
uploads_dir: str,
|
||||
public_dir: str | None = None,
|
||||
) -> str:
|
||||
"""HTML shell with live bundled and uploads file tables (for ``/`` and ``/index.html``)."""
|
||||
"""HTML shell with live bundled and uploads file tables (for / and /index.html)."""
|
||||
template = _repository_index_template_path(public_dir)
|
||||
shell: str
|
||||
if template is not None:
|
||||
|
|
@ -356,7 +356,7 @@ def make_repository_http_request_handler(
|
|||
root: str,
|
||||
public_dir: str | None = None,
|
||||
) -> type[http.server.SimpleHTTPRequestHandler]:
|
||||
"""``SimpleHTTPRequestHandler`` subclass: dynamic index listing at ``/`` and ``/index.html``."""
|
||||
"""SimpleHTTPRequestHandler subclass: dynamic index listing at / and /index.html."""
|
||||
root_abs = os.path.abspath(root)
|
||||
uploads_dir = os.path.join(root_abs, "uploads")
|
||||
bundled_dir = os.path.join(root_abs, "bundled")
|
||||
|
|
@ -411,7 +411,7 @@ def _safe_any_upload_filename(name: str) -> str | None:
|
|||
|
||||
|
||||
class RepositoryServerManager:
|
||||
"""Keeps user uploads and a ``bundled`` directory of wheels (PyPI over HTTPS, stdlib only)."""
|
||||
"""Keeps user uploads and a bundled directory of wheels (PyPI over HTTPS, stdlib only)."""
|
||||
|
||||
def __init__(self, storage_path: str, public_dir: str | None = None) -> None:
|
||||
self.root = os.path.join(storage_path, "repository-server")
|
||||
|
|
@ -523,7 +523,7 @@ class RepositoryServerManager:
|
|||
host: str | None = None,
|
||||
port: int | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Serve ``repository-server`` root over plain HTTP (no TLS) on a background thread."""
|
||||
"""Serve repository-server root over plain HTTP (no TLS) on a background thread."""
|
||||
bind_host = _normalize_listen_host(host or self._http_last_host or "127.0.0.1")
|
||||
if not bind_host:
|
||||
return {"ok": False, "error": "invalid_host"}
|
||||
|
|
@ -671,7 +671,7 @@ class RepositoryServerManager:
|
|||
}
|
||||
|
||||
def refresh_bundled_wheels(self) -> dict[str, Any]:
|
||||
"""Download wheels into ``bundled_dir`` (PyPI JSON + ``urllib``).
|
||||
"""Download wheels into bundled_dir (PyPI JSON + urllib).
|
||||
|
||||
Downloads into a temporary directory first, then atomically replaces
|
||||
the live bundled directory so a failed refresh cannot wipe existing
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ def repair_unparseable_reticulum_config(config_path: str, *, write_default) -> b
|
|||
"""Back up and rewrite *config_path* when ConfigObj cannot parse it.
|
||||
|
||||
*write_default* must be a callable accepting the config path and writing
|
||||
stock RNS defaults (``ReticulumMeshChat._write_rns_reticulum_default_config_file``).
|
||||
stock RNS defaults (ReticulumMeshChat._write_rns_reticulum_default_config_file).
|
||||
|
||||
Returns True when the file was replaced.
|
||||
"""
|
||||
|
|
@ -88,8 +88,8 @@ def repair_unparseable_reticulum_config(config_path: str, *, write_default) -> b
|
|||
def ensure_safe_reticulum_runtime_flags(config_path: str) -> bool:
|
||||
"""Force runtime flags that keep MeshChatX alive when interfaces fail.
|
||||
|
||||
Currently forces ``panic_on_interface_error = No`` so RNS does not call
|
||||
``os._exit`` on interface faults.
|
||||
Currently forces panic_on_interface_error = No so RNS does not call
|
||||
os._exit on interface faults.
|
||||
"""
|
||||
from meshchatx.src.backend.rns_startup_recovery import (
|
||||
ensure_panic_on_interface_error_disabled,
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ class OutboundPathOutcome:
|
|||
|
||||
|
||||
def format_outbound_path_finding_measure(outcome: OutboundPathOutcome) -> str:
|
||||
"""Single string for storage/API: base measure, plus ``+nudge`` if a nudge was used."""
|
||||
"""Single string for storage/API: base measure, plus +nudge if a nudge was used."""
|
||||
base = outcome.prepare_measure
|
||||
if outcome.used_nudge:
|
||||
return f"{base}+nudge"
|
||||
|
|
@ -213,8 +213,8 @@ def prepare_fresh_path_request(
|
|||
"""Ensure a path request is in flight if needed.
|
||||
|
||||
Returns a stable label for what was done before waiting:
|
||||
``reused_valid_path`` (no new request), ``path_refresh_requested`` (dropped
|
||||
or expired then requested), or ``new_path_requested`` (no prior path).
|
||||
reused_valid_path (no new request), path_refresh_requested (dropped
|
||||
or expired then requested), or new_path_requested (no prior path).
|
||||
"""
|
||||
if not should_rediscover_path(destination_hash):
|
||||
return "reused_valid_path"
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from .audio_codec import encode_audio_to_ogg_opus
|
|||
|
||||
|
||||
def _ringtone_profile():
|
||||
"""Stereo 48 kHz Opus ``audio`` profile for music-grade ringtones."""
|
||||
"""Stereo 48 kHz Opus audio profile for music-grade ringtones."""
|
||||
from LXST.Codecs import Opus
|
||||
|
||||
return Opus.PROFILE_AUDIO_MAX
|
||||
|
|
@ -29,11 +29,11 @@ class RingtoneManager:
|
|||
os.makedirs(self.storage_dir, exist_ok=True)
|
||||
|
||||
def convert_to_ringtone(self, input_path, ringtone_id=None):
|
||||
"""Decode ``input_path`` and re-encode it as an OGG/Opus ringtone.
|
||||
"""Decode input_path and re-encode it as an OGG/Opus ringtone.
|
||||
|
||||
Accepts any audio container miniaudio can decode (WAV, MP3, FLAC,
|
||||
OGG/Vorbis) plus OGG/Opus via LXST. Encoded with the music
|
||||
``audio`` profile so trim edits preserve the original duration
|
||||
audio profile so trim edits preserve the original duration
|
||||
and stereo image instead of being forced through the low-bitrate
|
||||
voice profile. Returns the stored filename.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# SPDX-License-Identifier: 0BSD
|
||||
|
||||
"""Sparse fetch of specific files from an RNGit ``rns://`` repository."""
|
||||
"""Sparse fetch of specific files from an RNGit rns:// repository."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ def rnode_transport_supported(iface: dict, *, is_android: bool | None = None) ->
|
|||
usbserial4a + jnius on Android. BLE needs able on Android. On desktop,
|
||||
serial and classic-Bluetooth rely on pyserial; BLE relies on bleak.
|
||||
|
||||
``is_android`` lets a caller that already determined the platform pass
|
||||
is_android lets a caller that already determined the platform pass
|
||||
that result through explicitly, instead of re-detecting it here.
|
||||
"""
|
||||
if is_android is None:
|
||||
|
|
@ -160,7 +160,7 @@ def normalize_rnode_tcp_host_in_config(config_path: str) -> bool:
|
|||
RNS's desktop RNodeInterface derives tcp_host from a tcp:// port itself,
|
||||
but the Android-specific implementation reads tcp_host as its own,
|
||||
separate config key and never looks at port for that. Configs written or
|
||||
hand-edited with only ``port = tcp://host:port`` therefore silently try
|
||||
hand-edited with only port = tcp://host:port therefore silently try
|
||||
(and fail) to open the RNode as a serial device on Android. This keeps
|
||||
both keys in sync regardless of how the entry was created, so RNode over
|
||||
TCP works the same way on both platforms.
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ def sweep_stale_links():
|
|||
def clear_all_cached_links() -> int:
|
||||
"""Tear down every cached RNS link (used after RNS hot reload).
|
||||
|
||||
``sweep_stale_links`` leaves ACTIVE links alone. After Transport reset those
|
||||
sweep_stale_links leaves ACTIVE links alone. After Transport reset those
|
||||
objects are tied to the old stack and must be dropped.
|
||||
"""
|
||||
with _rns_links_lock:
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@
|
|||
|
||||
"""Contain RNS process-killing exits and recover from bad interface configs.
|
||||
|
||||
Reticulum's ``RNS.panic()`` calls ``os._exit(255)``, which kills the whole
|
||||
Reticulum's RNS.panic() calls os._exit(255), which kills the whole
|
||||
MeshChatX process (fatal on Android where Python runs in-process). Interface
|
||||
init failures can also leave the app unable to start until the user wipes
|
||||
storage. This module:
|
||||
|
||||
1. Replaces ``RNS.panic`` / ``RNS.exit`` with catchable exceptions
|
||||
2. Forces ``panic_on_interface_error = No`` in the Reticulum config
|
||||
1. Replaces RNS.panic / RNS.exit with catchable exceptions
|
||||
2. Forces panic_on_interface_error = No in the Reticulum config
|
||||
3. Progressively disables risky interfaces and retries RNS construction
|
||||
"""
|
||||
|
||||
|
|
@ -44,7 +44,7 @@ _HIGH_RISK_TYPES = (
|
|||
|
||||
|
||||
class RnsPanicError(RuntimeError):
|
||||
"""Raised instead of ``os._exit`` when RNS would panic or hard-exit."""
|
||||
"""Raised instead of os._exit when RNS would panic or hard-exit."""
|
||||
|
||||
|
||||
def install_rns_panic_containment(*, force: bool = False) -> bool:
|
||||
|
|
@ -102,7 +102,7 @@ def install_rns_panic_containment(*, force: bool = False) -> bool:
|
|||
|
||||
|
||||
def ensure_panic_on_interface_error_disabled(config_path: str) -> bool:
|
||||
"""Force ``panic_on_interface_error = No`` so interface faults cannot kill RNS."""
|
||||
"""Force panic_on_interface_error = No so interface faults cannot kill RNS."""
|
||||
if not os.path.isfile(config_path):
|
||||
return False
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ _LISTEN_ADDRESS_RE = re.compile(
|
|||
|
||||
_RNSH_MODULE = "RNS.Utilities.rnsh.rnsh"
|
||||
# cx_Freeze / AppImage bundles set sys.executable to MeshChatX itself, which
|
||||
# does not accept Python's ``-m``. meshchat.main() dispatches this flag to
|
||||
# does not accept Python's -m. meshchat.main() dispatches this flag to
|
||||
# runpy.run_module before argparse (see meshchatx/meshchat.py).
|
||||
_MESHCHATX_RUN_MODULE_FLAG = "--meshchatx-run-module"
|
||||
|
||||
|
|
@ -151,7 +151,7 @@ class RNSHSession:
|
|||
def resolved_config_dir(self):
|
||||
"""The Reticulum config directory rnsh is launched against.
|
||||
|
||||
A per-session ``config_path`` override wins; otherwise the manager's
|
||||
A per-session config_path override wins; otherwise the manager's
|
||||
shared directory (the MeshChatX app's Reticulum instance) is used so
|
||||
rnsh attaches to the same shared instance.
|
||||
"""
|
||||
|
|
@ -233,7 +233,7 @@ class RNSHSession:
|
|||
def _maybe_detect_listen_address(self):
|
||||
"""Extract the listener destination hash from rnsh log output.
|
||||
|
||||
Must be called while holding ``self._lock``.
|
||||
Must be called while holding self._lock.
|
||||
Returns True when a new listen address was stored.
|
||||
"""
|
||||
if self.mode != "listen" or self.listen_address:
|
||||
|
|
@ -263,11 +263,11 @@ class RNSHSession:
|
|||
Prefer the Python module entry point so sessions work when the PATH
|
||||
console-script wrapper is not executable (common with pip --user
|
||||
installs) or when Landlock denies executing paths outside allowed
|
||||
read roots (for example ``~/.local/bin/rnsh``).
|
||||
read roots (for example ~/.local/bin/rnsh).
|
||||
|
||||
Frozen desktop builds (Windows EXE, AppImage, macOS) set
|
||||
``sys.executable`` to MeshChatX itself, which rejects ``-m``. Those
|
||||
builds re-enter via ``--meshchatx-run-module`` instead.
|
||||
sys.executable to MeshChatX itself, which rejects -m. Those
|
||||
builds re-enter via --meshchatx-run-module instead.
|
||||
"""
|
||||
if RNSHSession._rnsh_module_available():
|
||||
if RNSHSession._is_frozen_executable():
|
||||
|
|
@ -393,8 +393,8 @@ class RNSHSession:
|
|||
def _acquire_controlling_tty(): # pragma: no cover - runs in child process
|
||||
"""Make the slave pty the controlling terminal of the child.
|
||||
|
||||
Runs in the forked child after ``start_new_session`` has called
|
||||
``setsid`` and after stdio has been redirected to the slave pty.
|
||||
Runs in the forked child after start_new_session has called
|
||||
setsid and after stdio has been redirected to the slave pty.
|
||||
"""
|
||||
with contextlib.suppress(Exception):
|
||||
fcntl.ioctl(0, termios.TIOCSCTTY, 0)
|
||||
|
|
@ -614,7 +614,7 @@ class RNSHSession:
|
|||
self.manager.save()
|
||||
|
||||
def _waiter_loop(self, process):
|
||||
"""Wait for ``process`` and update status only if it is still current.
|
||||
"""Wait for process and update status only if it is still current.
|
||||
|
||||
The process is passed in so a later restart cannot be clobbered by an
|
||||
older waiter finishing after a new session process was started.
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ _LISTEN_ADDRESS_RE = re.compile(
|
|||
|
||||
_RNX_MODULE = "RNS.Utilities.rnx"
|
||||
# cx_Freeze / AppImage bundles set sys.executable to MeshChatX itself, which
|
||||
# does not accept Python's ``-m``. meshchat.main() dispatches this flag to
|
||||
# does not accept Python's -m. meshchat.main() dispatches this flag to
|
||||
# runpy.run_module before argparse (see meshchatx/meshchat.py).
|
||||
_MESHCHATX_RUN_MODULE_FLAG = "--meshchatx-run-module"
|
||||
|
||||
|
|
@ -154,7 +154,7 @@ class RNXSession:
|
|||
def resolved_config_dir(self):
|
||||
"""The Reticulum config directory rnx is launched against.
|
||||
|
||||
A per-session ``config_path`` override wins; otherwise the manager's
|
||||
A per-session config_path override wins; otherwise the manager's
|
||||
shared directory (the MeshChatX app's Reticulum instance) is used so
|
||||
rnx attaches to the same shared instance.
|
||||
"""
|
||||
|
|
@ -236,7 +236,7 @@ class RNXSession:
|
|||
def _maybe_detect_listen_address(self):
|
||||
"""Extract the listener destination hash from rnx log output.
|
||||
|
||||
Must be called while holding ``self._lock``.
|
||||
Must be called while holding self._lock.
|
||||
Returns True when a new listen address was stored.
|
||||
"""
|
||||
if self.mode != "listen" or self.listen_address:
|
||||
|
|
@ -266,11 +266,11 @@ class RNXSession:
|
|||
Prefer the Python module entry point so sessions work when the PATH
|
||||
console-script wrapper is not executable (common with pip --user
|
||||
installs) or when Landlock denies executing paths outside allowed
|
||||
read roots (for example ``~/.local/bin/rnx``).
|
||||
read roots (for example ~/.local/bin/rnx).
|
||||
|
||||
Frozen desktop builds (Windows EXE, AppImage, macOS) set
|
||||
``sys.executable`` to MeshChatX itself, which rejects ``-m``. Those
|
||||
builds re-enter via ``--meshchatx-run-module`` instead.
|
||||
sys.executable to MeshChatX itself, which rejects -m. Those
|
||||
builds re-enter via --meshchatx-run-module instead.
|
||||
"""
|
||||
if RNXSession._rnx_module_available():
|
||||
if RNXSession._is_frozen_executable():
|
||||
|
|
@ -402,8 +402,8 @@ class RNXSession:
|
|||
def _acquire_controlling_tty(): # pragma: no cover - runs in child process
|
||||
"""Make the slave pty the controlling terminal of the child.
|
||||
|
||||
Runs in the forked child after ``start_new_session`` has called
|
||||
``setsid`` and after stdio has been redirected to the slave pty.
|
||||
Runs in the forked child after start_new_session has called
|
||||
setsid and after stdio has been redirected to the slave pty.
|
||||
"""
|
||||
with contextlib.suppress(Exception):
|
||||
fcntl.ioctl(0, termios.TIOCSCTTY, 0)
|
||||
|
|
@ -623,7 +623,7 @@ class RNXSession:
|
|||
self.manager.save()
|
||||
|
||||
def _waiter_loop(self, process):
|
||||
"""Wait for ``process`` and update status only if it is still current.
|
||||
"""Wait for process and update status only if it is still current.
|
||||
|
||||
The process is passed in so a later restart cannot be clobbered by an
|
||||
older waiter finishing after a new session process was started.
|
||||
|
|
|
|||
|
|
@ -1390,12 +1390,12 @@ class RRCHub:
|
|||
}
|
||||
|
||||
def room_messages(self, room, limit=None, before_seq=None):
|
||||
"""Return ``(messages, has_more)`` for a room, newest page last.
|
||||
"""Return (messages, has_more) for a room, newest page last.
|
||||
|
||||
``before_seq``, when given, restricts results to messages recorded
|
||||
before_seq, when given, restricts results to messages recorded
|
||||
before that sequence number, letting callers page backwards through
|
||||
history. ``limit`` caps how many of the most recent matching messages
|
||||
are returned; ``has_more`` reports whether older messages remain.
|
||||
history. limit caps how many of the most recent matching messages
|
||||
are returned; has_more reports whether older messages remain.
|
||||
"""
|
||||
msgs = self.get_messages(proto.normalize_room(room))
|
||||
if before_seq is not None:
|
||||
|
|
@ -1461,7 +1461,7 @@ class RRCManager:
|
|||
self._server_manager = server_manager
|
||||
|
||||
def find_local_server(self, hub_hash):
|
||||
"""Return a running locally hosted hub matching ``hub_hash``, if any."""
|
||||
"""Return a running locally hosted hub matching hub_hash, if any."""
|
||||
sm = self._server_manager
|
||||
if sm is None:
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ def decode(data):
|
|||
|
||||
|
||||
def load(fp):
|
||||
"""Read a single CBOR value from a stream, raising ``EOFError`` at the end."""
|
||||
"""Read a single CBOR value from a stream, raising EOFError at the end."""
|
||||
try:
|
||||
return cbor2.load(fp)
|
||||
except cbor2.CBORDecodeEOF as exc:
|
||||
|
|
@ -119,7 +119,7 @@ _MENTION_RE_CACHE = {}
|
|||
|
||||
|
||||
def mention_re(nick):
|
||||
"""Return a compiled regex matching ``@nick`` mentions, or ``None``."""
|
||||
"""Return a compiled regex matching @nick mentions, or None."""
|
||||
if not isinstance(nick, str) or not nick:
|
||||
return None
|
||||
pat = _MENTION_RE_CACHE.get(nick)
|
||||
|
|
@ -135,7 +135,7 @@ def mention_re(nick):
|
|||
|
||||
|
||||
def text_mentions(text, nick):
|
||||
"""Return ``True`` when ``text`` mentions ``nick``."""
|
||||
"""Return True when text mentions nick."""
|
||||
pat = mention_re(nick)
|
||||
return bool(pat is not None and isinstance(text, str) and pat.search(text))
|
||||
|
||||
|
|
@ -159,10 +159,10 @@ def make_envelope(msg_type, src, room=None, body=None, nick=None, mid=None, ts=N
|
|||
|
||||
|
||||
def display_name_from_hub_app_data(app_data_b64):
|
||||
"""Return the hub name from a base64-encoded RRC announce, or ``None``.
|
||||
"""Return the hub name from a base64-encoded RRC announce, or None.
|
||||
|
||||
Hosted hubs announce CBOR ``app_data`` of the form
|
||||
``{"proto": "rrc", "v": 1, "hub": name}``.
|
||||
Hosted hubs announce CBOR app_data of the form
|
||||
{"proto": "rrc", "v": 1, "hub": name}.
|
||||
"""
|
||||
if not app_data_b64:
|
||||
return None
|
||||
|
|
@ -179,7 +179,7 @@ def display_name_from_hub_app_data(app_data_b64):
|
|||
|
||||
|
||||
def normalize_nick(nick, max_bytes=DEFAULT_MAX_NICK_BYTES):
|
||||
"""Normalize a nickname, returning ``None`` when empty or invalid."""
|
||||
"""Normalize a nickname, returning None when empty or invalid."""
|
||||
if not isinstance(nick, str):
|
||||
return None
|
||||
n = " ".join(nick.split()).strip()
|
||||
|
|
@ -212,11 +212,11 @@ _WHO_ENTRY_RE = re.compile(
|
|||
|
||||
|
||||
def parse_who_notice(text):
|
||||
"""Parse a hub ``/who`` notice into ``(room, [(nick, hex), ...])``.
|
||||
"""Parse a hub /who notice into (room, [(nick, hex), ...]).
|
||||
|
||||
Nicked users carry only a 12-hex prefix of their identity hash, while
|
||||
un-nicked users appear as their full hex hash. Returns ``None`` when the
|
||||
notice is not a ``/who`` response.
|
||||
un-nicked users appear as their full hex hash. Returns None when the
|
||||
notice is not a /who response.
|
||||
"""
|
||||
if not isinstance(text, str):
|
||||
return None
|
||||
|
|
@ -241,7 +241,7 @@ def parse_who_notice(text):
|
|||
|
||||
|
||||
def parse_room_list_notice(text):
|
||||
"""Parse a hub ``/list`` notice into ``{room: topic_or_None}`` or ``None``."""
|
||||
"""Parse a hub /list notice into {room: topic_or_None} or None."""
|
||||
if not isinstance(text, str):
|
||||
return None
|
||||
stripped = text.strip()
|
||||
|
|
|
|||
|
|
@ -277,7 +277,7 @@ def _is_frozen_executable() -> bool:
|
|||
def _frontend_source_available() -> bool:
|
||||
"""True when running from a source tree with Vite frontend sources.
|
||||
|
||||
Built ``meshchatx/public/`` is gitignored and often absent in CI / E2E
|
||||
Built meshchatx/public/ is gitignored and often absent in CI / E2E
|
||||
(Vite serves the UI). Frozen desktop builds still require bundled public.
|
||||
"""
|
||||
try:
|
||||
|
|
@ -313,7 +313,7 @@ def check_public_assets(public_path_fn: Callable[[str], str]) -> dict[str, str]:
|
|||
|
||||
|
||||
def check_meshchatx_run_module() -> dict[str, str]:
|
||||
"""Verify ``--meshchatx-run-module`` re-entry used by bots/rnsh on frozen builds."""
|
||||
"""Verify --meshchatx-run-module re-entry used by bots/rnsh on frozen builds."""
|
||||
marker_dir = tempfile.mkdtemp(prefix="meshchatx_run_module_check_")
|
||||
marker = os.path.join(marker_dir, "probe.out")
|
||||
env = os.environ.copy()
|
||||
|
|
@ -378,9 +378,9 @@ def check_meshchatx_run_module() -> dict[str, str]:
|
|||
def check_subprocess_spawn() -> dict[str, str]:
|
||||
"""Spawn a short-lived child process (covers Windows CreateProcess / POSIX fork).
|
||||
|
||||
Frozen desktop builds (AppImage / EXE / macOS) set ``sys.executable`` to
|
||||
MeshChatX itself, which rejects Python ``-c``. Those builds re-enter via
|
||||
``--meshchatx-run-module`` like bots and rnsh.
|
||||
Frozen desktop builds (AppImage / EXE / macOS) set sys.executable to
|
||||
MeshChatX itself, which rejects Python -c. Those builds re-enter via
|
||||
--meshchatx-run-module like bots and rnsh.
|
||||
"""
|
||||
try:
|
||||
env = {**os.environ, "PYTHONUNBUFFERED": "1"}
|
||||
|
|
@ -639,7 +639,7 @@ def _ensure_app_session_secret(app: Any) -> None:
|
|||
|
||||
|
||||
def _ensure_awaitable_method(app: Any, name: str) -> None:
|
||||
"""Ensure ``app.name`` is awaitable (unit tests often patch with sync MagicMock)."""
|
||||
"""Ensure app.name is awaitable (unit tests often patch with sync MagicMock)."""
|
||||
import asyncio
|
||||
|
||||
method = getattr(app, name, None)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# SPDX-License-Identifier: 0BSD
|
||||
|
||||
"""Probe module for self-check ``--meshchatx-run-module`` verification."""
|
||||
"""Probe module for self-check --meshchatx-run-module verification."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"""Validation and (de)serialization for MeshChatX sticker packs.
|
||||
|
||||
A sticker pack is a named collection of stickers. Packs use the
|
||||
``meshchatx-stickerpack`` JSON document format so they can be exported to a
|
||||
meshchatx-stickerpack JSON document format so they can be exported to a
|
||||
local file, attached to an LXMF message for peer sharing, or installed back
|
||||
into another identity's library.
|
||||
"""
|
||||
|
|
@ -20,7 +20,7 @@ _VALID_PACK_TYPES = frozenset({"static", "animated", "video", "mixed"})
|
|||
|
||||
|
||||
def sanitize_pack_title(title: str | None) -> str:
|
||||
"""Return a printable title for a pack, defaulting to ``"Untitled pack"``."""
|
||||
"""Return a printable title for a pack, defaulting to "Untitled pack"."""
|
||||
if title is None:
|
||||
return "Untitled pack"
|
||||
s = "".join(ch for ch in str(title).strip() if ch.isprintable())
|
||||
|
|
@ -28,7 +28,7 @@ def sanitize_pack_title(title: str | None) -> str:
|
|||
|
||||
|
||||
def sanitize_pack_short_name(name: str | None) -> str | None:
|
||||
"""Return a slug-compatible short name (alnum/underscore/dash) or ``None``."""
|
||||
"""Return a slug-compatible short name (alnum/underscore/dash) or None."""
|
||||
if name is None:
|
||||
return None
|
||||
s = str(name).strip().lower()
|
||||
|
|
@ -45,7 +45,7 @@ def sanitize_pack_description(description: str | None) -> str | None:
|
|||
|
||||
|
||||
def sanitize_pack_type(pack_type: str | None) -> str:
|
||||
"""Normalize a pack type to one of ``static|animated|video|mixed``."""
|
||||
"""Normalize a pack type to one of static|animated|video|mixed."""
|
||||
if not pack_type:
|
||||
return "mixed"
|
||||
s = str(pack_type).strip().lower()
|
||||
|
|
@ -57,10 +57,10 @@ def build_pack_document(
|
|||
stickers: list[dict],
|
||||
exported_at_iso: str,
|
||||
) -> dict:
|
||||
"""Build a ``meshchatx-stickerpack`` document from a pack and its stickers.
|
||||
"""Build a meshchatx-stickerpack document from a pack and its stickers.
|
||||
|
||||
``pack`` is a row from ``user_sticker_packs``. ``stickers`` is a list of
|
||||
rows containing ``name``, ``emoji``, ``image_type``, ``image_bytes`` (base64
|
||||
pack is a row from user_sticker_packs. stickers is a list of
|
||||
rows containing name, emoji, image_type, image_bytes (base64
|
||||
string), and optional metadata fields.
|
||||
"""
|
||||
return {
|
||||
|
|
@ -80,10 +80,10 @@ def build_pack_document(
|
|||
|
||||
|
||||
def validate_pack_document(data: object) -> dict:
|
||||
"""Parse and validate a ``meshchatx-stickerpack`` document.
|
||||
"""Parse and validate a meshchatx-stickerpack document.
|
||||
|
||||
Returns a dict with normalized ``pack`` and ``stickers`` lists. Raises
|
||||
``ValueError`` with a short reason on invalid input.
|
||||
Returns a dict with normalized pack and stickers lists. Raises
|
||||
ValueError with a short reason on invalid input.
|
||||
"""
|
||||
if not isinstance(data, dict):
|
||||
msg = "invalid_pack_document"
|
||||
|
|
|
|||
|
|
@ -3,24 +3,24 @@
|
|||
"""Validation, hashing, and metadata extraction for user sticker payloads.
|
||||
|
||||
MeshChatX aligns with the Telegram sticker specification while keeping the
|
||||
historical "saved image" formats supported as a separate ``legacy`` class so
|
||||
historical "saved image" formats supported as a separate legacy class so
|
||||
existing libraries continue to work after migration.
|
||||
|
||||
Sticker classes:
|
||||
|
||||
``static``
|
||||
static
|
||||
PNG or WebP. Telegram-strict: max 512 KB, max 512x512 px and at least one
|
||||
side must be exactly 512 px.
|
||||
|
||||
``animated``
|
||||
animated
|
||||
TGS (gzipped Lottie JSON). Telegram-strict: max 64 KB, canvas 512x512,
|
||||
30-60 FPS, max 3 s, looped (loop attribute is informational only).
|
||||
|
||||
``video``
|
||||
video
|
||||
WebM/VP9 with no audio. Telegram-strict: max 256 KB, max 512x512 with at
|
||||
least one side exactly 512 px, max 30 FPS, max 3 s.
|
||||
|
||||
``legacy``
|
||||
legacy
|
||||
PNG/JPEG/GIF/WebP/BMP without dimension/duration enforcement. Capped at
|
||||
512 KB. Used for the historical free-form sticker library and for save
|
||||
images coming from chats. Not exportable as part of a Telegram-style pack.
|
||||
|
|
@ -96,7 +96,7 @@ def detect_image_format_from_magic(image_bytes: bytes) -> str | None:
|
|||
"""Detect a sticker payload format from its magic bytes.
|
||||
|
||||
Recognises PNG, JPEG, GIF, WebP, BMP, gzipped TGS (Lottie), and EBML/WebM
|
||||
containers. Returns a normalized type key or ``None`` for unknown input.
|
||||
containers. Returns a normalized type key or None for unknown input.
|
||||
"""
|
||||
if not isinstance(image_bytes, (bytes, bytearray)) or len(image_bytes) < 4:
|
||||
return None
|
||||
|
|
@ -172,7 +172,7 @@ def _read_bmp_dimensions(data: bytes) -> tuple[int, int] | None:
|
|||
|
||||
|
||||
def detect_image_dimensions(image_type: str, data: bytes) -> tuple[int, int] | None:
|
||||
"""Return ``(width, height)`` for a static sticker payload, or ``None``.
|
||||
"""Return (width, height) for a static sticker payload, or None.
|
||||
|
||||
Works for PNG, WebP (lossy/lossless/extended), GIF, and BMP without any
|
||||
third-party imaging dependency.
|
||||
|
|
@ -230,13 +230,13 @@ _GZIP_WBITS = 31
|
|||
|
||||
|
||||
def _decompress_gzip_bounded(data: bytes, max_bytes: int) -> bytes:
|
||||
"""Decompress a gzip stream, never buffering more than ``max_bytes``.
|
||||
"""Decompress a gzip stream, never buffering more than max_bytes.
|
||||
|
||||
Unlike ``gzip.decompress`` (which expands the whole stream into memory
|
||||
Unlike gzip.decompress (which expands the whole stream into memory
|
||||
before any size check), this caps each decompression step so a small
|
||||
"gzip bomb" cannot force an unbounded allocation. Raises ``ValueError``
|
||||
with ``invalid_tgs_too_large_decompressed`` once the output would exceed
|
||||
``max_bytes``.
|
||||
"gzip bomb" cannot force an unbounded allocation. Raises ValueError
|
||||
with invalid_tgs_too_large_decompressed once the output would exceed
|
||||
max_bytes.
|
||||
"""
|
||||
decompressor = zlib.decompressobj(_GZIP_WBITS)
|
||||
chunks: list[bytes] = []
|
||||
|
|
@ -266,8 +266,8 @@ def _decompress_gzip_bounded(data: bytes, max_bytes: int) -> bytes:
|
|||
def parse_tgs(data: bytes) -> dict:
|
||||
"""Decompress a TGS payload and parse the Lottie JSON inside.
|
||||
|
||||
Returns a dict with ``width``, ``height``, ``fps``, ``duration_ms`` and the
|
||||
raw lottie ``data``. Raises ``ValueError`` if the file is not a valid
|
||||
Returns a dict with width, height, fps, duration_ms and the
|
||||
raw lottie data. Raises ValueError if the file is not a valid
|
||||
Lottie animation.
|
||||
"""
|
||||
if not isinstance(data, (bytes, bytearray)) or len(data) < 2:
|
||||
|
|
@ -319,7 +319,7 @@ def _ebml_read_vint(
|
|||
*,
|
||||
mask_marker: bool = True,
|
||||
) -> tuple[int, int] | None:
|
||||
"""Read an EBML variable-length integer at ``pos``; returns ``(value, next_pos)``."""
|
||||
"""Read an EBML variable-length integer at pos; returns (value, next_pos)."""
|
||||
if pos >= len(buf):
|
||||
return None
|
||||
first = buf[pos]
|
||||
|
|
@ -376,8 +376,8 @@ def _ebml_read_float(buf: bytes, start: int, end: int) -> float | None:
|
|||
def parse_webm(data: bytes) -> dict:
|
||||
"""Parse a WebM container header to extract sticker-relevant metadata.
|
||||
|
||||
Returns ``width``, ``height``, ``fps`` (best-effort), ``duration_ms``, the
|
||||
detected video ``codec_id`` and ``has_audio`` flag. Raises ``ValueError``
|
||||
Returns width, height, fps (best-effort), duration_ms, the
|
||||
detected video codec_id and has_audio flag. Raises ValueError
|
||||
when the container is not a valid WebM file.
|
||||
"""
|
||||
if not isinstance(data, (bytes, bytearray)) or len(data) < 32:
|
||||
|
|
@ -482,12 +482,12 @@ def validate_sticker_payload(
|
|||
) -> tuple[str, str]:
|
||||
"""Validate a sticker payload against the legacy or strict Telegram rules.
|
||||
|
||||
When ``strict`` is False the validator preserves the historical behaviour
|
||||
When strict is False the validator preserves the historical behaviour
|
||||
of the MeshChatX library (PNG/JPEG/GIF/WebP/BMP up to 512 KB, no dimension
|
||||
enforcement). When ``strict`` is True the validator additionally enforces
|
||||
enforcement). When strict is True the validator additionally enforces
|
||||
Telegram-aligned size, dimension, FPS and duration limits per format.
|
||||
|
||||
Returns ``(normalized_image_type, content_hash_hex)``. Raises ``ValueError``
|
||||
Returns (normalized_image_type, content_hash_hex). Raises ValueError
|
||||
with a short machine-readable reason on invalid input.
|
||||
"""
|
||||
if not isinstance(image_bytes, (bytes, bytearray)):
|
||||
|
|
@ -626,8 +626,8 @@ _EXPORT_VERSION = 1
|
|||
def validate_export_document(data: object) -> list[dict]:
|
||||
"""Parse and validate a single-sticker export JSON document.
|
||||
|
||||
Each sticker dict has ``name``, ``image_type``, ``image_bytes`` (base64),
|
||||
optional ``source_message_hash``, and optional ``emoji`` (sticker tag).
|
||||
Each sticker dict has name, image_type, image_bytes (base64),
|
||||
optional source_message_hash, and optional emoji (sticker tag).
|
||||
"""
|
||||
if not isinstance(data, dict):
|
||||
msg = "invalid_document"
|
||||
|
|
@ -688,7 +688,7 @@ def build_export_document(stickers: list[dict], exported_at_iso: str) -> dict:
|
|||
|
||||
|
||||
def mime_for_image_type(normalized_type: str) -> str:
|
||||
"""Return the HTTP ``Content-Type`` for a normalized sticker type key."""
|
||||
"""Return the HTTP Content-Type for a normalized sticker type key."""
|
||||
return {
|
||||
"jpeg": "image/jpeg",
|
||||
"png": "image/png",
|
||||
|
|
|
|||
|
|
@ -163,7 +163,7 @@ class TranslatorHandler:
|
|||
"""List installed/reachable language pairs for the translator UI.
|
||||
|
||||
LibreTranslate is queried only when it is enabled in config or when the
|
||||
caller passes ``libretranslate_url`` (non-empty) to probe a specific server.
|
||||
caller passes libretranslate_url (non-empty) to probe a specific server.
|
||||
"""
|
||||
languages: list[dict[str, str]] = []
|
||||
libretranslate_reachable = False
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ class VoicemailManager:
|
|||
os.remove(wav_path)
|
||||
|
||||
def convert_to_greeting(self, input_path):
|
||||
"""Decode ``input_path`` and write the OGG/Opus voicemail greeting.
|
||||
"""Decode input_path and write the OGG/Opus voicemail greeting.
|
||||
|
||||
Any miniaudio-supported format is accepted; output uses LXST's voice profile.
|
||||
"""
|
||||
|
|
@ -500,11 +500,11 @@ class VoicemailManager:
|
|||
self.telephone_manager.is_voicemail_session_active = False
|
||||
|
||||
def _fix_recording(self, filepath):
|
||||
"""Ensure ``filepath`` is a valid OGG/Opus file.
|
||||
"""Ensure filepath is a valid OGG/Opus file.
|
||||
|
||||
OpusFileSink already produces valid OGG containers, so the common
|
||||
case is a no-op (the file already starts with ``OggS``). For any
|
||||
other input, we try to decode and re-encode using ``audio_codec``
|
||||
case is a no-op (the file already starts with OggS). For any
|
||||
other input, we try to decode and re-encode using audio_codec
|
||||
which covers WAV/MP3/FLAC/Vorbis/Opus.
|
||||
"""
|
||||
if not os.path.exists(filepath):
|
||||
|
|
@ -535,7 +535,7 @@ class VoicemailManager:
|
|||
RNS.log(f"Voicemail: Error fixing recording {filepath}: {e}", RNS.LOG_ERROR)
|
||||
|
||||
def _write_silence_file(self, filepath, seconds=1):
|
||||
"""Write a minimal OGG/Opus silence file at ``filepath``."""
|
||||
"""Write a minimal OGG/Opus silence file at filepath."""
|
||||
try:
|
||||
audio_codec.write_silence_ogg_opus(filepath, seconds=max(1, seconds))
|
||||
return os.path.exists(filepath) and os.path.getsize(filepath) > 0
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"""WebSocket guards for config updates and authenticated mutators.
|
||||
|
||||
Settings that change the HTTP security boundary must go through CSRF-protected
|
||||
HTTP endpoints, not the unauthenticated ``config.set`` WebSocket message.
|
||||
HTTP endpoints, not the unauthenticated config.set WebSocket message.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export default class MarkdownRenderer {
|
|||
return placeholder;
|
||||
});
|
||||
|
||||
// Inline code before emphasis so snake_case inside `code` / ``code`` is safe.
|
||||
// Inline code before emphasis so snake_case inside `code` / code is safe.
|
||||
const inline_codes = [];
|
||||
const pushInline = (code) => {
|
||||
const placeholder = `[[IC${inline_codes.length}]]`;
|
||||
|
|
|
|||
|
|
@ -213,7 +213,7 @@ export default class MicronParser extends BaseMicronParser {
|
|||
}
|
||||
|
||||
/**
|
||||
* Browsers insert newlines between adjacent ``inline-block`` Mu-mnt cells
|
||||
* Browsers insert newlines between adjacent inline-block Mu-mnt cells
|
||||
* when copying. Rebuild clipboard text without those spurious breaks while
|
||||
* keeping intentional block-level line breaks.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Download repository bundled wheels at build time (offline-first installs).
|
||||
|
||||
Wheels are written to ``meshchatx/public/repository-server-bundled/bundled`` so they
|
||||
Wheels are written to meshchatx/public/repository-server-bundled/bundled so they
|
||||
ship with the same artifact layout as the Vite output. At runtime,
|
||||
:class:`~meshchatx.src.backend.repository_server_manager.RepositoryServerManager`
|
||||
copies any missing ``*.whl`` files from that directory into each identity's
|
||||
``repository-server/bundled`` folder (no network required).
|
||||
copies any missing *.whl files from that directory into each identity's
|
||||
repository-server/bundled folder (no network required).
|
||||
|
||||
The PyPI/sdist wheel intentionally omits this tree (see ``MANIFEST.in`` and
|
||||
``tool.setuptools.exclude-package-data``); use this script for desktop or
|
||||
The PyPI/sdist wheel intentionally omits this tree (see MANIFEST.in and
|
||||
tool.setuptools.exclude-package-data); use this script for desktop or
|
||||
Android builds, or refresh bundled wheels when online. If
|
||||
``dist/reticulum_meshchatx-*.whl`` exists at the project root, it is copied into
|
||||
dist/reticulum_meshchatx-*.whl exists at the project root, it is copied into
|
||||
the bundled directory after PyPI downloads so the shipped wheel matches this
|
||||
tree.
|
||||
|
||||
|
|
@ -20,7 +20,7 @@ Usage::
|
|||
|
||||
Environment::
|
||||
|
||||
MESHCHATX_SKIP_REPOSITORY_WHEELS_FETCH If ``1``/``true``, exit without downloading.
|
||||
MESHCHATX_SKIP_REPOSITORY_WHEELS_FETCH If 1/true, exit without downloading.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@
|
|||
# SPDX-License-Identifier: 0BSD
|
||||
"""Fetch the Reticulum manual at build time and stage it for bundling.
|
||||
|
||||
The downloaded archive is extracted into ``meshchatx/public/reticulum-docs-bundled/current``
|
||||
The downloaded archive is extracted into meshchatx/public/reticulum-docs-bundled/current
|
||||
so that the application ships with an offline copy of the manual. At runtime the
|
||||
backend will serve those files for any ``/reticulum-docs/`` request that does not
|
||||
backend will serve those files for any /reticulum-docs/ request that does not
|
||||
have a user-uploaded version overriding it.
|
||||
|
||||
Usage::
|
||||
|
|
@ -12,21 +12,21 @@ Usage::
|
|||
python scripts/build/fetch_reticulum_manual.py [--source URL] [--dest DIR]
|
||||
[--force] [--include-pdf]
|
||||
|
||||
Sources may be HTTPS ZIP URLs, local directories that contain a ``docs/`` tree, or
|
||||
``rns://`` rngit remotes (requires ``git`` and ``git-remote-rns``).
|
||||
Sources may be HTTPS ZIP URLs, local directories that contain a docs/ tree, or
|
||||
rns:// rngit remotes (requires git and git-remote-rns).
|
||||
|
||||
By default the upstream PDF/EPUB copies of the manual are excluded from the
|
||||
bundle because the in-app viewer only renders the HTML version. Pass
|
||||
``--include-pdf`` (or set ``MESHCHATX_DOCS_INCLUDE_PDF=1``) to keep them.
|
||||
--include-pdf (or set MESHCHATX_DOCS_INCLUDE_PDF=1) to keep them.
|
||||
|
||||
Environment variables::
|
||||
|
||||
MESHCHATX_RETICULUM_DOCS_URL Override the default source URL (single value).
|
||||
MESHCHATX_RETICULUM_DOCS_DEST Override the destination directory.
|
||||
MESHCHATX_RETICULUM_DOCS_VIA_RNS If set, prefer the default rngit website remote.
|
||||
MESHCHATX_RETICULUM_DOCS_REF Git ref for ``rns://`` clones (default HEAD).
|
||||
MESHCHATX_SKIP_DOCS_FETCH If set to ``1``/``true``, exit without fetching.
|
||||
MESHCHATX_DOCS_INCLUDE_PDF If set to ``1``/``true``, include PDF/EPUB.
|
||||
MESHCHATX_RETICULUM_DOCS_REF Git ref for rns:// clones (default HEAD).
|
||||
MESHCHATX_SKIP_DOCS_FETCH If set to 1/true, exit without fetching.
|
||||
MESHCHATX_DOCS_INCLUDE_PDF If set to 1/true, include PDF/EPUB.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -115,9 +115,9 @@ def _extract(
|
|||
dest: Path,
|
||||
include_pdf: bool = False,
|
||||
) -> tuple[int, int]:
|
||||
"""Extract docs/ tree from ``archive`` into ``dest``.
|
||||
"""Extract docs/ tree from archive into dest.
|
||||
|
||||
Returns ``(extracted_count, skipped_binary_count)``. When ``include_pdf`` is
|
||||
Returns (extracted_count, skipped_binary_count). When include_pdf is
|
||||
false, large alternate-format manuals listed in :data:`EXTRA_BINARY_SUFFIXES`
|
||||
are skipped to keep shipped artifacts small.
|
||||
"""
|
||||
|
|
@ -152,7 +152,7 @@ def _extract_from_docs_dir(
|
|||
dest: Path,
|
||||
include_pdf: bool = False,
|
||||
) -> tuple[int, int]:
|
||||
"""Copy a local ``docs/`` tree into ``dest``."""
|
||||
"""Copy a local docs/ tree into dest."""
|
||||
if not docs_dir.is_dir():
|
||||
raise ValueError(f"docs directory missing: {docs_dir}")
|
||||
extracted = 0
|
||||
|
|
@ -212,7 +212,7 @@ def _clone_rns_docs(
|
|||
timeout: float,
|
||||
ref: str,
|
||||
) -> Path:
|
||||
"""Clone an ``rns://`` website repo sparsely and return its ``docs/`` path."""
|
||||
"""Clone an rns:// website repo sparsely and return its docs/ path."""
|
||||
if shutil.which("git") is None:
|
||||
raise ValueError("git is required for rns:// docs sources")
|
||||
if shutil.which("git-remote-rns") is None:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env bash
|
||||
# Build a Flatpak via electron-builder (same stack as AppImage/deb/macOS/Windows CI).
|
||||
#
|
||||
# Expects ``meshchatx/public/`` to already contain a prebuilt frontend bundle
|
||||
# Expects meshchatx/public/ to already contain a prebuilt frontend bundle
|
||||
# (downloaded from the reusable Frontend build workflow), so this script only
|
||||
# rebuilds the cx_Freeze backend before running electron-builder.
|
||||
#
|
||||
|
|
|
|||
|
|
@ -3,13 +3,13 @@
|
|||
|
||||
"""Alpine/musl Docker: copy cffi-built filter shared library to LXST.filterlib name.
|
||||
|
||||
LXST ships glibc-tagged ``filterlib*.so`` wheels; musl ignores them and cffi
|
||||
``verify()`` drops the musl artifact under ``LXST/__pycache__/_cffi__*.so``.
|
||||
Without this step, a fresh process cannot resolve ``LXST.filterlib`` for
|
||||
``ffi.dlopen()`` and would try to compile again at runtime (no gcc).
|
||||
LXST ships glibc-tagged filterlib*.so wheels; musl ignores them and cffi
|
||||
verify() drops the musl artifact under LXST/__pycache__/_cffi__*.so.
|
||||
Without this step, a fresh process cannot resolve LXST.filterlib for
|
||||
ffi.dlopen() and would try to compile again at runtime (no gcc).
|
||||
|
||||
The cffi artifact is a plain shared library (loaded via dlopen), not a Python
|
||||
extension module (no ``PyInit_filterlib``); do not ``import LXST.filterlib``.
|
||||
extension module (no PyInit_filterlib); do not import LXST.filterlib.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# SPDX-License-Identifier: 0BSD
|
||||
|
||||
"""Move Poetry-built wheels from ``dist/`` into ``python-dist/``.
|
||||
"""Move Poetry-built wheels from dist/ into python-dist/.
|
||||
|
||||
Avoids filename clashes with Electron build outputs.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: 0BSD
|
||||
"""Patch LXST bundled pyogg ``ogg.py`` for Python 3.14+.
|
||||
"""Patch LXST bundled pyogg ogg.py for Python 3.14+.
|
||||
|
||||
``opus.py`` does ``from .ogg import *`` but needs extra ctypes names that
|
||||
``ogg.py`` never defined: POINTER aliases, and ``c_uchar`` (used as ``c_uchar*0``
|
||||
for flexible array argtypes; same layout as ``c_ubyte``).
|
||||
opus.py does from .ogg import * but needs extra ctypes names that
|
||||
ogg.py never defined: POINTER aliases, and c_uchar (used as c_uchar*0
|
||||
for flexible array argtypes; same layout as c_ubyte).
|
||||
|
||||
Idempotent: safe to run after every ``pip install`` / ``poetry install``.
|
||||
Idempotent: safe to run after every pip install / poetry install.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ DEFAULT_INSTALL_PACKAGES = ("rns", "lxmf", "lxst")
|
|||
|
||||
|
||||
def parse_aliases(path: Path | None = None) -> dict[str, str]:
|
||||
"""Parse a pip-rns aliases file into ``name -> identity/group/repo``."""
|
||||
"""Parse a pip-rns aliases file into name -> identity/group/repo."""
|
||||
target = path or ALIASES_PATH
|
||||
result: dict[str, str] = {}
|
||||
if not target.is_file():
|
||||
|
|
@ -46,7 +46,7 @@ def parse_aliases(path: Path | None = None) -> dict[str, str]:
|
|||
|
||||
|
||||
def remote_url(alias_or_path: str, aliases: dict[str, str] | None = None) -> str:
|
||||
"""Return an ``rns://`` URL for an alias name or raw ``identity/group/repo``."""
|
||||
"""Return an rns:// URL for an alias name or raw identity/group/repo."""
|
||||
table = aliases if aliases is not None else parse_aliases()
|
||||
raw = table.get(alias_or_path, alias_or_path).strip()
|
||||
if raw.lower().startswith("rns://"):
|
||||
|
|
@ -55,7 +55,7 @@ def remote_url(alias_or_path: str, aliases: dict[str, str] | None = None) -> str
|
|||
|
||||
|
||||
def website_docs_source(aliases: dict[str, str] | None = None) -> str:
|
||||
"""Preferred ``rns://`` source for the Reticulum website/manual repo."""
|
||||
"""Preferred rns:// source for the Reticulum website/manual repo."""
|
||||
table = aliases if aliases is not None else parse_aliases()
|
||||
if "website" in table:
|
||||
return remote_url("website", table)
|
||||
|
|
|
|||
|
|
@ -48,8 +48,8 @@ def _urlsafe_sha256_digest(data: bytes) -> str:
|
|||
def _rewrite_wheel_record(root: Path) -> None:
|
||||
"""Regenerate dist-info/RECORD after wheel contents change.
|
||||
|
||||
Chaquopy's pip post-processor parses RECORD sizes with ``int()`` and rejects
|
||||
directory placeholder lines (``path,,``). Rebuilding RECORD from file bytes
|
||||
Chaquopy's pip post-processor parses RECORD sizes with int() and rejects
|
||||
directory placeholder lines (path,,). Rebuilding RECORD from file bytes
|
||||
keeps repacked pycodec2 wheels installable.
|
||||
"""
|
||||
dist_infos = sorted(root.glob("*.dist-info"))
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ def median(values):
|
|||
|
||||
|
||||
def median_abs_deviation(values, center=None):
|
||||
"""Median absolute deviation (MAD) of ``values`` around ``center``."""
|
||||
"""Median absolute deviation (MAD) of values around center."""
|
||||
if not values:
|
||||
return 0.0
|
||||
if center is None:
|
||||
|
|
@ -82,7 +82,7 @@ class BenchmarkResult:
|
|||
def benchmark(name=None, iterations=1, warmup=1):
|
||||
"""Decorator to benchmark a function's execution time and memory delta.
|
||||
|
||||
Each iteration is timed separately with ``perf_counter``. The reported
|
||||
Each iteration is timed separately with perf_counter. The reported
|
||||
duration is the median of per-iteration samples (more stable than mean
|
||||
under CI noise). A short warmup pass is discarded.
|
||||
"""
|
||||
|
|
@ -142,7 +142,7 @@ def benchmark(name=None, iterations=1, warmup=1):
|
|||
def aggregate_run_results(runs):
|
||||
"""Aggregate a list of result-lists (one per suite run) by benchmark name.
|
||||
|
||||
Returns a list of ``BenchmarkResult`` with median-of-run-medians duration.
|
||||
Returns a list of BenchmarkResult with median-of-run-medians duration.
|
||||
"""
|
||||
if not runs:
|
||||
return []
|
||||
|
|
@ -212,7 +212,7 @@ def should_alert_regression(
|
|||
):
|
||||
"""Decide whether a slower current value is a real regression.
|
||||
|
||||
Returns ``(alert: bool, reason: str)``. Skips alerts when both values sit
|
||||
Returns (alert: bool, reason: str). Skips alerts when both values sit
|
||||
under the noise floor, when the absolute delta is tiny, or when the ratio
|
||||
is within an adaptive threshold. High CV on either side widens the bar.
|
||||
"""
|
||||
|
|
@ -258,7 +258,7 @@ def should_alert_regression(
|
|||
|
||||
|
||||
def parse_extra_stats(extra):
|
||||
"""Parse ``mad=`` / ``cv=`` / ``runs=`` fields from github-action-benchmark extra."""
|
||||
"""Parse mad= / cv= / runs= fields from github-action-benchmark extra."""
|
||||
out = {}
|
||||
if not extra:
|
||||
return out
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ flat ratio alert is useless. This script:
|
|||
2. Applies noise-floor, absolute-delta, and adaptive-ratio heuristics.
|
||||
3. Writes a human-readable summary and exits non-zero only on real regressions.
|
||||
4. Optionally updates the baseline cache when the run is clean (or always when
|
||||
``--update-baseline`` is set), so the next push compares against a stable
|
||||
--update-baseline is set), so the next push compares against a stable
|
||||
median rather than a single noisy sample.
|
||||
"""
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# SPDX-License-Identifier: 0BSD
|
||||
|
||||
"""Probe module for ``--meshchatx-run-module`` live/subprocess tests."""
|
||||
"""Probe module for --meshchatx-run-module live/subprocess tests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
"""SQLite integration: announce spam and bounded storage (anti-exhaustion).
|
||||
|
||||
Multi-minute soak scenarios live in ``test_long_running_stress.py`` (opt-in via
|
||||
``MESHCHAT_LONG_TEST_SECONDS``).
|
||||
Multi-minute soak scenarios live in test_long_running_stress.py (opt-in via
|
||||
MESHCHAT_LONG_TEST_SECONDS).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# SPDX-License-Identifier: 0BSD
|
||||
|
||||
"""Critical-path tests for ``AsyncUtils``: cross-thread scheduling and memory caps."""
|
||||
"""Critical-path tests for AsyncUtils: cross-thread scheduling and memory caps."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@
|
|||
"""Tests for the in-process audio decode/encode helpers.
|
||||
|
||||
These tests stand in for the previous ffmpeg subprocess pipeline.
|
||||
Coverage is targeted at the public ``audio_codec`` API:
|
||||
Coverage is targeted at the public audio_codec API:
|
||||
|
||||
* ``decode_audio`` for WAV (built-in), miniaudio formats and OGG/Opus
|
||||
* ``encode_pcm_to_ogg_opus`` round-trips
|
||||
* ``encode_audio_to_ogg_opus`` for arbitrary inputs
|
||||
* ``write_silence_ogg_opus`` for empty greetings/voicemails
|
||||
* ``encode_audio_bytes_to_ogg_opus`` passthrough + decode-and-reencode
|
||||
* decode_audio for WAV (built-in), miniaudio formats and OGG/Opus
|
||||
* encode_pcm_to_ogg_opus round-trips
|
||||
* encode_audio_to_ogg_opus for arbitrary inputs
|
||||
* write_silence_ogg_opus for empty greetings/voicemails
|
||||
* encode_audio_bytes_to_ogg_opus passthrough + decode-and-reencode
|
||||
"""
|
||||
|
||||
import io
|
||||
|
|
@ -208,7 +208,7 @@ def test_encode_pcm_to_ogg_opus_preserves_duration(duration_seconds):
|
|||
|
||||
|
||||
def test_encode_pcm_to_ogg_opus_audio_profile_keeps_stereo():
|
||||
"""``PROFILE_AUDIO_MAX`` must keep stereo input as stereo, not collapse to mono."""
|
||||
"""PROFILE_AUDIO_MAX must keep stereo input as stereo, not collapse to mono."""
|
||||
out = _tmp_opus_path()
|
||||
try:
|
||||
from LXST.Codecs import Opus
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@
|
|||
|
||||
These lock in bugs that broke real calls:
|
||||
1. Contacts saved under LXMF destination hashes were rejected by contacts-only.
|
||||
2. Configured audio profiles never reached LXST ``telephone.call()``.
|
||||
3. Invalid legacy profile id ``2`` silently mapped to Opus instead of a real profile.
|
||||
2. Configured audio profiles never reached LXST telephone.call().
|
||||
3. Invalid legacy profile id 2 silently mapped to Opus instead of a real profile.
|
||||
4. Codec2 profiles crashed or were unusable when pycodec2/libcodec2 was missing.
|
||||
5. Incoming calls during outbound dial left the remote ringing forever.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -442,7 +442,7 @@ class TestNameResolutionPriority:
|
|||
announce_dao,
|
||||
contacts_dao,
|
||||
):
|
||||
"""When ``app_data`` is NULL, fall back to the contact name."""
|
||||
"""When app_data is NULL, fall back to the contact name."""
|
||||
dest = "f" * 32
|
||||
contacts_dao.add_contact("ContactFallback", dest, lxmf_address=dest)
|
||||
announce_dao.upsert_announce(_base_announce(dest=dest, app_data=None))
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ from meshchatx.src.backend.telemetry_utils import Telemeter
|
|||
|
||||
|
||||
def test_parse_lxmf_display_name_bytes_and_strings_in_msgpack_list():
|
||||
"""``parse_lxmf_display_name`` accepts msgpack list elements as bytes or str."""
|
||||
"""parse_lxmf_display_name accepts msgpack list elements as bytes or str."""
|
||||
display_name_bytes = b"Test User"
|
||||
app_data_list = [display_name_bytes, None, None]
|
||||
app_data_bytes = msgpack.packb(app_data_list)
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ async def test_https_serves_over_tls_only_plain_http_gets_no_http_response(
|
|||
):
|
||||
"""TLS-only server must not emit a plaintext HTTP response to raw HTTP bytes.
|
||||
|
||||
Raw TCP clients should see handshake noise or close, not ``HTTP/`` headers.
|
||||
Raw TCP clients should see handshake noise or close, not HTTP/ headers.
|
||||
"""
|
||||
ssl_context, _, _ = ssl_context_and_cert
|
||||
app = web.Application()
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
"""Multi-minute soak tests (announce DB + websocket fan-out).
|
||||
|
||||
These are **opt-in**: unset ``MESHCHAT_LONG_TEST_SECONDS`` skips them immediately.
|
||||
These are **opt-in**: unset MESHCHAT_LONG_TEST_SECONDS skips them immediately.
|
||||
|
||||
Examples::
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
"""Integration-oriented tests for real LXST telephony classes.
|
||||
|
||||
These tests intentionally use LXST's real ``Telephone`` implementation while stubbing
|
||||
These tests intentionally use LXST's real Telephone implementation while stubbing
|
||||
RNS/network/audio backends, so we validate LXST behavior without requiring hardware.
|
||||
"""
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# SPDX-License-Identifier: 0BSD
|
||||
|
||||
"""Path traversal and fuzz tests for PageNode and ``normalize_page_filename``."""
|
||||
"""Path traversal and fuzz tests for PageNode and normalize_page_filename."""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# SPDX-License-Identifier: 0BSD
|
||||
|
||||
"""Tests for MeshChatX ``--meshchatx-run-module`` frozen re-entry."""
|
||||
"""Tests for MeshChatX --meshchatx-run-module frozen re-entry."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# SPDX-License-Identifier: 0BSD
|
||||
|
||||
"""Regression: Nomad net downloads must emit websocket ``started`` before scheduling ``download()``."""
|
||||
"""Regression: Nomad net downloads must emit websocket started before scheduling download()."""
|
||||
|
||||
import json
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@ Covers:
|
|||
- the pure helper :func:`is_user_facing_lxmf_payload`
|
||||
- the conversation-row helper
|
||||
:func:`compute_lxmf_conversation_unread_from_latest_row` with
|
||||
``require_user_facing=True``
|
||||
require_user_facing=True
|
||||
- the DAO method
|
||||
:func:`MessageDAO.get_latest_user_facing_incoming_message`
|
||||
- end-to-end ``GET /api/v1/notifications`` integration: reactions,
|
||||
- end-to-end GET /api/v1/notifications integration: reactions,
|
||||
generic telemetry-only payloads, icon-only, empty pings and
|
||||
delivery-status updates must not produce false unread badges or empty
|
||||
dropdown entries; location shares, telemetry streams, and Sideband
|
||||
|
|
@ -96,7 +96,7 @@ class TestIsUserFacingLxmfPayload:
|
|||
|
||||
def test_icon_only_is_not_user_facing(self):
|
||||
# Icon appearance updates are processed separately and never appear in
|
||||
# the converted ``fields`` dict; an icon-only message therefore looks
|
||||
# the converted fields dict; an icon-only message therefore looks
|
||||
# like an empty payload to this helper.
|
||||
assert not is_user_facing_lxmf_payload({}, "", "")
|
||||
|
||||
|
|
@ -160,7 +160,7 @@ class TestRequireUserFacingFlag:
|
|||
"reaction": {"reaction_to": "abc", "reaction_content": "\U0001f44d"},
|
||||
},
|
||||
)
|
||||
# Without ``require_user_facing`` the helper preserves its old behavior
|
||||
# Without require_user_facing the helper preserves its old behavior
|
||||
# so the conversation list (which renders reactions) is unaffected.
|
||||
assert compute_lxmf_conversation_unread_from_latest_row(row) is True
|
||||
|
||||
|
|
@ -719,7 +719,7 @@ class TestNotificationsGetUserFacingFilter:
|
|||
assert body["unread_count"] == 2
|
||||
|
||||
async def test_count_is_consistent_with_unread_filter_off(self, bell_app):
|
||||
# When ``unread=false`` the lxmf unread_count must still ignore
|
||||
# When unread=false the lxmf unread_count must still ignore
|
||||
# silent payloads so the badge never disagrees with the dropdown.
|
||||
bell_app.database.messages.upsert_lxmf_message(
|
||||
_mk_message(
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
# SPDX-License-Identifier: 0BSD
|
||||
"""Tests for :mod:`meshchatx.src.backend.ringtone_manager`.
|
||||
|
||||
The manager performs conversion in-process via ``audio_codec``
|
||||
The manager performs conversion in-process via audio_codec
|
||||
(miniaudio + LXST). These tests pin the contract that
|
||||
``convert_to_ringtone`` decodes any supported audio container and
|
||||
convert_to_ringtone decodes any supported audio container and
|
||||
produces a stored OGG/Opus ringtone.
|
||||
"""
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ from meshchatx.src.backend import rnode_support
|
|||
def test_normalize_rnode_tcp_host_backfills_from_port(tmp_path):
|
||||
"""RNS's Android RNodeInterface reads tcp_host as its own config key.
|
||||
|
||||
Configs written with only ``port = tcp://host:port`` (which is all the
|
||||
Configs written with only port = tcp://host:port (which is all the
|
||||
desktop RNodeInterface needs) silently try to open the RNode as a serial
|
||||
device on Android unless tcp_host is also present.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -190,7 +190,7 @@ def test_attach_client_returns_false_without_active_call():
|
|||
|
||||
|
||||
class _TeleMgrNoTelephone:
|
||||
"""Telephone manager shape without a ``telephone`` attribute (edge case)."""
|
||||
"""Telephone manager shape without a telephone attribute (edge case)."""
|
||||
|
||||
|
||||
def test_tele_returns_none_when_manager_has_no_telephone():
|
||||
|
|
@ -281,7 +281,7 @@ async def test_send_status_defaults_when_no_telephone():
|
|||
|
||||
@patch("meshchatx.src.backend.web_audio_bridge.Pipeline")
|
||||
def test_attach_client_success_wires_telephony_and_dedupes_client(mock_pipeline_cls):
|
||||
"""LXST ``Pipeline`` validates sources; mock it so we only assert bridge wiring."""
|
||||
"""LXST Pipeline validates sources; mock it so we only assert bridge wiring."""
|
||||
mock_receive_pipeline = MagicMock()
|
||||
mock_pipeline_cls.return_value = mock_receive_pipeline
|
||||
tele = MagicMock()
|
||||
|
|
|
|||
|
|
@ -72,8 +72,8 @@ async def test_websocket_broadcast_soak_iterations(mock_app):
|
|||
async def test_websocket_broadcast_iterates_snapshot_not_live_list(mock_app):
|
||||
"""Broadcast must iterate a snapshot of websocket clients, not the live list.
|
||||
|
||||
If another coroutine mutates ``websocket_clients`` during iteration, using
|
||||
``list(...)`` avoids skipping entries (classic mutating-list pitfall).
|
||||
If another coroutine mutates websocket_clients during iteration, using
|
||||
list(...) avoids skipping entries (classic mutating-list pitfall).
|
||||
"""
|
||||
mock_app.websocket_clients.clear()
|
||||
clients = [MagicWs() for _ in range(5)]
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ describe("postInstallPromptRegistry", () => {
|
|||
id: "bad",
|
||||
revision: 0,
|
||||
titleKey: "post_install.demo_title",
|
||||
}),
|
||||
})
|
||||
).toThrow(/revision/);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -331,8 +331,7 @@ describe("createVisualiserWebGLEngine interactions", () => {
|
|||
});
|
||||
globalThis.meshchatxVisualiserSceneZoomAt = (...args) => zoomAt(...args);
|
||||
globalThis.meshchatxVisualiserScenePanBy = vi.fn();
|
||||
globalThis.meshchatxVisualiserSceneGetPositions = () =>
|
||||
JSON.stringify({ positions: { me: { x: 0, y: 0 } } });
|
||||
globalThis.meshchatxVisualiserSceneGetPositions = () => JSON.stringify({ positions: { me: { x: 0, y: 0 } } });
|
||||
globalThis.meshchatxVisualiserSceneResize = vi.fn();
|
||||
canvas = makeCanvas(gl);
|
||||
engine = createVisualiserWebGLEngine(canvas, {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue