0.0.1
This commit is contained in:
0
__init__.py
Normal file
0
__init__.py
Normal file
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>
|
||||||
|
'''
|
||||||
34
providers/base.py
Normal file
34
providers/base.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
class EmbedProvider:
|
||||||
|
name = "base"
|
||||||
|
pattern = None
|
||||||
|
|
||||||
|
def __init__(self, config=None):
|
||||||
|
self.config = config or {}
|
||||||
|
|
||||||
|
def match(self, content):
|
||||||
|
if not self.pattern:
|
||||||
|
return content
|
||||||
|
return re.sub(self.pattern, self.replace, content)
|
||||||
|
|
||||||
|
def replace(self, match):
|
||||||
|
raise NotImplementedError("Provider replace() missing!")
|
||||||
|
|
||||||
|
def wrap_template(self, provider, embed_html):
|
||||||
|
return f'''
|
||||||
|
<div class="embed-consent" data-provider="{provider}">
|
||||||
|
<div class="embed-overlay">
|
||||||
|
<div class="embed-box">
|
||||||
|
<h3>External Content</h3>
|
||||||
|
<p>This content is provided by <strong>{provider}</strong> and may set cookies.</p>
|
||||||
|
<button class="embed-accept" onclick="acceptProvider('{provider}')">
|
||||||
|
Load Content
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template class="embed-template">
|
||||||
|
{embed_html}
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
'''
|
||||||
18
providers/coub.py
Normal file
18
providers/coub.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from .base import EmbedProvider
|
||||||
|
|
||||||
|
class CoubProvider(EmbedProvider):
|
||||||
|
name = "coub"
|
||||||
|
pattern = r"\{\{COUB:([A-Za-z0-9]+)\}\}"
|
||||||
|
|
||||||
|
def replace(self, match):
|
||||||
|
coub_id = match.group(1)
|
||||||
|
|
||||||
|
embed_html = f'''
|
||||||
|
<iframe src="https://coub.com/embed/{coub_id}"
|
||||||
|
width="640"
|
||||||
|
height="480"
|
||||||
|
frameborder="0"
|
||||||
|
allowfullscreen>
|
||||||
|
</iframe>
|
||||||
|
'''
|
||||||
|
return self.wrap_template("coub", embed_html)
|
||||||
15
providers/reddit.py
Normal file
15
providers/reddit.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
from .base import EmbedProvider
|
||||||
|
|
||||||
|
class RedditProvider(EmbedProvider):
|
||||||
|
name = "reddit"
|
||||||
|
pattern = r"\{\{REDDIT:(https?://[^\}]+)\}\}"
|
||||||
|
|
||||||
|
def replace(self, match):
|
||||||
|
url = match.group(1)
|
||||||
|
|
||||||
|
embed_html = f'''
|
||||||
|
<blockquote class="reddit-embed-bq" data-embed-locale="en-EN" data-embed-theme="dark" data-embed-showedits="false" data-embed-created="2026-02-25T21:30:20.003Z" data-embed-showusername="false" style="height:500px" data-embed-height="546" >
|
||||||
|
<a href="{url}"></a>
|
||||||
|
</blockquote><script async="" src="https://embed.reddit.com/widgets.js" charset="UTF-8"></script>
|
||||||
|
'''
|
||||||
|
return self.wrap_template("reddit", embed_html)
|
||||||
19
providers/youtube.py
Normal file
19
providers/youtube.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from .base import EmbedProvider
|
||||||
|
|
||||||
|
class YouTubeProvider(EmbedProvider):
|
||||||
|
name = "youtube"
|
||||||
|
pattern = r"\{\{YOUTUBE:([A-Za-z0-9_-]+)\}\}"
|
||||||
|
|
||||||
|
def replace(self, match):
|
||||||
|
video_id = match.group(1)
|
||||||
|
nocookie = self.config.get("nocookie", True)
|
||||||
|
domain = "www.youtube-nocookie.com" if nocookie else "www.youtube.com"
|
||||||
|
|
||||||
|
embed_html = f'''
|
||||||
|
<iframe width="560" height="315"
|
||||||
|
src="https://{domain}/embed/{video_id}"
|
||||||
|
frameborder="0"
|
||||||
|
allowfullscreen>
|
||||||
|
</iframe>
|
||||||
|
'''
|
||||||
|
return self.wrap_template("youtube", embed_html)
|
||||||
Reference in New Issue
Block a user