Added plugin functionality

This commit is contained in:
Mark Qvist 2024-03-25 00:58:58 +01:00
parent 02805290b0
commit ced7e881b9
5 changed files with 384 additions and 7 deletions

View File

@ -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:

View File

@ -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)

View File

@ -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

View File

@ -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"

View File

@ -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: