use std::collections::HashSet; use std::time::{Duration, Instant}; use tauri::plugin::TauriPlugin; use tauri::{Manager, PhysicalPosition, PhysicalSize, Runtime}; use tauri_plugin_opener::OpenerExt; use theseus::settings; use tokio::sync::RwLock; pub struct AdsState { pub shown: bool, pub modal_shown: bool, pub last_click: Option, pub malicious_origins: HashSet, } const AD_LINK: &str = "https://modrinth.com/wrapper/app-ads-cookie"; pub fn init() -> TauriPlugin { tauri::plugin::Builder::::new("ads") .setup(|app, _api| { app.manage(RwLock::new(AdsState { shown: true, modal_shown: false, last_click: None, malicious_origins: HashSet::new(), })); // We refresh the ads window every 5 minutes for performance let app = app.clone(); tauri::async_runtime::spawn(async move { loop { if let Some(webview) = app.webviews().get_mut("ads-window") { let _ = webview.navigate(AD_LINK.parse().unwrap()); } tokio::time::sleep(std::time::Duration::from_secs(60 * 5)) .await; } }); Ok(()) }) .invoke_handler(tauri::generate_handler![ init_ads_window, hide_ads_window, show_ads_window, record_ads_click, open_link, get_ads_personalization, ]) .build() } fn get_webview_position( app: &tauri::AppHandle, dpr: f32, ) -> crate::api::Result<(PhysicalPosition, PhysicalSize)> { let main_window = app.get_window("main").unwrap(); let width = 300.0 * dpr; let height = 250.0 * dpr; let main_window_size = main_window.outer_size()?; let x = (main_window_size.width as f32) - width; let y = (main_window_size.height as f32) - height; Ok(( PhysicalPosition::new(x, y), PhysicalSize::new(width, height), )) } #[tauri::command] #[cfg(not(target_os = "linux"))] pub async fn init_ads_window( app: tauri::AppHandle, dpr: f32, override_shown: bool, ) -> crate::api::Result<()> { use tauri::WebviewUrl; const LINK_SCRIPT: &str = include_str!("ads-init.js"); let state = app.state::>(); let mut state = state.write().await; if override_shown { state.shown = true; } if state.modal_shown { return Ok(()); } if let Ok((position, size)) = get_webview_position(&app, dpr) { if let Some(webview) = app.webviews().get("ads-window") { if state.shown { let _ = webview.set_position(position); let _ = webview.set_size(size); } else { let _ = webview.set_position(PhysicalPosition::new(-1000, -1000)); } } else if let Some(window) = app.get_window("main") { let _ = window.add_child( tauri::webview::WebviewBuilder::new( "ads-window", WebviewUrl::External( AD_LINK.parse().unwrap(), ), ) .initialization_script(LINK_SCRIPT) // .initialization_script_for_main_only(LINK_SCRIPT, false) .user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36") .zoom_hotkeys_enabled(false) .transparent(true), if state.shown { position } else { PhysicalPosition::new(-1000.0, -1000.0) }, size, ); } } Ok(()) } // TODO: make ads work on linux #[tauri::command] #[cfg(target_os = "linux")] pub async fn init_ads_window() {} #[tauri::command] pub async fn show_ads_window( app: tauri::AppHandle, dpr: f32, ) -> crate::api::Result<()> { if let Some(webview) = app.webviews().get("ads-window") { let state = app.state::>(); let mut state = state.write().await; state.modal_shown = false; if state.shown { let (position, size) = get_webview_position(&app, dpr)?; let _ = webview.set_size(size); let _ = webview.set_position(position); } } Ok(()) } #[tauri::command] pub async fn hide_ads_window( app: tauri::AppHandle, reset: Option, ) -> crate::api::Result<()> { if let Some(webview) = app.webviews().get("ads-window") { let state = app.state::>(); let mut state = state.write().await; if reset.unwrap_or(false) { state.shown = false; } else { state.modal_shown = true; } let _ = webview.set_position(PhysicalPosition::new(-1000, -1000)); } Ok(()) } #[tauri::command] pub async fn record_ads_click( app: tauri::AppHandle, ) -> crate::api::Result<()> { let state = app.state::>(); let mut state = state.write().await; state.last_click = Some(Instant::now()); Ok(()) } #[tauri::command] pub async fn open_link( app: tauri::AppHandle, path: String, origin: String, ) -> crate::api::Result<()> { let state = app.state::>(); let mut state = state.write().await; if url::Url::parse(&path).is_ok() && !state.malicious_origins.contains(&origin) && let Some(last_click) = state.last_click && last_click.elapsed() < Duration::from_millis(100) { let _ = app.opener().open_url(&path, None::); state.last_click = None; return Ok(()); } tracing::info!("Malicious click: {path} origin {origin}"); state.malicious_origins.insert(origin); Ok(()) } #[tauri::command] pub async fn get_ads_personalization() -> crate::api::Result { let res = settings::get().await?; Ok(res.personalized_ads) }