mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-06-13 12:15:09 +03:00
Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 414e0da2f3 | |||
| cb9b888dc6 | |||
| f3ab0f5989 | |||
| e132aba0f8 | |||
| 0d0e57e35a | |||
| 945db5de47 | |||
| 28746e3962 | |||
| 279b1e8c40 | |||
| 89ecca7c30 | |||
| 7d74e98f4f | |||
| fe1f6473e9 | |||
| 73559ae8c7 | |||
| 340ea91d1c | |||
| 3f3f6bd475 | |||
| 4b2f8cd8f5 | |||
| 604006a7cb | |||
| 7aa4e9106a | |||
| fa776f0d0b | |||
| 4576fed01b | |||
| de435e22fb | |||
| 6694d5ab71 | |||
| c084abecfc | |||
| f685eeb420 | |||
| 6250fafa6e | |||
| efa5dc61c7 | |||
| e3d7b50bd9 | |||
| 0359fd966a | |||
| fe9faefd0b | |||
| 012274b1a0 | |||
| 070d11154f | |||
| 02f73b795d | |||
| 4ffe2fdf24 |
Vendored
+14
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Stop Service",
|
||||
"type": "shell",
|
||||
"command":"systemctl --user stop plugin_loader",
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
Vendored
+16
-7
@@ -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
|
||||
|
||||
Vendored
+15
-12
@@ -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
|
||||
|
||||
Vendored
+17
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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()
|
||||
|
||||
@@ -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"]
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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");
|
||||
@@ -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
@@ -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")
|
||||
})();
|
||||
""")
|
||||
Reference in New Issue
Block a user