From 7076fae5cc554d603dbcd1925eee80a119e2dc13 Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Mon, 30 Oct 2023 02:28:35 +0100 Subject: [PATCH] Added command handling and sending --- sbapp/main.py | 62 +++++++++++++++++------ sbapp/sideband/core.py | 108 +++++++++++++++++++++++++++++++++++++--- sbapp/sideband/sense.py | 2 + sbapp/ui/messages.py | 25 ++++++++-- 4 files changed, 172 insertions(+), 25 deletions(-) diff --git a/sbapp/main.py b/sbapp/main.py index 8104260..1ecb11b 100644 --- a/sbapp/main.py +++ b/sbapp/main.py @@ -829,7 +829,9 @@ class SidebandApp(MDApp): self.message_send_action() if text == "l": - if self.root.ids.screen_manager.current == "map_screen": + if self.root.ids.screen_manager.current == "messages_screen": + self.message_propagation_action(self) + elif self.root.ids.screen_manager.current == "map_screen": self.map_layers_action() else: self.announces_action(self) @@ -1006,6 +1008,7 @@ class SidebandApp(MDApp): def open_conversation(self, context_dest, direction="left"): self.outbound_mode_paper = False + self.outbound_mode_command = False if self.sideband.config["propagation_by_default"]: self.outbound_mode_propagation = True else: @@ -1075,7 +1078,26 @@ class SidebandApp(MDApp): else: msg_content = self.messages_view.ids.message_text.text context_dest = self.messages_view.ids.messages_scrollview.active_conversation - if self.outbound_mode_paper: + if self.outbound_mode_command: + if self.sideband.send_command(msg_content, context_dest, False): + self.messages_view.ids.message_text.text = "" + self.messages_view.ids.messages_scrollview.scroll_y = 0 + self.jobs(0) + else: + self.messages_view.send_error_dialog = MDDialog( + title="Error", + text="Could not send the command. Check that the syntax is correct, and that the command is supported.", + buttons=[ + MDRectangleFlatButton( + text="OK", + font_size=dp(18), + on_release=self.messages_view.close_send_error_dialog + ) + ], + ) + self.messages_view.send_error_dialog.open() + + elif self.outbound_mode_paper: if self.sideband.paper_message(msg_content, context_dest): self.messages_view.ids.message_text.text = "" self.messages_view.ids.messages_scrollview.scroll_y = 0 @@ -1117,16 +1139,24 @@ class SidebandApp(MDApp): self.object_details_action(self.messages_view, from_conv=True) def message_propagation_action(self, sender): - if self.outbound_mode_paper: + if self.outbound_mode_command: self.outbound_mode_paper = False self.outbound_mode_propagation = False + self.outbound_mode_command = False else: - if self.outbound_mode_propagation: - self.outbound_mode_paper = True - self.outbound_mode_propagation = False - else: - self.outbound_mode_propagation = True + if self.outbound_mode_paper: self.outbound_mode_paper = False + self.outbound_mode_propagation = False + self.outbound_mode_command = True + else: + if self.outbound_mode_propagation: + self.outbound_mode_paper = True + self.outbound_mode_propagation = False + self.outbound_mode_command = False + else: + self.outbound_mode_propagation = True + self.outbound_mode_paper = False + self.outbound_mode_command = False self.update_message_widgets() @@ -1138,13 +1168,17 @@ class SidebandApp(MDApp): mode_item.icon = "qrcode" self.messages_view.ids.message_text.hint_text = "Paper message" else: - if not self.outbound_mode_propagation: - mode_item.icon = "lan-connect" - self.messages_view.ids.message_text.hint_text = "Message for direct delivery" + if self.outbound_mode_command: + mode_item.icon = "console" + self.messages_view.ids.message_text.hint_text = "Send command or request" else: - mode_item.icon = "upload-network" - self.messages_view.ids.message_text.hint_text = "Message for propagation" - # self.root.ids.message_text.hint_text = "Write message for delivery via propagation nodes" + if not self.outbound_mode_propagation: + mode_item.icon = "lan-connect" + self.messages_view.ids.message_text.hint_text = "Message for direct delivery" + else: + mode_item.icon = "upload-network" + self.messages_view.ids.message_text.hint_text = "Message for propagation" + # self.root.ids.message_text.hint_text = "Write message for delivery via propagation nodes" def key_query_action(self, sender): context_dest = self.messages_view.ids.messages_scrollview.active_conversation diff --git a/sbapp/sideband/core.py b/sbapp/sideband/core.py index 6fd4a0e..cf09b34 100644 --- a/sbapp/sideband/core.py +++ b/sbapp/sideband/core.py @@ -1976,7 +1976,7 @@ class SidebandCore(): messages = messages[-limit:] return messages - def _db_save_lxm(self, lxm, context_dest, originator = False): + def _db_save_lxm(self, lxm, context_dest, originator = False, own_command = False): state = lxm.state packed_telemetry = None @@ -1998,7 +1998,7 @@ class SidebandCore(): # TODO: Implement pass - if len(lxm.content) != 0 or len(lxm.title) != 0: + if own_command or len(lxm.content) != 0 or len(lxm.title) != 0: db = self.__db_connect() dbc = db.cursor() @@ -2977,7 +2977,7 @@ class SidebandCore(): RNS.log("Error while creating paper message: "+str(e), RNS.LOG_ERROR) return False - def send_message(self, content, destination_hash, propagation): + def send_message(self, content, destination_hash, propagation, skip_fields=False): try: if content == "": raise ValueError("Message content cannot be empty") @@ -2991,7 +2991,54 @@ class SidebandCore(): else: desired_method = LXMF.LXMessage.DIRECT - lxm = LXMF.LXMessage(dest, source, content, title="", desired_method=desired_method, fields = self.get_message_fields(destination_hash)) + if skip_fields: + fields = {} + else: + fields = self.get_message_fields(destination_hash) + + lxm = LXMF.LXMessage(dest, source, content, title="", desired_method=desired_method, fields = fields) + 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 send_command(self, content, destination_hash, propagation): + try: + if content == "": + return False + + 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}) + + 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: + desired_method = LXMF.LXMessage.DIRECT + + lxm = LXMF.LXMessage(dest, source, "", title="", desired_method=desired_method, fields = {LXMF.FIELD_COMMANDS: commands}) lxm.register_delivery_callback(self.message_notification) lxm.register_failed_callback(self.message_notification) @@ -3060,6 +3107,7 @@ class SidebandCore(): should_notify = False is_trusted = False telemetry_only = False + own_command = False unread_reason_tx = False if originator: @@ -3069,18 +3117,21 @@ class SidebandCore(): context_dest = message.source_hash is_trusted = self.is_trusted(context_dest) + if originator and LXMF.FIELD_COMMANDS in message.fields: + own_command = True + if self._db_message(message.hash): RNS.log("Message exists, setting state to: "+str(message.state), RNS.LOG_DEBUG) self._db_message_set_state(message.hash, message.state) else: RNS.log("Message does not exist, saving", RNS.LOG_DEBUG) - self._db_save_lxm(message, context_dest, originator) + self._db_save_lxm(message, context_dest, originator, own_command=own_command) if is_trusted: should_notify = True if len(message.content) == 0 and len(message.title) == 0: - if (LXMF.FIELD_TELEMETRY in message.fields or LXMF.FIELD_TELEMETRY_STREAM in message.fields): + if (LXMF.FIELD_TELEMETRY in message.fields or LXMF.FIELD_TELEMETRY_STREAM in message.fields or LXMF.FIELD_COMMANDS in message.fields): RNS.log("Squelching notification due to telemetry-only message", RNS.LOG_DEBUG) telemetry_only = True @@ -3236,16 +3287,57 @@ class SidebandCore(): RNS.log("LXMF delivery "+str(time_string)+". "+str(signature_string)+".", RNS.LOG_DEBUG) try: + context_dest = message.source_hash if self.config["lxmf_ignore_unknown"] == True: - context_dest = message.source_hash if self._db_conversation(context_dest) == None: RNS.log("Dropping message from unknown sender "+RNS.prettyhexrep(context_dest), RNS.LOG_DEBUG) return - self.lxm_ingest(message) + if message.signature_validated and LXMF.FIELD_COMMANDS in message.fields: + if self.requests_allowed_from(context_dest): + commands = message.fields[LXMF.FIELD_COMMANDS] + self.handle_commands(commands, message) + + else: + self.lxm_ingest(message) + except Exception as e: RNS.log("Error while ingesting LXMF message "+RNS.prettyhexrep(message.hash)+" to database: "+str(e)) + def handle_commands(self, commands, message): + try: + context_dest = message.source_hash + RNS.log("Handling commands from "+str(context_dest), RNS.LOG_DEBUG) + for command in commands: + if Commands.TELEMETRY_REQUEST in command: + timebase = int(command[Commands.TELEMETRY_REQUEST]) + RNS.log("Handling telemetry request with timebase "+str(timebase), RNS.LOG_DEBUG) + + elif Commands.ECHO in command: + msg_content = "Echo reply: "+command[Commands.ECHO].decode("utf-8") + RNS.log("Handling echo request", RNS.LOG_DEBUG) + self.send_message(msg_content, context_dest, False, skip_fields=True) + + elif Commands.SIGNAL_REPORT in command: + RNS.log("Handling signal report", RNS.LOG_DEBUG) + phy_str = "" + if message.q != None: + phy_str += f"Link Quality: {message.q}%\n" + if message.rssi != None: + phy_str += f"RSSI: {message.rssi} dBm\n" + if message.snr != None: + phy_str += f"SNR: {message.rssi} dB\n" + if len(phy_str) != 0: + phy_str = phy_str[:-1] + else: + phy_str = "No reception info available" + + self.send_message(phy_str, context_dest, False, skip_fields=True) + + except Exception as e: + RNS.log("Error while handling commands: "+str(e), RNS.LOG_ERROR) + + def get_display_name_bytes(self): return self.config["display_name"].encode("utf-8") diff --git a/sbapp/sideband/sense.py b/sbapp/sideband/sense.py index 431c919..33f7887 100644 --- a/sbapp/sideband/sense.py +++ b/sbapp/sideband/sense.py @@ -10,6 +10,8 @@ from .geo import azalt, angle_to_horizon, radio_horizon, shared_radio_horizon class Commands(): TELEMETRY_REQUEST = 0x01 + ECHO = 0x02 + SIGNAL_REPORT = 0x03 class Telemeter(): @staticmethod diff --git a/sbapp/ui/messages.py b/sbapp/ui/messages.py index d9b9f38..7d969d7 100644 --- a/sbapp/ui/messages.py +++ b/sbapp/ui/messages.py @@ -22,11 +22,11 @@ import subprocess import shlex if RNS.vendor.platformutils.get_platform() == "android": - from sideband.sense import Telemeter + from sideband.sense import Telemeter, Commands from ui.helpers import ts_format, file_ts_format, mdc from ui.helpers import color_received, color_delivered, color_propagated, color_paper, color_failed, color_unknown, intensity_msgs_dark, intensity_msgs_light else: - from sbapp.sideband.sense import Telemeter + from sbapp.sideband.sense import Telemeter, Commands from .helpers import ts_format, file_ts_format, mdc from .helpers import color_received, color_delivered, color_propagated, color_paper, color_failed, color_unknown, intensity_msgs_dark, intensity_msgs_light @@ -178,14 +178,29 @@ class Messages(): txstr = time.strftime(ts_format, time.localtime(m["sent"])) rxstr = time.strftime(ts_format, time.localtime(m["received"])) titlestr = "" + extra_content = "" extra_telemetry = {} telemeter = None + signature_valid = False + + if "lxm" in m and m["lxm"] != None and m["lxm"].signature_validated: + signature_valid = True + if "extras" in m and m["extras"] != None and "packed_telemetry" in m["extras"]: try: telemeter = Telemeter.from_packed(m["extras"]["packed_telemetry"]) except Exception as e: pass + if "lxm" in m and m["lxm"] != None and m["lxm"].fields != None and LXMF.FIELD_COMMANDS in m["lxm"].fields: + commands = m["lxm"].fields[LXMF.FIELD_COMMANDS] + for command in commands: + if Commands.ECHO in command: + extra_content = "[font=RobotoMono-Regular]> echo "+command[Commands.ECHO].decode("utf-8")+"[/font]\n" + if Commands.SIGNAL_REPORT in command: + extra_content = "[font=RobotoMono-Regular]> sig[/font]\n" + extra_content = extra_content[:-1] + if telemeter == None and "lxm" in m and m["lxm"] and m["lxm"].fields != None and LXMF.FIELD_TELEMETRY in m["lxm"].fields: try: packed_telemetry = m["lxm"].fields[LXMF.FIELD_TELEMETRY] @@ -272,8 +287,12 @@ class Messages(): if rcvd_d_str != "": heading_str += rcvd_d_str + pre_content = "" + if not signature_valid: + pre_content += "[b]Warning![/b] The signature for this message could not be validated. Check that you have received an announce from this sender. If you already have, or other messages from the sender do not display this warning, [b]this message is likely to be fake[/b].\n\n" + item = ListLXMessageCard( - text=m["content"].decode("utf-8"), + text=pre_content+m["content"].decode("utf-8")+extra_content, heading=heading_str, md_bg_color=msg_color, )