refactor(meshchat): update map to render tiles faster (online), message handling by adding context support to forwarding and delivery methods; improve LXMF message processing and router initialization
Some checks failed
CI / test-backend (push) Successful in 4s
CI / build-frontend (push) Successful in 1m54s
CI / test-lang (push) Successful in 1m53s
CI / test-backend (pull_request) Successful in 21s
Build and Publish Docker Image / build (pull_request) Has been skipped
CI / test-lang (pull_request) Successful in 49s
OSV-Scanner PR Scan / scan-pr (pull_request) Successful in 23s
CI / lint (push) Successful in 9m46s
CI / build-frontend (pull_request) Successful in 9m44s
CI / lint (pull_request) Successful in 9m47s
Build Test / Build and Test (pull_request) Successful in 13m17s
Benchmarks / benchmark (push) Successful in 14m34s
Benchmarks / benchmark (pull_request) Successful in 14m42s
Build and Publish Docker Image / build-dev (pull_request) Successful in 13m53s
Tests / test (push) Failing after 26m51s
Tests / test (pull_request) Failing after 24m53s
Build Test / Build and Test (push) Successful in 45m21s

This commit is contained in:
2026-01-04 19:10:22 -06:00
parent ff69de1346
commit 629bbbc7c6
5 changed files with 119 additions and 45 deletions

View File

@@ -9091,7 +9091,7 @@ class ReticulumMeshChat:
self.db_upsert_lxmf_message(lxmf_message, is_spam=is_spam, context=ctx) self.db_upsert_lxmf_message(lxmf_message, is_spam=is_spam, context=ctx)
# handle forwarding # handle forwarding
self.handle_forwarding(lxmf_message) self.handle_forwarding(lxmf_message, context=ctx)
# handle telemetry # handle telemetry
try: try:
@@ -9200,8 +9200,12 @@ class ReticulumMeshChat:
print(f"lxmf_delivery error: {e}") print(f"lxmf_delivery error: {e}")
# handles lxmf message forwarding logic # handles lxmf message forwarding logic
def handle_forwarding(self, lxmf_message: LXMF.LXMessage): def handle_forwarding(self, lxmf_message: LXMF.LXMessage, context=None):
try: try:
ctx = context or self.current_context
if not ctx:
return
source_hash = lxmf_message.source_hash.hex() source_hash = lxmf_message.source_hash.hex()
destination_hash = lxmf_message.destination_hash.hex() destination_hash = lxmf_message.destination_hash.hex()
@@ -9227,7 +9231,7 @@ class ReticulumMeshChat:
file_attachments_field = LxmfFileAttachmentsField(attachments) file_attachments_field = LxmfFileAttachmentsField(attachments)
# check if this message is for an alias identity (REPLY PATH) # check if this message is for an alias identity (REPLY PATH)
mapping = self.database.messages.get_forwarding_mapping( mapping = ctx.database.messages.get_forwarding_mapping(
alias_hash=destination_hash, alias_hash=destination_hash,
) )
@@ -9246,13 +9250,14 @@ class ReticulumMeshChat:
image_field=image_field, image_field=image_field,
audio_field=audio_field, audio_field=audio_field,
file_attachments_field=file_attachments_field, file_attachments_field=file_attachments_field,
context=ctx,
), ),
) )
return return
# check if this message matches a forwarding rule (FORWARD PATH) # check if this message matches a forwarding rule (FORWARD PATH)
# we check for rules that apply to the destination of this message # we check for rules that apply to the destination of this message
rules = self.database.misc.get_forwarding_rules( rules = ctx.database.misc.get_forwarding_rules(
identity_hash=destination_hash, identity_hash=destination_hash,
active_only=True, active_only=True,
) )
@@ -9266,7 +9271,7 @@ class ReticulumMeshChat:
continue continue
# find or create mapping for this (Source, Final Recipient) pair # find or create mapping for this (Source, Final Recipient) pair
mapping = self.forwarding_manager.get_or_create_mapping( mapping = ctx.forwarding_manager.get_or_create_mapping(
source_hash, source_hash,
rule["forward_to_hash"], rule["forward_to_hash"],
destination_hash, destination_hash,
@@ -9287,6 +9292,7 @@ class ReticulumMeshChat:
image_field=image_field, image_field=image_field,
audio_field=audio_field, audio_field=audio_field,
file_attachments_field=file_attachments_field, file_attachments_field=file_attachments_field,
context=ctx,
), ),
) )
except Exception as e: except Exception as e:
@@ -9296,9 +9302,9 @@ class ReticulumMeshChat:
traceback.print_exc() traceback.print_exc()
# handle delivery status update for an outbound lxmf message # handle delivery status update for an outbound lxmf message
def on_lxmf_sending_state_updated(self, lxmf_message): def on_lxmf_sending_state_updated(self, lxmf_message, context=None):
# upsert lxmf message to database # upsert lxmf message to database
self.db_upsert_lxmf_message(lxmf_message) self.db_upsert_lxmf_message(lxmf_message, context=context)
# send lxmf message state to all websocket clients # send lxmf message state to all websocket clients
AsyncUtils.run_async( AsyncUtils.run_async(
@@ -9317,20 +9323,26 @@ class ReticulumMeshChat:
) )
# handle delivery failed for an outbound lxmf message # handle delivery failed for an outbound lxmf message
def on_lxmf_sending_failed(self, lxmf_message): def on_lxmf_sending_failed(self, lxmf_message, context=None):
# check if this failed message should fall back to sending via a propagation node # check if this failed message should fall back to sending via a propagation node
if ( if (
lxmf_message.state == LXMF.LXMessage.FAILED lxmf_message.state == LXMF.LXMessage.FAILED
and hasattr(lxmf_message, "try_propagation_on_fail") and hasattr(lxmf_message, "try_propagation_on_fail")
and lxmf_message.try_propagation_on_fail and lxmf_message.try_propagation_on_fail
): ):
self.send_failed_message_via_propagation_node(lxmf_message) self.send_failed_message_via_propagation_node(lxmf_message, context=context)
# update state # update state
self.on_lxmf_sending_state_updated(lxmf_message) self.on_lxmf_sending_state_updated(lxmf_message)
# sends a previously failed message via a propagation node # sends a previously failed message via a propagation node
def send_failed_message_via_propagation_node(self, lxmf_message: LXMF.LXMessage): def send_failed_message_via_propagation_node(
self, lxmf_message: LXMF.LXMessage, context=None
):
ctx = context or self.current_context
if not ctx:
return
# reset internal message state # reset internal message state
lxmf_message.packed = None lxmf_message.packed = None
lxmf_message.delivery_attempts = 0 lxmf_message.delivery_attempts = 0
@@ -9343,12 +9355,12 @@ class ReticulumMeshChat:
# resend message # resend message
source_hash = lxmf_message.source_hash.hex() source_hash = lxmf_message.source_hash.hex()
router = self.message_router router = ctx.message_router
if ( if (
self.forwarding_manager ctx.forwarding_manager
and source_hash in self.forwarding_manager.forwarding_routers and source_hash in ctx.forwarding_manager.forwarding_routers
): ):
router = self.forwarding_manager.forwarding_routers[source_hash] router = ctx.forwarding_manager.forwarding_routers[source_hash]
router.handle_outbound(lxmf_message) router.handle_outbound(lxmf_message)
# upserts the provided lxmf message to the database # upserts the provided lxmf message to the database
@@ -9393,7 +9405,12 @@ class ReticulumMeshChat:
title: str = "", title: str = "",
sender_identity_hash: str = None, sender_identity_hash: str = None,
no_display: bool = False, no_display: bool = False,
context=None,
) -> LXMF.LXMessage: ) -> LXMF.LXMessage:
ctx = context or self.current_context
if not ctx:
raise RuntimeError("No identity context available for sending message")
# convert destination hash to bytes # convert destination hash to bytes
destination_hash_bytes = bytes.fromhex(destination_hash) destination_hash_bytes = bytes.fromhex(destination_hash)
@@ -9442,7 +9459,7 @@ class ReticulumMeshChat:
# send messages over a direct link by default # send messages over a direct link by default
desired_delivery_method = LXMF.LXMessage.DIRECT desired_delivery_method = LXMF.LXMessage.DIRECT
if ( if (
not self.message_router.delivery_link_available(destination_hash_bytes) not ctx.message_router.delivery_link_available(destination_hash_bytes)
and RNS.Identity.current_ratchet_id(destination_hash_bytes) is not None and RNS.Identity.current_ratchet_id(destination_hash_bytes) is not None
): ):
# since there's no link established to the destination, it's faster to send opportunistically # since there's no link established to the destination, it's faster to send opportunistically
@@ -9452,14 +9469,14 @@ class ReticulumMeshChat:
desired_delivery_method = LXMF.LXMessage.OPPORTUNISTIC desired_delivery_method = LXMF.LXMessage.OPPORTUNISTIC
# determine which identity to send from # determine which identity to send from
source_destination = self.local_lxmf_destination source_destination = ctx.local_lxmf_destination
if sender_identity_hash is not None: if sender_identity_hash is not None:
if ( if (
self.forwarding_manager ctx.forwarding_manager
and sender_identity_hash and sender_identity_hash
in self.forwarding_manager.forwarding_destinations in ctx.forwarding_manager.forwarding_destinations
): ):
source_destination = self.forwarding_manager.forwarding_destinations[ source_destination = ctx.forwarding_manager.forwarding_destinations[
sender_identity_hash sender_identity_hash
] ]
else: else:
@@ -9476,7 +9493,7 @@ class ReticulumMeshChat:
desired_method=desired_delivery_method, desired_method=desired_delivery_method,
) )
lxmf_message.try_propagation_on_fail = ( lxmf_message.try_propagation_on_fail = (
self.config.auto_send_failed_messages_to_propagation_node.get() ctx.config.auto_send_failed_messages_to_propagation_node.get()
) )
lxmf_message.fields = {} lxmf_message.fields = {}
@@ -9541,28 +9558,33 @@ class ReticulumMeshChat:
] ]
# update last sent icon hash for this destination # update last sent icon hash for this destination
self.database.misc.update_last_sent_icon_hash( ctx.database.misc.update_last_sent_icon_hash(
destination_hash, current_icon_hash destination_hash, current_icon_hash
) )
# register delivery callbacks # register delivery callbacks
lxmf_message.register_delivery_callback(self.on_lxmf_sending_state_updated) lxmf_message.register_delivery_callback(
lxmf_message.register_failed_callback(self.on_lxmf_sending_failed) lambda msg: self.on_lxmf_sending_state_updated(msg, context=ctx)
)
lxmf_message.register_failed_callback(
lambda msg: self.on_lxmf_sending_failed(msg, context=ctx)
)
# determine which router to use # determine which router to use
router = self.message_router router = ctx.message_router
if ( if (
sender_identity_hash is not None sender_identity_hash is not None
and sender_identity_hash in self.forwarding_manager.forwarding_routers and ctx.forwarding_manager
and sender_identity_hash in ctx.forwarding_manager.forwarding_routers
): ):
router = self.forwarding_manager.forwarding_routers[sender_identity_hash] router = ctx.forwarding_manager.forwarding_routers[sender_identity_hash]
# send lxmf message to be routed to destination # send lxmf message to be routed to destination
router.handle_outbound(lxmf_message) router.handle_outbound(lxmf_message)
# upsert lxmf message to database # upsert lxmf message to database
if not no_display: if not no_display:
self.db_upsert_lxmf_message(lxmf_message) self.db_upsert_lxmf_message(lxmf_message, context=ctx)
# tell all websocket clients that old failed message was deleted so it can remove from ui # tell all websocket clients that old failed message was deleted so it can remove from ui
if not no_display: if not no_display:
@@ -9583,7 +9605,9 @@ class ReticulumMeshChat:
# otherwise other incoming websocket packets will not be processed until sending is complete # otherwise other incoming websocket packets will not be processed until sending is complete
# which results in the next message not showing up until the first message is finished # which results in the next message not showing up until the first message is finished
if not no_display: if not no_display:
AsyncUtils.run_async(self.handle_lxmf_message_progress(lxmf_message)) AsyncUtils.run_async(
self.handle_lxmf_message_progress(lxmf_message, context=ctx)
)
return lxmf_message return lxmf_message
@@ -9642,16 +9666,20 @@ class ReticulumMeshChat:
print(f"Failed to respond to telemetry request: {e}") print(f"Failed to respond to telemetry request: {e}")
# updates lxmf message in database and broadcasts to websocket until it's delivered, or it fails # updates lxmf message in database and broadcasts to websocket until it's delivered, or it fails
async def handle_lxmf_message_progress(self, lxmf_message): async def handle_lxmf_message_progress(self, lxmf_message, context=None):
# FIXME: there's no register_progress_callback on the lxmf message, so manually send progress until delivered, propagated or failed # FIXME: there's no register_progress_callback on the lxmf message, so manually send progress until delivered, propagated or failed
# we also can't use on_lxmf_sending_state_updated method to do this, because of async/await issues... # we also can't use on_lxmf_sending_state_updated method to do this, because of async/await issues...
ctx = context or self.current_context
if not ctx:
return
should_update_message = True should_update_message = True
while should_update_message: while should_update_message:
# wait 1 second between sending updates # wait 1 second between sending updates
await asyncio.sleep(1) await asyncio.sleep(1)
# upsert lxmf message to database (as we want to update the progress in database too) # upsert lxmf message to database (as we want to update the progress in database too)
self.db_upsert_lxmf_message(lxmf_message) self.db_upsert_lxmf_message(lxmf_message, context=ctx)
# send update to websocket clients # send update to websocket clients
await self.websocket_broadcast( await self.websocket_broadcast(
@@ -9816,9 +9844,11 @@ class ReticulumMeshChat:
) )
# resend all failed messages that were intended for this destination # resend all failed messages that were intended for this destination
if self.config.auto_resend_failed_messages_when_announce_received.get(): if ctx.config.auto_resend_failed_messages_when_announce_received.get():
AsyncUtils.run_async( AsyncUtils.run_async(
self.resend_failed_messages_for_destination(destination_hash.hex()), self.resend_failed_messages_for_destination(
destination_hash.hex(), context=ctx
),
) )
# handle an announce received from reticulum, for an lxmf propagation node address # handle an announce received from reticulum, for an lxmf propagation node address
@@ -9874,9 +9904,15 @@ class ReticulumMeshChat:
) )
# resends all messages that previously failed to send to the provided destination hash # resends all messages that previously failed to send to the provided destination hash
async def resend_failed_messages_for_destination(self, destination_hash: str): async def resend_failed_messages_for_destination(
self, destination_hash: str, context=None
):
ctx = context or self.current_context
if not ctx:
return
# get messages that failed to send to this destination # get messages that failed to send to this destination
failed_messages = self.database.messages.get_failed_messages_for_destination( failed_messages = ctx.database.messages.get_failed_messages_for_destination(
destination_hash, destination_hash,
) )
@@ -9915,7 +9951,7 @@ class ReticulumMeshChat:
file_attachments_field = LxmfFileAttachmentsField(file_attachments) file_attachments_field = LxmfFileAttachmentsField(file_attachments)
# don't resend message with attachments if not allowed # don't resend message with attachments if not allowed
if not self.config.allow_auto_resending_failed_messages_with_attachments.get(): if not ctx.config.allow_auto_resending_failed_messages_with_attachments.get():
if ( if (
image_field is not None image_field is not None
or audio_field is not None or audio_field is not None
@@ -9933,10 +9969,11 @@ class ReticulumMeshChat:
image_field=image_field, image_field=image_field,
audio_field=audio_field, audio_field=audio_field,
file_attachments_field=file_attachments_field, file_attachments_field=file_attachments_field,
context=ctx,
) )
# remove original failed message from database # remove original failed message from database
self.database.messages.delete_lxmf_message_by_hash( ctx.database.messages.delete_lxmf_message_by_hash(
failed_message["hash"], failed_message["hash"],
) )

View File

@@ -1,10 +1,10 @@
import base64 import base64
import os import os
import LXMF
import RNS import RNS
from .database import Database from .database import Database
from .meshchat_utils import create_lxmf_router
class ForwardingManager: class ForwardingManager:
@@ -34,7 +34,7 @@ class ForwardingManager:
) )
os.makedirs(router_storage_path, exist_ok=True) os.makedirs(router_storage_path, exist_ok=True)
router = LXMF.LXMRouter( router = create_lxmf_router(
identity=alias_identity, identity=alias_identity,
storagepath=router_storage_path, storagepath=router_storage_path,
) )
@@ -79,7 +79,7 @@ class ForwardingManager:
) )
os.makedirs(router_storage_path, exist_ok=True) os.makedirs(router_storage_path, exist_ok=True)
router = LXMF.LXMRouter( router = create_lxmf_router(
identity=alias_identity, identity=alias_identity,
storagepath=router_storage_path, storagepath=router_storage_path,
) )

View File

@@ -2,7 +2,6 @@ import os
import asyncio import asyncio
import threading import threading
import RNS import RNS
import LXMF
from meshchatx.src.backend.database import Database from meshchatx.src.backend.database import Database
from meshchatx.src.backend.integrity_manager import IntegrityManager from meshchatx.src.backend.integrity_manager import IntegrityManager
from meshchatx.src.backend.config_manager import ConfigManager from meshchatx.src.backend.config_manager import ConfigManager
@@ -21,6 +20,7 @@ from meshchatx.src.backend.rnpath_handler import RNPathHandler
from meshchatx.src.backend.rnprobe_handler import RNProbeHandler from meshchatx.src.backend.rnprobe_handler import RNProbeHandler
from meshchatx.src.backend.translator_handler import TranslatorHandler from meshchatx.src.backend.translator_handler import TranslatorHandler
from meshchatx.src.backend.forwarding_manager import ForwardingManager from meshchatx.src.backend.forwarding_manager import ForwardingManager
from meshchatx.src.backend.meshchat_utils import create_lxmf_router
from meshchatx.src.backend.announce_handler import AnnounceHandler from meshchatx.src.backend.announce_handler import AnnounceHandler
from meshchatx.src.backend.community_interfaces import CommunityInterfacesManager from meshchatx.src.backend.community_interfaces import CommunityInterfacesManager
@@ -168,7 +168,7 @@ class IdentityContext:
# 4. Initialize LXMF Router # 4. Initialize LXMF Router
propagation_stamp_cost = self.config.lxmf_propagation_node_stamp_cost.get() propagation_stamp_cost = self.config.lxmf_propagation_node_stamp_cost.get()
self.message_router = LXMF.LXMRouter( self.message_router = create_lxmf_router(
identity=self.identity, identity=self.identity,
storagepath=self.lxmf_router_path, storagepath=self.lxmf_router_path,
propagation_cost=propagation_stamp_cost, propagation_cost=propagation_stamp_cost,

View File

@@ -1,11 +1,39 @@
import base64 import base64
import json import json
import signal
import threading
import LXMF import LXMF
import RNS.vendor.umsgpack as msgpack import RNS.vendor.umsgpack as msgpack
from LXMF import LXMRouter from LXMF import LXMRouter
def create_lxmf_router(identity, storagepath, propagation_cost=None):
"""
Creates an LXMF.LXMRouter instance safely, avoiding signal handler crashes
when called from non-main threads.
"""
if threading.current_thread() != threading.main_thread():
# signal.signal can only be called from the main thread in Python
# We monkeypatch it temporarily to avoid the ValueError
original_signal = signal.signal
try:
signal.signal = lambda s, h: None
return LXMF.LXMRouter(
identity=identity,
storagepath=storagepath,
propagation_cost=propagation_cost,
)
finally:
signal.signal = original_signal
else:
return LXMF.LXMRouter(
identity=identity,
storagepath=storagepath,
propagation_cost=propagation_cost,
)
def parse_bool_query_param(value: str | None) -> bool: def parse_bool_query_param(value: str | None) -> bool:
if value is None: if value is None:
return False return False

View File

@@ -1520,7 +1520,10 @@ export default {
throw new Error(`HTTP ${response.status}`); throw new Error(`HTTP ${response.status}`);
} }
const blob = await response.blob(); const blob = await response.blob();
tile.getImage().src = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
tile.getImage().src = url;
// Cleanup to prevent memory leaks
setTimeout(() => URL.revokeObjectURL(url), 10000);
} catch { } catch {
tile.setState(3); tile.setState(3);
} }
@@ -1535,7 +1538,9 @@ export default {
try { try {
const cached = await TileCache.getTile(src); const cached = await TileCache.getTile(src);
if (cached) { if (cached) {
tile.getImage().src = URL.createObjectURL(cached); const url = URL.createObjectURL(cached);
tile.getImage().src = url;
setTimeout(() => URL.revokeObjectURL(url), 10000);
return; return;
} }
@@ -1544,8 +1549,12 @@ export default {
throw new Error(`HTTP ${response.status}`); throw new Error(`HTTP ${response.status}`);
} }
const blob = await response.blob(); const blob = await response.blob();
await TileCache.setTile(src, blob); const url = URL.createObjectURL(blob);
tile.getImage().src = URL.createObjectURL(blob); tile.getImage().src = url;
setTimeout(() => URL.revokeObjectURL(url), 10000);
// Background cache write to avoid blocking UI
TileCache.setTile(src, blob).catch(() => {});
} catch { } catch {
originalTileLoadFunction(tile, src); originalTileLoadFunction(tile, src);
} }