From ced7e881b9f1b1d5ca7c06a383ca3e75732a3f09 Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Mon, 25 Mar 2024 00:58:58 +0100 Subject: [PATCH] Added plugin functionality --- sbapp/main.py | 216 ++++++++++++++++++++++++++++++++++++++-- sbapp/sideband/core.py | 84 ++++++++++++++++ sbapp/sideband/sense.py | 1 + sbapp/ui/layouts.py | 87 ++++++++++++++++ sbapp/ui/messages.py | 3 + 5 files changed, 384 insertions(+), 7 deletions(-) diff --git a/sbapp/main.py b/sbapp/main.py index a36eb51..26a1ad7 100644 --- a/sbapp/main.py +++ b/sbapp/main.py @@ -1372,14 +1372,32 @@ class SidebandApp(MDApp): self.file_manager.show(path) except Exception as e: - self.sideband.config["map_storage_path"] = None - self.sideband.save_configuration() - toast("Error reading directory, check permissions!") + if RNS.vendor.platformutils.get_platform() == "android": + toast("Error reading directory, check permissions!") + else: + ok_button = MDRectangleFlatButton(text="OK",font_size=dp(18)) + ate_dialog = MDDialog( + title="Attachment Error", + text="Error reading directory, check permissions!", + buttons=[ ok_button ], + ) + ok_button.bind(on_release=ate_dialog.dismiss) + ate_dialog.open() else: self.sideband.config["map_storage_path"] = None self.sideband.save_configuration() - toast("No file access, check permissions!") + if RNS.vendor.platformutils.get_platform() == "android": + toast("No file access, check permissions!") + else: + ok_button = MDRectangleFlatButton(text="OK",font_size=dp(18)) + ate_dialog = MDDialog( + title="Attachment Error", + text="No file access, check permissions!", + buttons=[ ok_button ], + ) + ok_button.bind(on_release=ate_dialog.dismiss) + ate_dialog.open() def message_attach_action(self, attach_type=None): self.attach_path = None @@ -3781,6 +3799,158 @@ class SidebandApp(MDApp): c_dialog.open() + ### Plugins & Services screen + ###################################### + + def plugins_action(self, sender=None, direction="left"): + if self.root.ids.screen_manager.has_screen("plugins_screen"): + self.plugins_open(direction=direction) + else: + self.loader_action(direction=direction) + def final(dt): + self.plugins_init() + def o(dt): + self.plugins_open(no_transition=True) + Clock.schedule_once(o, ll_ot) + Clock.schedule_once(final, ll_ft) + + def plugins_init(self): + if not self.root.ids.screen_manager.has_screen("plugins_screen"): + self.plugins_screen = Builder.load_string(layout_plugins_screen) + self.plugins_screen.app = self + self.root.ids.screen_manager.add_widget(self.plugins_screen) + self.bind_clipboard_actions(self.plugins_screen.ids) + + self.plugins_screen.ids.plugins_scrollview.effect_cls = ScrollEffect + info = "You can extend Sideband functionality with command and service plugins. This lets you to add your own custom functionality, or add community-developed features.\n\n" + info += "[b]Take extreme caution![/b]\nIf you add a plugin that you did not write yourself, make [b]absolutely[/b] sure you know what it is doing! Loaded plugins have full access to your Sideband application, and should only be added if you are completely certain they are trustworthy.\n\n" + info += "Command plugins allow you to define custom commands that can be carried out in response to LXMF command messages, and they can respond with any kind of information or data to the requestor (or to any LXMF address).\n\n" + info += "By using service plugins, you can start additional services or programs within the Sideband application context, that other plugins (or Sideband itself) can interact with." + info += "Restart Sideband for changes to these settings to take effect." + self.plugins_screen.ids.plugins_info.text = info + + self.plugins_screen.ids.settings_command_plugins_enabled.active = self.sideband.config["command_plugins_enabled"] + self.plugins_screen.ids.settings_service_plugins_enabled.active = self.sideband.config["service_plugins_enabled"] + + def plugins_settings_save(sender=None, event=None): + self.sideband.config["command_plugins_enabled"] = self.plugins_screen.ids.settings_command_plugins_enabled.active + self.sideband.config["service_plugins_enabled"] = self.plugins_screen.ids.settings_service_plugins_enabled.active + self.sideband.save_configuration() + + self.plugins_screen.ids.settings_command_plugins_enabled.bind(active=plugins_settings_save) + self.plugins_screen.ids.settings_service_plugins_enabled.bind(active=plugins_settings_save) + + def plugins_open(self, sender=None, direction="left", no_transition=False): + if no_transition: + self.root.ids.screen_manager.transition = self.no_transition + else: + self.root.ids.screen_manager.transition = self.slide_transition + self.root.ids.screen_manager.transition.direction = direction + + self.root.ids.screen_manager.transition.direction = "left" + self.root.ids.screen_manager.current = "plugins_screen" + self.root.ids.nav_drawer.set_state("closed") + self.sideband.setstate("app.displaying", self.root.ids.screen_manager.current) + + if no_transition: + self.root.ids.screen_manager.transition = self.slide_transition + + def close_plugins_action(self, sender=None): + self.open_conversations(direction="right") + + def plugins_fm_got_path(self, path): + self.plugins_fm_exited() + try: + if os.path.isdir(path): + self.sideband.config["command_plugins_path"] = path + self.sideband.save_configuration() + + if RNS.vendor.platformutils.is_android(): + toast("Using \""+str(path)+"\" as plugin directory") + else: + ok_button = MDRectangleFlatButton(text="OK",font_size=dp(18)) + ate_dialog = MDDialog( + title="Directory Set", + text="Using \""+str(path)+"\" as plugin directory", + buttons=[ ok_button ], + ) + ok_button.bind(on_release=ate_dialog.dismiss) + ate_dialog.open() + + except Exception as e: + RNS.log(f"Error while setting plugins directory to \"{path}\": "+str(e), RNS.LOG_ERROR) + if RNS.vendor.platformutils.get_platform() == "android": + toast("Could not set plugins directory to \""+str(path)+"\"") + else: + ok_button = MDRectangleFlatButton(text="OK",font_size=dp(18)) + e_dialog = MDDialog( + title="Error", + text="Could not set plugins directory to \""+str(path)+"\"", + buttons=[ ok_button ], + ) + ok_button.bind(on_release=e_dialog.dismiss) + e_dialog.open() + + def plugins_fm_exited(self, *args): + self.manager_open = False + self.file_manager.close() + + def plugins_select_directory_action(self, sender=None): + perm_ok = False + if self.sideband.config["command_plugins_path"] == None: + if RNS.vendor.platformutils.is_android(): + perm_ok = self.check_storage_permission() + path = primary_external_storage_path() + + else: + perm_ok = True + path = os.path.expanduser("~") + + else: + perm_ok = True + path = self.sideband.config["command_plugins_path"] + + if perm_ok and path != None: + try: + self.file_manager = MDFileManager( + exit_manager=self.plugins_fm_exited, + select_path=self.plugins_fm_got_path, + ) + + self.file_manager.show(path) + + except Exception as e: + self.sideband.config["command_plugins_path"] = None + self.sideband.save_configuration() + + if RNS.vendor.platformutils.is_android(): + toast("Error reading directory, check permissions!") + else: + ok_button = MDRectangleFlatButton(text="OK",font_size=dp(18)) + ate_dialog = MDDialog( + title="Error", + text="Could not read directory, check permissions!", + buttons=[ ok_button ], + ) + ok_button.bind(on_release=ate_dialog.dismiss) + ate_dialog.open() + + else: + self.sideband.config["command_plugins_path"] = None + self.sideband.save_configuration() + if RNS.vendor.platformutils.is_android(): + toast("No file access, check permissions!") + else: + ok_button = MDRectangleFlatButton(text="OK",font_size=dp(18)) + ate_dialog = MDDialog( + title="Error", + text="No file access, check permissions!", + buttons=[ ok_button ], + ) + ok_button.bind(on_release=ate_dialog.dismiss) + ate_dialog.open() + + ### Telemetry Screen ###################################### @@ -3929,7 +4099,19 @@ class SidebandApp(MDApp): self.sideband.config["map_storage_file"] = path self.sideband.config["map_storage_path"] = str(pathlib.Path(path).parent.resolve()) self.sideband.save_configuration() - toast("Using \""+os.path.basename(path)+"\" as offline map") + + if RNS.vendor.platformutils.is_android(): + toast("Using \""+os.path.basename(path)+"\" as offline map") + else: + ok_button = MDRectangleFlatButton(text="OK",font_size=dp(18)) + ate_dialog = MDDialog( + title="Map Set", + text="Using \""+os.path.basename(path)+"\" as offline map", + buttons=[ ok_button ], + ) + ok_button.bind(on_release=ate_dialog.dismiss) + ate_dialog.open() + except Exception as e: RNS.log(f"Error while loading map \"{path}\": "+str(e), RNS.LOG_ERROR) if RNS.vendor.platformutils.get_platform() == "android": @@ -3988,12 +4170,32 @@ class SidebandApp(MDApp): except Exception as e: self.sideband.config["map_storage_path"] = None self.sideband.save_configuration() - toast("Error reading directory, check permissions!") + if RNS.vendor.platformutils.is_android(): + toast("Error reading directory, check permissions!") + else: + ok_button = MDRectangleFlatButton(text="OK",font_size=dp(18)) + ate_dialog = MDDialog( + title="Error", + text="Could not read directory, check permissions!", + buttons=[ ok_button ], + ) + ok_button.bind(on_release=ate_dialog.dismiss) + ate_dialog.open() else: self.sideband.config["map_storage_path"] = None self.sideband.save_configuration() - toast("No file access, check permissions!") + if RNS.vendor.platformutils.is_android(): + toast("No file access, check permissions!") + else: + ok_button = MDRectangleFlatButton(text="OK",font_size=dp(18)) + ate_dialog = MDDialog( + title="Error", + text="No file access, check permissions!", + buttons=[ ok_button ], + ) + ok_button.bind(on_release=ate_dialog.dismiss) + ate_dialog.open() def map_get_offline_source(self): if self.offline_source != None: diff --git a/sbapp/sideband/core.py b/sbapp/sideband/core.py index 5d06fc3..75653f1 100644 --- a/sbapp/sideband/core.py +++ b/sbapp/sideband/core.py @@ -7,6 +7,7 @@ import time import struct import sqlite3 import random +import shlex import RNS.vendor.umsgpack as msgpack import RNS.Interfaces.Interface as Interface @@ -16,6 +17,7 @@ import multiprocessing.connection from threading import Lock from .res import sideband_fb_data from .sense import Telemeter, Commands +from .plugins import SidebandCommandPlugin, SidebandServicePlugin if RNS.vendor.platformutils.get_platform() == "android": from jnius import autoclass, cast @@ -243,6 +245,11 @@ class SidebandCore(): RNS.Transport.register_announce_handler(self) RNS.Transport.register_announce_handler(self.propagation_detector) + self.active_command_plugins = {} + self.active_service_plugins = {} + if self.is_service or self.is_standalone: + self.__load_plugins() + def clear_tmp_dir(self): if os.path.isdir(self.tmp_dir): for file in os.listdir(self.tmp_dir): @@ -591,6 +598,13 @@ class SidebandCore(): if not "telemetry_s_information_text" in self.config: self.config["telemetry_s_information_text"] = "" + if not "service_plugins_enabled" in self.config: + self.config["service_plugins_enabled"] = False + if not "command_plugins_enabled" in self.config: + self.config["command_plugins_enabled"] = False + if not "command_plugins_path" in self.config: + self.config["command_plugins_path"] = None + if not "map_history_limit" in self.config: self.config["map_history_limit"] = 7*24*60*60 if not "map_lat" in self.config: @@ -656,6 +670,58 @@ class SidebandCore(): if self.is_client: self.setstate("wants.settings_reload", True) + def __load_plugins(self): + plugins_path = self.config["command_plugins_path"] + command_plugins_enabled = self.config["command_plugins_enabled"] == True + service_plugins_enabled = self.config["service_plugins_enabled"] == True + plugins_enabled = service_plugins_enabled + + if plugins_enabled: + if plugins_path != None: + if os.path.isdir(plugins_path): + for file in os.listdir(plugins_path): + if file.lower().endswith(".py"): + plugin_globals = {} + plugin_globals["SidebandServicePlugin"] = SidebandServicePlugin + plugin_globals["SidebandCommandPlugin"] = SidebandCommandPlugin + RNS.log("Loading plugin \""+str(file)+"\"", RNS.LOG_NOTICE) + plugin_path = os.path.join(plugins_path, file) + exec(open(plugin_path).read(), plugin_globals) + plugin_class = plugin_globals["plugin_class"] + + if plugin_class != None: + plugin = plugin_class(self) + plugin.start() + + if plugin.is_running(): + if issubclass(type(plugin), SidebandCommandPlugin) and command_plugins_enabled: + command_name = plugin.command_name + if not command_name in self.active_command_plugins: + self.active_command_plugins[command_name] = plugin + RNS.log("Registered "+str(plugin)+" as handler for command \""+str(command_name)+"\"", RNS.LOG_NOTICE) + else: + RNS.log("Could not register "+str(plugin)+" as handler for command \""+str(command_name)+"\". Command name was already registered", RNS.LOG_ERROR) + + elif issubclass(type(plugin), SidebandServicePlugin): + service_name = plugin.service_name + if not service_name in self.active_service_plugins: + self.active_service_plugins[service_name] = plugin + RNS.log("Registered "+str(plugin)+" for service \""+str(service_name)+"\"", RNS.LOG_NOTICE) + else: + RNS.log("Could not register "+str(plugin)+" for service \""+str(service_name)+"\". Service name was already registered", RNS.LOG_ERROR) + try: + plugin.stop() + except Exception as e: + pass + del plugin + + else: + RNS.log("Unknown plugin type was loaded, ignoring it.", RNS.LOG_ERROR) + else: + RNS.log("Plugin "+str(plugin)+" failed to start, ignoring it.", RNS.LOG_ERROR) + del plugin + + def reload_configuration(self): self.__reload_config() @@ -3325,6 +3391,8 @@ class SidebandCore(): 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 @@ -3608,6 +3676,19 @@ class SidebandCore(): except Exception as e: RNS.log("Error while ingesting LXMF message "+RNS.prettyhexrep(message.hash)+" to database: "+str(e), RNS.LOG_ERROR) + def handle_plugin_command(self, command_string, message): + try: + call = shlex.split(command_string) + command = call[0] + arguments = call[1:] + if command in self.active_command_plugins: + RNS.log("Handling command \""+str(command)+"\" via command plugin "+str(self.active_command_plugins[command]), RNS.LOG_DEBUG) + self.active_command_plugins[command].handle_command(arguments, message) + + except Exception as e: + RNS.log("An error occurred while handling a plugin command. The contained exception was: "+str(e), RNS.LOG_ERROR) + RNS.trace_exception(e) + def handle_commands(self, commands, message): try: context_dest = message.source_hash @@ -3648,6 +3729,9 @@ class SidebandCore(): self.send_message(phy_str, context_dest, False, skip_fields=True, no_display=True) + elif self.config["command_plugins_enabled"] and Commands.PLUGIN_COMMAND in command: + self.handle_plugin_command(command[Commands.PLUGIN_COMMAND], message) + except Exception as e: RNS.log("Error while handling commands: "+str(e), RNS.LOG_ERROR) diff --git a/sbapp/sideband/sense.py b/sbapp/sideband/sense.py index d2c3d33..471613c 100644 --- a/sbapp/sideband/sense.py +++ b/sbapp/sideband/sense.py @@ -9,6 +9,7 @@ from .geo import orthodromic_distance, euclidian_distance from .geo import azalt, angle_to_horizon, radio_horizon, shared_radio_horizon class Commands(): + PLUGIN_COMMAND = 0x00 TELEMETRY_REQUEST = 0x01 PING = 0x02 ECHO = 0x03 diff --git a/sbapp/ui/layouts.py b/sbapp/ui/layouts.py index 877486e..fe0c82b 100644 --- a/sbapp/ui/layouts.py +++ b/sbapp/ui/layouts.py @@ -133,6 +133,15 @@ MDNavigationLayout: on_release: root.ids.screen_manager.app.keys_action(self) + OneLineIconListItem: + text: "Plugins" + on_release: root.ids.screen_manager.app.plugins_action(self) + + IconLeftWidget: + icon: "google-circles-extended" + on_release: root.ids.screen_manager.app.keys_action(self) + + OneLineIconListItem: text: "Guide" on_release: root.ids.screen_manager.app.guide_action(self) @@ -1125,6 +1134,84 @@ MDScreen: on_release: root.app.identity_restore_action(self) """ +layout_plugins_screen = """ +MDScreen: + name: "plugins_screen" + + BoxLayout: + orientation: "vertical" + + MDTopAppBar: + title: "Plugins & Services" + anchor_title: "left" + elevation: 0 + left_action_items: + [['menu', lambda x: root.app.nav_drawer.set_state("open")]] + right_action_items: + [ + ['close', lambda x: root.app.close_plugins_action(self)], + ] + + ScrollView: + id:plugins_scrollview + + MDBoxLayout: + orientation: "vertical" + spacing: "24dp" + size_hint_y: None + height: self.minimum_height + padding: [dp(35), dp(35), dp(35), dp(35)] + + + MDLabel: + id: plugins_info + markup: True + text: "" + size_hint_y: None + text_size: self.width, None + height: self.texture_size[1] + + MDBoxLayout: + orientation: "horizontal" + size_hint_y: None + padding: [0,0,dp(26),dp(0)] + height: dp(24) + + MDLabel: + text: "Enable Plugins" + font_style: "H6" + + MDSwitch: + id: settings_service_plugins_enabled + pos_hint: {"center_y": 0.3} + active: False + + MDBoxLayout: + orientation: "horizontal" + size_hint_y: None + padding: [0,0,dp(26),dp(0)] + height: dp(24) + + MDLabel: + text: "Enable Command Plugins" + font_style: "H6" + + MDSwitch: + id: settings_command_plugins_enabled + pos_hint: {"center_y": 0.3} + active: False + + MDRectangleFlatIconButton: + id: plugins_display + icon: "folder-cog-outline" + text: "Select Plugins Directory" + padding: [dp(0), dp(14), dp(0), dp(14)] + icon_size: dp(24) + font_size: dp(16) + size_hint: [1.0, None] + on_release: root.app.plugins_select_directory_action(self) +""" + layout_settings_screen = """ MDScreen: name: "settings_screen" diff --git a/sbapp/ui/messages.py b/sbapp/ui/messages.py index 340d28e..51cd727 100644 --- a/sbapp/ui/messages.py +++ b/sbapp/ui/messages.py @@ -254,6 +254,9 @@ class Messages(): extra_content = "[font=RobotoMono-Regular]> ping[/font]\n" if Commands.SIGNAL_REPORT in command: extra_content = "[font=RobotoMono-Regular]> sig[/font]\n" + if Commands.PLUGIN_COMMAND in command: + cmd_content = command[Commands.PLUGIN_COMMAND] + extra_content = "[font=RobotoMono-Regular]> "+str(cmd_content)+"[/font]\n" extra_content = extra_content[:-1] force_markup = True except Exception as e: