diff options
| author | Jonas Dellinger <jonas@dellinger.dev> | 2022-05-13 19:14:47 +0200 |
|---|---|---|
| committer | Jonas Dellinger <jonas@dellinger.dev> | 2022-05-13 19:14:47 +0200 |
| commit | 74438a31458af8bddd08d90eacc6d63677bab844 (patch) | |
| tree | a7bfc044941f65c7f9971c5386c463eac31be768 /plugin_loader | |
| parent | 945db5de4788feefebc845817752472419051640 (diff) | |
| download | decky-loader-74438a31458af8bddd08d90eacc6d63677bab844.tar.gz decky-loader-74438a31458af8bddd08d90eacc6d63677bab844.zip | |
Work on react frontend loader
Diffstat (limited to 'plugin_loader')
| -rw-r--r-- | plugin_loader/browser.py | 89 | ||||
| -rw-r--r-- | plugin_loader/injector.py | 97 | ||||
| -rw-r--r-- | plugin_loader/loader.py | 191 | ||||
| -rw-r--r-- | plugin_loader/main.py | 144 | ||||
| -rw-r--r-- | plugin_loader/plugin.py | 104 | ||||
| -rw-r--r-- | plugin_loader/static/library.js | 71 | ||||
| -rw-r--r-- | plugin_loader/static/plugin_page.js | 98 | ||||
| -rw-r--r-- | plugin_loader/static/styles.css | 3 | ||||
| -rw-r--r-- | plugin_loader/templates/plugin_view.html | 76 | ||||
| -rw-r--r-- | plugin_loader/utilities.py | 106 |
10 files changed, 0 insertions, 979 deletions
diff --git a/plugin_loader/browser.py b/plugin_loader/browser.py deleted file mode 100644 index ffec26b3..00000000 --- a/plugin_loader/browser.py +++ /dev/null @@ -1,89 +0,0 @@ -from injector import get_tab -from logging import getLogger -from os import path, rename -from shutil import rmtree -from aiohttp import ClientSession, web -from io import BytesIO -from zipfile import ZipFile -from concurrent.futures import ProcessPoolExecutor -from asyncio import get_event_loop -from time import time -from hashlib import sha256 -from subprocess import Popen - -class PluginInstallContext: - def __init__(self, gh_url, version, hash) -> None: - self.gh_url = gh_url - self.version = version - self.hash = hash - -class PluginBrowser: - def __init__(self, plugin_path, server_instance, store_url) -> None: - self.log = getLogger("browser") - self.plugin_path = plugin_path - self.store_url = store_url - self.install_requests = {} - - server_instance.add_routes([ - web.post("/browser/install_plugin", self.install_plugin), - web.get("/browser/iframe", self.redirect_to_store) - ]) - - def _unzip_to_plugin_dir(self, zip, name, hash): - zip_hash = sha256(zip.getbuffer()).hexdigest() - if zip_hash != hash: - return False - zip_file = ZipFile(zip) - zip_file.extractall(self.plugin_path) - rename(path.join(self.plugin_path, zip_file.namelist()[0]), path.join(self.plugin_path, name)) - Popen(["chown", "-R", "deck:deck", self.plugin_path]) - Popen(["chmod", "-R", "555", self.plugin_path]) - return True - - async def _install(self, artifact, version, hash): - name = artifact.split("/")[-1] - rmtree(path.join(self.plugin_path, name), ignore_errors=True) - self.log.info(f"Installing {artifact} (Version: {version})") - async with ClientSession() as client: - url = f"https://github.com/{artifact}/archive/refs/tags/{version}.zip" - self.log.debug(f"Fetching {url}") - res = await client.get(url) - if res.status == 200: - self.log.debug("Got 200. Reading...") - data = await res.read() - self.log.debug(f"Read {len(data)} bytes") - res_zip = BytesIO(data) - with ProcessPoolExecutor() as executor: - self.log.debug("Unzipping...") - ret = await get_event_loop().run_in_executor( - executor, - self._unzip_to_plugin_dir, - res_zip, - name, - hash - ) - if ret: - self.log.info(f"Installed {artifact} (Version: {version})") - else: - self.log.fatal(f"SHA-256 Mismatch!!!! {artifact} (Version: {version})") - else: - self.log.fatal(f"Could not fetch from github. {await res.text()}") - - async def redirect_to_store(self, request): - return web.Response(status=302, headers={"Location": self.store_url}) - - async def install_plugin(self, request): - data = await request.post() - get_event_loop().create_task(self.request_plugin_install(data["artifact"], data["version"], data["hash"])) - return web.Response(text="Requested plugin install") - - async def request_plugin_install(self, artifact, version, hash): - request_id = str(time()) - self.install_requests[request_id] = PluginInstallContext(artifact, version, hash) - tab = await get_tab("QuickAccess") - await tab.open_websocket() - await tab.evaluate_js(f"addPluginInstallPrompt('{artifact}', '{version}', '{request_id}')") - - async def confirm_plugin_install(self, request_id): - request = self.install_requests.pop(request_id) - await self._install(request.gh_url, request.version, request.hash)
\ No newline at end of file diff --git a/plugin_loader/injector.py b/plugin_loader/injector.py deleted file mode 100644 index c2157472..00000000 --- a/plugin_loader/injector.py +++ /dev/null @@ -1,97 +0,0 @@ -#Injector code from https://github.com/SteamDeckHomebrew/steamdeck-ui-inject. More info on how it works there. - -from aiohttp import ClientSession -from logging import debug, getLogger -from asyncio import sleep -from traceback import format_exc - -BASE_ADDRESS = "http://localhost:8080" - -logger = getLogger("Injector") - -class Tab: - def __init__(self, res) -> None: - self.title = res["title"] - self.id = res["id"] - self.ws_url = res["webSocketDebuggerUrl"] - - self.websocket = None - self.client = None - - async def open_websocket(self): - self.client = ClientSession() - self.websocket = await self.client.ws_connect(self.ws_url) - - async def listen_for_message(self): - async for message in self.websocket: - yield message - - async def _send_devtools_cmd(self, dc, receive=True): - if self.websocket: - await self.websocket.send_json(dc) - return (await self.websocket.receive_json()) if receive else None - raise RuntimeError("Websocket not opened") - - async def evaluate_js(self, js, run_async=False): - await self.open_websocket() - res = await self._send_devtools_cmd({ - "id": 1, - "method": "Runtime.evaluate", - "params": { - "expression": js, - "userGesture": True, - "awaitPromise": run_async - } - }) - await self.client.close() - return res - - async def get_steam_resource(self, url): - res = await self.evaluate_js(f'(async function test() {{ return await (await fetch("{url}")).text() }})()', True) - return res["result"]["result"]["value"] - - def __repr__(self): - return self.title - -async def get_tabs(): - async with ClientSession() as web: - res = {} - - while True: - try: - res = await web.get(f"{BASE_ADDRESS}/json") - break - except: - logger.debug("Steam isn't available yet. Wait for a moment...") - logger.debug(format_exc()) - await sleep(5) - - if res.status == 200: - r = await res.json() - return [Tab(i) for i in r] - else: - raise Exception(f"/json did not return 200. {await r.text()}") - -async def get_tab(tab_name): - tabs = await get_tabs() - tab = next((i for i in tabs if i.title == tab_name), None) - if not tab: - raise ValueError(f"Tab {tab_name} not found") - return tab - -async def inject_to_tab(tab_name, js, run_async=False): - tab = await get_tab(tab_name) - - return await tab.evaluate_js(js, run_async) - -async def tab_has_element(tab_name, element_name): - try: - tab = await get_tab(tab_name) - except ValueError: - return False - res = await tab.evaluate_js(f"document.getElementById('{element_name}') != null", False) - - if not "result" in res or not "result" in res["result"] or not "value" in res["result"]["result"]: - return False - - return res["result"]["result"]["value"] diff --git a/plugin_loader/loader.py b/plugin_loader/loader.py deleted file mode 100644 index fea5f149..00000000 --- a/plugin_loader/loader.py +++ /dev/null @@ -1,191 +0,0 @@ -from aiohttp import web -from aiohttp_jinja2 import template -from watchdog.observers.polling import PollingObserver as Observer -from watchdog.events import FileSystemEventHandler -from asyncio import Queue -from os import path, listdir -from logging import getLogger -from time import time - -from injector import get_tabs, get_tab -from plugin import PluginWrapper -from traceback import print_exc - -class FileChangeHandler(FileSystemEventHandler): - def __init__(self, queue, plugin_path) -> None: - super().__init__() - self.logger = getLogger("file-watcher") - self.plugin_path = plugin_path - self.queue = queue - - def on_created(self, event): - src_path = event.src_path - if "__pycache__" in src_path: - return - - # check to make sure this isn't a directory - if path.isdir(src_path): - return - - # get the directory name of the plugin so that we can find its "main.py" and reload it; the - # file that changed is not necessarily the one that needs to be reloaded - self.logger.debug(f"file created: {src_path}") - rel_path = path.relpath(src_path, path.commonprefix([self.plugin_path, src_path])) - plugin_dir = path.split(rel_path)[0] - main_file_path = path.join(self.plugin_path, plugin_dir, "main.py") - self.queue.put_nowait((main_file_path, plugin_dir, True)) - - def on_modified(self, event): - src_path = event.src_path - if "__pycache__" in src_path: - return - - # check to make sure this isn't a directory - if path.isdir(src_path): - return - - # get the directory name of the plugin so that we can find its "main.py" and reload it; the - # file that changed is not necessarily the one that needs to be reloaded - self.logger.debug(f"file modified: {src_path}") - plugin_dir = path.split(path.relpath(src_path, path.commonprefix([self.plugin_path, src_path])))[0] - self.queue.put_nowait((path.join(self.plugin_path, plugin_dir, "main.py"), plugin_dir, True)) - -class Loader: - def __init__(self, server_instance, plugin_path, loop, live_reload=False) -> None: - self.loop = loop - self.logger = getLogger("Loader") - self.plugin_path = plugin_path - self.logger.info(f"plugin_path: {self.plugin_path}") - self.plugins = {} - self.callsigns = {} - self.callsign_matches = {} - self.import_plugins() - - if live_reload: - self.reload_queue = Queue() - self.observer = Observer() - self.observer.schedule(FileChangeHandler(self.reload_queue, plugin_path), self.plugin_path, recursive=True) - self.observer.start() - self.loop.create_task(self.handle_reloads()) - - server_instance.add_routes([ - web.get("/plugins/iframe", self.plugin_iframe_route), - web.get("/plugins/load_main/{name}", self.load_plugin_main_view), - web.get("/plugins/plugin_resource/{name}/{path:.+}", self.handle_sub_route), - web.get("/plugins/load_tile/{name}", self.load_plugin_tile_view), - web.get("/steam_resource/{path:.+}", self.get_steam_resource) - ]) - - def import_plugin(self, file, plugin_directory, refresh=False): - try: - plugin = PluginWrapper(file, plugin_directory, self.plugin_path) - if plugin.name in self.plugins: - if not "debug" in plugin.flags and refresh: - self.logger.info(f"Plugin {plugin.name} is already loaded and has requested to not be re-loaded") - return - else: - self.plugins[plugin.name].stop() - self.plugins.pop(plugin.name, None) - self.callsigns.pop(self.callsign_matches[file], None) - if plugin.passive: - self.logger.info(f"Plugin {plugin.name} is passive") - callsign = str(time()) - plugin.callsign = callsign - self.plugins[plugin.name] = plugin.start() - self.callsigns[callsign] = plugin - self.callsign_matches[file] = callsign - self.logger.info(f"Loaded {plugin.name}") - except Exception as e: - self.logger.error(f"Could not load {file}. {e}") - print_exc() - finally: - if refresh: - self.loop.create_task(self.refresh_iframe()) - - def import_plugins(self): - self.logger.info(f"import plugins from {self.plugin_path}") - - directories = [i for i in listdir(self.plugin_path) if path.isdir(path.join(self.plugin_path, i)) and path.isfile(path.join(self.plugin_path, i, "plugin.json"))] - for directory in directories: - self.logger.info(f"found plugin: {directory}") - self.import_plugin(path.join(self.plugin_path, directory, "main.py"), directory) - - async def handle_reloads(self): - while True: - args = await self.reload_queue.get() - self.import_plugin(*args) - - async def handle_plugin_method_call(self, callsign, method_name, **kwargs): - if method_name.startswith("_"): - raise RuntimeError("Tried to call private method") - return await self.callsigns[callsign].execute_method(method_name, kwargs) - - async def get_steam_resource(self, request): - tab = (await get_tabs())[0] - try: - return web.Response(text=await tab.get_steam_resource(f"https://steamloopback.host/{request.match_info['path']}"), content_type="text/html") - except Exception as e: - return web.Response(text=str(e), status=400) - - async def load_plugin_main_view(self, request): - plugin = self.callsigns[request.match_info["name"]] - - # open up the main template - with open(path.join(self.plugin_path, plugin.plugin_directory, plugin.main_view_html), 'r') as template: - template_data = template.read() - # setup the main script, plugin, and pull in the template - ret = f""" - <script src="/static/library.js"></script> - <script>const plugin_name = '{plugin.callsign}' </script> - <base href="http://127.0.0.1:1337/plugins/plugin_resource/{plugin.callsign}/"> - {template_data} - """ - return web.Response(text=ret, content_type="text/html") - - async def handle_sub_route(self, request): - plugin = self.callsigns[request.match_info["name"]] - route_path = request.match_info["path"] - self.logger.info(path) - - ret = "" - - file_path = path.join(self.plugin_path, plugin.plugin_directory, route_path) - with open(file_path, 'r') as resource_data: - ret = resource_data.read() - - return web.Response(text=ret) - - async def load_plugin_tile_view(self, request): - plugin = self.callsigns[request.match_info["name"]] - - inner_content = "" - - # open up the tile template (if we have one defined) - if hasattr(plugin, "tile_view_html"): - with open(path.join(self.plugin_path, plugin.plugin_directory, plugin.tile_view_html), 'r') as template: - template_data = template.read() - inner_content = template_data - - # setup the default template - ret = f""" - <html style="height: fit-content;"> - <head> - <link rel="stylesheet" href="/static/styles.css"> - <script src="/static/library.js"></script> - <script>const plugin_name = '{plugin.callsign}';</script> - </head> - <body style="height: fit-content; display: block;"> - {inner_content} - </body> - <html> - """ - return web.Response(text=ret, content_type="text/html") - - @template('plugin_view.html') - async def plugin_iframe_route(self, request): - return {"plugins": self.plugins.values()} - - async def refresh_iframe(self): - tab = await get_tab("QuickAccess") - await tab.open_websocket() - return await tab.evaluate_js("reloadIframe()", False) diff --git a/plugin_loader/main.py b/plugin_loader/main.py deleted file mode 100644 index 9ed30760..00000000 --- a/plugin_loader/main.py +++ /dev/null @@ -1,144 +0,0 @@ -from logging import getLogger, basicConfig, INFO, DEBUG, Filter, root -from os import getenv - -CONFIG = { - "plugin_path": getenv("PLUGIN_PATH", "/home/deck/homebrew/plugins"), - "server_host": getenv("SERVER_HOST", "127.0.0.1"), - "server_port": int(getenv("SERVER_PORT", "1337")), - "live_reload": getenv("LIVE_RELOAD", "1") == "1", - "log_level": {"CRITICAL": 50, "ERROR": 40, "WARNING":30, "INFO": 20, "DEBUG": 10}[getenv("LOG_LEVEL", "INFO")], - "store_url": getenv("STORE_URL", "https://plugins.deckbrew.xyz"), - "log_base_events": getenv("LOG_BASE_EVENTS", "0")=="1" -} - -class NoBaseEvents(Filter): - def filter(self, record): - return not "asyncio" in record.name - -basicConfig(level=CONFIG["log_level"], format="[%(module)s][%(levelname)s]: %(message)s") -for handler in root.handlers: - if not CONFIG["log_base_events"]: - handler.addFilter(NoBaseEvents()) - -from aiohttp.web import Application, run_app, static -from aiohttp_jinja2 import setup as jinja_setup -from jinja2 import FileSystemLoader -from os import path -from asyncio import get_event_loop, sleep -from json import loads, dumps -from subprocess import Popen - -from loader import Loader -from injector import inject_to_tab, get_tab, tab_has_element -from utilities import Utilities -from browser import PluginBrowser - -logger = getLogger("Main") -from traceback import print_exc - -async def chown_plugin_dir(_): - Popen(["chown", "-R", "deck:deck", CONFIG["plugin_path"]]) - Popen(["chmod", "-R", "555", CONFIG["plugin_path"]]) - -class PluginManager: - def __init__(self) -> None: - self.loop = get_event_loop() - self.web_app = Application() - self.plugin_loader = Loader(self.web_app, CONFIG["plugin_path"], self.loop, CONFIG["live_reload"]) - self.plugin_browser = PluginBrowser(CONFIG["plugin_path"], self.web_app, CONFIG["store_url"]) - self.utilities = Utilities(self) - - jinja_setup(self.web_app, loader=FileSystemLoader(path.join(path.dirname(__file__), 'templates'))) - self.web_app.on_startup.append(self.inject_javascript) - self.web_app.on_startup.append(chown_plugin_dir) - self.web_app.add_routes([static("/static", path.join(path.dirname(__file__), 'static'))]) - self.loop.create_task(self.method_call_listener()) - self.loop.create_task(self.loader_reinjector()) - - self.loop.set_exception_handler(self.exception_handler) - - def exception_handler(self, loop, context): - if context["message"] == "Unclosed connection": - return - loop.default_exception_handler(context) - - async def loader_reinjector(self): - finished_reinjection = False - logger.info("Plugin loader isn't present in Steam anymore, reinjecting...") - while True: - await sleep(1) - if not await tab_has_element("QuickAccess", "plugin_iframe"): - logger.debug("Plugin loader isn't present in Steam anymore, reinjecting...") - await self.inject_javascript() - finished_reinjection = True - elif finished_reinjection: - finished_reinjection = False - logger.info("Reinjecting successful!") - - self.loop.create_task(self.method_call_listener()) - - async def inject_javascript(self, request=None): - try: - await inject_to_tab("QuickAccess", open(path.join(path.dirname(__file__), "static/library.js"), "r").read()) - await inject_to_tab("QuickAccess", open(path.join(path.dirname(__file__), "static/plugin_page.js"), "r").read()) - except: - logger.info("Failed to inject JavaScript into tab") - pass - - async def resolve_method_call(self, tab, call_id, response): - try: - r = dumps(response) - except Exception as e: - logger.error(response["result"]) - response["result"] = str(response["result"]) - r = response - await tab._send_devtools_cmd({ - "id": 1, - "method": "Runtime.evaluate", - "params": { - "expression": f"resolveMethodCall({call_id}, {r})", - "userGesture": True - } - }, receive=False) - - async def handle_method_call(self, method, tab): - res = {} - try: - if method["method"] == "plugin_method": - res["result"] = await self.plugin_loader.handle_plugin_method_call( - method["args"]["plugin_name"], - method["args"]["method_name"], - **method["args"]["args"] - ) - res["success"] = True - else: - r = await self.utilities.util_methods[method["method"]](**method["args"]) - res["result"] = r - res["success"] = True - except Exception as e: - res["result"] = str(e) - res["success"] = False - finally: - await self.resolve_method_call(tab, method["id"], res) - - async def method_call_listener(self): - while True: - try: - tab = await get_tab("QuickAccess") - break - except: - await sleep(1) - await tab.open_websocket() - await tab._send_devtools_cmd({"id": 1, "method": "Runtime.discardConsoleEntries"}) - await tab._send_devtools_cmd({"id": 1, "method": "Runtime.enable"}) - async for message in tab.listen_for_message(): - data = message.json() - if not "id" in data and data["method"] == "Runtime.consoleAPICalled" and data["params"]["type"] == "debug": - method = loads(data["params"]["args"][0]["value"]) - self.loop.create_task(self.handle_method_call(method, tab)) - - def run(self): - return run_app(self.web_app, host=CONFIG["server_host"], port=CONFIG["server_port"], loop=self.loop, access_log=None) - -if __name__ == "__main__": - PluginManager().run()
\ No newline at end of file diff --git a/plugin_loader/plugin.py b/plugin_loader/plugin.py deleted file mode 100644 index 30626058..00000000 --- a/plugin_loader/plugin.py +++ /dev/null @@ -1,104 +0,0 @@ -from importlib.util import spec_from_file_location, module_from_spec -from asyncio import get_event_loop, new_event_loop, set_event_loop, start_unix_server, open_unix_connection, sleep, Lock -from os import path, setuid -from json import loads, dumps, load -from time import time -from multiprocessing import Process -from signal import signal, SIGINT -from sys import exit - -class PluginWrapper: - def __init__(self, file, plugin_directory, plugin_path) -> None: - self.file = file - self.plugin_directory = plugin_directory - self.reader = None - self.writer = None - self.socket_addr = f"/tmp/plugin_socket_{time()}" - self.method_call_lock = Lock() - - json = load(open(path.join(plugin_path, plugin_directory, "plugin.json"), "r")) - - self.name = json["name"] - self.author = json["author"] - self.main_view_html = json["main_view_html"] - self.tile_view_html = json["tile_view_html"] if "tile_view_html" in json else "" - self.flags = json["flags"] - - self.passive = not path.isfile(self.file) - - def _init(self): - signal(SIGINT, lambda s, f: exit(0)) - - set_event_loop(new_event_loop()) - if self.passive: - return - setuid(0 if "root" in self.flags else 1000) - spec = spec_from_file_location("_", self.file) - module = module_from_spec(spec) - spec.loader.exec_module(module) - self.Plugin = module.Plugin - - if hasattr(self.Plugin, "_main"): - get_event_loop().create_task(self.Plugin._main(self.Plugin)) - get_event_loop().create_task(self._setup_socket()) - get_event_loop().run_forever() - - async def _setup_socket(self): - self.socket = await start_unix_server(self._listen_for_method_call, path=self.socket_addr) - - async def _listen_for_method_call(self, reader, writer): - while True: - data = loads((await reader.readline()).decode("utf-8")) - if "stop" in data: - get_event_loop().stop() - while get_event_loop().is_running(): - await sleep(0) - get_event_loop().close() - return - d = {"res": None, "success": True} - try: - d["res"] = await getattr(self.Plugin, data["method"])(self.Plugin, **data["args"]) - except Exception as e: - d["res"] = str(e) - d["success"] = False - finally: - writer.write((dumps(d)+"\n").encode("utf-8")) - await writer.drain() - - async def _open_socket_if_not_exists(self): - if not self.reader: - while True: - try: - self.reader, self.writer = await open_unix_connection(self.socket_addr) - break - except: - await sleep(0) - - def start(self): - if self.passive: - return self - Process(target=self._init).start() - return self - - def stop(self): - if self.passive: - return - async def _(self): - await self._open_socket_if_not_exists() - self.writer.write((dumps({"stop": True})+"\n").encode("utf-8")) - await self.writer.drain() - self.writer.close() - get_event_loop().create_task(_(self)) - - async def execute_method(self, method_name, kwargs): - if self.passive: - raise RuntimeError("This plugin is passive (aka does not implement main.py)") - async with self.method_call_lock: - await self._open_socket_if_not_exists() - self.writer.write( - (dumps({"method": method_name, "args": kwargs})+"\n").encode("utf-8")) - await self.writer.drain() - res = loads((await self.reader.readline()).decode("utf-8")) - if not res["success"]: - raise Exception(res["res"]) - return res["res"] diff --git a/plugin_loader/static/library.js b/plugin_loader/static/library.js deleted file mode 100644 index 744cc77f..00000000 --- a/plugin_loader/static/library.js +++ /dev/null @@ -1,71 +0,0 @@ -class PluginEventTarget extends EventTarget { } -method_call_ev_target = new PluginEventTarget(); - -window.addEventListener("message", function(evt) { - let ev = new Event(evt.data.call_id); - ev.data = evt.data.result; - method_call_ev_target.dispatchEvent(ev); -}, false); - -async function call_server_method(method_name, arg_object={}) { - let id = `${uuidv4()}`; - console.debug(JSON.stringify({ - "id": id, - "method": method_name, - "args": arg_object - })); - return new Promise((resolve, reject) => { - method_call_ev_target.addEventListener(`${id}`, function (event) { - if (event.data.success) resolve(event.data.result); - else reject(event.data.result); - }); - }); -} - -// Source: https://stackoverflow.com/a/2117523 Thanks! -function uuidv4() { - return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c => - (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) - ); -} - -async function fetch_nocors(url, request={}) { - let args = { method: "POST", headers: {}, body: "" }; - request = {...args, ...request}; - request.url = url; - request.data = request.body; - delete request.body; //maintain api-compatibility with fetch - return await call_server_method("http_request", request); -} - -async function call_plugin_method(method_name, arg_object={}) { - if (plugin_name == undefined) - throw new Error("Plugin methods can only be called from inside plugins (duh)"); - return await call_server_method("plugin_method", { - 'plugin_name': plugin_name, - 'method_name': method_name, - 'args': arg_object - }); -} - -async function execute_in_tab(tab, run_async, code) { - return await call_server_method("execute_in_tab", { - 'tab': tab, - 'run_async': run_async, - 'code': code - }); -} - -async function inject_css_into_tab(tab, style) { - return await call_server_method("inject_css_into_tab", { - 'tab': tab, - 'style': style - }); -} - -async function remove_css_from_tab(tab, css_id) { - return await call_server_method("remove_css_from_tab", { - 'tab': tab, - 'css_id': css_id - }); -}
\ No newline at end of file diff --git a/plugin_loader/static/plugin_page.js b/plugin_loader/static/plugin_page.js deleted file mode 100644 index 0531f04e..00000000 --- a/plugin_loader/static/plugin_page.js +++ /dev/null @@ -1,98 +0,0 @@ -function reloadIframe() { - document.getElementById("plugin_iframe").contentWindow.location.href = "http://127.0.0.1:1337/plugins/iframe"; -} - -function resolveMethodCall(call_id, result) { - let iframe = document.getElementById("plugin_iframe").contentWindow; - iframe.postMessage({'call_id': call_id, 'result': result}, "http://127.0.0.1:1337"); -} - -function installPlugin(request_id) { - let id = `${new Date().getTime()}`; - console.debug(JSON.stringify({ - "id": id, - "method": "confirm_plugin_install", - "args": {"request_id": request_id} - })); - document.getElementById('plugin_install_list').removeChild(document.getElementById(`plugin_install_prompt_${request_id}`)); -} - -function addPluginInstallPrompt(artifact, version, request_id) { - let text = ` - <link rel="stylesheet" href="/static/styles.css"> - - <div id="plugin_install_prompt_${request_id}" style="background-color: #0c131b; display: block; border: 1px solid #22262f; box-shadow: 0px 0px 8px #202020; width: calc(100% - 50px); padding: 0px 10px 10px 10px;"> - <h3>Install Plugin?</h3> - <p style="font-size: 12px;"> - ${artifact} - Version: ${version} - </p> - <button type="button" tabindex="0" class="DialogButton _DialogLayout Secondary basicdialog_Button_1Ievp Focusable" - onclick="installPlugin('${request_id}')"> - Install - </button> - <p style="margin: 2px;"></p> - <button type="button" tabindex="0" class="DialogButton _DialogLayout Secondary basicdialog_Button_1Ievp Focusable" - onclick="document.getElementById('plugin_install_list').removeChild(document.getElementById('plugin_install_prompt_${request_id}'))"> - Cancel - </button> - </div> - `; - document.getElementById('plugin_install_list').innerHTML = text; - - execute_in_tab('SP', false, 'FocusNavController.DispatchVirtualButtonClick(28)') -} - -(function () { - const PLUGIN_ICON = ` - <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-plugin" viewBox="0 0 16 16"> - <path fill-rule="evenodd" d="M1 8a7 7 0 1 1 2.898 5.673c-.167-.121-.216-.406-.002-.62l1.8-1.8a3.5 3.5 0 0 0 - 4.572-.328l1.414-1.415a.5.5 0 0 0 0-.707l-.707-.707 1.559-1.563a.5.5 0 1 0-.708-.706l-1.559 1.562-1.414-1.414 - 1.56-1.562a.5.5 0 1 0-.707-.706l-1.56 1.56-.707-.706a.5.5 0 0 0-.707 0L5.318 5.975a3.5 3.5 0 0 0-.328 - 4.571l-1.8 1.8c-.58.58-.62 1.6.121 2.137A8 8 0 1 0 0 8a.5.5 0 0 0 1 0Z"/> - </svg> - `; - - function createTitle(text) { - return `<div id="plugin_title" class="quickaccessmenu_Title_34nl5">${text}</div>`; - } - - function createPluginList() { - let pages = document.getElementsByClassName("quickaccessmenu_AllTabContents_2yKG4 quickaccessmenu_Down_3rR0o")[0]; - let pluginPage = pages.children[pages.children.length - 1]; - pluginPage.innerHTML = createTitle("Plugins"); - - pluginPage.innerHTML += `<div id="plugin_install_list" style="position: fixed; height: 100%; z-index: 99; transform: translate(5%, 0);"></div>` - - pluginPage.innerHTML += `<iframe id="plugin_iframe" style="border: none; width: 100%; height: 100%;" src="http://127.0.0.1:1337/plugins/iframe"></iframe>`; - } - - function inject() { - let tabs = document.getElementsByClassName("quickaccessmenu_TabContentColumn_2z5NL Panel Focusable")[0]; - tabs.children[tabs.children.length - 1].innerHTML = PLUGIN_ICON; - - createPluginList(); - } - - let injector = setInterval(function () { - if (document.hasFocus()) { - inject(); - document.getElementById("plugin_title").onclick = function() { - reloadIframe(); - document.getElementById("plugin_title").innerText = "Plugins"; - } - window.onmessage = function(ev) { - let title = ev.data; - if (title.startsWith("PLUGIN_LOADER__")) { - document.getElementById("plugin_title").innerHTML = ` - <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-left-square-fill" viewBox="0 0 16 16"> - <path d="M16 14a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12zm-4.5-6.5H5.707l2.147-2.146a.5.5 0 1 0-.708-.708l-3 3a.5.5 0 0 0 0 .708l3 3a.5.5 0 0 0 .708-.708L5.707 8.5H11.5a.5.5 0 0 0 0-1z"/> - </svg> - ${title.replace("PLUGIN_LOADER__", "")} - `; - } - } - clearInterval(injector); - } - }, 100); -})();
\ No newline at end of file diff --git a/plugin_loader/static/styles.css b/plugin_loader/static/styles.css deleted file mode 100644 index 8d27a538..00000000 --- a/plugin_loader/static/styles.css +++ /dev/null @@ -1,3 +0,0 @@ -@import url("/steam_resource/css/2.css"); -@import url("/steam_resource/css/39.css"); -@import url("/steam_resource/css/library.css"); diff --git a/plugin_loader/templates/plugin_view.html b/plugin_loader/templates/plugin_view.html deleted file mode 100644 index 9d7ba1bc..00000000 --- a/plugin_loader/templates/plugin_view.html +++ /dev/null @@ -1,76 +0,0 @@ -<link rel="stylesheet" href="/static/styles.css"> -<script> - const tile_iframes = []; - window.addEventListener("message", function (evt) { - tile_iframes.forEach(iframe => { - iframe.contentWindow.postMessage(evt.data, "http://127.0.0.1:1337"); - }); - }, false); - - function loadPlugin(callsign, name) { - this.parent.postMessage("PLUGIN_LOADER__"+name, "https://steamloopback.host"); - location.href = `/plugins/load_main/${callsign}`; - } -</script> - -{% if not plugins|length %} - <div class="quickaccessmenu_TabGroupPanel_1QO7b Panel Focusable"> - <div class="quickaccesscontrols_EmptyNotifications_3ZjbM" style="padding-top:7px;"> - No plugins installed - </div> - </div> -{% endif %} - -<div class="quickaccessmenu_TabGroupPanel_1QO7b Panel Focusable"> - {% for plugin in plugins %} - {% if plugin.tile_view_html|length %} - <div class="quickaccesscontrols_PanelSectionRow_26R5w"> - <div onclick="loadPlugin('{{ plugin.callsign }}', '{{ plugin.name }}')" - class="basicdialog_Field_ugL9c basicdialog_WithChildrenBelow_1RjOd basicdialog_InlineWrapShiftsChildrenBelow_3a6QZ basicdialog_ExtraPaddingOnChildrenBelow_2-owv basicdialog_StandardPadding_1HrfN basicdialog_HighlightOnFocus_1xh2W Panel Focusable" - style="--indent-level:0; margin: 0px; padding: 0px; padding-top: 8px;"> - <iframe id="tile_view_iframe_{{ plugin.callsign }}" - scrolling="no" marginwidth="0" marginheight="0" - hspace="0" vspace="0" frameborder="0" - style="border-radius: 2px;" - src="/plugins/load_tile/{{ plugin.callsign }}"> - </iframe> - <script> - (function() { - let iframe = document.getElementById("tile_view_iframe_{{ plugin.callsign }}"); - tile_iframes.push(document.getElementById("tile_view_iframe_{{ plugin.callsign }}")); - - iframe.onload = function() { - let html = iframe.contentWindow.document.children[0]; - let last_height = 0; - - setInterval(function() { - let height = iframe.contentWindow.document.children[0].scrollHeight; - if (height != last_height) { - iframe.height = height + "px"; - last_height = height; - } - }, 100); - - iframe.contentWindow.document.body.onclick = function () { - loadPlugin('{{ plugin.callsign }}', '{{ plugin.name }}'); - }; - } - })(); - </script> - </div> - </div> - {% else %} - <div class="quickaccesscontrols_PanelSectionRow_26R5w"> - <div onclick="loadPlugin('{{ plugin.callsign }}', '{{ plugin.name }}')" - class="basicdialog_Field_ugL9c basicdialog_WithChildrenBelow_1RjOd basicdialog_InlineWrapShiftsChildrenBelow_3a6QZ basicdialog_ExtraPaddingOnChildrenBelow_2-owv basicdialog_StandardPadding_1HrfN basicdialog_HighlightOnFocus_1xh2W Panel Focusable" - style="--indent-level:0; margin: 0px; padding: 0px; padding-top: 8px;"> - <div class="basicdialog_FieldChildren_279n8"> - <button type="button" tabindex="0" - class="DialogButton _DialogLayout Secondary basicdialog_Button_1Ievp Focusable">{{ plugin.name }} - </button> - </div> - </div> - </div> - {% endif %} - {% endfor %} -</div> diff --git a/plugin_loader/utilities.py b/plugin_loader/utilities.py deleted file mode 100644 index 39f9ca55..00000000 --- a/plugin_loader/utilities.py +++ /dev/null @@ -1,106 +0,0 @@ -from aiohttp import ClientSession -from injector import inject_to_tab -import uuid - -class Utilities: - def __init__(self, context) -> None: - self.context = context - self.util_methods = { - "ping": self.ping, - "http_request": self.http_request, - "confirm_plugin_install": self.confirm_plugin_install, - "execute_in_tab": self.execute_in_tab, - "inject_css_into_tab": self.inject_css_into_tab, - "remove_css_from_tab": self.remove_css_from_tab - } - - async def confirm_plugin_install(self, request_id): - return await self.context.plugin_browser.confirm_plugin_install(request_id) - - async def http_request(self, method="", url="", **kwargs): - async with ClientSession() as web: - async with web.request(method, url, **kwargs) as res: - return { - "status": res.status, - "headers": dict(res.headers), - "body": await res.text() - } - - async def ping(self, **kwargs): - return "pong" - - async def execute_in_tab(self, tab, run_async, code): - try: - result = await inject_to_tab(tab, code, run_async) - if "exceptionDetails" in result["result"]: - return { - "success": False, - "result": result["result"] - } - - return { - "success": True, - "result" : result["result"]["result"].get("value") - } - except Exception as e: - return { - "success": False, - "result": e - } - - async def inject_css_into_tab(self, tab, style): - try: - css_id = str(uuid.uuid4()) - - result = await inject_to_tab(tab, - f""" - (function() {{ - const style = document.createElement('style'); - style.id = "{css_id}"; - document.head.append(style); - style.textContent = `{style}`; - }})() - """, False) - - if "exceptionDetails" in result["result"]: - return { - "success": False, - "result": result["result"] - } - - return { - "success": True, - "result" : css_id - } - except Exception as e: - return { - "success": False, - "result": e - } - - async def remove_css_from_tab(self, tab, css_id): - try: - result = await inject_to_tab(tab, - f""" - (function() {{ - let style = document.getElementById("{css_id}"); - - if (style.nodeName.toLowerCase() == 'style') - style.parentNode.removeChild(style); - }})() - """, False) - - if "exceptionDetails" in result["result"]: - return { - "success": False, - "result": result - } - - return { - "success": True - } - except Exception as e: - return { - "success": False, - "result": e - } |
