0.0.1
This commit is contained in:
349
manager.py
Normal file
349
manager.py
Normal file
@@ -0,0 +1,349 @@
|
||||
import importlib
|
||||
import pkgutil
|
||||
import re
|
||||
from engine.plugin import Plugin
|
||||
from plugins.embed.providers.base import EmbedProvider
|
||||
|
||||
class EmbedConsentManager(Plugin):
|
||||
name = "embed"
|
||||
priority = 45
|
||||
|
||||
def __init__(self, config):
|
||||
super().__init__(config)
|
||||
self.providers = []
|
||||
self.load_providers()
|
||||
|
||||
def load_providers(self):
|
||||
package = "plugins.embed.providers"
|
||||
pkg = importlib.import_module(package)
|
||||
|
||||
for _, name, _ in pkgutil.iter_modules(pkg.__path__):
|
||||
module = importlib.import_module(f"{package}.{name}")
|
||||
for attr in dir(module):
|
||||
cls = getattr(module, attr)
|
||||
if isinstance(cls, type) and issubclass(cls, EmbedProvider) and cls is not EmbedProvider:
|
||||
if hasattr(cls, "pattern") and hasattr(cls, "name"):
|
||||
provider_config = self.config.get(cls.name, {})
|
||||
self.providers.append(cls(provider_config))
|
||||
|
||||
def after_markdown(self, builder, html):
|
||||
for provider in self.providers:
|
||||
html = provider.match(html)
|
||||
return html
|
||||
|
||||
return super().after_markdown(builder, html)
|
||||
def after_page_render(self, builder, page, content):
|
||||
pattern = r"<\/body>"
|
||||
content = re.sub(pattern, self.inject_assets() + "BLAAA</body>", content, 0, re.MULTILINE)
|
||||
return content
|
||||
|
||||
def after_build(self, builder):
|
||||
settings_path = builder.config.output_dir / "privacy-settings.html"
|
||||
settings_path.write_text(self.generate_settings_page(), encoding="utf-8")
|
||||
|
||||
def inject_assets(self):
|
||||
return self.css() + self.script()
|
||||
|
||||
def generate_settings_page(self):
|
||||
|
||||
cards = ""
|
||||
|
||||
for p in self.providers:
|
||||
cards += f"""
|
||||
<div class="provider-card">
|
||||
<div>
|
||||
<strong>{p.name.title()}</strong>
|
||||
<div style="font-size:0.9rem;color:var(--muted);">
|
||||
External content provider
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label class="switch">
|
||||
<input type="checkbox"
|
||||
onchange="toggleProvider('{p.name}', this)"
|
||||
id="toggle-{p.name}">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
"""
|
||||
|
||||
return f"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Privacy Settings</title>
|
||||
{self.css()}
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="settings-container">
|
||||
<h1>Privacy Settings</h1>
|
||||
<p>Control which external providers may load content on this site.</p>
|
||||
|
||||
{cards}
|
||||
|
||||
<button class="btn-danger" onclick="revokeAll()">
|
||||
Revoke All Permissions
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
{self.script()}
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {{
|
||||
{''.join([f"""
|
||||
if(localStorage.getItem("embed-provider-{p.name}") === "granted") {{
|
||||
document.getElementById("toggle-{p.name}").checked = true;
|
||||
}}
|
||||
""" for p in self.providers])}
|
||||
}});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
def css(self):
|
||||
return """
|
||||
<style>
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
}
|
||||
|
||||
/* ===== Embed Card ===== */
|
||||
.embed-consent {
|
||||
position: relative;
|
||||
margin: 30px 0;
|
||||
border-radius: 18px;
|
||||
overflow: hidden;
|
||||
backdrop-filter: blur(20px);
|
||||
background: var(--card);
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.embed-overlay {
|
||||
padding: 60px 25px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.embed-box h3 {
|
||||
margin-bottom: 10px;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.embed-box p {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.embed-accept {
|
||||
margin-top: 20px;
|
||||
padding: 12px 28px;
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.embed-accept:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
/* ===== Spinner ===== */
|
||||
.embed-spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
border: 5px solid rgba(255,255,255,0.2);
|
||||
border-top: 5px solid var(--accent);
|
||||
animation: spin 0.8s linear infinite;
|
||||
margin: 80px auto;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* ===== Settings Page ===== */
|
||||
.settings-container {
|
||||
max-width: 800px;
|
||||
margin: 80px auto;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.settings-container h1 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.settings-container p {
|
||||
color: var(--muted);
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
/* Provider Card */
|
||||
.provider-card {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 16px;
|
||||
backdrop-filter: blur(20px);
|
||||
background: var(--card);
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
transition: 0.2s ease;
|
||||
}
|
||||
|
||||
.provider-card:hover {
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
|
||||
/* Toggle Switch */
|
||||
.switch {
|
||||
position: relative;
|
||||
width: 52px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.switch input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
background: #ccc;
|
||||
border-radius: 50px;
|
||||
inset: 0;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
left: 3px;
|
||||
bottom: 3px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
transform: translateX(24px);
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn-danger {
|
||||
margin-top: 30px;
|
||||
padding: 12px 25px;
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
background: var(--danger);
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
</style>
|
||||
"""
|
||||
|
||||
|
||||
# ---------------------------------
|
||||
# JavaScript
|
||||
# ---------------------------------
|
||||
def script(self):
|
||||
return """
|
||||
<script>
|
||||
|
||||
function getState(provider) {
|
||||
return localStorage.getItem("embed-provider-" + provider);
|
||||
}
|
||||
|
||||
function setState(provider, state) {
|
||||
localStorage.setItem("embed-provider-" + provider, state);
|
||||
}
|
||||
|
||||
function acceptProvider(provider) {
|
||||
setState(provider, "granted");
|
||||
document.querySelectorAll('.embed-consent[data-provider="' + provider + '"]')
|
||||
.forEach(loadEmbed);
|
||||
}
|
||||
|
||||
function loadEmbed(container) {
|
||||
const template = container.querySelector(".embed-template");
|
||||
if (!template) return;
|
||||
|
||||
container.innerHTML = '<div class="embed-spinner"></div>';
|
||||
|
||||
setTimeout(() => {
|
||||
const clone = template.content.cloneNode(true);
|
||||
container.innerHTML = "";
|
||||
container.appendChild(clone);
|
||||
container.style.opacity = "0";
|
||||
setTimeout(()=> container.style.opacity="1",50);
|
||||
}, 600);
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function(){
|
||||
document.querySelectorAll(".embed-consent").forEach(container => {
|
||||
const provider = container.dataset.provider;
|
||||
if (getState(provider) === "granted") {
|
||||
loadEmbed(container);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function toggleProvider(provider, checkbox) {
|
||||
if (checkbox.checked) {
|
||||
setState(provider, "granted");
|
||||
} else {
|
||||
setState(provider, "denied");
|
||||
}
|
||||
}
|
||||
|
||||
function revokeAll() {
|
||||
Object.keys(localStorage).forEach(k=>{
|
||||
if(k.startsWith("embed-provider-")) localStorage.removeItem(k);
|
||||
});
|
||||
alert("All provider consent revoked.");
|
||||
}
|
||||
</script>
|
||||
"""
|
||||
|
||||
def revocation_script(self):
|
||||
return '''
|
||||
<script>
|
||||
function revokeProvider(provider){
|
||||
localStorage.removeItem("embed-consent-" + provider);
|
||||
alert(provider + " consent revoked. Reloading.");
|
||||
location.reload();
|
||||
}
|
||||
|
||||
function revokeAll(){
|
||||
Object.keys(localStorage).forEach(k=>{
|
||||
if(k.startsWith("embed-consent-")) localStorage.removeItem(k);
|
||||
});
|
||||
alert("All embed consent revoked. Reloading.");
|
||||
location.reload();
|
||||
}
|
||||
</script>
|
||||
'''
|
||||
Reference in New Issue
Block a user