diff --git a/meshchatx/meshchat.py b/meshchatx/meshchat.py index 867f7fa..ea0ea07 100644 --- a/meshchatx/meshchat.py +++ b/meshchatx/meshchat.py @@ -2,8 +2,11 @@ import argparse import asyncio +import atexit import base64 +import configparser import copy +import gc import io import ipaddress import json @@ -11,15 +14,18 @@ import os import platform import secrets import shutil +import socket import ssl import sys import tempfile import threading import time +import traceback import webbrowser import zipfile from collections.abc import Callable from datetime import UTC, datetime, timedelta +import hashlib import bcrypt import LXMF @@ -260,7 +266,8 @@ class ReticulumMeshChat: self.database.initialize() # Try to auto-migrate from legacy database if this is a fresh start self.database.migrate_from_legacy( - self.reticulum_config_dir, identity.hash.hex() + self.reticulum_config_dir, + identity.hash.hex(), ) self._tune_sqlite_pragmas() except Exception as exc: @@ -413,7 +420,7 @@ class ReticulumMeshChat: # init RNStatus handler self.rnstatus_handler = RNStatusHandler( - reticulum_instance=getattr(self, "reticulum", None) + reticulum_instance=getattr(self, "reticulum", None), ) # init RNProbe handler @@ -430,7 +437,8 @@ class ReticulumMeshChat: # start background thread for auto announce loop thread = threading.Thread( - target=asyncio.run, args=(self.announce_loop(session_id),) + target=asyncio.run, + args=(self.announce_loop(session_id),), ) thread.daemon = True thread.start() @@ -657,7 +665,7 @@ class ReticulumMeshChat: if match: print( - f"Deregistering RNS destination {destination} ({RNS.prettyhexrep(destination.hash)})" + f"Deregistering RNS destination {destination} ({RNS.prettyhexrep(destination.hash)})", ) RNS.Transport.deregister_destination(destination) except Exception as e: @@ -703,7 +711,7 @@ class ReticulumMeshChat: # Deregister delivery destinations if hasattr(self.message_router, "delivery_destinations"): for dest_hash in list( - self.message_router.delivery_destinations.keys() + self.message_router.delivery_destinations.keys(), ): dest = self.message_router.delivery_destinations[dest_hash] RNS.Transport.deregister_destination(dest) @@ -714,7 +722,7 @@ class ReticulumMeshChat: and self.message_router.propagation_destination ): RNS.Transport.deregister_destination( - self.message_router.propagation_destination + self.message_router.propagation_destination, ) if hasattr(self, "telephone_manager") and self.telephone_manager: @@ -727,7 +735,7 @@ class ReticulumMeshChat: and self.telephone_manager.telephone.destination ): RNS.Transport.deregister_destination( - self.telephone_manager.telephone.destination + self.telephone_manager.telephone.destination, ) # Use the global helper for thorough cleanup @@ -842,18 +850,27 @@ class ReticulumMeshChat: except Exception: pass + # For LocalServerInterface which Reticulum doesn't close properly + if hasattr(interface, "server") and interface.server: + try: + interface.server.shutdown() + interface.server.server_close() + except Exception: + pass + # TCPClientInterface/etc if hasattr(interface, "socket") and interface.socket: try: - import socket - # Check if socket is still valid before shutdown - if interface.socket.fileno() != -1: + if ( + hasattr(interface.socket, "fileno") + and interface.socket.fileno() != -1 + ): try: interface.socket.shutdown(socket.SHUT_RDWR) except Exception: pass - interface.socket.close() + interface.socket.close() except Exception: pass @@ -911,8 +928,6 @@ class ReticulumMeshChat: # Unregister old exit handlers from atexit if possible try: - import atexit - # Reticulum uses a staticmethod exit_handler atexit.unregister(RNS.Reticulum.exit_handler) except Exception: @@ -932,43 +947,178 @@ class ReticulumMeshChat: rpc_addrs = [] if old_reticulum: if hasattr(old_reticulum, "rpc_addr") and old_reticulum.rpc_addr: - rpc_addrs.append((old_reticulum.rpc_addr, getattr(old_reticulum, "rpc_type", "AF_INET"))) - + rpc_addrs.append( + ( + old_reticulum.rpc_addr, + getattr(old_reticulum, "rpc_type", "AF_INET"), + ), + ) + + # Also check the config file for ports + try: + config_dir = getattr(self, "reticulum_config_dir", None) + if not config_dir: + if hasattr(RNS.Reticulum, "configdir") and RNS.Reticulum.configdir: + config_dir = RNS.Reticulum.configdir + else: + config_dir = os.path.expanduser("~/.reticulum") + + config_path = os.path.join(config_dir, "config") + if os.path.isfile(config_path): + cp = configparser.ConfigParser() + cp.read(config_path) + if cp.has_section("reticulum"): + rpc_port = cp.getint("reticulum", "rpc_port", fallback=37429) + rpc_bind = cp.get("reticulum", "rpc_bind", fallback="127.0.0.1") + shared_port = cp.getint( + "reticulum", + "shared_instance_port", + fallback=37428, + ) + shared_bind = cp.get( + "reticulum", + "shared_instance_bind", + fallback="127.0.0.1", + ) + + # Only add if not already there + if not any( + addr == (rpc_bind, rpc_port) for addr, _ in rpc_addrs + ): + rpc_addrs.append(((rpc_bind, rpc_port), "AF_INET")) + if not any( + addr == (shared_bind, shared_port) for addr, _ in rpc_addrs + ): + rpc_addrs.append(((shared_bind, shared_port), "AF_INET")) + except Exception as e: + print(f"Warning reading Reticulum config for ports: {e}") + if not rpc_addrs: # Defaults rpc_addrs.append((("127.0.0.1", 37429), "AF_INET")) + rpc_addrs.append((("127.0.0.1", 37428), "AF_INET")) if platform.system() == "Linux": rpc_addrs.append(("\0rns/default/rpc", "AF_UNIX")) - for i in range(10): + for i in range(15): await asyncio.sleep(1) all_free = True for addr, family_str in rpc_addrs: try: - import socket - family = socket.AF_INET if family_str == "AF_INET" else socket.AF_UNIX + family = ( + socket.AF_INET + if family_str == "AF_INET" + else socket.AF_UNIX + ) s = socket.socket(family, socket.SOCK_STREAM) s.settimeout(0.5) try: + # Use SO_REUSEADDR to check if we can actually bind + if family == socket.AF_INET: + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.bind(addr) s.close() except OSError: - print(f"RPC addr {addr} still in use... (attempt {i+1}/10)") + print( + f"RPC addr {addr} still in use... (attempt {i + 1}/15)", + ) s.close() all_free = False + + # If we are stuck, try to force close the connection manually + if i > 5: + try: + current_process = psutil.Process() + # We use kind='all' to catch both TCP and UNIX sockets + for conn in current_process.connections(kind="all"): + try: + match = False + if conn.laddr: + if ( + family_str == "AF_INET" + and isinstance(conn.laddr, tuple) + ): + # Match IP and port for IPv4 + if conn.laddr.port == addr[1] and ( + conn.laddr.ip == addr[0] + or addr[0] == "0.0.0.0" + ): + match = True + elif ( + family_str == "AF_UNIX" + and conn.laddr == addr + ): + # Match path for UNIX sockets + match = True + + if match: + # If we found a match, force close the file descriptor + # to tell the OS to release the socket immediately. + status_str = getattr( + conn, + "status", + "UNKNOWN", + ) + print( + f"Force closing lingering {family_str} connection {conn.laddr} (status: {status_str})", + ) + + try: + if ( + hasattr(conn, "fd") + and conn.fd != -1 + ): + os.close(conn.fd) + except Exception as fd_err: + print( + f"Failed to close FD {getattr(conn, 'fd', 'N/A')}: {fd_err}", + ) + except Exception: + pass + except Exception as e: + print( + f"Error during manual RPC connection kill: {e}", + ) + break except Exception as e: print(f"Error checking RPC addr {addr}: {e}") - + if all_free: print("All RNS ports/sockets are free.") break if not all_free: - raise OSError("Timeout waiting for RNS ports to be released. Cannot restart.") + # One last attempt with a very short sleep before failing + await asyncio.sleep(2) + # Check again one last time + last_check_all_free = True + for addr, family_str in rpc_addrs: + try: + family = ( + socket.AF_INET + if family_str == "AF_INET" + else socket.AF_UNIX + ) + s = socket.socket(family, socket.SOCK_STREAM) + try: + s.bind(addr) + s.close() + except OSError: + last_check_all_free = False + s.close() + break + except Exception: + pass + + if not last_check_all_free: + raise OSError( + "Timeout waiting for RNS ports to be released. Cannot restart.", + ) + else: + print("RNS ports finally free after last-second check.") # Final GC to ensure everything is released - import gc gc.collect() # Re-setup identity (this starts background loops again) @@ -978,7 +1128,6 @@ class ReticulumMeshChat: return True except Exception as e: print(f"Hot reload failed: {e}") - import traceback traceback.print_exc() @@ -1010,9 +1159,9 @@ class ReticulumMeshChat: # 2. update main identity file main_identity_file = self.identity_file_path or os.path.join( - self.storage_dir, "identity" + self.storage_dir, + "identity", ) - import shutil shutil.copy2(identity_file, main_identity_file) @@ -1033,14 +1182,13 @@ class ReticulumMeshChat: if hasattr(self, "config") else "Unknown" ), - } + }, ), ) return True except Exception as e: print(f"Hotswap failed: {e}") - import traceback traceback.print_exc() return False @@ -1093,10 +1241,10 @@ class ReticulumMeshChat: display_name = temp_config_dao.get("display_name", "Anonymous Peer") icon_name = temp_config_dao.get("lxmf_user_icon_name") icon_foreground_colour = temp_config_dao.get( - "lxmf_user_icon_foreground_colour" + "lxmf_user_icon_foreground_colour", ) icon_background_colour = temp_config_dao.get( - "lxmf_user_icon_background_colour" + "lxmf_user_icon_background_colour", ) temp_provider.close() except Exception as e: @@ -1110,7 +1258,7 @@ class ReticulumMeshChat: "icon_foreground_colour": icon_foreground_colour, "icon_background_colour": icon_background_colour, "is_current": identity_hash == self.identity.hash.hex(), - } + }, ) return identities @@ -1155,8 +1303,6 @@ class ReticulumMeshChat: identity_dir = os.path.join(self.storage_dir, "identities", identity_hash) if os.path.exists(identity_dir): - import shutil - shutil.rmtree(identity_dir) return True return False @@ -1658,7 +1804,8 @@ class ReticulumMeshChat: if self.telephone_manager.telephone: # Use a small delay to ensure LXST state is ready for hangup threading.Timer( - 0.5, lambda: self.telephone_manager.telephone.hangup() + 0.5, + lambda: self.telephone_manager.telephone.hangup(), ).start() return @@ -1669,7 +1816,8 @@ class ReticulumMeshChat: print(f"Rejecting incoming call from non-contact: {caller_hash}") if self.telephone_manager.telephone: threading.Timer( - 0.5, lambda: self.telephone_manager.telephone.hangup() + 0.5, + lambda: self.telephone_manager.telephone.hangup(), ).start() return @@ -1758,7 +1906,7 @@ class ReticulumMeshChat: is_filtered = True elif self.config.telephone_allow_calls_from_contacts_only.get(): contact = self.database.contacts.get_contact_by_identity_hash( - remote_identity_hash + remote_identity_hash, ) if not contact: is_filtered = True @@ -2992,7 +3140,9 @@ class ReticulumMeshChat: ), "is_connected_to_shared_instance": ( getattr( - self.reticulum, "is_connected_to_shared_instance", False + self.reticulum, + "is_connected_to_shared_instance", + False, ) if hasattr(self, "reticulum") and self.reticulum else False @@ -3322,21 +3472,19 @@ class ReticulumMeshChat: # fallback to restart if hotswap failed # (this part should probably be unreachable if hotswap is reliable) main_identity_file = self.identity_file_path or os.path.join( - self.storage_dir, "identity" + self.storage_dir, + "identity", ) identity_dir = os.path.join( - self.storage_dir, "identities", identity_hash + self.storage_dir, + "identities", + identity_hash, ) identity_file = os.path.join(identity_dir, "identity") - import shutil shutil.copy2(identity_file, main_identity_file) def restart(): - import os - import sys - import time - time.sleep(1) try: os.execv(sys.executable, [sys.executable] + sys.argv) @@ -3344,8 +3492,6 @@ class ReticulumMeshChat: print(f"Failed to restart: {e}") os._exit(0) - import threading - threading.Thread(target=restart).start() return web.json_response( @@ -3432,11 +3578,10 @@ class ReticulumMeshChat: success = await self.reload_reticulum() if success: return web.json_response({"message": "Reticulum reloaded successfully"}) - else: - return web.json_response( - {"error": "Failed to reload Reticulum"}, - status=500, - ) + return web.json_response( + {"error": "Failed to reload Reticulum"}, + status=500, + ) # serve telephone status @routes.get("/api/v1/telephone/status") @@ -3465,7 +3610,7 @@ class ReticulumMeshChat: telephone_active_call.get_remote_identity().hash.hex() ) contact = self.database.contacts.get_contact_by_identity_hash( - caller_hash + caller_hash, ) if not contact: # Don't report active call if contacts-only is on and caller is not a contact @@ -3490,7 +3635,7 @@ class ReticulumMeshChat: "delivery", ).hex() remote_icon = self.database.misc.get_user_icon( - lxmf_destination_hash + lxmf_destination_hash, ) active_call = { @@ -3517,8 +3662,8 @@ class ReticulumMeshChat: "call_start_time": self.telephone_manager.call_start_time, "is_contact": bool( self.database.contacts.get_contact_by_identity_hash( - remote_identity_hash - ) + remote_identity_hash, + ), ) if remote_identity_hash else False, @@ -3634,7 +3779,7 @@ class ReticulumMeshChat: remote_identity_hash = d.get("remote_identity_hash") if remote_identity_hash: lxmf_hash = self.get_lxmf_destination_hash_for_identity_hash( - remote_identity_hash + remote_identity_hash, ) if lxmf_hash: icon = self.database.misc.get_user_icon(lxmf_hash) @@ -3642,8 +3787,8 @@ class ReticulumMeshChat: d["remote_icon"] = dict(icon) d["is_contact"] = bool( self.database.contacts.get_contact_by_identity_hash( - remote_identity_hash - ) + remote_identity_hash, + ), ) call_history.append(d) @@ -3827,7 +3972,7 @@ class ReticulumMeshChat: remote_identity_hash = d.get("remote_identity_hash") if remote_identity_hash: lxmf_hash = self.get_lxmf_destination_hash_for_identity_hash( - remote_identity_hash + remote_identity_hash, ) if lxmf_hash: icon = self.database.misc.get_user_icon(lxmf_hash) @@ -4006,12 +4151,13 @@ class ReticulumMeshChat: return web.json_response({"message": "Ringtone not found"}, status=404) filepath = self.ringtone_manager.get_ringtone_path( - ringtone["storage_filename"] + ringtone["storage_filename"], ) if os.path.exists(filepath): return web.FileResponse(filepath) return web.json_response( - {"message": "Ringtone audio file not found"}, status=404 + {"message": "Ringtone audio file not found"}, + status=404, ) @routes.post("/api/v1/telephone/ringtones/upload") @@ -4021,7 +4167,8 @@ class ReticulumMeshChat: field = await reader.next() if field.name != "file": return web.json_response( - {"message": "File field required"}, status=400 + {"message": "File field required"}, + status=400, ) filename = field.filename @@ -4118,7 +4265,7 @@ class ReticulumMeshChat: remote_identity_hash = d.get("remote_identity_hash") if remote_identity_hash: lxmf_hash = self.get_lxmf_destination_hash_for_identity_hash( - remote_identity_hash + remote_identity_hash, ) if lxmf_hash: icon = self.database.misc.get_user_icon(lxmf_hash) @@ -4136,7 +4283,8 @@ class ReticulumMeshChat: if not name or not remote_identity_hash: return web.json_response( - {"message": "Name and identity hash required"}, status=400 + {"message": "Name and identity hash required"}, + status=400, ) self.database.contacts.add_contact(name, remote_identity_hash) @@ -4150,7 +4298,9 @@ class ReticulumMeshChat: remote_identity_hash = data.get("remote_identity_hash") self.database.contacts.update_contact( - contact_id, name, remote_identity_hash + contact_id, + name, + remote_identity_hash, ) return web.json_response({"message": "Contact updated"}) @@ -4168,7 +4318,7 @@ class ReticulumMeshChat: { "is_contact": contact is not None, "contact": dict(contact) if contact else None, - } + }, ) # announce @@ -4594,7 +4744,7 @@ class ReticulumMeshChat: if hasattr(self, "reticulum") and self.reticulum: next_hop_bytes = self.reticulum.get_next_hop(destination_hash) next_hop_interface = self.reticulum.get_next_hop_if_name( - destination_hash + destination_hash, ) # ensure next hop provided @@ -5543,7 +5693,7 @@ class ReticulumMeshChat: # unless we stored the raw bytes. MeshChatX seems to store fields. return web.json_response( { - "message": "Original message bytes not available for URI generation" + "message": "Original message bytes not available for URI generation", }, status=404, ) @@ -5766,7 +5916,7 @@ class ReticulumMeshChat: if destination_hashes: # mark LXMF conversations as viewed self.database.messages.mark_all_notifications_as_viewed( - destination_hashes + destination_hashes, ) if notification_ids: @@ -5783,13 +5933,14 @@ class ReticulumMeshChat: async def notifications_get(request): try: filter_unread = ReticulumMeshChat.parse_bool_query_param( - request.query.get("unread", "false") + request.query.get("unread", "false"), ) limit = int(request.query.get("limit", 50)) # 1. Fetch system notifications system_notifications = self.database.misc.get_notifications( - filter_unread=filter_unread, limit=limit + filter_unread=filter_unread, + limit=limit, ) # 2. Fetch unread LXMF conversations if requested @@ -5797,7 +5948,8 @@ class ReticulumMeshChat: if filter_unread: local_hash = self.local_lxmf_destination.hexhash db_conversations = self.message_handler.get_conversations( - local_hash, filter_unread=True + local_hash, + filter_unread=True, ) for db_message in db_conversations: # Convert to dict if needed @@ -5812,11 +5964,11 @@ class ReticulumMeshChat: # Determine display name display_name = self.get_name_for_lxmf_destination_hash( - other_user_hash + other_user_hash, ) custom_display_name = ( self.database.announces.get_custom_display_name( - other_user_hash + other_user_hash, ) ) @@ -5840,9 +5992,10 @@ class ReticulumMeshChat: "content" ][:100], "updated_at": datetime.fromtimestamp( - latest_message_data["timestamp"] or 0, UTC + latest_message_data["timestamp"] or 0, + UTC, ).isoformat(), - } + }, ) # Combine and sort by timestamp @@ -5859,7 +6012,7 @@ class ReticulumMeshChat: if n["remote_hash"]: # Try to find associated LXMF hash for telephony identity hash lxmf_hash = self.get_lxmf_destination_hash_for_identity_hash( - n["remote_hash"] + n["remote_hash"], ) if not lxmf_hash: # Fallback to direct name lookup by identity hash @@ -5869,7 +6022,7 @@ class ReticulumMeshChat: ) else: display_name = self.get_name_for_lxmf_destination_hash( - lxmf_hash + lxmf_hash, ) icon = self.database.misc.get_user_icon(lxmf_hash) @@ -5884,9 +6037,10 @@ class ReticulumMeshChat: "content": n["content"], "is_viewed": n["is_viewed"] == 1, "updated_at": datetime.fromtimestamp( - n["timestamp"] or 0, UTC + n["timestamp"] or 0, + UTC, ).isoformat(), - } + }, ) all_notifications.extend(conversations) @@ -5901,7 +6055,8 @@ class ReticulumMeshChat: lxmf_unread_count = 0 local_hash = self.local_lxmf_destination.hexhash unread_conversations = self.message_handler.get_conversations( - local_hash, filter_unread=True + local_hash, + filter_unread=True, ) if unread_conversations: lxmf_unread_count = len(unread_conversations) @@ -5912,7 +6067,7 @@ class ReticulumMeshChat: { "notifications": all_notifications[:limit], "unread_count": total_unread_count, - } + }, ) except Exception as e: RNS.log(f"Error in notifications_get: {e}", RNS.LOG_ERROR) @@ -6377,10 +6532,8 @@ class ReticulumMeshChat: secret_key_bytes = secret_key_bytes[:32] except Exception: # Fallback to direct encoding and hashing to get exactly 32 bytes - import hashlib - secret_key_bytes = hashlib.sha256( - self.session_secret_key.encode("utf-8") + self.session_secret_key.encode("utf-8"), ).digest() setup_session( @@ -6484,19 +6637,19 @@ class ReticulumMeshChat: if "auto_resend_failed_messages_when_announce_received" in data: value = self._parse_bool( - data["auto_resend_failed_messages_when_announce_received"] + data["auto_resend_failed_messages_when_announce_received"], ) self.config.auto_resend_failed_messages_when_announce_received.set(value) if "allow_auto_resending_failed_messages_with_attachments" in data: value = self._parse_bool( - data["allow_auto_resending_failed_messages_with_attachments"] + data["allow_auto_resending_failed_messages_with_attachments"], ) self.config.allow_auto_resending_failed_messages_with_attachments.set(value) if "auto_send_failed_messages_to_propagation_node" in data: value = self._parse_bool( - data["auto_send_failed_messages_to_propagation_node"] + data["auto_send_failed_messages_to_propagation_node"], ) self.config.auto_send_failed_messages_to_propagation_node.set(value) @@ -6583,7 +6736,7 @@ class ReticulumMeshChat: # update archiver settings if "page_archiver_enabled" in data: self.config.page_archiver_enabled.set( - self._parse_bool(data["page_archiver_enabled"]) + self._parse_bool(data["page_archiver_enabled"]), ) if "page_archiver_max_versions" in data: @@ -6623,7 +6776,7 @@ class ReticulumMeshChat: # update map settings if "map_offline_enabled" in data: self.config.map_offline_enabled.set( - self._parse_bool(data["map_offline_enabled"]) + self._parse_bool(data["map_offline_enabled"]), ) if "map_default_lat" in data: @@ -6640,7 +6793,7 @@ class ReticulumMeshChat: if "map_tile_cache_enabled" in data: self.config.map_tile_cache_enabled.set( - self._parse_bool(data["map_tile_cache_enabled"]) + self._parse_bool(data["map_tile_cache_enabled"]), ) if "map_tile_server_url" in data: @@ -6652,7 +6805,7 @@ class ReticulumMeshChat: # update voicemail settings if "voicemail_enabled" in data: self.config.voicemail_enabled.set( - self._parse_bool(data["voicemail_enabled"]) + self._parse_bool(data["voicemail_enabled"]), ) if "voicemail_greeting" in data: @@ -6671,7 +6824,7 @@ class ReticulumMeshChat: # update ringtone settings if "custom_ringtone_enabled" in data: self.config.custom_ringtone_enabled.set( - self._parse_bool(data["custom_ringtone_enabled"]) + self._parse_bool(data["custom_ringtone_enabled"]), ) # send config to websocket clients @@ -7157,7 +7310,9 @@ class ReticulumMeshChat: elif _type == "lxmf.forwarding.rule.add": rule_data = data.get("rule") if not rule_data or "forward_to_hash" not in rule_data: - print("Missing rule data or forward_to_hash in lxmf.forwarding.rule.add") + print( + "Missing rule data or forward_to_hash in lxmf.forwarding.rule.add", + ) return self.database.misc.create_forwarding_rule( @@ -7208,7 +7363,7 @@ class ReticulumMeshChat: try: # ensure uri starts with lxmf:// or lxm:// if not uri.lower().startswith( - LXMF.LXMessage.URI_SCHEMA + "://" + LXMF.LXMessage.URI_SCHEMA + "://", ) and not uri.lower().startswith("lxm://"): if ":" in uri and "//" not in uri: uri = LXMF.LXMessage.URI_SCHEMA + "://" + uri @@ -7271,16 +7426,16 @@ class ReticulumMeshChat: if destination_identity is None: # try to find in database announce = self.database.announces.get_announce_by_hash( - destination_hash + destination_hash, ) if announce and announce.get("identity_public_key"): destination_identity = RNS.Identity.from_bytes( - base64.b64decode(announce["identity_public_key"]) + base64.b64decode(announce["identity_public_key"]), ) if destination_identity is None: raise Exception( - "Recipient identity not found. Please wait for an announce or add them as a contact." + "Recipient identity not found. Please wait for an announce or add them as a contact.", ) lxmf_destination = RNS.Destination( @@ -7351,7 +7506,9 @@ class ReticulumMeshChat: action = data["action"] keys = json.dumps(data["keys"]) self.database.misc.upsert_keyboard_shortcut( - self.identity.hexhash, action, keys + self.identity.hexhash, + action, + keys, ) # notify updated AsyncUtils.run_async( @@ -7780,7 +7937,7 @@ class ReticulumMeshChat: # Also update display name if telephony one was empty if not display_name or display_name == "Anonymous Peer": display_name = self.parse_lxmf_display_name( - lxmf_a["app_data"] + lxmf_a["app_data"], ) break @@ -8673,7 +8830,9 @@ class ReticulumMeshChat: ): # check if announced identity or its hash is missing if not announced_identity or not announced_identity.hash: - print(f"Dropping announce with missing identity or hash: {RNS.prettyhexrep(destination_hash)}") + print( + f"Dropping announce with missing identity or hash: {RNS.prettyhexrep(destination_hash)}", + ) return # check if source is blocked - drop announce and path if blocked