# coding=utf-8 from __future__ import absolute_import import octoprint.plugin from .tailscale_interface import TailscaleInterface, TailscaleError, TailscaleNotInstalledError, TailscaleNotRunningError from .status_monitor import StatusMonitor class TailscaleFunnelPlugin(octoprint.plugin.StartupPlugin, octoprint.plugin.SettingsPlugin, octoprint.plugin.AssetPlugin, octoprint.plugin.TemplatePlugin, octoprint.plugin.BlueprintPlugin): def __init__(self): self._logger = None self.tailscale_interface = None self.status_monitor = None ##~~ StartupPlugin mixin def on_after_startup(self): self._logger = self._plugin_manager.get_logger("octoprint_tailscale_funnel") self._logger.info("Tailscale Funnel Plugin started") self.tailscale_interface = TailscaleInterface(self._logger) self.status_monitor = StatusMonitor(self) self.status_monitor.start() def on_shutdown(self): if self.status_monitor: self.status_monitor.stop() ##~~ SettingsPlugin mixin def get_settings_defaults(self): return dict( enabled=False, port=80, confirm_enable=True ) def get_settings_version(self): return 1 def on_settings_migrate(self, target, current): if current is None: # Migrate from settings version 0 to 1 if self._settings.has(["funnel_enabled"]): self._settings.set_boolean(["enabled"], self._settings.get_boolean(["funnel_enabled"])) self._settings.remove(["funnel_enabled"]) def on_settings_save(self, data): # Save the settings octoprint.plugin.SettingsPlugin.on_settings_save(self, data) # If enabled setting changed, update funnel status if "enabled" in data: enabled = self._settings.get_boolean(["enabled"]) port = self._settings.get_int(["port"]) try: if enabled: # Enable funnel if self.tailscale_interface: success = self.tailscale_interface.enable_funnel(port) if success: self._logger.info("Tailscale Funnel enabled on port {}".format(port)) else: self._logger.error("Failed to enable Tailscale Funnel on port {}".format(port)) else: # Disable funnel if self.tailscale_interface: success = self.tailscale_interface.disable_funnel(port) if success: self._logger.info("Tailscale Funnel disabled on port {}".format(port)) else: self._logger.error("Failed to disable Tailscale Funnel on port {}".format(port)) except TailscaleError as e: self._logger.error("Tailscale error: {}".format(str(e))) except Exception as e: self._logger.error("Unexpected error: {}".format(str(e))) ##~~ AssetPlugin mixin def get_assets(self): return dict( js=["js/tailscale_funnel.js"], css=["css/tailscale_funnel.css"], less=["less/tailscale_funnel.less"] ) ##~~ TemplatePlugin mixin def get_template_configs(self): return [ dict(type="settings", custom_bindings=False) ] ##~~ BlueprintPlugin mixin @octoprint.plugin.BlueprintPlugin.route("/status", methods=["GET"]) def get_status(self): if not self.tailscale_interface: return {"status": "error", "message": "Tailscale interface not initialized"}, 500 try: # Check if tailscale is installed if not self.tailscale_interface.is_tailscale_installed(): return {"status": "error", "message": "Tailscale is not installed"}, 500 # Check if tailscale is running if not self.tailscale_interface.is_tailscale_running(): return {"status": "error", "message": "Tailscale is not running"}, 500 # Get funnel status funnel_enabled = self.tailscale_interface.is_funnel_enabled() public_url = self.tailscale_interface.get_public_url() if funnel_enabled else None return { "status": "success", "data": { "funnel_enabled": funnel_enabled, "public_url": public_url, "tailscale_installed": True, "tailscale_running": True } } except TailscaleNotInstalledError as e: return {"status": "error", "message": "Tailscale is not installed: {}".format(str(e))}, 500 except TailscaleNotRunningError as e: return {"status": "error", "message": "Tailscale is not running: {}".format(str(e))}, 500 except TailscaleError as e: return {"status": "error", "message": "Tailscale error: {}".format(str(e))}, 500 except Exception as e: self._logger.error("Unexpected error in get_status: {}".format(str(e))) return {"status": "error", "message": "Unexpected error: {}".format(str(e))}, 500 @octoprint.plugin.BlueprintPlugin.route("/enable", methods=["POST"]) def enable_funnel(self): if not self.tailscale_interface: return {"status": "error", "message": "Tailscale interface not initialized"}, 500 try: port = self._settings.get_int(["port"]) success = self.tailscale_interface.enable_funnel(port) if success: # Update settings self._settings.set_boolean(["enabled"], True) self._settings.save() public_url = self.tailscale_interface.get_public_url() return { "status": "success", "message": "Tailscale Funnel enabled successfully", "data": { "public_url": public_url } } else: return {"status": "error", "message": "Failed to enable Tailscale Funnel"}, 500 except TailscaleNotInstalledError as e: return {"status": "error", "message": "Tailscale is not installed: {}".format(str(e))}, 500 except TailscaleError as e: return {"status": "error", "message": "Tailscale error: {}".format(str(e))}, 500 except Exception as e: self._logger.error("Unexpected error in enable_funnel: {}".format(str(e))) return {"status": "error", "message": "Unexpected error: {}".format(str(e))}, 500 @octoprint.plugin.BlueprintPlugin.route("/disable", methods=["POST"]) def disable_funnel(self): if not self.tailscale_interface: return {"status": "error", "message": "Tailscale interface not initialized"}, 500 try: port = self._settings.get_int(["port"]) success = self.tailscale_interface.disable_funnel(port) if success: # Update settings self._settings.set_boolean(["enabled"], False) self._settings.save() return { "status": "success", "message": "Tailscale Funnel disabled successfully" } else: return {"status": "error", "message": "Failed to disable Tailscale Funnel"}, 500 except TailscaleNotInstalledError as e: return {"status": "error", "message": "Tailscale is not installed: {}".format(str(e))}, 500 except TailscaleError as e: return {"status": "error", "message": "Tailscale error: {}".format(str(e))}, 500 except Exception as e: self._logger.error("Unexpected error in disable_funnel: {}".format(str(e))) return {"status": "error", "message": "Unexpected error: {}".format(str(e))}, 500 ##~~ Softwareupdate hook def get_update_information(self): return dict( tailscale_funnel=dict( displayName="Tailscale Funnel Plugin", displayVersion=self._plugin_version, # version check: github repository type="github_release", user="markus", repo="OctoPrint-Tailscale-Funnel", current=self._plugin_version, # update method: pip pip="https://github.com/markus/OctoPrint-Tailscale-Funnel/archive/{target_version}.zip" ) ) # If you want your plugin to be registered within OctoPrint under its own name, # define it here. __plugin_name__ = "Tailscale Funnel Plugin" __plugin_pythoncompat__ = ">=3.7,<4" # python 3.7+ def __plugin_load__(): global __plugin_implementation__ __plugin_implementation__ = TailscaleFunnelPlugin() global __plugin_hooks__ __plugin_hooks__ = { "octoprint.plugin.softwareupdate.check_config": __plugin_implementation__.get_update_information }