Compare commits

...

32 Commits

Author SHA1 Message Date
Jonas Dellinger 414e0da2f3 Fix hot-reload when there are subdirs (#56) 2022-05-11 02:11:14 +03:00
tza cb9b888dc6 Merge branch 'main' of https://github.com/SteamDeckHomebrew/PluginLoader 2022-05-10 23:17:12 +03:00
tza f3ab0f5989 Plugin store button now uses built-in browser 2022-05-10 23:17:09 +03:00
marios e132aba0f8 Fixed race condition pr 2022-05-10 21:11:51 +03:00
tza 0d0e57e35a Added store button 2022-05-10 20:31:39 +03:00
Patrick Kubiak 945db5de47 Use unique ids in call_server_method (#55) 2022-05-10 17:13:53 +03:00
marios 28746e3962 Update README.md 2022-05-04 11:43:28 +03:00
Gabriel Jones 279b1e8c40 Uninstall script addition (#48)
* Create uninstall.sh

* Update uninstall.sh

First pass for a version that looks similar to the install scripts

* Update readme with uninstall info

Add uninstall script info to readme

* Update dist/uninstall.sh

Only remove services for now

Co-authored-by: TrainDoctor <11465594+TrainDoctor@users.noreply.github.com>

* Adjust echo to accurately reflect script actions

No longer deletes installed plugins, adjusted echo to match actions.

Co-authored-by: TrainDoctor <11465594+TrainDoctor@users.noreply.github.com>
2022-05-04 11:42:03 +03:00
marios 89ecca7c30 Fixed callsign debug bug, Fixed process spawn and termination bug 2022-04-29 21:51:01 +03:00
marios 7d74e98f4f Bug fixes
- Fixed KeyError in execute_in_tab
- Changed the plugin process dispatch method, this *should* fix that annoying server hang issue.
2022-04-29 12:52:24 +03:00
marios fe1f6473e9 method call listener retry bug fix, method call response serializaiton failure fix,
- Added retry logic to the QuickAccess tab fetching in the method call listener.
- Added exception handling, in case a plugin method returns something that can't be serialized as JSON.
- Changed a few log calls from info to debug to prevent spam
- Added a filter for asyncio base_event log records, since they get spamy and don't provide any useful info most of the time. This can be turned off with the LOG_BASE_EVENTS envar.
2022-04-26 23:37:01 +03:00
WerWolv 73559ae8c7 Make sure install scripts don't create folders as root 2022-04-22 21:48:36 +02:00
WerWolv 340ea91d1c Fixed calling backend functions after restarting steam 2022-04-22 18:43:52 +02:00
WerWolv 3f3f6bd475 Allow inject_css_into_tab to create more than just a single css rule 2022-04-22 14:30:58 +02:00
WerWolv 4b2f8cd8f5 Make sure old user plugin loader is being removed 2022-04-21 18:03:05 +02:00
WerWolv 604006a7cb Fixed root check 2022-04-21 17:46:53 +02:00
WerWolv 7aa4e9106a Make release script actually work again 2022-04-21 17:44:12 +02:00
marios fa776f0d0b Callsigns (#37)
* Plugin callsigns, filechangehandler thread bug fix, plugin file perms

- Plugins are now assigned a callsign (a random string), which they use for all internal identification, like resource fetching and method calls. This is to ensure that plugins only access their own resources and methods.
- Made FileChangeHandler send off events to a queue, that is then consumed by the Loader, instead of calling import_plugin on its own, since that caused weird issues with the event loop and the thread watchdog is using.
- Plugins are now owned by root and have read-only permissions. This is handled automatically.

* Improved general look and feel of plugin tab

* Make all plugin entries have the same padding between them

* Make "No plugins installed" text look the same as "No new notifications"

Co-authored-by: WerWolv <werwolv98@gmail.com>
2022-04-18 15:57:51 +03:00
WerWolv 4576fed01b Properly delete old user plugin loader service on install 2022-04-13 23:18:58 +02:00
tza de435e22fb added default value to injector tab run_async 2022-04-14 00:01:35 +03:00
tza 6694d5ab71 fixed passive plugin reload bug and close event loop properly 2022-04-13 23:50:26 +03:00
WerWolv c084abecfc Fixed install script root access 2022-04-13 22:19:48 +02:00
tza f685eeb420 Added support for passive plugins (that don't implement main.py) 2022-04-13 22:47:22 +03:00
marios 6250fafa6e Fix release script 2022-04-13 21:50:18 +03:00
WerWolv efa5dc61c7 Update install scripts to install loader as system service 2022-04-13 12:55:33 +02:00
marios e3d7b50bd9 Root plugins (#35)
* root plugins

plugins can now specify if they want their methods to be ran as root. this is done via the multiprocess module. method calls are delegated to a separate process that is then down-privileged by default to user 1000, so the loader can safely be ran as root

except it isn't really safe because the plugin is imported as root anyway

* working implementation

- follows the new plugin format with the plugin.json file
- plugins are loaded in their own isolated process along with their own event loop and unix socket server for calling methods
- private methods are now prepended with _ instead of __

* converted format to f-strings
2022-04-13 02:14:44 +03:00
WerWolv 0359fd966a Use f-strings instead of .format 2022-04-12 22:27:46 +02:00
WerWolv fe9faefd0b Added functions to inject and remove css from tabs 2022-04-12 21:59:09 +02:00
WerWolv 012274b1a0 Added library function to execute code in a different tab 2022-04-12 21:15:36 +02:00
Spyrex 070d11154f Bundle stylesheets (#34) 2022-04-11 11:48:41 +02:00
Spyrex 02f73b795d Add vscode debugging (#33) 2022-04-11 12:45:00 +03:00
tza 4ffe2fdf24 added sha-256 hash checking to browser 2022-04-09 00:14:15 +03:00
16 changed files with 585 additions and 223 deletions
+14
View File
@@ -0,0 +1,14 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug",
"type": "python",
"request": "launch",
"program": "${workspaceFolder}/plugin_loader/main.py",
"preLaunchTask": "Stop Service",
"console": "integratedTerminal",
"justMyCode": true
}
]
}
+10
View File
@@ -0,0 +1,10 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Stop Service",
"type": "shell",
"command":"systemctl --user stop plugin_loader",
}
]
}
+7 -1
View File
@@ -12,12 +12,18 @@
- For users:
- `curl -L https://github.com/SteamDeckHomebrew/PluginLoader/raw/main/dist/install_release.sh | sh`
- For developers:
- `curl -L https://github.com/SteamDeckHomebrew/PluginLoader/raw/main/dist/install_nightly.sh | sh`
~~- `curl -L https://github.com/SteamDeckHomebrew/PluginLoader/raw/main/dist/install_nightly.sh | sh`~~
Nightly releases are currently broken.
8. Done! Reboot back into Gaming mode and enjoy your plugins!
### Install Plugins
- Simply copy the plugin's folder into `~/homebrew/plugins`
### Uninstall
- Open a terminal and paste the following command into it:
- For both users and developers:
- `curl -L https://github.com/SteamDeckHomebrew/PluginLoader/raw/main/dist/uninstall.sh | sh`
### Developing plugins
- There is no complete plugin development documentation yet. However a good starting point is the [Plugin Template](https://github.com/SteamDeckHomebrew/Plugin-Template) repository
+16 -7
View File
@@ -1,13 +1,15 @@
#!/bin/sh
[ "$UID" -eq 0 ] || exec sudo "$0" "$@"
echo "Installing Steam Deck Plugin Loader nightly..."
HOMEBREW_FOLDER=/home/deck/homebrew
# Create folder structure
rm -rf ${HOMEBREW_FOLDER}/services
mkdir -p ${HOMEBREW_FOLDER}/services
mkdir -p ${HOMEBREW_FOLDER}/plugins
sudo -u deck mkdir -p ${HOMEBREW_FOLDER}/services
sudo -u deck mkdir -p ${HOMEBREW_FOLDER}/plugins
# Download latest nightly build and install it
rm -rf /tmp/plugin_loader
@@ -21,12 +23,19 @@ chmod +x ${HOMEBREW_FOLDER}/services/PluginLoader
systemctl --user stop plugin_loader 2> /dev/null
systemctl --user disable plugin_loader 2> /dev/null
rm -f /home/deck/.config/systemd/user/plugin_loader.service
cat > /home/deck/.config/systemd/user/plugin_loader.service <<- EOM
systemctl stop plugin_loader 2> /dev/null
systemctl disable plugin_loader 2> /dev/null
rm -f /etc/systemd/system/plugin_loader.service
cat > /etc/systemd/system/plugin_loader.service <<- EOM
[Unit]
Description=SteamDeck Plugin Loader
[Service]
Type=simple
User=root
Restart=always
ExecStart=/home/deck/homebrew/services/PluginLoader
WorkingDirectory=/home/deck/homebrew/services
@@ -34,8 +43,8 @@ WorkingDirectory=/home/deck/homebrew/services
Environment=PLUGIN_PATH=/home/deck/homebrew/plugins
[Install]
WantedBy=default.target
WantedBy=multi-user.target
EOM
systemctl --user daemon-reload
systemctl --user start plugin_loader
systemctl --user enable plugin_loader
systemctl daemon-reload
systemctl start plugin_loader
systemctl enable plugin_loader
+15 -12
View File
@@ -1,13 +1,15 @@
#!/bin/sh
[ "$UID" -eq 0 ] || exec sudo "$0" "$@"
echo "Installing Steam Deck Plugin Loader release..."
HOMEBREW_FOLDER=/home/deck/homebrew
# Create folder structure
rm -rf ${HOMEBREW_FOLDER}/services
mkdir -p ${HOMEBREW_FOLDER}/services
mkdir -p ${HOMEBREW_FOLDER}/plugins
sudo -u deck mkdir -p ${HOMEBREW_FOLDER}/services
sudo -u deck mkdir -p ${HOMEBREW_FOLDER}/plugins
# Download latest release and install it
curl -L https://github.com/SteamDeckHomebrew/PluginLoader/releases/latest/download/PluginLoader --output ${HOMEBREW_FOLDER}/services/PluginLoader
@@ -15,22 +17,23 @@ chmod +x ${HOMEBREW_FOLDER}/services/PluginLoader
systemctl --user stop plugin_loader 2> /dev/null
systemctl --user disable plugin_loader 2> /dev/null
rm -f /home/deck/.config/systemd/user/plugin_loader.service
cat > /home/deck/.config/systemd/user/plugin_loader.service <<- EOM
systemctl stop plugin_loader 2> /dev/null
systemctl disable plugin_loader 2> /dev/null
rm -f /etc/systemd/system/plugin_loader.service
cat > /etc/systemd/system/plugin_loader.service <<- EOM
[Unit]
Description=SteamDeck Plugin Loader
[Service]
Type=simple
User=root
Restart=always
ExecStart=/home/deck/homebrew/services/PluginLoader
WorkingDirectory=/home/deck/homebrew/services
Environment=PLUGIN_PATH=/home/deck/homebrew/plugins
[Install]
WantedBy=default.target
WantedBy=multi-user.target
EOM
systemctl --user daemon-reload
systemctl --user start plugin_loader
systemctl --user enable plugin_loader
systemctl daemon-reload
systemctl start plugin_loader
systemctl enable plugin_loader
+17
View File
@@ -0,0 +1,17 @@
#!/bin/sh
echo "Uninstalling Steam Deck Plugin Loader..."
HOMEBREW_FOLDER=/home/deck/homebrew
# Disable and remove services
sudo systemctl disable --now plugin_loader.service > /dev/null
sudo rm -f /home/deck/.config/systemd/user/plugin_loader.service
sudo rm -f /etc/systemd/system/plugin_loader.service
# Remove temporary folder if it exists from the install process
rm -rf /tmp/plugin_loader
# Cleanup services folder
sudo rm ${HOMEBREW_FOLDER}/services/PluginLoader
+32 -19
View File
@@ -1,4 +1,4 @@
from injector import get_tab
from injector import get_tab, inject_to_tab
from logging import getLogger
from os import path, rename
from shutil import rmtree
@@ -8,11 +8,14 @@ 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) -> None:
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:
@@ -23,54 +26,64 @@ class PluginBrowser:
server_instance.add_routes([
web.post("/browser/install_plugin", self.install_plugin),
web.get("/browser/iframe", self.redirect_to_store)
web.get("/browser/redirect", self.redirect_to_store),
])
def _unzip_to_plugin_dir(self, zip, name):
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)))
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):
async def _install(self, artifact, version, hash):
name = artifact.split("/")[-1]
rmtree(path.join(self.plugin_path, name), ignore_errors=True)
self.log.info("Installing {} (Version: {})".format(artifact, version))
self.log.info(f"Installing {artifact} (Version: {version})")
async with ClientSession() as client:
url = "https://github.com/{}/archive/refs/tags/{}.zip".format(artifact, version)
self.log.debug("Fetching {}".format(url))
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("Read {} bytes".format(len(data)))
self.log.debug(f"Read {len(data)} bytes")
res_zip = BytesIO(data)
with ProcessPoolExecutor() as executor:
self.log.debug("Unzipping...")
await get_event_loop().run_in_executor(
ret = await get_event_loop().run_in_executor(
executor,
self._unzip_to_plugin_dir,
res_zip,
name
name,
hash
)
self.log.info("Installed {} (Version: {})".format(artifact, version))
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("Could not fetch from github. {}".format(await res.text()))
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"]))
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):
async def request_plugin_install(self, artifact, version, hash):
request_id = str(time())
self.install_requests[request_id] = PluginInstallContext(artifact, version)
self.install_requests[request_id] = PluginInstallContext(artifact, version, hash)
tab = await get_tab("QuickAccess")
await tab.open_websocket()
await tab.evaluate_js("addPluginInstallPrompt('{}', '{}', '{}')".format(artifact, version, request_id))
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)
await self._install(request.gh_url, request.version, request.hash)
+16 -22
View File
@@ -3,6 +3,7 @@
from aiohttp import ClientSession
from logging import debug, getLogger
from asyncio import sleep
from traceback import format_exc
BASE_ADDRESS = "http://localhost:8080"
@@ -31,31 +32,22 @@ class Tab:
return (await self.websocket.receive_json()) if receive else None
raise RuntimeError("Websocket not opened")
async def evaluate_js(self, js):
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
"userGesture": True,
"awaitPromise": run_async
}
})
await self.client.close()
return res
async def get_steam_resource(self, url):
await self.open_websocket()
res = await self._send_devtools_cmd({
"id": 1,
"method": "Runtime.evaluate",
"params": {
"expression": f'(async function test() {{ return await (await fetch("{url}")).text() }})()',
"userGesture": True,
"awaitPromise": True
}
})
await self.client.close()
res = await self.evaluate_js(f'(async function test() {{ return await (await fetch("{url}")).text() }})()', True)
return res["result"]["result"]["value"]
def __repr__(self):
@@ -67,35 +59,37 @@ async def get_tabs():
while True:
try:
res = await web.get("{}/json".format(BASE_ADDRESS))
res = await web.get(f"{BASE_ADDRESS}/json")
break
except:
logger.info("Steam isn't available yet. Wait for a moment...")
logger.debug("Steam isn't available yet. Wait for a moment...")
logger.debug(format_exc())
await sleep(5)
if res.status == 200:
res = await res.json()
return [Tab(i) for i in res]
r = await res.json()
return [Tab(i) for i in r]
else:
raise Exception("/json did not return 200. {}".format(await res.text()))
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("Tab {} not found".format(tab_name))
raise ValueError(f"Tab {tab_name} not found")
return tab
async def inject_to_tab(tab_name, js):
async def inject_to_tab(tab_name, js, run_async=False):
tab = await get_tab(tab_name)
logger.debug(f"Injected JavaScript Result: {await tab.evaluate_js(js)}")
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")
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
+65 -69
View File
@@ -2,19 +2,29 @@ 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 importlib.util import spec_from_file_location, module_from_spec
from logging import getLogger
from time import time
from genericpath import exists
from pathlib import Path
from injector import get_tabs, get_tab
from plugin import PluginWrapper
from traceback import print_exc
class FileChangeHandler(FileSystemEventHandler):
def __init__(self, loader, plugin_path) -> None:
def __init__(self, queue, plugin_path) -> None:
super().__init__()
self.logger = getLogger("file-watcher")
self.loader : Loader = loader
self.plugin_path = plugin_path
self.queue = queue
def maybe_reload(self, src_path):
plugin_dir = Path(path.relpath(src_path, self.plugin_path)).parts[0]
self.logger.info(path.join(self.plugin_path, plugin_dir, "plugin.json"))
if exists(path.join(self.plugin_path, plugin_dir, "plugin.json")):
self.queue.put_nowait((path.join(self.plugin_path, plugin_dir, "main.py"), plugin_dir, True))
def on_created(self, event):
src_path = event.src_path
@@ -25,15 +35,8 @@ class FileChangeHandler(FileSystemEventHandler):
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")
if not path.isfile(main_file_path):
return
self.loader.import_plugin(main_file_path, plugin_dir, refresh=True)
self.maybe_reload(src_path)
def on_modified(self, event):
src_path = event.src_path
@@ -44,11 +47,8 @@ class FileChangeHandler(FileSystemEventHandler):
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.loader.import_plugin(path.join(self.plugin_path, plugin_dir, "main.py"), plugin_dir, refresh=True)
self.maybe_reload(src_path)
class Loader:
def __init__(self, server_instance, plugin_path, loop, live_reload=False) -> None:
@@ -57,17 +57,19 @@ class 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, plugin_path), self.plugin_path, recursive=True)
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/reload", self.reload_plugins),
web.post("/plugins/method_call", self.handle_plugin_method_call),
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),
@@ -76,31 +78,26 @@ class Loader:
def import_plugin(self, file, plugin_directory, refresh=False):
try:
spec = spec_from_file_location("_", file)
module = module_from_spec(spec)
spec.loader.exec_module(module)
# add member for what directory the given plugin lives under
module.Plugin._plugin_directory = plugin_directory
if not hasattr(module.Plugin, "name"):
raise KeyError("Plugin {} has not defined a name".format(file))
if module.Plugin.name in self.plugins:
if hasattr(module.Plugin, "hot_reload") and not module.Plugin.hot_reload and refresh:
self.logger.info("Plugin {} is already loaded and has requested to not be re-loaded"
.format(module.Plugin.name))
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:
if hasattr(self.plugins[module.Plugin.name], "task"):
self.plugins[module.Plugin.name].task.cancel()
self.plugins.pop(module.Plugin.name, None)
self.plugins[module.Plugin.name] = module.Plugin()
if hasattr(module.Plugin, "__main"):
setattr(self.plugins[module.Plugin.name], "task",
self.loop.create_task(self.plugins[module.Plugin.name].__main()))
self.logger.info("Loaded {}".format(module.Plugin.name))
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("Could not load {}. {}".format(file, e))
self.logger.error(f"Could not load {file}. {e}")
print_exc()
finally:
if refresh:
self.loop.create_task(self.refresh_iframe())
@@ -108,81 +105,80 @@ class Loader:
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, "main.py"))]
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 reload_plugins(self, request=None):
self.logger.info("Re-importing plugins.")
self.import_plugins()
async def handle_reloads(self):
while True:
args = await self.reload_queue.get()
self.import_plugin(*args)
async def handle_plugin_method_call(self, plugin_name, method_name, **kwargs):
if method_name.startswith("__"):
async def handle_plugin_method_call(self, callsign, method_name, **kwargs):
if method_name.startswith("_"):
raise RuntimeError("Tried to call private method")
return await getattr(self.plugins[plugin_name], method_name)(**kwargs)
return await self.callsigns[callsign].execute_method(method_name, kwargs)
async def get_steam_resource(self, request):
tab = (await get_tabs())[0]
tab = await get_tab("QuickAccess")
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.plugins[request.match_info["name"]]
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:
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 = """
ret = f"""
<script src="/static/library.js"></script>
<script>const plugin_name = '{}' </script>
<base href="http://127.0.0.1:1337/plugins/plugin_resource/{}/">
{}
""".format(plugin.name, plugin.name, template_data)
<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.plugins[request.match_info["name"]]
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)
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.plugins[request.match_info["name"]]
plugin = self.callsigns[request.match_info["name"]]
inner_content = ""
# open up the tile template (if we have one defined)
if len(plugin.tile_view_html) > 0:
with open(path.join(self.plugin_path, plugin._plugin_directory, plugin.tile_view_html), 'r') as template:
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 = """
ret = f"""
<html style="height: fit-content;">
<head>
<link rel="stylesheet" href="/steam_resource/css/2.css">
<link rel="stylesheet" href="/steam_resource/css/39.css">
<link rel="stylesheet" href="/steam_resource/css/library.css">
<link rel="stylesheet" href="/static/styles.css">
<script src="/static/library.js"></script>
<script>const plugin_name = '{name}';</script>
<script>const plugin_name = '{plugin.callsign}';</script>
</head>
<body style="height: fit-content; display: block;">
{content}
{inner_content}
</body>
<html>
""".format(name=plugin.name, content=inner_content)
"""
return web.Response(text=ret, content_type="text/html")
@template('plugin_view.html')
@@ -192,4 +188,4 @@ class Loader:
async def refresh_iframe(self):
tab = await get_tab("QuickAccess")
await tab.open_websocket()
return await tab.evaluate_js("reloadIframe()")
return await tab.evaluate_js("reloadIframe()", False)
+54 -20
View File
@@ -1,4 +1,4 @@
from logging import getLogger, basicConfig, INFO, DEBUG
from logging import getLogger, basicConfig, INFO, DEBUG, Filter, root
from os import getenv
CONFIG = {
@@ -7,10 +7,18 @@ CONFIG = {
"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://sdh.tzatzi.me/browse")
"store_url": getenv("STORE_URL", "https://beta.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
@@ -18,14 +26,19 @@ 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:
@@ -37,6 +50,7 @@ class PluginManager:
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())
@@ -47,13 +61,42 @@ class PluginManager:
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": "resolveMethodCall({}, {})".format(call_id, dumps(response)),
"expression": f"resolveMethodCall('{call_id}', {r})",
"userGesture": True
}
}, receive=False)
@@ -79,7 +122,12 @@ class PluginManager:
await self.resolve_method_call(tab, method["id"], res)
async def method_call_listener(self):
tab = await get_tab("QuickAccess")
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"})
@@ -88,23 +136,9 @@ class PluginManager:
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))
async def loader_reinjector(self):
while True:
await sleep(1)
if not await tab_has_element("QuickAccess", "plugin_iframe"):
logger.info("Plugin loader isn't present in Steam anymore, reinjecting...")
await self.inject_javascript()
async def inject_javascript(self, request=None):
try:
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
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()
PluginManager().run()
+104
View File
@@ -0,0 +1,104 @@
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"]
+30 -1
View File
@@ -8,7 +8,7 @@ window.addEventListener("message", function(evt) {
}, false);
async function call_server_method(method_name, arg_object={}) {
let id = `${new Date().getTime()}`;
let id = `${uuidv4()}`;
console.debug(JSON.stringify({
"id": id,
"method": method_name,
@@ -22,6 +22,13 @@ async function call_server_method(method_name, arg_object={}) {
});
}
// 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};
@@ -39,4 +46,26 @@ async function call_plugin_method(method_name, arg_object={}) {
'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
});
}
+45 -14
View File
@@ -19,20 +19,28 @@ function installPlugin(request_id) {
function addPluginInstallPrompt(artifact, version, request_id) {
let text = `
<div id="plugin_install_prompt_${request_id}" style="display: block; background: #304375; border-radius: 5px;">
<h3 style="padding-left: 1rem;">Install plugin</h3>
<ul style="padding-left: 10px; padding-right: 10px; padding-bottom: 20px; margin: 0;">
<li>${artifact}</li>
<li>${version}</li>
</ul>
<div style="text-align: center; padding-bottom: 10px;">
<button onclick="installPlugin('${request_id}')" style="display: inline-block; background-color: green;">Install</button>
<button onclick="document.getElementById('plugin_install_list').removeChild(document.getElementById('plugin_install_prompt_${request_id}'))"
style="display: inline-block; background-color: red;">Ignore</button>
</div>
<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;
document.getElementById('plugin_install_list').innerHTML = text;
execute_in_tab('SP', false, 'FocusNavController.DispatchVirtualButtonClick(28)')
}
(function () {
@@ -45,8 +53,22 @@ function addPluginInstallPrompt(artifact, version, request_id) {
</svg>
`;
const SHOP_ICON = `
<button
class="DialogButton _DialogLayout Secondary basicdialog_Button_1Ievp Focusable"
style="width: auto; padding-left: 10px; padding-right: 10px; margin-right: 1rem; margin-left: auto; padding-top: 3px;"
id="open_shop_button"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-bag-fill" viewBox="0 0 16 16">
<path d="M8 1a2.5 2.5 0 0 1 2.5 2.5V4h-5v-.5A2.5 2.5 0 0 1 8 1zm3.5 3v-.5a3.5 3.5 0 1 0-7 0V4H1v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V4h-3.5z"/>
</svg>
</button>
`
function createTitle(text) {
return `<div id="plugin_title" class="quickaccessmenu_Title_34nl5">${text}</div>`;
return `
<div class="quickaccessmenu_Title_34nl5"><div id="plugin_title">${text}</div>${SHOP_ICON}</div>
`;
}
function createPluginList() {
@@ -71,11 +93,20 @@ function addPluginInstallPrompt(artifact, version, request_id) {
inject();
document.getElementById("plugin_title").onclick = function() {
reloadIframe();
document.getElementById("plugin_title").innerText = "Plugins";
document.getElementById("plugin_title").innerText = `Plugins`;
document.getElementById("open_shop_button").style.display = 'block';
}
document.getElementById("open_shop_button").onclick = function(ev) {
console.debug(JSON.stringify({
"id": 1,
"method": "open_plugin_store",
"args": {}
}));
}
window.onmessage = function(ev) {
let title = ev.data;
if (title.startsWith("PLUGIN_LOADER__")) {
document.getElementById("open_shop_button").style.display = 'none';
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"/>
+3
View File
@@ -0,0 +1,3 @@
@import url("/steam_resource/css/2.css");
@import url("/steam_resource/css/39.css");
@import url("/steam_resource/css/library.css");
+54 -50
View File
@@ -1,6 +1,4 @@
<link rel="stylesheet" href="/steam_resource/css/2.css">
<link rel="stylesheet" href="/steam_resource/css/39.css">
<link rel="stylesheet" href="/steam_resource/css/library.css">
<link rel="stylesheet" href="/static/styles.css">
<script>
const tile_iframes = [];
window.addEventListener("message", function (evt) {
@@ -9,64 +7,70 @@
});
}, false);
function loadPlugin(name) {
function loadPlugin(callsign, name) {
this.parent.postMessage("PLUGIN_LOADER__"+name, "https://steamloopback.host");
location.href = `/plugins/load_main/${name}`;
location.href = `/plugins/load_main/${callsign}`;
}
</script>
{% if not plugins|length %}
<div 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;">
<div class="basicdialog_FieldChildren_279n8" style="color: white; font-size: large; padding-top: 10px;">
No plugins installed :(
<div class="quickaccessmenu_TabGroupPanel_1QO7b Panel Focusable">
<div class="quickaccesscontrols_EmptyNotifications_3ZjbM" style="padding-top:7px;">
No plugins installed
</div>
</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.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;">
<iframe id="tile_view_iframe_{{ plugin.name }}" style="display:block; padding: 0; border: none;" scrolling="no"
src="/plugins/load_tile/{{ plugin.name }}"></iframe>
<script>
(function() {
let iframe = document.getElementById("tile_view_iframe_{{ plugin.name }}");
tile_iframes.push(document.getElementById("tile_view_iframe_{{ plugin.name }}"));
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.name }}');
};
}
})();
</script>
</div>
</div>
{% else %}
<div class="quickaccesscontrols_PanelSectionRow_26R5w">
<div onclick="loadPlugin('{{ 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;">
<div class="basicdialog_FieldChildren_279n8">
<button type="button" tabindex="0"
class="DialogButton _DialogLayout Secondary basicdialog_Button_1Ievp Focusable">{{ plugin.name
}}</button>
{% 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>
</div>
{% endif %}
{% endfor %}
{% endfor %}
</div>
+103 -8
View File
@@ -1,4 +1,6 @@
from aiohttp import ClientSession
from injector import inject_to_tab
import uuid
class Utilities:
def __init__(self, context) -> None:
@@ -6,7 +8,11 @@ class Utilities:
self.util_methods = {
"ping": self.ping,
"http_request": self.http_request,
"confirm_plugin_install": self.confirm_plugin_install
"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,
"open_plugin_store": self.open_plugin_store
}
async def confirm_plugin_install(self, request_id):
@@ -14,12 +20,101 @@ class Utilities:
async def http_request(self, method="", url="", **kwargs):
async with ClientSession() as web:
res = await web.request(method, url, **kwargs)
return {
"status": res.status,
"headers": dict(res.headers),
"body": await res.text()
}
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"
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
}
async def open_plugin_store(self):
await inject_to_tab("SP", """
(function() {
wpRequire = webpackJsonp.push([[], { get_require: (mod, _exports, wpRequire) => mod.exports = wpRequire }, [["get_require"]]]);
const all = () => Object.keys(wpRequire.c).map((x) => wpRequire.c[x].exports).filter((x) => x);
router = all().map(m => {
if (typeof m !== "object") return undefined;
for (let prop in m) { if (m[prop]?.Navigate) return m[prop]}
}).find(x => x)
router.NavigateToExternalWeb("http://127.0.0.1:1337/browser/redirect")
})();
""")