backwards-compatible plugin store, legacy plugin library

This commit is contained in:
AAGaming
2022-06-30 16:04:29 -04:00
parent da0f7dd337
commit d72f364a8d
6 changed files with 194 additions and 38 deletions
+12 -16
View File
@@ -12,21 +12,20 @@ from hashlib import sha256
from subprocess import Popen
class PluginInstallContext:
def __init__(self, gh_url, version, hash) -> None:
self.gh_url = gh_url
def __init__(self, artifact, name, version, hash) -> None:
self.artifact = artifact
self.name = name
self.version = version
self.hash = hash
class PluginBrowser:
def __init__(self, plugin_path, server_instance, store_url) -> None:
def __init__(self, plugin_path, server_instance) -> None:
self.log = getLogger("browser")
self.plugin_path = plugin_path
self.store_url = store_url
self.install_requests = {}
server_instance.add_routes([
web.post("/browser/install_plugin", self.install_plugin),
web.get("/browser/redirect", self.redirect_to_store)
web.post("/browser/install_plugin", self.install_plugin)
])
def _unzip_to_plugin_dir(self, zip, name, hash):
@@ -40,12 +39,12 @@ class PluginBrowser:
Popen(["chmod", "-R", "555", self.plugin_path])
return True
async def _install(self, name, version, hash):
async def _install(self, artifact, name, version, hash):
rmtree(path.join(self.plugin_path, name), ignore_errors=True)
self.log.info(f"Installing {name} (Version: {version})")
async with ClientSession() as client:
self.log.debug(f"Fetching {name}")
res = await client.get(name)
self.log.debug(f"Fetching {artifact}")
res = await client.get(artifact)
if res.status == 200:
self.log.debug("Got 200. Reading...")
data = await res.read()
@@ -67,24 +66,21 @@ class PluginBrowser:
else:
self.log.fatal(f"Could not fetch from URL. {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.get("name", "No name"), data.get("version", "dev"), data.get("hash", False)))
get_event_loop().create_task(self.request_plugin_install(data.get("artifact", ""), data.get("name", "No name"), data.get("version", "dev"), data.get("hash", False)))
return web.Response(text="Requested plugin install")
async def request_plugin_install(self, name, version, hash):
async def request_plugin_install(self, artifact, name, version, hash):
request_id = str(time())
self.install_requests[request_id] = PluginInstallContext(name, version, hash)
self.install_requests[request_id] = PluginInstallContext(artifact, name, version, hash)
tab = await get_tab("SP")
await tab.open_websocket()
await tab.evaluate_js(f"DeckyPluginLoader.addPluginInstallPrompt('{name}', '{version}', '{request_id}', '{hash}')")
async def confirm_plugin_install(self, request_id):
request = self.install_requests.pop(request_id)
await self._install(request.gh_url, request.version, request.hash)
await self._install(request.artifact, request.name, request.version, request.hash)
def cancel_plugin_install(self, request_id):
self.install_requests.pop(request_id)
+78
View File
@@ -0,0 +1,78 @@
class PluginEventTarget extends EventTarget { }
method_call_ev_target = new PluginEventTarget();
window.addEventListener("message", function(evt) {
let ev = new Event(evt.data.call_id);
ev.data = evt.data.result;
method_call_ev_target.dispatchEvent(ev);
}, false);
async function call_server_method(method_name, arg_object={}) {
const response = await fetch(`http://127.0.0.1:1337/methods/${method_name}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(arg_object),
});
const dta = await response.json();
if (!dta.success) throw dta.result;
return dta.result;
}
// Source: https://stackoverflow.com/a/2117523 Thanks!
function uuidv4() {
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
);
}
async function fetch_nocors(url, request={}) {
let args = { method: "POST", headers: {}, body: "" };
request = {...args, ...request};
request.url = url;
request.data = request.body;
delete request.body; //maintain api-compatibility with fetch
return await call_server_method("http_request", request);
}
async function call_plugin_method(method_name, arg_object={}) {
if (plugin_name == undefined)
throw new Error("Plugin methods can only be called from inside plugins (duh)");
const response = await fetch(`http://127.0.0.1:1337/plugins/${plugin_name}/methods/${method_name}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
args: arg_object,
}),
});
const dta = await response.json();
if (!dta.success) throw dta.result;
return dta.result;
}
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
});
}
+3 -3
View File
@@ -116,7 +116,7 @@ class Loader:
self.logger.info(f"Plugin {plugin.name} is passive")
self.plugins[plugin.name] = plugin.start()
self.logger.info(f"Loaded {plugin.name}")
self.loop.create_task(self.dispatch_plugin(plugin.name))
self.loop.create_task(self.dispatch_plugin(plugin.name if not plugin.legacy else "$LEGACY_" + plugin.name))
except Exception as e:
self.logger.error(f"Could not load {file}. {e}")
print_exc()
@@ -168,8 +168,8 @@ class Loader:
with open(path.join(self.plugin_path, plugin.plugin_directory, plugin.main_view_html), 'r') as template:
template_data = template.read()
ret = f"""
<script src="/static/legacy-library.js"></script>
<script>const plugin_name = '{plugin.name}' </script>
<script src="/legacy/library.js"></script>
<script>window.plugin_name = '{plugin.name}' </script>
<base href="http://127.0.0.1:1337/plugins/plugin_resource/{plugin.name}/">
{template_data}
"""
+3 -3
View File
@@ -9,8 +9,7 @@ CONFIG = {
"server_host": getenv("SERVER_HOST", "127.0.0.1"),
"server_port": int(getenv("SERVER_PORT", "1337")),
"live_reload": getenv("LIVE_RELOAD", "1") == "1",
"log_level": {"CRITICAL": 50, "ERROR": 40, "WARNING":30, "INFO": 20, "DEBUG": 10}[getenv("LOG_LEVEL", "INFO")],
"store_url": getenv("STORE_URL", "https://beta.deckbrew.xyz")
"log_level": {"CRITICAL": 50, "ERROR": 40, "WARNING":30, "INFO": 20, "DEBUG": 10}[getenv("LOG_LEVEL", "INFO")]
}
basicConfig(level=CONFIG["log_level"], format="[%(module)s][%(levelname)s]: %(message)s")
@@ -44,7 +43,7 @@ class PluginManager:
allow_headers="*")
})
self.plugin_loader = Loader(self.web_app, CONFIG["plugin_path"], self.loop, CONFIG["live_reload"])
self.plugin_browser = PluginBrowser(CONFIG["plugin_path"], self.web_app, CONFIG["store_url"])
self.plugin_browser = PluginBrowser(CONFIG["plugin_path"], self.web_app)
self.utilities = Utilities(self)
jinja_setup(self.web_app)
@@ -57,6 +56,7 @@ class PluginManager:
for route in list(self.web_app.router.routes()):
self.cors.add(route)
self.web_app.add_routes([static("/static", path.join(path.dirname(__file__), 'static'))])
self.web_app.add_routes([static("/legacy", path.join(path.dirname(__file__), 'legacy'))])
def exception_handler(self, loop, context):
if context["message"] == "Unclosed connection":
+59 -14
View File
@@ -10,16 +10,26 @@ import {
} from 'decky-frontend-lib';
import { FC, useRef, useState } from 'react';
import { StorePlugin, StorePluginVersion, requestPluginInstall } from './Store';
import {
LegacyStorePlugin,
StorePlugin,
StorePluginVersion,
requestLegacyPluginInstall,
requestPluginInstall,
} from './Store';
interface PluginCardProps {
plugin: StorePlugin;
plugin: StorePlugin | LegacyStorePlugin;
}
const classNames = (...classes: string[]) => {
return classes.join(' ');
};
function isLegacyPlugin(plugin: LegacyStorePlugin | StorePlugin): plugin is LegacyStorePlugin {
return 'artifact' in plugin;
}
const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
const [selectedOption, setSelectedOption] = useState<number>(0);
const buttonRef = useRef<HTMLDivElement>(null);
@@ -36,10 +46,10 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
<Focusable
// className="Panel Focusable"
ref={containerRef}
onActivate={(e: CustomEvent) => {
onActivate={(_: CustomEvent) => {
buttonRef.current!.focus();
}}
onCancel={(e: CustomEvent) => {
onCancel={(_: CustomEvent) => {
if (containerRef.current!.querySelectorAll('* :focus').length === 0) {
Router.NavigateBackOrOpenMenu();
setTimeout(() => Router.OpenQuickAccessMenu(QuickAccessTab.Decky), 1000);
@@ -64,7 +74,14 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
className={classNames(staticClasses.Text)}
// onClick={() => Router.NavigateToExternalWeb('https://github.com/' + plugin.artifact)}
>
{plugin.name}
{isLegacyPlugin(plugin) ? (
<div>
<span style={{ color: 'grey' }}>{plugin.artifact.split('/')[0]}/</span>
{plugin.artifact.split('/')[1]}
</div>
) : (
plugin.name
)}
</a>
</div>
<div
@@ -79,10 +96,17 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
width: 'auto',
height: '160px',
}}
src={`https://cdn.tzatzikiweeb.moe/file/steam-deck-homebrew/artifact_images/${plugin.name.replace(
'/',
'_',
)}.png`}
src={
isLegacyPlugin(plugin)
? `https://cdn.tzatzikiweeb.moe/file/steam-deck-homebrew/artifact_images/${plugin.artifact.replace(
'/',
'_',
)}.png`
: `https://cdn.tzatzikiweeb.moe/file/steam-deck-homebrew/artifact_images/${plugin.name.replace(
'/',
'_',
)}.png`
}
/>
<div
style={{
@@ -107,6 +131,18 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
{tag == 'root' ? 'Requires root' : tag}
</span>
))}
{isLegacyPlugin(plugin) && (
<span
style={{
padding: '5px',
marginRight: '10px',
borderRadius: '5px',
background: '#ACB2C947',
}}
>
legacy
</span>
)}
</p>
</div>
</div>
@@ -132,7 +168,11 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
>
<DialogButton
ref={buttonRef}
onClick={() => requestPluginInstall(plugin, plugin.versions[selectedOption])}
onClick={() =>
isLegacyPlugin(plugin)
? requestLegacyPluginInstall(plugin, Object.keys(plugin.versions)[selectedOption])
: requestPluginInstall(plugin, plugin.versions[selectedOption])
}
>
Install
</DialogButton>
@@ -144,10 +184,15 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
>
<Dropdown
rgOptions={
plugin.versions.map((version: StorePluginVersion, index) => ({
data: index,
label: version.name,
})) as SingleDropdownOption[]
(isLegacyPlugin(plugin)
? Object.keys(plugin.versions).map((v, k) => ({
data: k,
label: v,
}))
: plugin.versions.map((version: StorePluginVersion, index) => ({
data: index,
label: version.name,
}))) as SingleDropdownOption[]
}
strDefaultLabel={'Select a version'}
selectedOption={selectedOption}
+39 -2
View File
@@ -17,6 +17,16 @@ export interface StorePlugin {
tags: string[];
}
export interface LegacyStorePlugin {
artifact: string;
versions: {
[version: string]: string;
};
author: string;
description: string;
tags: string[];
}
export async function installFromURL(url: string) {
const formData = new FormData();
const splitURL = url.split('/');
@@ -28,6 +38,18 @@ export async function installFromURL(url: string) {
});
}
export async function requestLegacyPluginInstall(plugin: LegacyStorePlugin, selectedVer: string) {
const formData = new FormData();
formData.append('name', plugin.artifact);
formData.append('artifact', `https://github.com/${plugin.artifact}/archive/refs/tags/${selectedVer}.zip`);
formData.append('version', selectedVer);
formData.append('hash', plugin.versions[selectedVer]);
await fetch('http://localhost:1337/browser/install_plugin', {
method: 'POST',
body: formData,
});
}
export async function requestPluginInstall(plugin: StorePlugin, selectedVer: StorePluginVersion) {
const formData = new FormData();
formData.append('name', plugin.name);
@@ -42,6 +64,7 @@ export async function requestPluginInstall(plugin: StorePlugin, selectedVer: Sto
const StorePage: FC<{}> = () => {
const [data, setData] = useState<StorePlugin[] | null>(null);
const [legacyData, setLegacyData] = useState<LegacyStorePlugin[] | null>(null);
useEffect(() => {
(async () => {
@@ -49,6 +72,11 @@ const StorePage: FC<{}> = () => {
console.log(res);
setData(res);
})();
(async () => {
const res = await fetch('https://plugins.deckbrew.xyz/get_plugins', { method: 'GET' }).then((r) => r.json());
console.log(res);
setLegacyData(res);
})();
}, []);
return (
@@ -67,12 +95,21 @@ const StorePage: FC<{}> = () => {
height: '100%',
}}
>
{data === null ? (
{!data ? (
<div style={{ height: '100%' }}>
<SteamSpinner />
</div>
) : (
data.map((plugin: StorePlugin) => <PluginCard plugin={plugin} />)
<div>
{data.map((plugin: StorePlugin) => (
<PluginCard plugin={plugin} />
))}
{!legacyData ? (
<SteamSpinner />
) : (
legacyData.map((plugin: LegacyStorePlugin) => <PluginCard plugin={plugin} />)
)}
</div>
)}
</div>
</div>