Merge remote-tracking branch 'upstream/main'

This commit is contained in:
jacob.eva 2024-09-20 11:07:04 +01:00
commit 0d0fcaf572
No known key found for this signature in database
GPG Key ID: 0B92E083BBCCAA1E
9 changed files with 676 additions and 341 deletions

View File

@ -21,7 +21,7 @@ Sideband provides many useful and interesting functions, such as:
- Geospatial awareness calculations. - Geospatial awareness calculations.
- Exchanging messages through **encrypted QR-codes on paper**, or through messages embedded directly in **lxm://** links. - Exchanging messages through **encrypted QR-codes on paper**, or through messages embedded directly in **lxm://** links.
- Using **Android devices as impromptu Reticulum routers** (*Transport Instances*), for setting up or extending networks easily. - Using **Android devices as impromptu Reticulum routers** (*Transport Instances*), for setting up or extending networks easily.
- Remote **command execution and response engine**, with built-in commands, such as `ping`, `signal` reports and `echo`, and **full plug-ing expandability**. - Remote **command execution and response engine**, with built-in commands, such as `ping`, `signal` reports and `echo`, and **full plugin expandability**.
- Remote **telemetry querying**, with strong, secure and cryptographically robust authentication and control. - Remote **telemetry querying**, with strong, secure and cryptographically robust authentication and control.
- **Plugin system** that allows you to easily **create your own commands**, services and telemetry sources. - **Plugin system** that allows you to easily **create your own commands**, services and telemetry sources.
@ -45,12 +45,12 @@ After the application is installed on your Android device, it is also possible t
## On Linux ## On Linux
On all Linux-based operating systems, Sideband is available as a `pipx`/`pip` package. This installation method **includes desktop integration**, so that Sideband will show up in your applications menu and launchers. On all Linux-based operating systems, Sideband is available as a `pipx`/`pip` package. This installation method **includes desktop integration**, so that Sideband will show up in your applications menu and launchers. Below are install steps for the most common recent Linux distros. For Debian 11, see the end of this section.
You will first need to install a few dependencies for audio messaging and Codec2 support to work: You will first need to install a few dependencies for audio messaging and Codec2 support to work:
```bash ```bash
# For Debian, Ubuntu and derivatives # For Debian (12+), Ubuntu (22.04+) and derivatives
sudo apt install pipx python3-pyaudio python3-dev build-essential libopusfile0 portaudio19-dev codec2 xclip xsel sudo apt install pipx python3-pyaudio python3-dev build-essential libopusfile0 portaudio19-dev codec2 xclip xsel
# For Manjaro and derivatives # For Manjaro and derivatives
@ -112,6 +112,14 @@ pip install sbapp --no-dependencies
# In the above case, you will still need to # In the above case, you will still need to
# manually install the RNS and LXMF dependencies: # manually install the RNS and LXMF dependencies:
pip install rns lxmf pip install rns lxmf
# Install Sideband on Debian 11 and derivatives:
sudo apt install python3-pip python3-pyaudio python3-dev build-essential libopusfile0 portaudio19-dev codec2 xclip xsel
pip install sbapp
# On Debian 11, run Sideband manually via the
# terminal once to install desktop integration:
python3 -m sbapp.main
``` ```
## On Raspberry Pi ## On Raspberry Pi

View File

@ -10,7 +10,7 @@ source.exclude_patterns = app_storage/*,venv/*,Makefile,./Makefil*,requirements,
version.regex = __version__ = ['"](.*)['"] version.regex = __version__ = ['"](.*)['"]
version.filename = %(source.dir)s/main.py version.filename = %(source.dir)s/main.py
android.numeric_version = 20240911 android.numeric_version = 20240920
requirements = kivy==2.3.0,libbz2,pillow==10.2.0,qrcode==7.3.1,usb4a,usbserial4a,libwebp,libogg,libopus,opusfile,numpy,cryptography,ffpyplayer,codec2,pycodec2,sh,pynacl,android,able_recipe requirements = kivy==2.3.0,libbz2,pillow==10.2.0,qrcode==7.3.1,usb4a,usbserial4a,libwebp,libogg,libopus,opusfile,numpy,cryptography,ffpyplayer,codec2,pycodec2,sh,pynacl,android,able_recipe

View File

@ -1,6 +1,6 @@
__debug_build__ = False __debug_build__ = False
__disable_shaders__ = False __disable_shaders__ = False
__version__ = "0.9.5" __version__ = "0.9.7"
__variant__ = "beta" __variant__ = "beta"
import sys import sys
@ -303,6 +303,7 @@ class SidebandApp(MDApp):
self.final_load_completed = False self.final_load_completed = False
self.service_last_available = 0 self.service_last_available = 0
self.closing_app = False
self.attach_path = None self.attach_path = None
self.attach_type = None self.attach_type = None
@ -988,6 +989,11 @@ class SidebandApp(MDApp):
ok_button.bind(on_release=dl_ok) ok_button.bind(on_release=dl_ok)
dialog.open() dialog.open()
def close_requested(self, *args):
if not self.closing_app:
self.quit_action(None)
return True
def on_start(self): def on_start(self):
self.last_exit_event = time.time() self.last_exit_event = time.time()
self.root.ids.screen_manager.transition = self.slide_transition self.root.ids.screen_manager.transition = self.slide_transition
@ -997,6 +1003,7 @@ class SidebandApp(MDApp):
EventLoop.window.bind(on_keyboard=self.keyboard_event) EventLoop.window.bind(on_keyboard=self.keyboard_event)
EventLoop.window.bind(on_key_down=self.keydown_event) EventLoop.window.bind(on_key_down=self.keydown_event)
EventLoop.window.bind(on_key_up=self.keyup_event) EventLoop.window.bind(on_key_up=self.keyup_event)
Window.bind(on_request_close=self.close_requested)
if __variant__ != "": if __variant__ != "":
variant_str = " "+__variant__ variant_str = " "+__variant__
@ -1241,6 +1248,7 @@ class SidebandApp(MDApp):
self.root.ids.nav_drawer.set_state("closed") self.root.ids.nav_drawer.set_state("closed")
def quit_action(self, sender): def quit_action(self, sender):
self.closing_app = True
self.root.ids.nav_drawer.set_state("closed") self.root.ids.nav_drawer.set_state("closed")
self.sideband.should_persist_data() self.sideband.should_persist_data()
@ -2760,8 +2768,9 @@ class SidebandApp(MDApp):
pre = self.settings_screen.ids.settings_lxmf_sync_periodic.text pre = self.settings_screen.ids.settings_lxmf_sync_periodic.text
self.settings_screen.ids.settings_lxmf_sync_periodic.text = "Auto sync every "+interval_text self.settings_screen.ids.settings_lxmf_sync_periodic.text = "Auto sync every "+interval_text
if save: if save:
self.sideband.config["lxmf_sync_interval"] = interval if (event == None or not hasattr(event, "button") or not event.button) or not "scroll" in event.button:
self.sideband.save_configuration() self.sideband.config["lxmf_sync_interval"] = interval
self.sideband.save_configuration()
def stamp_cost_change(sender=None, event=None, save=True): def stamp_cost_change(sender=None, event=None, save=True):
slider_val = int(self.settings_screen.ids.settings_lxmf_require_stamps_cost.value) slider_val = int(self.settings_screen.ids.settings_lxmf_require_stamps_cost.value)
@ -2774,7 +2783,8 @@ class SidebandApp(MDApp):
if slider_val < 1: if slider_val < 1:
slider_val = 1 slider_val = 1
self.sideband.config["lxmf_inbound_stamp_cost"] = slider_val self.sideband.config["lxmf_inbound_stamp_cost"] = slider_val
self.sideband.save_configuration() if (event == None or not hasattr(event, "button") or not event.button) or not "scroll" in event.button:
self.sideband.save_configuration()
self.settings_screen.ids.settings_lxmf_address.text = RNS.hexrep(self.sideband.lxmf_destination.hash, delimit=False) self.settings_screen.ids.settings_lxmf_address.text = RNS.hexrep(self.sideband.lxmf_destination.hash, delimit=False)
self.settings_screen.ids.settings_identity_hash.text = RNS.hexrep(self.sideband.lxmf_destination.identity.hash, delimit=False) self.settings_screen.ids.settings_identity_hash.text = RNS.hexrep(self.sideband.lxmf_destination.identity.hash, delimit=False)
@ -2991,35 +3001,35 @@ class SidebandApp(MDApp):
self.widget_hide(self.connectivity_screen.ids.connectivity_transport_fields) self.widget_hide(self.connectivity_screen.ids.connectivity_transport_fields)
def con_collapse_local(collapse=True): def con_collapse_local(collapse=True):
# self.widget_hide(self.root.ids.connectivity_local_fields, collapse) # self.widget_hide(self.connectivity_screen.ids.connectivity_local_fields, collapse)
pass pass
def con_collapse_tcp(collapse=True): def con_collapse_tcp(collapse=True):
# self.widget_hide(self.root.ids.connectivity_tcp_fields, collapse) # self.widget_hide(self.connectivity_screen.ids.connectivity_tcp_fields, collapse)
pass pass
def con_collapse_i2p(collapse=True): def con_collapse_i2p(collapse=True):
# self.widget_hide(self.root.ids.connectivity_i2p_fields, collapse) # self.widget_hide(self.connectivity_screen.ids.connectivity_i2p_fields, collapse)
pass pass
def con_collapse_bluetooth(collapse=True): def con_collapse_bluetooth(collapse=True):
# self.widget_hide(self.root.ids.connectivity_bluetooth_fields, collapse) # self.widget_hide(self.connectivity_screen.ids.connectivity_bluetooth_fields, collapse)
pass pass
def con_collapse_rnode(collapse=True): def con_collapse_rnode(collapse=True):
# self.widget_hide(self.root.ids.connectivity_rnode_fields, collapse) # self.widget_hide(self.connectivity_screen.ids.connectivity_rnode_fields, collapse)
pass pass
def con_collapse_modem(collapse=True): def con_collapse_modem(collapse=True):
# self.widget_hide(self.root.ids.connectivity_modem_fields, collapse) # self.widget_hide(self.connectivity_screen.ids.connectivity_modem_fields, collapse)
pass pass
def con_collapse_serial(collapse=True): def con_collapse_serial(collapse=True):
# self.widget_hide(self.root.ids.connectivity_serial_fields, collapse) # self.widget_hide(self.connectivity_screen.ids.connectivity_serial_fields, collapse)
pass pass
def con_collapse_transport(collapse=True): def con_collapse_transport(collapse=True):
# self.widget_hide(self.root.ids.connectivity_transport_fields, collapse) # self.widget_hide(self.connectivity_screen.ids.connectivity_transport_fields, collapse)
pass pass
def save_connectivity(sender=None, event=None): def save_connectivity(sender=None, event=None):
@ -6132,7 +6142,7 @@ Thank you very much for using Free Communications Systems.
self.root.ids.screen_manager.transition = self.slide_transition self.root.ids.screen_manager.transition = self.slide_transition
self.root.ids.screen_manager.transition.direction = direction self.root.ids.screen_manager.transition.direction = direction
info = "The [b]Local Broadcasts[/b] feature will allow you to send and listen for local broadcast transmissions on connected radio, LoRa and WiFi interfaces.\n\n[b]Local Broadcasts[/b] makes it easy to establish public information exchange with anyone in direct radio range, or even with large areas far away using the [i]Remote Broadcast Repeater[/i] feature.\n\nThese features are not yet implemented in openCom Companion.\n\nWant it faster? Go to [u][ref=link]https://unsigned.io/donate[/ref][/u] to support the project." info = "The [b]Local Broadcasts[/b] feature will allow you to send and listen for local broadcast transmissions on all connected interfaces.\n\n[b]Local Broadcasts[/b] makes it easy to establish public information exchange with anyone in direct radio range, or even with large areas far away using the [i]Remote Broadcast Repeater[/i] feature.\n\nThese features are not yet implemented in openCom Companion.\n\nWant it faster? Go to [u][ref=link]https://unsigned.io/donate[/ref][/u] to support the project."
if self.theme_cls.theme_style == "Dark": if self.theme_cls.theme_style == "Dark":
info = "[color=#"+dark_theme_text_color+"]"+info+"[/color]" info = "[color=#"+dark_theme_text_color+"]"+info+"[/color]"
self.broadcasts_screen.ids.broadcasts_info.text = info self.broadcasts_screen.ids.broadcasts_info.text = info

View File

@ -96,9 +96,9 @@ class SidebandCore():
TELEMETRY_INTERVAL = 60 TELEMETRY_INTERVAL = 60
SERVICE_TELEMETRY_INTERVAL = 300 SERVICE_TELEMETRY_INTERVAL = 300
IF_CHANGE_ANNOUNCE_MIN_INTERVAL = 6 # In seconds IF_CHANGE_ANNOUNCE_MIN_INTERVAL = 3.5 # In seconds
AUTO_ANNOUNCE_RANDOM_MIN = 90 # In minutes AUTO_ANNOUNCE_RANDOM_MIN = 90 # In minutes
AUTO_ANNOUNCE_RANDOM_MAX = 480 # In minutes AUTO_ANNOUNCE_RANDOM_MAX = 300 # In minutes
DEFAULT_APPEARANCE = ["account", [0,0,0,1], [1,1,1,1]] DEFAULT_APPEARANCE = ["account", [0,0,0,1], [1,1,1,1]]
@ -110,11 +110,12 @@ class SidebandCore():
# This reformats the new v0.5.0 announce data back to the expected format # This reformats the new v0.5.0 announce data back to the expected format
# for Sidebands database and other handling functions. # for Sidebands database and other handling functions.
dn = LXMF.display_name_from_app_data(app_data) dn = LXMF.display_name_from_app_data(app_data)
sc = LXMF.stamp_cost_from_app_data(app_data)
app_data = b"" app_data = b""
if dn != None: if dn != None:
app_data = dn.encode("utf-8") app_data = dn.encode("utf-8")
self.log_announce(destination_hash, app_data, dest_type=SidebandCore.aspect_filter) self.log_announce(destination_hash, app_data, dest_type=SidebandCore.aspect_filter, stamp_cost=sc)
def __init__(self, owner_app, config_path = None, is_service=False, is_client=False, android_app_dir=None, verbose=False, owner_service=None, service_context=None, is_daemon=False, load_config_only=False): def __init__(self, owner_app, config_path = None, is_service=False, is_client=False, android_app_dir=None, verbose=False, owner_service=None, service_context=None, is_daemon=False, load_config_only=False):
self.is_service = is_service self.is_service = is_service
@ -154,6 +155,7 @@ class SidebandCore():
self.service_stopped = False self.service_stopped = False
self.service_context = service_context self.service_context = service_context
self.owner_service = owner_service self.owner_service = owner_service
self.allow_service_dispatch = True
self.version_str = "" self.version_str = ""
if config_path == None: if config_path == None:
@ -943,11 +945,12 @@ class SidebandCore():
else: else:
plyer.notification.notify(title, content, app_icon=self.icon_32) plyer.notification.notify(title, content, app_icon=self.icon_32)
def log_announce(self, dest, app_data, dest_type): def log_announce(self, dest, app_data, dest_type, stamp_cost=None):
try: try:
if app_data == None: if app_data == None:
app_data = b"" app_data = b""
RNS.log("Received "+str(dest_type)+" announce for "+RNS.prettyhexrep(dest)+" with data: "+app_data.decode("utf-8"), RNS.LOG_DEBUG) app_data = msgpack.packb([app_data, stamp_cost])
RNS.log("Received "+str(dest_type)+" announce for "+RNS.prettyhexrep(dest)+" with data: "+str(app_data), RNS.LOG_DEBUG)
self._db_save_announce(dest, app_data, dest_type) self._db_save_announce(dest, app_data, dest_type)
self.setstate("app.flags.new_announces", True) self.setstate("app.flags.new_announces", True)
@ -1142,9 +1145,11 @@ class SidebandCore():
else: else:
app_data = RNS.Identity.recall_app_data(context_dest) app_data = RNS.Identity.recall_app_data(context_dest)
if app_data != None: if app_data != None:
return LXMF.display_name_from_app_data(app_data)+" "+RNS.prettyhexrep(context_dest) name_str = LXMF.display_name_from_app_data(app_data)
addr_str = RNS.prettyhexrep(context_dest)
return name_str+" "+addr_str
else: else:
return RNS.prettyhexrep(context_dest) return "Anonymous Peer "+RNS.prettyhexrep(context_dest)
except Exception as e: except Exception as e:
@ -1252,130 +1257,192 @@ class SidebandCore():
else: else:
self.setstate(f"telemetry.{RNS.hexrep(message.destination_hash, delimit=False)}.request_sending", False) self.setstate(f"telemetry.{RNS.hexrep(message.destination_hash, delimit=False)}.request_sending", False)
def _service_request_latest_telemetry(self, from_addr=None):
if not RNS.vendor.platformutils.is_android():
return False
else:
if self.is_client:
try:
if self.rpc_connection == None:
self.rpc_connection = multiprocessing.connection.Client(self.rpc_addr, authkey=self.rpc_key)
self.rpc_connection.send({"request_latest_telemetry": {"from_addr": from_addr}})
response = self.rpc_connection.recv()
return response
except Exception as e:
RNS.log("Error while requesting latest telemetry over RPC: "+str(e), RNS.LOG_DEBUG)
RNS.trace_exception(e)
return False
else:
return False
def request_latest_telemetry(self, from_addr=None): def request_latest_telemetry(self, from_addr=None):
if from_addr == None or from_addr == self.lxmf_destination.hash: if self.allow_service_dispatch and self.is_client:
return "no_address" try:
else: return self._service_request_latest_telemetry(from_addr)
if self.getstate(f"telemetry.{RNS.hexrep(from_addr, delimit=False)}.request_sending") == True:
RNS.log("Not sending new telemetry request, since an earlier transfer is already in progress", RNS.LOG_DEBUG)
return "in_progress"
if from_addr != None: except Exception as e:
dest_identity = RNS.Identity.recall(from_addr) RNS.log("Error requesting latest telemetry: "+str(e), RNS.LOG_ERROR)
RNS.trace_exception(e)
if dest_identity == None:
RNS.log("The identity for "+RNS.prettyhexrep(from_addr)+" could not be recalled. Requesting identity from network...", RNS.LOG_DEBUG)
RNS.Transport.request_path(from_addr)
return "destination_unknown"
else:
now = time.time()
dest = RNS.Destination(dest_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "lxmf", "delivery")
source = self.lxmf_destination
if self.config["telemetry_use_propagation_only"] == True:
desired_method = LXMF.LXMessage.PROPAGATED
else:
desired_method = LXMF.LXMessage.DIRECT
request_timebase = self.getpersistent(f"telemetry.{RNS.hexrep(from_addr, delimit=False)}.timebase") or now - self.telemetry_request_max_history
lxm_fields = { LXMF.FIELD_COMMANDS: [
{Commands.TELEMETRY_REQUEST: request_timebase},
]}
lxm = LXMF.LXMessage(dest, source, "", desired_method=desired_method, fields = lxm_fields, include_ticket=True)
lxm.request_timebase = request_timebase
lxm.register_delivery_callback(self.telemetry_request_finished)
lxm.register_failed_callback(self.telemetry_request_finished)
if self.message_router.get_outbound_propagation_node() != None:
if self.config["telemetry_try_propagation_on_fail"]:
lxm.try_propagation_on_fail = True
RNS.log(f"Sending telemetry request with timebase {request_timebase}", RNS.LOG_DEBUG)
self.setpersistent(f"telemetry.{RNS.hexrep(from_addr, delimit=False)}.last_request_attempt", time.time())
self.setstate(f"telemetry.{RNS.hexrep(from_addr, delimit=False)}.request_sending", True)
self.message_router.handle_outbound(lxm)
return "sent"
else:
return "not_sent" return "not_sent"
else:
if from_addr == None or from_addr == self.lxmf_destination.hash:
return "no_address"
else:
if self.getstate(f"telemetry.{RNS.hexrep(from_addr, delimit=False)}.request_sending") == True:
RNS.log("Not sending new telemetry request, since an earlier transfer is already in progress", RNS.LOG_DEBUG)
return "in_progress"
def send_latest_telemetry(self, to_addr=None, stream=None, is_authorized_telemetry_request=False): if from_addr != None:
if to_addr == None or to_addr == self.lxmf_destination.hash: dest_identity = RNS.Identity.recall(from_addr)
return "no_address"
else: if dest_identity == None:
if to_addr == self.config["telemetry_collector"]: RNS.log("The identity for "+RNS.prettyhexrep(from_addr)+" could not be recalled. Requesting identity from network...", RNS.LOG_DEBUG)
is_authorized_telemetry_request = True RNS.Transport.request_path(from_addr)
return "destination_unknown"
if self.getstate(f"telemetry.{RNS.hexrep(to_addr, delimit=False)}.update_sending") == True: else:
RNS.log("Not sending new telemetry update, since an earlier transfer is already in progress", RNS.LOG_DEBUG) now = time.time()
return "in_progress" dest = RNS.Destination(dest_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "lxmf", "delivery")
source = self.lxmf_destination
if self.config["telemetry_use_propagation_only"] == True:
desired_method = LXMF.LXMessage.PROPAGATED
else:
desired_method = LXMF.LXMessage.DIRECT
if (self.latest_packed_telemetry != None and self.latest_telemetry != None) or stream != None: request_timebase = self.getpersistent(f"telemetry.{RNS.hexrep(from_addr, delimit=False)}.timebase") or now - self.telemetry_request_max_history
dest_identity = RNS.Identity.recall(to_addr) lxm_fields = { LXMF.FIELD_COMMANDS: [
{Commands.TELEMETRY_REQUEST: request_timebase},
if dest_identity == None: ]}
RNS.log("The identity for "+RNS.prettyhexrep(to_addr)+" could not be recalled. Requesting identity from network...", RNS.LOG_DEBUG)
RNS.Transport.request_path(to_addr) lxm = LXMF.LXMessage(dest, source, "", desired_method=desired_method, fields = lxm_fields, include_ticket=True)
return "destination_unknown" lxm.request_timebase = request_timebase
lxm.register_delivery_callback(self.telemetry_request_finished)
lxm.register_failed_callback(self.telemetry_request_finished)
if self.message_router.get_outbound_propagation_node() != None:
if self.config["telemetry_try_propagation_on_fail"]:
lxm.try_propagation_on_fail = True
RNS.log(f"Sending telemetry request with timebase {request_timebase}", RNS.LOG_DEBUG)
self.setpersistent(f"telemetry.{RNS.hexrep(from_addr, delimit=False)}.last_request_attempt", time.time())
self.setstate(f"telemetry.{RNS.hexrep(from_addr, delimit=False)}.request_sending", True)
self.message_router.handle_outbound(lxm)
return "sent"
else: else:
dest = RNS.Destination(dest_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "lxmf", "delivery") return "not_sent"
source = self.lxmf_destination
if self.config["telemetry_use_propagation_only"] == True:
desired_method = LXMF.LXMessage.PROPAGATED
else:
desired_method = LXMF.LXMessage.DIRECT
lxm_fields = self.get_message_fields(to_addr, is_authorized_telemetry_request=is_authorized_telemetry_request, signal_already_sent=True) def _service_send_latest_telemetry(self, to_addr=None, stream=None, is_authorized_telemetry_request=False):
if lxm_fields == False and stream == None: if not RNS.vendor.platformutils.is_android():
return "already_sent" return False
else:
if stream != None and len(stream) > 0: if self.is_client:
if lxm_fields == False: try:
lxm_fields = {} if self.rpc_connection == None:
lxm_fields[LXMF.FIELD_TELEMETRY_STREAM] = stream self.rpc_connection = multiprocessing.connection.Client(self.rpc_addr, authkey=self.rpc_key)
if lxm_fields != None and lxm_fields != False and (LXMF.FIELD_TELEMETRY in lxm_fields or LXMF.FIELD_TELEMETRY_STREAM in lxm_fields):
if LXMF.FIELD_TELEMETRY in lxm_fields:
telemeter = Telemeter.from_packed(lxm_fields[LXMF.FIELD_TELEMETRY])
telemetry_timebase = telemeter.read_all()["time"]["utc"]
elif LXMF.FIELD_TELEMETRY_STREAM in lxm_fields:
telemetry_timebase = 0
for te in lxm_fields[LXMF.FIELD_TELEMETRY_STREAM]:
ts = te[1]
telemetry_timebase = max(telemetry_timebase, ts)
if telemetry_timebase > (self.getpersistent(f"telemetry.{RNS.hexrep(to_addr, delimit=False)}.last_send_success_timebase") or 0):
lxm = LXMF.LXMessage(dest, source, "", desired_method=desired_method, fields = lxm_fields, include_ticket=self.is_trusted(to_addr))
lxm.telemetry_timebase = telemetry_timebase
lxm.register_delivery_callback(self.outbound_telemetry_finished)
lxm.register_failed_callback(self.outbound_telemetry_finished)
if self.message_router.get_outbound_propagation_node() != None:
if self.config["telemetry_try_propagation_on_fail"]:
lxm.try_propagation_on_fail = True
RNS.log(f"Sending telemetry update with timebase {telemetry_timebase}", RNS.LOG_DEBUG)
self.setpersistent(f"telemetry.{RNS.hexrep(to_addr, delimit=False)}.last_send_attempt", time.time())
self.setstate(f"telemetry.{RNS.hexrep(to_addr, delimit=False)}.update_sending", True)
self.message_router.handle_outbound(lxm)
return "sent"
else:
RNS.log(f"Telemetry update with timebase {telemetry_timebase} was already successfully sent", RNS.LOG_DEBUG)
return "already_sent"
else:
return "nothing_to_send"
self.rpc_connection.send({"send_latest_telemetry": {
"to_addr": to_addr,
"stream": stream,
"is_authorized_telemetry_request": is_authorized_telemetry_request}
})
response = self.rpc_connection.recv()
return response
except Exception as e:
RNS.log("Error while sending latest telemetry over RPC: "+str(e), RNS.LOG_DEBUG)
RNS.trace_exception(e)
return False
else: else:
RNS.log("A telemetry update was requested, but there was nothing to send.", RNS.LOG_WARNING) return False
return "nothing_to_send"
def send_latest_telemetry(self, to_addr=None, stream=None, is_authorized_telemetry_request=False):
if self.allow_service_dispatch and self.is_client:
try:
return self._service_send_latest_telemetry(to_addr, stream, is_authorized_telemetry_request)
except Exception as e:
RNS.log("Error requesting latest telemetry: "+str(e), RNS.LOG_ERROR)
RNS.trace_exception(e)
return "not_sent"
else:
if to_addr == None or to_addr == self.lxmf_destination.hash:
return "no_address"
else:
if to_addr == self.config["telemetry_collector"]:
is_authorized_telemetry_request = True
if self.getstate(f"telemetry.{RNS.hexrep(to_addr, delimit=False)}.update_sending") == True:
RNS.log("Not sending new telemetry update, since an earlier transfer is already in progress", RNS.LOG_DEBUG)
return "in_progress"
if (self.latest_packed_telemetry != None and self.latest_telemetry != None) or stream != None:
dest_identity = RNS.Identity.recall(to_addr)
if dest_identity == None:
RNS.log("The identity for "+RNS.prettyhexrep(to_addr)+" could not be recalled. Requesting identity from network...", RNS.LOG_DEBUG)
RNS.Transport.request_path(to_addr)
return "destination_unknown"
else:
dest = RNS.Destination(dest_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "lxmf", "delivery")
source = self.lxmf_destination
if self.config["telemetry_use_propagation_only"] == True:
desired_method = LXMF.LXMessage.PROPAGATED
else:
desired_method = LXMF.LXMessage.DIRECT
lxm_fields = self.get_message_fields(to_addr, is_authorized_telemetry_request=is_authorized_telemetry_request, signal_already_sent=True)
if lxm_fields == False and stream == None:
return "already_sent"
if stream != None and len(stream) > 0:
if lxm_fields == False:
lxm_fields = {}
lxm_fields[LXMF.FIELD_TELEMETRY_STREAM] = stream
if lxm_fields != None and lxm_fields != False and (LXMF.FIELD_TELEMETRY in lxm_fields or LXMF.FIELD_TELEMETRY_STREAM in lxm_fields):
if LXMF.FIELD_TELEMETRY in lxm_fields:
telemeter = Telemeter.from_packed(lxm_fields[LXMF.FIELD_TELEMETRY])
telemetry_timebase = telemeter.read_all()["time"]["utc"]
elif LXMF.FIELD_TELEMETRY_STREAM in lxm_fields:
telemetry_timebase = 0
for te in lxm_fields[LXMF.FIELD_TELEMETRY_STREAM]:
ts = te[1]
telemetry_timebase = max(telemetry_timebase, ts)
if telemetry_timebase > (self.getpersistent(f"telemetry.{RNS.hexrep(to_addr, delimit=False)}.last_send_success_timebase") or 0):
lxm = LXMF.LXMessage(dest, source, "", desired_method=desired_method, fields = lxm_fields, include_ticket=self.is_trusted(to_addr))
lxm.telemetry_timebase = telemetry_timebase
lxm.register_delivery_callback(self.outbound_telemetry_finished)
lxm.register_failed_callback(self.outbound_telemetry_finished)
if self.message_router.get_outbound_propagation_node() != None:
if self.config["telemetry_try_propagation_on_fail"]:
lxm.try_propagation_on_fail = True
RNS.log(f"Sending telemetry update with timebase {telemetry_timebase}", RNS.LOG_DEBUG)
self.setpersistent(f"telemetry.{RNS.hexrep(to_addr, delimit=False)}.last_send_attempt", time.time())
self.setstate(f"telemetry.{RNS.hexrep(to_addr, delimit=False)}.update_sending", True)
self.message_router.handle_outbound(lxm)
return "sent"
else:
RNS.log(f"Telemetry update with timebase {telemetry_timebase} was already successfully sent", RNS.LOG_DEBUG)
return "already_sent"
else:
return "nothing_to_send"
else:
RNS.log("A telemetry update was requested, but there was nothing to send.", RNS.LOG_WARNING)
return "nothing_to_send"
def list_telemetry(self, context_dest = None, after = None, before = None, limit = None): def list_telemetry(self, context_dest = None, after = None, before = None, limit = None):
@ -1447,6 +1514,7 @@ class SidebandCore():
return [] return []
def service_available(self): def service_available(self):
heartbeat_stale_time = 7.5
now = time.time() now = time.time()
service_heartbeat = self.getstate("service.heartbeat") service_heartbeat = self.getstate("service.heartbeat")
if not service_heartbeat: if not service_heartbeat:
@ -1454,9 +1522,16 @@ class SidebandCore():
return False return False
else: else:
try: try:
if now - service_heartbeat > 4.0: if now - service_heartbeat > heartbeat_stale_time:
RNS.log("Stale service heartbeat at "+str(now), RNS.LOG_DEBUG) RNS.log("Stale service heartbeat at "+str(now)+", retrying...", RNS.LOG_DEBUG)
return False now = time.time()
service_heartbeat = self.getstate("service.heartbeat")
if now - service_heartbeat > heartbeat_stale_time:
RNS.log("Service heartbeat did not recover after retry", RNS.LOG_DEBUG)
return False
else:
RNS.log("Service heartbeat recovered at"+str(time), RNS.LOG_DEBUG)
return True
else: else:
return True return True
except Exception as e: except Exception as e:
@ -1625,7 +1700,45 @@ class SidebandCore():
RNS.log(ed, RNS.LOG_DEBUG) RNS.log(ed, RNS.LOG_DEBUG)
return ed return ed
def _get_destination_establishment_rate(self, destination_hash):
try:
mr = self.message_router
oh = destination_hash
ol = None
if oh in mr.direct_links:
ol = mr.direct_links[oh]
elif oh in mr.backchannel_links:
ol = mr.backchannel_links[oh]
if ol != None:
ler = ol.get_establishment_rate()
if ler:
return ler
return None
except Exception as e:
RNS.trace_exception(e)
return None
def get_destination_establishment_rate(self, destination_hash):
if not RNS.vendor.platformutils.is_android():
return self._get_destination_establishment_rate(destination_hash)
else:
if self.is_service:
return self._get_destination_establishment_rate(destination_hash)
else:
try:
if self.rpc_connection == None:
self.rpc_connection = multiprocessing.connection.Client(self.rpc_addr, authkey=self.rpc_key)
self.rpc_connection.send({"get_destination_establishment_rate": destination_hash})
response = self.rpc_connection.recv()
return response
except Exception as e:
ed = "Error while getting destination link etablishment rate over RPC: "+str(e)
RNS.log(ed, RNS.LOG_DEBUG)
return None
def __start_rpc_listener(self): def __start_rpc_listener(self):
try: try:
RNS.log("Starting RPC listener", RNS.LOG_DEBUG) RNS.log("Starting RPC listener", RNS.LOG_DEBUG)
@ -1668,8 +1781,44 @@ class SidebandCore():
connection.send(True) connection.send(True)
elif "get_plugins_info" in call: elif "get_plugins_info" in call:
connection.send(self._get_plugins_info()) connection.send(self._get_plugins_info())
elif "get_destination_establishment_rate" in call:
connection.send(self._get_destination_establishment_rate(call["get_destination_establishment_rate"]))
elif "send_message" in call:
args = call["send_message"]
send_result = self.send_message(
args["content"],
args["destination_hash"],
args["propagation"],
skip_fields=args["skip_fields"],
no_display=args["no_display"],
attachment=args["attachment"],
image=args["image"],
audio=args["audio"])
connection.send(send_result)
elif "send_command" in call:
args = call["send_command"]
send_result = self.send_command(
args["content"],
args["destination_hash"],
args["propagation"])
connection.send(send_result)
elif "request_latest_telemetry" in call:
args = call["request_latest_telemetry"]
send_result = self.request_latest_telemetry(args["from_addr"])
connection.send(send_result)
elif "send_latest_telemetry" in call:
args = call["send_latest_telemetry"]
send_result = self.send_latest_telemetry(
to_addr=args["to_addr"],
stream=args["stream"],
is_authorized_telemetry_request=args["is_authorized_telemetry_request"]
)
connection.send(send_result)
elif "get_lxm_progress" in call:
args = call["get_lxm_progress"]
connection.send(self.get_lxm_progress(args["lxm_hash"]))
else: else:
return None connection.send(None)
except Exception as e: except Exception as e:
RNS.log("Error on client RPC connection: "+str(e), RNS.LOG_ERROR) RNS.log("Error on client RPC connection: "+str(e), RNS.LOG_ERROR)
@ -2320,9 +2469,11 @@ class SidebandCore():
for entry in result: for entry in result:
try: try:
if not entry[2] in added_dests: if not entry[2] in added_dests:
app_data = entry[3]
announce = { announce = {
"dest": entry[2], "dest": entry[2],
"data": entry[3].decode("utf-8"), "name": LXMF.display_name_from_app_data(app_data),
"cost": LXMF.stamp_cost_from_app_data(app_data),
"time": entry[1], "time": entry[1],
"type": entry[4] "type": entry[4]
} }
@ -3906,9 +4057,34 @@ class SidebandCore():
RNS.log("Error while creating paper message: "+str(e), RNS.LOG_ERROR) RNS.log("Error while creating paper message: "+str(e), RNS.LOG_ERROR)
return False return False
def _service_get_lxm_progress(self, lxm_hash):
if not RNS.vendor.platformutils.is_android():
return False
else:
if self.is_client:
try:
if self.rpc_connection == None:
self.rpc_connection = multiprocessing.connection.Client(self.rpc_addr, authkey=self.rpc_key)
self.rpc_connection.send({"get_lxm_progress": {"lxm_hash": lxm_hash}})
response = self.rpc_connection.recv()
return response
except Exception as e:
RNS.log("Error while getting LXM progress over RPC: "+str(e), RNS.LOG_DEBUG)
RNS.trace_exception(e)
return False
else:
return False
def get_lxm_progress(self, lxm_hash): def get_lxm_progress(self, lxm_hash):
try: try:
return self.message_router.get_outbound_progress(lxm_hash) prg = self.message_router.get_outbound_progress(lxm_hash)
if not prg and self.is_client:
prg = self._service_get_lxm_progress(lxm_hash)
return prg
except Exception as e: except Exception as e:
RNS.log("An error occurred while getting message transfer progress: "+str(e), RNS.LOG_ERROR) RNS.log("An error occurred while getting message transfer progress: "+str(e), RNS.LOG_ERROR)
return None return None
@ -3920,103 +4096,184 @@ class SidebandCore():
RNS.log("An error occurred while getting message transfer stamp cost: "+str(e), RNS.LOG_ERROR) RNS.log("An error occurred while getting message transfer stamp cost: "+str(e), RNS.LOG_ERROR)
return None return None
def send_message(self, content, destination_hash, propagation, skip_fields=False, no_display=False, attachment = None, image = None, audio = None): def _service_send_message(self, content, destination_hash, propagation, skip_fields=False, no_display=False, attachment = None, image = None, audio = None):
try: if not RNS.vendor.platformutils.is_android():
if content == "":
raise ValueError("Message content cannot be empty")
dest_identity = RNS.Identity.recall(destination_hash)
dest = RNS.Destination(dest_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "lxmf", "delivery")
source = self.lxmf_destination
if propagation:
desired_method = LXMF.LXMessage.PROPAGATED
else:
desired_method = LXMF.LXMessage.DIRECT
if skip_fields:
fields = {}
else:
fields = self.get_message_fields(destination_hash)
if attachment != None:
fields[LXMF.FIELD_FILE_ATTACHMENTS] = [attachment]
if image != None:
fields[LXMF.FIELD_IMAGE] = image
if audio != None:
fields[LXMF.FIELD_AUDIO] = audio
lxm = LXMF.LXMessage(dest, source, content, title="", desired_method=desired_method, fields = fields, include_ticket=self.is_trusted(destination_hash))
if not no_display:
lxm.register_delivery_callback(self.message_notification)
lxm.register_failed_callback(self.message_notification)
else:
lxm.register_delivery_callback(self.message_notification_no_display)
lxm.register_failed_callback(self.message_notification_no_display)
if self.message_router.get_outbound_propagation_node() != None:
if self.config["lxmf_try_propagation_on_fail"]:
lxm.try_propagation_on_fail = True
self.message_router.handle_outbound(lxm)
if not no_display:
self.lxm_ingest(lxm, originator=True)
return True
except Exception as e:
RNS.log("Error while sending message: "+str(e), RNS.LOG_ERROR)
RNS.trace_exception(e)
return False return False
else:
if self.is_client:
try:
if self.rpc_connection == None:
self.rpc_connection = multiprocessing.connection.Client(self.rpc_addr, authkey=self.rpc_key)
self.rpc_connection.send({"send_message": {
"content": content,
"destination_hash": destination_hash,
"propagation": propagation,
"skip_fields": skip_fields,
"no_display": no_display,
"attachment": attachment,
"image": image,
"audio": audio}
})
response = self.rpc_connection.recv()
return response
except Exception as e:
RNS.log("Error while sending message over RPC: "+str(e), RNS.LOG_DEBUG)
RNS.trace_exception(e)
return False
else:
return False
def _service_send_command(self, content, destination_hash, propagation):
if not RNS.vendor.platformutils.is_android():
return False
else:
if self.is_client:
try:
if self.rpc_connection == None:
self.rpc_connection = multiprocessing.connection.Client(self.rpc_addr, authkey=self.rpc_key)
self.rpc_connection.send({"send_command": {
"content": content,
"destination_hash": destination_hash,
"propagation": propagation}
})
response = self.rpc_connection.recv()
return response
except Exception as e:
RNS.log("Error while sending command over RPC: "+str(e), RNS.LOG_DEBUG)
RNS.trace_exception(e)
return False
else:
return False
def send_message(self, content, destination_hash, propagation, skip_fields=False, no_display=False, attachment = None, image = None, audio = None):
if self.allow_service_dispatch and self.is_client:
try:
return self._service_send_message(content, destination_hash, propagation, skip_fields, no_display, attachment, image, audio)
except Exception as e:
RNS.log("Error while sending message: "+str(e), RNS.LOG_ERROR)
RNS.trace_exception(e)
return False
else:
try:
if content == "":
raise ValueError("Message content cannot be empty")
dest_identity = RNS.Identity.recall(destination_hash)
dest = RNS.Destination(dest_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "lxmf", "delivery")
source = self.lxmf_destination
if propagation:
desired_method = LXMF.LXMessage.PROPAGATED
else:
if not self.message_router.delivery_link_available(destination_hash) and RNS.Identity.current_ratchet_id(destination_hash) != None:
RNS.log(f"Have ratchet for {RNS.prettyhexrep(destination_hash)}, requesting opportunistic delivery of message", RNS.LOG_DEBUG)
desired_method = LXMF.LXMessage.OPPORTUNISTIC
else:
desired_method = LXMF.LXMessage.DIRECT
if skip_fields:
fields = {}
else:
fields = self.get_message_fields(destination_hash)
if attachment != None:
fields[LXMF.FIELD_FILE_ATTACHMENTS] = [attachment]
if image != None:
fields[LXMF.FIELD_IMAGE] = image
if audio != None:
fields[LXMF.FIELD_AUDIO] = audio
lxm = LXMF.LXMessage(dest, source, content, title="", desired_method=desired_method, fields = fields, include_ticket=self.is_trusted(destination_hash))
if not no_display:
lxm.register_delivery_callback(self.message_notification)
lxm.register_failed_callback(self.message_notification)
else:
lxm.register_delivery_callback(self.message_notification_no_display)
lxm.register_failed_callback(self.message_notification_no_display)
if self.message_router.get_outbound_propagation_node() != None:
if self.config["lxmf_try_propagation_on_fail"]:
lxm.try_propagation_on_fail = True
self.message_router.handle_outbound(lxm)
if not no_display:
self.lxm_ingest(lxm, originator=True)
return True
except Exception as e:
RNS.log("Error while sending message: "+str(e), RNS.LOG_ERROR)
RNS.trace_exception(e)
return False
def send_command(self, content, destination_hash, propagation): def send_command(self, content, destination_hash, propagation):
try: if self.allow_service_dispatch and self.is_client:
if content == "": try:
return self._service_send_command(content, destination_hash, propagation)
except Exception as e:
RNS.log("Error while sending message: "+str(e), RNS.LOG_ERROR)
RNS.trace_exception(e)
return False return False
commands = [] else:
if content.startswith("echo "): try:
echo_content = content.replace("echo ", "").encode("utf-8") if content == "":
if len(echo_content) > 0: return False
commands.append({Commands.ECHO: echo_content})
elif content.startswith("sig"):
commands.append({Commands.SIGNAL_REPORT: True})
elif content.startswith("ping"):
commands.append({Commands.PING: True})
else:
commands.append({Commands.PLUGIN_COMMAND: content})
if len(commands) == 0: commands = []
if content.startswith("echo "):
echo_content = content.replace("echo ", "").encode("utf-8")
if len(echo_content) > 0:
commands.append({Commands.ECHO: echo_content})
elif content.startswith("sig"):
commands.append({Commands.SIGNAL_REPORT: True})
elif content.startswith("ping"):
commands.append({Commands.PING: True})
else:
commands.append({Commands.PLUGIN_COMMAND: content})
if len(commands) == 0:
return False
dest_identity = RNS.Identity.recall(destination_hash)
dest = RNS.Destination(dest_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "lxmf", "delivery")
source = self.lxmf_destination
if propagation:
desired_method = LXMF.LXMessage.PROPAGATED
else:
if not self.message_router.delivery_link_available(destination_hash) and RNS.Identity.current_ratchet_id(destination_hash) != None:
RNS.log(f"Have ratchet for {RNS.prettyhexrep(destination_hash)}, requesting opportunistic delivery of command", RNS.LOG_DEBUG)
desired_method = LXMF.LXMessage.OPPORTUNISTIC
else:
desired_method = LXMF.LXMessage.DIRECT
lxm = LXMF.LXMessage(dest, source, "", title="", desired_method=desired_method, fields = {LXMF.FIELD_COMMANDS: commands}, include_ticket=self.is_trusted(destination_hash))
lxm.register_delivery_callback(self.message_notification)
lxm.register_failed_callback(self.message_notification)
if self.message_router.get_outbound_propagation_node() != None:
if self.config["lxmf_try_propagation_on_fail"]:
lxm.try_propagation_on_fail = True
self.message_router.handle_outbound(lxm)
self.lxm_ingest(lxm, originator=True)
return True
except Exception as e:
RNS.log("Error while sending message: "+str(e), RNS.LOG_ERROR)
return False return False
dest_identity = RNS.Identity.recall(destination_hash)
dest = RNS.Destination(dest_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "lxmf", "delivery")
source = self.lxmf_destination
if propagation:
desired_method = LXMF.LXMessage.PROPAGATED
else:
desired_method = LXMF.LXMessage.DIRECT
lxm = LXMF.LXMessage(dest, source, "", title="", desired_method=desired_method, fields = {LXMF.FIELD_COMMANDS: commands}, include_ticket=self.is_trusted(destination_hash))
lxm.register_delivery_callback(self.message_notification)
lxm.register_failed_callback(self.message_notification)
if self.message_router.get_outbound_propagation_node() != None:
if self.config["lxmf_try_propagation_on_fail"]:
lxm.try_propagation_on_fail = True
self.message_router.handle_outbound(lxm)
self.lxm_ingest(lxm, originator=True)
return True
except Exception as e:
RNS.log("Error while sending message: "+str(e), RNS.LOG_ERROR)
return False
def new_conversation(self, dest_str, name = "", trusted = False): def new_conversation(self, dest_str, name = "", trusted = False):
if len(dest_str) != RNS.Reticulum.TRUNCATED_HASHLENGTH//8*2: if len(dest_str) != RNS.Reticulum.TRUNCATED_HASHLENGTH//8*2:
return False return False

View File

@ -89,7 +89,8 @@ class Announces():
for announce in self.announces: for announce in self.announces:
context_dest = announce["dest"] context_dest = announce["dest"]
ts = announce["time"] ts = announce["time"]
a_data = announce["data"] a_name = announce["name"]
a_cost = announce["cost"]
dest_type = announce["type"] dest_type = announce["type"]
if not context_dest in self.added_item_dests: if not context_dest in self.added_item_dests:
@ -98,15 +99,16 @@ class Announces():
else: else:
trust_icon = "account-question" trust_icon = "account-question"
def gen_info(ts, dest, name, dtype): def gen_info(ts, dest, name, cost, dtype):
name = multilingual_markup(escape_markup(str(name)).encode("utf-8")).decode("utf-8") name = multilingual_markup(escape_markup(str(name)).encode("utf-8")).decode("utf-8")
cost = str(cost)
def x(sender): def x(sender):
yes_button = MDRectangleFlatButton(text="OK",font_size=dp(18)) yes_button = MDRectangleFlatButton(text="OK",font_size=dp(18))
if dtype == "lxmf.delivery": if dtype == "lxmf.delivery":
ad_text = "[size=22dp]LXMF Peer[/size]\n\nReceived: "+ts+"\nAnnounced Name: "+name+"\nAddress: "+RNS.prettyhexrep(dest) ad_text = "[size=22dp]LXMF Peer[/size]\n\n[b]Received[/b] "+ts+"\n[b]Address[/b] "+RNS.prettyhexrep(dest)+"\n[b]Name[/b] "+name+"\n[b]Stamp Cost[/b] "+cost
if dtype == "lxmf.propagation": if dtype == "lxmf.propagation":
ad_text = "[size=22dp]LXMF Propagation Node[/size]\n\nReceived: "+ts+"\nAddress: "+RNS.prettyhexrep(dest) ad_text = "[size=22dp]LXMF Propagation Node[/size]\n\n[b]Received[/b] "+ts+"\n[b]Address[/b] "+RNS.prettyhexrep(dest)
dialog = MDDialog( dialog = MDDialog(
text=ad_text, text=ad_text,
@ -135,7 +137,7 @@ class Announces():
disp_name = "Unknown Announce" disp_name = "Unknown Announce"
iconl = IconLeftWidget(icon="progress-question") iconl = IconLeftWidget(icon="progress-question")
item = TwoLineAvatarIconListItem(text=time_string, secondary_text=disp_name, on_release=gen_info(time_string, context_dest, a_data, dest_type)) item = TwoLineAvatarIconListItem(text=time_string, secondary_text=disp_name, on_release=gen_info(time_string, context_dest, a_name, a_cost, dest_type))
item.add_widget(iconl) item.add_widget(iconl)
item.sb_uid = context_dest item.sb_uid = context_dest
item.ts = ts item.ts = ts

View File

@ -616,7 +616,7 @@ MDScreen:
MDLabel: MDLabel:
text: "Shared Instance Access\\n" text: "Shared Instance Access\\n"
id: connectivity_shared_access_label id: connectivity_shared_access_label
font_style: "H6" font_style: "H5"
MDLabel: MDLabel:
id: connectivity_shared_access id: connectivity_shared_access
@ -1176,7 +1176,7 @@ MDScreen:
padding: [0,dp(0),dp(0),dp(0)] padding: [0,dp(0),dp(0),dp(0)]
text: "Plugin Settings" text: "Plugin Settings"
id: plugins_active_heading id: plugins_active_heading
font_style: "H6" font_style: "H5"
size_hint_y: None size_hint_y: None
height: self.texture_size[1] height: self.texture_size[1]
@ -1286,7 +1286,7 @@ MDScreen:
MDLabel: MDLabel:
text: "User Options" text: "User Options"
font_style: "H6" font_style: "H5"
size_hint_y: None size_hint_y: None
height: self.texture_size[1] height: self.texture_size[1]
@ -1326,8 +1326,17 @@ MDScreen:
height: dp(64) height: dp(64)
MDLabel: MDLabel:
text: "Address & Identity" text: ""
font_style: "H6" font_style: "H6"
text_size: self.size
halign: "center"
size_hint_y: None
height: self.texture_size[1]
padding: [0, dp(2), 0, dp(22)]
MDLabel:
text: "Address & Identity"
font_style: "H5"
size_hint_y: None size_hint_y: None
height: self.texture_size[1] height: self.texture_size[1]
@ -1361,8 +1370,17 @@ MDScreen:
height: dp(64) height: dp(64)
MDLabel: MDLabel:
text: "Appearance" text: ""
font_style: "H6" font_style: "H6"
text_size: self.size
halign: "center"
size_hint_y: None
height: self.texture_size[1]
padding: [0, dp(2), 0, dp(22)]
MDLabel:
text: "Appearance"
font_style: "H5"
size_hint_y: None size_hint_y: None
height: self.texture_size[1] height: self.texture_size[1]
@ -1464,8 +1482,17 @@ MDScreen:
active: False active: False
MDLabel: MDLabel:
text: "\\nBehaviour" text: ""
font_style: "H6" font_style: "H6"
text_size: self.size
halign: "center"
size_hint_y: None
height: self.texture_size[1]
padding: [0, dp(22), 0, dp(2)]
MDLabel:
text: "\\nBehaviour"
font_style: "H5"
size_hint_y: None size_hint_y: None
height: self.texture_size[1] height: self.texture_size[1]
@ -1498,7 +1525,7 @@ MDScreen:
height: dp(48) height: dp(48)
MDLabel: MDLabel:
text: "Try Propagation Node on direct delivery failure" text: "Try propagation on direct delivery failure"
font_style: "H6" font_style: "H6"
MDSwitch: MDSwitch:
@ -1684,8 +1711,17 @@ MDScreen:
active: False active: False
MDLabel: MDLabel:
text: "Input Options & Localisation" text: ""
font_style: "H6" font_style: "H6"
text_size: self.size
halign: "center"
size_hint_y: None
height: self.texture_size[1]
padding: [0, dp(0), 0, dp(30)]
MDLabel:
text: "Input Options & Localisation"
font_style: "H5"
size_hint_y: None size_hint_y: None
height: self.texture_size[1] height: self.texture_size[1]

View File

@ -102,7 +102,6 @@ class Messages():
self.list.remove_widget(self.load_more_button) self.list.remove_widget(self.load_more_button)
def message_details_dialog(self, lxm_hash): def message_details_dialog(self, lxm_hash):
RNS.log(f"Opening dialog for {RNS.prettyhexrep(lxm_hash)}", RNS.LOG_DEBUG)
ss = int(dp(16)) ss = int(dp(16))
ms = int(dp(14)) ms = int(dp(14))
@ -247,88 +246,100 @@ class Messages():
else: else:
w.line_color = (1.0, 1.0, 1.0, 0.5) w.line_color = (1.0, 1.0, 1.0, 0.5)
if m["state"] == LXMF.LXMessage.SENDING or m["state"] == LXMF.LXMessage.OUTBOUND: if m["state"] == LXMF.LXMessage.SENDING or m["state"] == LXMF.LXMessage.OUTBOUND or m["state"] == LXMF.LXMessage.SENT:
msg = self.app.sideband.message(m["hash"]) msg = self.app.sideband.message(m["hash"])
if msg["state"] == LXMF.LXMessage.OUTBOUND or msg["state"] == LXMF.LXMessage.SENDING: if msg != None:
w.md_bg_color = msg_color = mdc(color_unknown, intensity_msgs) delivery_syms = ""
txstr = time.strftime(ts_format, time.localtime(msg["sent"])) # if msg["extras"] != None and "ratchet_id" in m["extras"]:
titlestr = "" # delivery_syms += " ⚙️"
prgstr = "" if msg["method"] == LXMF.LXMessage.OPPORTUNISTIC:
sphrase = "Sending" delivery_syms += " 📨"
prg = self.app.sideband.get_lxm_progress(msg["hash"]) if msg["method"] == LXMF.LXMessage.DIRECT:
if prg != None: delivery_syms += " 🔗"
prgstr = ", "+str(round(prg*100, 1))+"% done" if msg["method"] == LXMF.LXMessage.PROPAGATED:
if prg <= 0.00: delivery_syms += " 📦"
stamp_cost = self.app.sideband.get_lxm_stamp_cost(msg["hash"]) delivery_syms = multilingual_markup(delivery_syms.encode("utf-8")).decode("utf-8")
if stamp_cost:
sphrase = f"Generating stamp with cost {stamp_cost}" if msg["state"] == LXMF.LXMessage.OUTBOUND or msg["state"] == LXMF.LXMessage.SENDING or msg["state"] == LXMF.LXMessage.SENT:
prgstr = "" w.md_bg_color = msg_color = mdc(color_unknown, intensity_msgs)
else: txstr = time.strftime(ts_format, time.localtime(msg["sent"]))
titlestr = ""
prgstr = ""
sphrase = "Sending"
prg = self.app.sideband.get_lxm_progress(msg["hash"])
if prg != None:
prgstr = ", "+str(round(prg*100, 1))+"% done"
if prg <= 0.00:
stamp_cost = self.app.sideband.get_lxm_stamp_cost(msg["hash"])
if stamp_cost:
sphrase = f"Generating stamp with cost {stamp_cost}"
prgstr = ""
else:
sphrase = "Waiting for path"
elif prg <= 0.01:
sphrase = "Waiting for path" sphrase = "Waiting for path"
elif prg <= 0.01: elif prg <= 0.03:
sphrase = "Waiting for path" sphrase = "Establishing link"
elif prg <= 0.03: elif prg <= 0.05:
sphrase = "Establishing link" sphrase = "Link established"
elif prg <= 0.05: elif prg >= 0.05:
sphrase = "Link established" sphrase = "Sending"
elif prg >= 0.05:
sphrase = "Sending" if msg["title"]:
titlestr = "[b]Title[/b] "+msg["title"].decode("utf-8")+"\n"
if msg["title"]: w.heading = titlestr+"[b]Sent[/b] "+txstr+"\n[b]State[/b] "+sphrase+prgstr+" "
titlestr = "[b]Title[/b] "+msg["title"].decode("utf-8")+"\n" if w.has_audio:
w.heading = titlestr+"[b]Sent[/b] "+txstr+"\n[b]State[/b] "+sphrase+prgstr+" " alstr = RNS.prettysize(w.audio_size)
if w.has_audio: w.heading += f"\n[b]Audio Message[/b] ({alstr})"
alstr = RNS.prettysize(w.audio_size) m["state"] = msg["state"]
w.heading += f"\n[b]Audio Message[/b] ({alstr})"
m["state"] = msg["state"]
if msg["state"] == LXMF.LXMessage.DELIVERED: if msg["state"] == LXMF.LXMessage.DELIVERED:
w.md_bg_color = msg_color = mdc(color_delivered, intensity_msgs) w.md_bg_color = msg_color = mdc(color_delivered, intensity_msgs)
txstr = time.strftime(ts_format, time.localtime(msg["sent"])) txstr = time.strftime(ts_format, time.localtime(msg["sent"]))
titlestr = "" titlestr = ""
if msg["title"]: if msg["title"]:
titlestr = "[b]Title[/b] "+msg["title"].decode("utf-8")+"\n" titlestr = "[b]Title[/b] "+msg["title"].decode("utf-8")+"\n"
w.heading = titlestr+"[b]Sent[/b] "+txstr+"\n[b]State[/b] Delivered" w.heading = titlestr+"[b]Sent[/b] "+txstr+delivery_syms+"\n[b]State[/b] Delivered"
if w.has_audio: if w.has_audio:
alstr = RNS.prettysize(w.audio_size) alstr = RNS.prettysize(w.audio_size)
w.heading += f"\n[b]Audio Message[/b] ({alstr})" w.heading += f"\n[b]Audio Message[/b] ({alstr})"
m["state"] = msg["state"] m["state"] = msg["state"]
if msg["method"] == LXMF.LXMessage.PAPER: if msg["method"] == LXMF.LXMessage.PAPER:
w.md_bg_color = msg_color = mdc(color_paper, intensity_msgs) w.md_bg_color = msg_color = mdc(color_paper, intensity_msgs)
txstr = time.strftime(ts_format, time.localtime(msg["sent"])) txstr = time.strftime(ts_format, time.localtime(msg["sent"]))
titlestr = "" titlestr = ""
if msg["title"]: if msg["title"]:
titlestr = "[b]Title[/b] "+msg["title"].decode("utf-8")+"\n" titlestr = "[b]Title[/b] "+msg["title"].decode("utf-8")+"\n"
w.heading = titlestr+"[b]Sent[/b] "+txstr+"\n[b]State[/b] Paper Message" w.heading = titlestr+"[b]Sent[/b] "+txstr+"\n[b]State[/b] Paper Message"
m["state"] = msg["state"] m["state"] = msg["state"]
if msg["method"] == LXMF.LXMessage.PROPAGATED and msg["state"] == LXMF.LXMessage.SENT: if msg["method"] == LXMF.LXMessage.PROPAGATED and msg["state"] == LXMF.LXMessage.SENT:
w.md_bg_color = msg_color = mdc(color_propagated, intensity_msgs) w.md_bg_color = msg_color = mdc(color_propagated, intensity_msgs)
txstr = time.strftime(ts_format, time.localtime(msg["sent"])) txstr = time.strftime(ts_format, time.localtime(msg["sent"]))
titlestr = "" titlestr = ""
if msg["title"]: if msg["title"]:
titlestr = "[b]Title[/b] "+msg["title"].decode("utf-8")+"\n" titlestr = "[b]Title[/b] "+msg["title"].decode("utf-8")+"\n"
w.heading = titlestr+"[b]Sent[/b] "+txstr+"\n[b]State[/b] On Propagation Net" w.heading = titlestr+"[b]Sent[/b] "+txstr+delivery_syms+"\n[b]State[/b] On Propagation Net"
if w.has_audio: if w.has_audio:
alstr = RNS.prettysize(w.audio_size) alstr = RNS.prettysize(w.audio_size)
w.heading += f"\n[b]Audio Message[/b] ({alstr})" w.heading += f"\n[b]Audio Message[/b] ({alstr})"
m["state"] = msg["state"] m["state"] = msg["state"]
if msg["state"] == LXMF.LXMessage.FAILED: if msg["state"] == LXMF.LXMessage.FAILED:
w.md_bg_color = msg_color = mdc(color_failed, intensity_msgs) w.md_bg_color = msg_color = mdc(color_failed, intensity_msgs)
txstr = time.strftime(ts_format, time.localtime(msg["sent"])) txstr = time.strftime(ts_format, time.localtime(msg["sent"]))
titlestr = "" titlestr = ""
if msg["title"]: if msg["title"]:
titlestr = "[b]Title[/b] "+msg["title"].decode("utf-8")+"\n" titlestr = "[b]Title[/b] "+msg["title"].decode("utf-8")+"\n"
w.heading = titlestr+"[b]Sent[/b] "+txstr+"\n[b]State[/b] Failed" w.heading = titlestr+"[b]Sent[/b] "+txstr+"\n[b]State[/b] Failed"
m["state"] = msg["state"] m["state"] = msg["state"]
if w.has_audio: if w.has_audio:
alstr = RNS.prettysize(w.audio_size) alstr = RNS.prettysize(w.audio_size)
w.heading += f"\n[b]Audio Message[/b] ({alstr})" w.heading += f"\n[b]Audio Message[/b] ({alstr})"
w.dmenu.items.append(w.dmenu.retry_item) w.dmenu.items.append(w.dmenu.retry_item)
def hide_widget(self, wid, dohide=True): def hide_widget(self, wid, dohide=True):
@ -357,10 +368,14 @@ class Messages():
for m in self.new_messages: for m in self.new_messages:
if not m["hash"] in self.added_item_hashes: if not m["hash"] in self.added_item_hashes:
if not self.is_trusted: try:
message_input = str( escape_markup(m["content"].decode("utf-8")) ).encode("utf-8") if not self.is_trusted:
else: message_input = str( escape_markup(m["content"].decode("utf-8")) ).encode("utf-8")
message_input = m["content"] else:
message_input = m["content"]
except Exception as e:
RNS.log(f"Message content could not be decoded: {e}", RNS.LOG_DEBUG)
message_input = b""
if message_input.strip() == b"": if message_input.strip() == b"":
if not ("lxm" in m and m["lxm"] != None and m["lxm"].fields != None and LXMF.FIELD_COMMANDS in m["lxm"].fields): if not ("lxm" in m and m["lxm"] != None and m["lxm"].fields != None and LXMF.FIELD_COMMANDS in m["lxm"].fields):
@ -385,6 +400,17 @@ class Messages():
stamp_valid = False stamp_valid = False
stamp_value = None stamp_value = None
delivery_syms = ""
# if m["extras"] != None and "ratchet_id" in m["extras"]:
# delivery_syms += " ⚙️"
if m["method"] == LXMF.LXMessage.OPPORTUNISTIC:
delivery_syms += " 📨"
if m["method"] == LXMF.LXMessage.DIRECT:
delivery_syms += " 🔗"
if m["method"] == LXMF.LXMessage.PROPAGATED:
delivery_syms += " 📦"
delivery_syms = multilingual_markup(delivery_syms.encode("utf-8")).decode("utf-8")
if "lxm" in m and m["lxm"] != None and m["lxm"].signature_validated: if "lxm" in m and m["lxm"] != None and m["lxm"].signature_validated:
signature_valid = True signature_valid = True
@ -490,11 +516,11 @@ class Messages():
if m["source"] == self.app.sideband.lxmf_destination.hash: if m["source"] == self.app.sideband.lxmf_destination.hash:
if m["state"] == LXMF.LXMessage.DELIVERED: if m["state"] == LXMF.LXMessage.DELIVERED:
msg_color = mdc(color_delivered, intensity_msgs) msg_color = mdc(color_delivered, intensity_msgs)
heading_str = titlestr+"[b]Sent[/b] "+txstr+"\n[b]State[/b] Delivered" heading_str = titlestr+"[b]Sent[/b] "+txstr+delivery_syms+"\n[b]State[/b] Delivered"
elif m["method"] == LXMF.LXMessage.PROPAGATED and m["state"] == LXMF.LXMessage.SENT: elif m["method"] == LXMF.LXMessage.PROPAGATED and m["state"] == LXMF.LXMessage.SENT:
msg_color = mdc(color_propagated, intensity_msgs) msg_color = mdc(color_propagated, intensity_msgs)
heading_str = titlestr+"[b]Sent[/b] "+txstr+"\n[b]State[/b] On Propagation Net" heading_str = titlestr+"[b]Sent[/b] "+txstr+delivery_syms+"\n[b]State[/b] On Propagation Net"
elif m["method"] == LXMF.LXMessage.PAPER: elif m["method"] == LXMF.LXMessage.PAPER:
msg_color = mdc(color_paper, intensity_msgs) msg_color = mdc(color_paper, intensity_msgs)
@ -521,7 +547,7 @@ class Messages():
# if stamp_valid: # if stamp_valid:
# txstr += f" [b]Stamp[/b] value is {stamp_value} " # txstr += f" [b]Stamp[/b] value is {stamp_value} "
heading_str += "[b]Sent[/b] "+txstr heading_str += "[b]Sent[/b] "+txstr+delivery_syms
heading_str += "\n[b]Received[/b] "+rxstr heading_str += "\n[b]Received[/b] "+rxstr
if rcvd_d_str != "": if rcvd_d_str != "":
@ -966,7 +992,7 @@ class Messages():
"viewclass": "OneLineListItem", "viewclass": "OneLineListItem",
"text": "Copy message text", "text": "Copy message text",
"height": dp(40), "height": dp(40),
"on_release": gen_copy(m["content"].decode("utf-8"), item) "on_release": gen_copy(message_input.decode("utf-8"), item)
}, },
{ {
"text": "Delete", "text": "Delete",
@ -1000,7 +1026,7 @@ class Messages():
"viewclass": "OneLineListItem", "viewclass": "OneLineListItem",
"text": "Copy message text", "text": "Copy message text",
"height": dp(40), "height": dp(40),
"on_release": gen_copy(m["content"].decode("utf-8"), item) "on_release": gen_copy(message_input.decode("utf-8"), item)
}, },
{ {
"text": "Delete", "text": "Delete",
@ -1018,7 +1044,7 @@ class Messages():
"viewclass": "OneLineListItem", "viewclass": "OneLineListItem",
"text": "Copy", "text": "Copy",
"height": dp(40), "height": dp(40),
"on_release": gen_copy(m["content"].decode("utf-8"), item) "on_release": gen_copy(message_input.decode("utf-8"), item)
}, },
{ {
"text": "Delete", "text": "Delete",
@ -1035,7 +1061,7 @@ class Messages():
"viewclass": "OneLineListItem", "viewclass": "OneLineListItem",
"text": "Copy", "text": "Copy",
"height": dp(40), "height": dp(40),
"on_release": gen_copy(m["content"].decode("utf-8"), item) "on_release": gen_copy(message_input.decode("utf-8"), item)
}, },
{ {
"viewclass": "OneLineListItem", "viewclass": "OneLineListItem",
@ -1058,7 +1084,7 @@ class Messages():
"viewclass": "OneLineListItem", "viewclass": "OneLineListItem",
"text": "Copy", "text": "Copy",
"height": dp(40), "height": dp(40),
"on_release": gen_copy(m["content"].decode("utf-8"), item) "on_release": gen_copy(message_input.decode("utf-8"), item)
}, },
{ {
"text": "Delete", "text": "Delete",

View File

@ -796,14 +796,10 @@ class RVDetails(MDRecycleView):
self.entries.append({"icon": "routes", "text": f"Current path on [b]{nhi}[/b]", "on_release": pass_job}) self.entries.append({"icon": "routes", "text": f"Current path on [b]{nhi}[/b]", "on_release": pass_job})
try: try:
mr = self.delegate.app.sideband.message_router ler = self.delegate.app.sideband.get_destination_establishment_rate(self.delegate.object_hash)
oh = self.delegate.object_hash if ler:
if oh in mr.direct_links: lers = RNS.prettyspeed(ler, "b")
ol = mr.direct_links[oh] self.entries.append({"icon": "lock-check-outline", "text": f"Direct link established, LER is [b]{lers}[/b]", "on_release": pass_job})
ler = ol.get_establishment_rate()
if ler:
lers = RNS.prettyspeed(ler, "b")
self.entries.append({"icon": "lock-check-outline", "text": f"Direct link established, LER is [b]{lers}[/b]", "on_release": pass_job})
except Exception as e: except Exception as e:
RNS.trace_exception(e) RNS.trace_exception(e)

View File

@ -96,8 +96,8 @@ setuptools.setup(
] ]
}, },
install_requires=[ install_requires=[
"rns>=0.7.7", "rns>=0.7.8",
"lxmf>=0.5.1", "lxmf>=0.5.3",
"kivy>=2.3.0", "kivy>=2.3.0",
"pillow>=10.2.0", "pillow>=10.2.0",
"qrcode", "qrcode",