from __future__ import annotations import json import logging import re from pathlib import Path from PySide6.QtWebEngineCore import ( QWebEngineProfile, QWebEngineScript, ) from config import ( PLUGIN_BLOCKED_PATTERNS, PLUGIN_CRASH_LIMIT, PLUGIN_MAX_SIZE, PLUGIN_PERMISSIONS, profile_extension_dir, profile_plugin_dir, profile_theme_dir, profile_vencord_dir, ) log = logging.getLogger(__name__) SCRIPT_WORLD = ( QWebEngineScript.ScriptWorldId.ApplicationWorld ) PLUGIN_FILE_PATTERN = ( "*.plugin.js" ) def _js_string( value: str, ) -> str: return json.dumps( value, ensure_ascii=False, ) class ThemeManager: def __init__( self, profile_name: str, ): self.profile_name = profile_name self.directory = profile_theme_dir( profile_name ) def discover( self, ) -> list[Path]: return sorted( self.directory.glob( "*.css" ) ) def load_scripts( self, ) -> list[QWebEngineScript]: scripts = [] for css_file in self.discover(): try: css = css_file.read_text( encoding="utf-8" ) script = ( QWebEngineScript() ) script.setName( f"theme:{css_file.stem}" ) script.setRunsOnSubFrames( True ) script.setInjectionPoint( QWebEngineScript.InjectionPoint.DocumentReady ) script.setWorldId( SCRIPT_WORLD ) script.setSourceCode( f""" (() => {{ const id = "theme-{css_file.stem}"; if (document.getElementById(id)) return; const style = document.createElement("style"); style.id = id; style.textContent = {_js_string(css)}; const install = () => {{ if (!document.head) {{ requestAnimationFrame(install); return; }} document.head.appendChild(style); }}; install(); }})(); """ ) scripts.append( script ) except Exception: log.exception( "theme load failed: %s", css_file, ) return scripts class ExtensionManager: def __init__( self, profile_name: str, ): self.profile_name = profile_name self.directory = ( profile_extension_dir( profile_name ) ) def discover( self, ) -> list[Path]: if not self.directory.exists(): return [] return sorted( path for path in self.directory.iterdir() if path.is_dir() ) def load_scripts( self, ) -> list[QWebEngineScript]: scripts = [] for extension in self.discover(): manifest_file = ( extension / "manifest.json" ) script_file = ( extension / "script.js" ) if ( not manifest_file.exists() or not script_file.exists() ): continue try: manifest = json.loads( manifest_file.read_text( encoding="utf-8" ) ) if not manifest.get( "enabled", True, ): continue source = ( script_file.read_text( encoding="utf-8" ) ) script = ( QWebEngineScript() ) script.setName( manifest.get( "name", extension.name, ) ) script.setRunsOnSubFrames( True ) script.setInjectionPoint( QWebEngineScript.InjectionPoint.DocumentCreation ) script.setWorldId( SCRIPT_WORLD ) script.setSourceCode( source ) scripts.append( script ) except Exception: log.exception( "extension load failed: %s", extension, ) return scripts class PluginManager: def __init__( self, profile_name: str, db=None, ): self.profile_name = profile_name self.db = db self.directory = profile_plugin_dir( profile_name ) self.directory.mkdir( parents=True, exist_ok=True, ) self.quarantine_dir = ( profile_vencord_dir( profile_name ) / "quarantine" ) self.quarantine_dir.mkdir( parents=True, exist_ok=True, ) def discover( self, ) -> list[Path]: plugins = [] for plugin in sorted( self.directory.glob( PLUGIN_FILE_PATTERN ) ): try: meta = self.metadata( plugin ) if ( self.db and not self.db.plugin_enabled( self.profile_name, meta["name"], True, ) ): continue plugins.append( plugin ) except Exception: log.exception( "plugin discovery failed: %s", plugin, ) return plugins def metadata( self, plugin_file: Path, ) -> dict: source = plugin_file.read_text( encoding="utf-8", errors="ignore", ) permissions = set() for match in re.findall( r"@permissions\s+(.+)", source, re.IGNORECASE, ): permissions.update( token.strip().lower() for token in match.split() if token.strip() ) permissions &= PLUGIN_PERMISSIONS metadata["permissions"] = sorted( permissions ) name = plugin_file.name if name.endswith( ".plugin.js" ): name = name[:-10] metadata = { "name": name, "version": "unknown", "author": "", "description": "", "enabled": True, "path": str( plugin_file ), "size": plugin_file.stat().st_size, } patterns = { "name": r"@name\s+(.+)", "version": r"@version\s+(.+)", "author": r"@author\s+(.+)", "description": r"@description\s+(.+)", } for key, pattern in ( patterns.items() ): match = re.search( pattern, source, re.IGNORECASE, ) if match: metadata[key] = ( match.group(1) .strip() ) if self.db: metadata[ "enabled" ] = self.db.plugin_enabled( self.profile_name, metadata["name"], True, ) return metadata def validate_file( self, source: Path, ) -> tuple[ bool, str, ]: filename = ( source.name.lower() ) if not filename.endswith( ".plugin.js" ): return ( False, "invalid extension", ) if ( source.stat().st_size > PLUGIN_MAX_SIZE ): return ( False, "plugin exceeds size limit", ) content = ( source.read_text( encoding="utf-8", errors="ignore", ) ) lowered = ( content.lower() ) for pattern in ( PLUGIN_BLOCKED_PATTERNS ): if ( pattern.lower() in lowered ): return ( False, f"blocked pattern: {pattern}", ) return ( True, "", ) def bootstrap_script( self, ) -> QWebEngineScript: script = ( QWebEngineScript() ) script.setName( "plugin-runtime" ) script.setRunsOnSubFrames( True ) script.setInjectionPoint( QWebEngineScript.InjectionPoint.DocumentCreation ) script.setWorldId( SCRIPT_WORLD ) script.setSourceCode( """ (() => { if (window.PluginRuntime) return; window.__bridgeReady = new Promise(resolve => { const complete = () => { if ( window.bridge ) { resolve( window.bridge ); return; } setTimeout( complete, 25, ); }; if ( typeof qt !== "undefined" && typeof QWebChannel !== "undefined" ) { new QWebChannel( qt.webChannelTransport, channel => { window.bridge = channel.objects.bridge; resolve( window.bridge ); } ); return; } complete(); }); window.PluginRuntime = { plugins:{}, failed:{}, started:{}, hasPermission( plugin, permission ){ return !!( this.permissions[ plugin ]?.[permission] ); }, async bridge(){ return await ( window.__bridgeReady ); }, log(name,msg){ console.log( `[Plugin:${name}]`, msg ); }, async fail( name, error ){ console.error( `[Plugin:${name}]`, error ); this.failed[name] = String(error); try{ const bridge = await this.bridge(); bridge.pluginCrash( name, String(error) ); } catch{} } }; window.BdApi = { Data:{ async load( plugin, key ){ try{ const bridge = await PluginRuntime.bridge(); const raw = bridge.pluginLoad( plugin, key ); return JSON.parse( raw ); } catch{ return null; } }, async save( plugin, key, value ){ try{ const bridge = await PluginRuntime.bridge(); bridge.pluginSave( plugin, key, JSON.stringify( value ) ); } catch{} } }, Logger:{ async log( ...a ){ try{ const bridge = await PluginRuntime.bridge(); bridge.pluginLog( "runtime", "info", a.join(" ") ); } catch{} console.log( ...a ); }, async warn( ...a ){ try{ const bridge = await PluginRuntime.bridge(); bridge.pluginLog( "runtime", "warning", a.join(" ") ); } catch{} console.warn( ...a ); }, async error( ...a ){ try{ const bridge = await PluginRuntime.bridge(); bridge.pluginLog( "runtime", "error", a.join(" ") ); } catch{} console.error( ...a ); } }, UI:{ showToast( message ){ console.log( "[Toast]", message ); } }, DOM:{ addStyle( id, css ){ let style = document.getElementById( id ); if(!style){ style = document.createElement( "style" ); style.id = id; document.head.appendChild( style ); } style.textContent = css; }, removeStyle( id ){ document .getElementById( id ) ?.remove(); } } }; window.ZeresPluginLibrary = { Logger: BdApi.Logger, Utilities:{}, DOMTools: BdApi.DOM, PluginUpdater:{ checkForUpdate(){} }, Patcher:{ before(){}, after(){}, instead(){}, unpatchAll(){} } }; })(); """ ) return script def plugin_script( self, plugin_file: Path, ) -> QWebEngineScript: meta = self.metadata( plugin_file ) source = ( plugin_file.read_text( encoding="utf-8", errors="ignore", ) ) wrapped = f""" (() => {{ const module = {{ exports:null }}; const exports = module.exports; try {{ {source} const PluginClass = module.exports; if( typeof PluginClass !== "function" ) return; const plugin = new PluginClass(); plugin.__permissions = Object.freeze( %PERMISSIONS% ); PluginRuntime.plugins[ {_js_string(meta["name"])} ] = plugin; try {{ if( typeof plugin.start === "function" ) plugin.start(); else if( typeof plugin.onStart === "function" ) plugin.onStart(); PluginRuntime.started[ {_js_string(meta["name"])} ] = true; }} catch(error) {{ PluginRuntime.fail( {_js_string(meta["name"])}, error ); }} }} catch(error) {{ PluginRuntime.fail( {_js_string(meta["name"])}, error ); }} }})(); """ script = ( QWebEngineScript() ) script.setName( f"plugin:{meta['name']}" ) script.setRunsOnSubFrames( True ) script.setInjectionPoint( QWebEngineScript.InjectionPoint.DocumentCreation ) script.setWorldId( SCRIPT_WORLD ) script.setSourceCode( wrapped ) return script def load_scripts( self, ) -> list[QWebEngineScript]: scripts = [ self.bootstrap_script() ] for plugin in ( self.discover() ): try: scripts.append( self.plugin_script( plugin ) ) except Exception: log.exception( "plugin load failed: %s", plugin, ) if self.db: self.db.plugin_crash( self.profile_name, plugin.stem, "load failure", ) return scripts def install_file( self, source: Path, ) -> Path: valid, error = ( self.validate_file( plugin ) ) if not valid: self.quarantine_plugin( plugin ) continue if self.is_quarantined( meta["name"] ): continue filename = re.sub( r"[^a-zA-Z0-9._-]", "_", source.name, ) destination = ( self.directory / filename ) destination.write_text( source.read_text( encoding="utf-8", errors="ignore", ), encoding="utf-8", ) if ( self.db and self.db.plugin_crash_count( self.profile_name, meta["name"], ) >= PLUGIN_CRASH_LIMIT ): self.quarantine_plugin( plugin ) continue return destination def uninstall( self, plugin_name: str, ) -> bool: for plugin in ( self.directory.glob( PLUGIN_FILE_PATTERN ) ): meta = self.metadata( plugin ) if ( meta["name"] != plugin_name ): continue plugin.unlink( missing_ok=True ) if self.db: self.db.delete_plugin( self.profile_name, plugin_name, ) return True return False def enable( self, plugin_name: str, ) -> None: if self.db: self.db.set_plugin_enabled( self.profile_name, plugin_name, True, ) def disable( self, plugin_name: str, ) -> None: if self.db: self.db.set_plugin_enabled( self.profile_name, plugin_name, False, ) def inventory( self, ) -> list[dict]: rows = [] for plugin in sorted( self.directory.glob( PLUGIN_FILE_PATTERN ) ): try: rows.append( self.metadata( plugin ) ) except Exception: log.exception( "inventory failed: %s", plugin, ) return rows def diagnostics( self, ) -> dict: plugins = ( self.inventory() ) return { "profile": self.profile_name, "plugin_count": len( plugins ), "enabled_count": len( [ p for p in plugins if p.get( "enabled" ) ] ), "disabled_count": len( [ p for p in plugins if not p.get( "enabled" ) ] ), "plugins": plugins, } def quarantine_plugin( self, plugin_file: Path, ) -> None: try: target = ( self.quarantine_dir / plugin_file.name ) plugin_file.replace( target ) meta = self.metadata( target ) if self.db: self.db.set_plugin_enabled( self.profile_name, meta["name"], False, ) self.db.set_plugin_quarantined( self.profile_name, meta["name"], True, ) except Exception: log.exception( "plugin quarantine failed" ) def is_quarantined( self, plugin_name: str, ) -> bool: if not self.db: return False return self.db.plugin_quarantined( self.profile_name, plugin_name, ) # v8 class AddonManager: def __init__( self, profile_name: str, db=None, ): self.profile_name = profile_name self.db = db self.themes = ThemeManager( profile_name ) self.extensions = ( ExtensionManager( profile_name ) ) self.plugins = ( PluginManager( profile_name, db, ) ) def install( self, profile: QWebEngineProfile, ) -> None: collection = ( profile.scripts() ) existing = { collection.script( i ).name() for i in range( collection.count() ) } for script in ( self.themes.load_scripts() ): if ( script.name() not in existing ): collection.insert( script ) for script in ( self.extensions.load_scripts() ): if ( script.name() not in existing ): collection.insert( script ) for script in ( self.plugins.load_scripts() ): if ( script.name() not in existing ): collection.insert( script ) def inventory( self, ) -> dict: themes = [] extensions = [] plugins = ( self.plugins.inventory() ) for theme in ( self.themes.discover() ): themes.append( { "name": theme.stem, "type": "theme", "enabled": True, "path": str( theme ), } ) for extension in ( self.extensions.discover() ): manifest = ( extension / "manifest.json" ) enabled = True try: if ( manifest.exists() ): enabled = bool( json.loads( manifest.read_text( encoding="utf-8" ) ).get( "enabled", True, ) ) except Exception: enabled = False extensions.append( { "name": extension.name, "type": "extension", "enabled": enabled, "path": str( extension ), } ) return { "themes": themes, "extensions": extensions, "plugins": plugins, } def diagnostics( self, ) -> dict: plugin_diag = ( self.plugins.diagnostics() ) return { "profile": self.profile_name, "themes": len( self.themes.discover() ), "extensions": len( self.extensions.discover() ), "plugins": plugin_diag, } def reload( self, profile: QWebEngineProfile, ) -> None: profile.scripts().clear() self.install( profile ) def install_plugin( self, source: Path, ) -> Path: return ( self.plugins.install_file( source ) ) def uninstall_plugin( self, plugin_name: str, ) -> bool: return ( self.plugins.uninstall( plugin_name ) ) def enable_plugin( self, plugin_name: str, ) -> None: self.plugins.enable( plugin_name ) def disable_plugin( self, plugin_name: str, ) -> None: self.plugins.disable( plugin_name ) def plugin_inventory( self, ) -> list[dict]: return ( self.plugins.inventory() ) def plugin_diagnostics( self, ) -> dict: return ( self.plugins.diagnostics() )