use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use dioxus::desktop::tao::dpi::{LogicalPosition, LogicalSize};
use dioxus::desktop::tao::event_loop::EventLoopWindowTarget;
use dioxus::desktop::tao::window::{Window, WindowBuilder};
use dioxus::desktop::wry::{WebView, WebViewBuilder};
use server::ytmusic::botguard::{self, MintRequest};
use tokio::sync::mpsc;
#[cfg(target_os = "linux")]
use dioxus::desktop::tao::platform::unix::WindowExtUnix;
#[cfg(target_os = "linux")]
use dioxus::desktop::wry::WebViewBuilderExtUnix;
const BGUTILS: &str = include_str!("bgutils.js");
const UA: &str = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) \
Chrome/131.0.0.0 Safari/537.36";
static REQ_ID: AtomicU64 = AtomicU64::new(1);
static WANT: AtomicBool = AtomicBool::new(false);
pub fn request() {
WANT.store(true, Ordering::Relaxed);
}
type Pending = Rc<RefCell<HashMap<u64, tokio::sync::oneshot::Sender<Result<String, String>>>>>;
struct Minter {
_window: Window,
webview: WebView,
pending: Pending,
rx: mpsc::UnboundedReceiver<MintRequest>,
}
thread_local! {
static INSTALLED: Cell<bool> = const { Cell::new(false) };
static STATE: RefCell<Option<Minter>> = const { RefCell::new(None) };
}
fn init_script() -> String {
format!(
r#"
window.module = {{ exports: {{}} }}; window.exports = window.module.exports;
// Chromium (WebView2) enforces Trusted Types on `new Function`/`eval`; the
// BotGuard VM does unwrapped `new Function(...)` internally, so a permissive
// `default` policy is needed to let those through. WebKit (Linux/macOS) ignores
// this (no enforcement). Best-effort: a pre-existing default policy throws.
try {{
if (window.trustedTypes && window.trustedTypes.createPolicy) {{
window.trustedTypes.createPolicy('default', {{
createHTML: function(s) {{ return s; }},
createScript: function(s) {{ return s; }},
createScriptURL: function(s) {{ return s; }}
}});
}}
}} catch (e) {{}}
{BGUTILS}
window.__KOPUZ_BGX = (window.module && window.module.exports) || null;
window.__kopuzMinter = null;
window.__kopuzMinterExp = 0;
window.__kopuzMinting = null;
// Diagnostic side-channel (no reqId): forwards minter progress/errors to the
// Rust side so a broken minter is debuggable from logs (issue: BotGuard VM
// throwing "Function@[native code]" on some webview setups).
window.__kopuzDiag = function(m) {{ try {{ window.ipc.postMessage(JSON.stringify({{diag: '' + m}})); }} catch(e) {{}} }};
window.__kopuzEnsureMinter = function(quiet) {{
var now = Date.now();
if (window.__kopuzMinter && now < window.__kopuzMinterExp) return Promise.resolve(window.__kopuzMinter);
if (window.__kopuzMinting) return window.__kopuzMinting;
window.__kopuzMinting = (async function() {{
var step = 'capture_bg';
try {{
var X = window.__KOPUZ_BGX;
if (!X || !X.BG) throw new Error('BgUtils BG not captured (page structure changed?)');
var BG = X.BG;
var bgConfig = {{
fetch: function(u, o) {{ return fetch(u, o); }},
globalObj: window, identifier: '', requestKey: "O43z0dpjhgX20SCx4KAo"
}};
step = 'challenge_create';
var ch = await BG.Challenge.create(bgConfig);
if (!ch) throw new Error('null challenge');
step = 'run_interpreter';
var js = ch.interpreterJavascript && ch.interpreterJavascript.privateDoNotAccessOrElseSafeScriptWrappedValue;
if (js) {{
var src = js;
if (window.trustedTypes && window.trustedTypes.createPolicy) {{
var pol = window.trustedTypes.createPolicy('kopuz-bg', {{ createScript: function(s){{ return s; }} }});
src = pol.createScript(js);
}}
new Function(src)();
}}
step = 'botguard_create';
var botguard = await BG.BotGuardClient.create({{ program: ch.program, globalName: ch.globalName, globalObj: window }});
step = 'snapshot';
var sig = [];
var botguardResponse = await botguard.snapshot({{ webPoSignalOutput: sig }});
step = 'generate_it';
var itUrl = X.buildURL('GenerateIT', bgConfig.useYouTubeAPI);
var itResp = await bgConfig.fetch(itUrl, {{
method: 'POST', headers: X.getHeaders(),
body: JSON.stringify([bgConfig.requestKey, botguardResponse])
}});
var it = await itResp.json();
step = 'webpominter_create';
var itData = {{ integrityToken: it[0], estimatedTtlSecs: it[1], mintRefreshThreshold: it[2], websafeFallbackToken: it[3] }};
var minter = await BG.WebPoMinter.create(itData, sig);
var ttl = (itData.estimatedTtlSecs > 0) ? itData.estimatedTtlSecs : 21600;
window.__kopuzMinter = minter;
window.__kopuzMinterExp = Date.now() + Math.floor(ttl * 0.8) * 1000;
window.__kopuzDiag('integrity token negotiated (ttl=' + ttl + 's)');
return minter;
}} catch (e) {{
var label = quiet ? 'ensureMinter warmup failed (will retry)' : 'ensureMinter FAILED';
window.__kopuzDiag(label + ' at step=' + step + ' :: ' + ((e && e.stack) ? e.stack : ('' + e)));
throw e;
}}
}})();
window.__kopuzMinting.then(function() {{ window.__kopuzMinting = null; }},
function() {{ window.__kopuzMinting = null; window.__kopuzMinter = null; window.__kopuzMinterExp = 0; }});
return window.__kopuzMinting;
}};
window.__kopuzMint = async function(videoId, reqId) {{
function send(o) {{ o.id = reqId; try {{ window.ipc.postMessage(JSON.stringify(o)); }} catch(e) {{}} }}
var phase = 'ensure_minter';
try {{
var minter = await window.__kopuzEnsureMinter();
phase = 'mint';
var pot = await minter.mintAsWebsafeString(videoId);
send({{pot: (pot || '') + ''}});
}} catch (e) {{
window.__kopuzMinter = null; window.__kopuzMinterExp = 0;
var stk = (e && e.stack) ? e.stack : ('' + e);
window.__kopuzDiag('mint FAILED in phase=' + phase + ' :: ' + stk);
send({{err: 'phase=' + phase + ': ' + stk}});
}}
}};
// One-shot environment snapshot: whether BgUtils captured BG, Trusted-Types
// presence, the automation flag, and the UA — the usual culprits when the
// BotGuard VM behaves differently across webview builds.
setTimeout(function() {{
var x = window.__KOPUZ_BGX;
window.__kopuzDiag('env: bgx=' + !!x + ' BG=' + !!(x && x.BG) + ' trustedTypes=' + !!(window.trustedTypes) + ' webdriver=' + navigator.webdriver + ' ua=' + navigator.userAgent);
}}, 2000);
// Pre-warm the integrity token as soon as the origin is live, so even the
// first track doesn't pay the negotiation. Best-effort, backs off, then stops.
(function warm(n) {{
if (!window.__KOPUZ_BGX || !window.__KOPUZ_BGX.BG) {{ if (n > 0) setTimeout(function() {{ warm(n - 1); }}, 500); return; }}
// quiet while retries remain: the first snapshot right after page load is
// flaky, and a self-healing attempt shouldn't read as a failure. The LAST
// attempt (and every on-demand mint) reports loudly.
window.__kopuzEnsureMinter(n > 0).catch(function() {{ if (n > 0) setTimeout(function() {{ warm(n - 1); }}, 2000); }});
}})(20);
"#
)
}
pub fn install_if_wanted<T: 'static>(target: &EventLoopWindowTarget<T>) {
if !WANT.load(Ordering::Relaxed) {
return;
}
if INSTALLED.with(|c| c.get()) {
return;
}
let window = match WindowBuilder::new()
.with_title("kopuz pot minter")
.with_inner_size(LogicalSize::new(1.0, 1.0))
.with_position(LogicalPosition::new(-32000.0, -32000.0))
.with_decorations(false)
.with_visible(false)
.build(target)
{
Ok(w) => w,
Err(e) => {
tracing::error!(error = %e, "pot-minter window build failed");
return;
}
};
let pending: Pending = Rc::new(RefCell::new(HashMap::new()));
let pending_ipc = pending.clone();
let builder = WebViewBuilder::new()
.with_url("https://music.youtube.com/")
.with_user_agent(UA)
.with_initialization_script(init_script())
.with_ipc_handler(move |req| {
let body = req.into_body();
let v: serde_json::Value = serde_json::from_str(&body).unwrap_or_default();
if let Some(diag) = v.get("diag").and_then(|d| d.as_str()) {
if diag.contains("FAILED") || diag.contains("error") {
tracing::warn!(%diag, "pot-minter diagnostic");
} else if diag.contains("will retry") {
tracing::debug!(%diag, "pot-minter diagnostic");
} else if diag.contains("integrity token negotiated") {
tracing::info!(%diag, "pot-minter diagnostic");
} else {
tracing::debug!(%diag, "pot-minter diagnostic");
}
return;
}
let Some(id) = v.get("id").and_then(|i| i.as_u64()) else {
return;
};
let result = match v.get("pot").and_then(|p| p.as_str()) {
Some(pot) => Ok(pot.to_string()),
None => {
let err = v
.get("err")
.and_then(|e| e.as_str())
.unwrap_or("mint failed")
.to_string();
tracing::error!(error = %err, "pot-minter mint error");
Err(err)
}
};
if let Some(reply) = pending_ipc.borrow_mut().remove(&id) {
let _ = reply.send(result);
}
});
#[cfg(target_os = "linux")]
let built = match window.default_vbox() {
Some(vbox) => builder.build_gtk(vbox),
None => {
tracing::error!("pot-minter: no GTK vbox on window");
return;
}
};
#[cfg(not(target_os = "linux"))]
let built = builder.build(&window);
let webview = match built {
Ok(w) => w,
Err(e) => {
tracing::error!(error = %e, "pot-minter webview build failed");
return;
}
};
let (tx, rx) = mpsc::unbounded_channel::<MintRequest>();
if botguard::set_minter(tx).is_err() {
tracing::warn!("pot-minter already registered");
}
STATE.with(|s| {
*s.borrow_mut() = Some(Minter {
_window: window,
webview,
pending,
rx,
});
});
INSTALLED.with(|c| c.set(true));
tracing::info!("pot-minter installed (anon PoToken minting via webview)");
}
pub fn pump() {
STATE.with(|s| {
let mut guard = s.borrow_mut();
let Some(state) = guard.as_mut() else {
return;
};
while let Ok(req) = state.rx.try_recv() {
let id = REQ_ID.fetch_add(1, Ordering::Relaxed);
state.pending.borrow_mut().insert(id, req.reply);
let vid: String = req
.video_id
.chars()
.filter(|c| c.is_ascii_alphanumeric() || *c == '_' || *c == '-')
.collect();
let _ = state.webview.evaluate_script(&format!(
"window.__kopuzMint && window.__kopuzMint('{vid}', {id})"
));
}
});
}