mod quine;
use serde::{Deserialize, Serialize};
use std::fs;
use std::sync::atomic::{AtomicU32, Ordering};
use tauri::webview::NewWindowResponse;
use tauri::{Emitter, Manager, Url, WebviewUrl, WebviewWindowBuilder};
#[derive(Serialize, Deserialize, Clone)]
struct AppConfig {
#[serde(default = "default_server_url")]
server_url: String,
#[serde(default = "default_jitsi_base")]
jitsi_base: String,
#[serde(default = "default_identity_url")]
identity_url: String,
}
fn default_server_url() -> String {
"https://meet.quineglobal.com".to_string()
}
fn default_jitsi_base() -> String {
"https://meet.jit.si".to_string()
}
fn default_identity_url() -> String {
"https://identity.quineglobal.com".to_string()
}
impl Default for AppConfig {
fn default() -> Self {
Self {
server_url: default_server_url(),
jitsi_base: default_jitsi_base(),
identity_url: default_identity_url(),
}
}
}
fn config_path(app: &tauri::AppHandle) -> Result<std::path::PathBuf, String> {
let dir = app
.path()
.app_data_dir()
.map_err(|e| format!("could not resolve app data dir: {e}"))?;
fs::create_dir_all(&dir).map_err(|e| format!("could not create app data dir: {e}"))?;
Ok(dir.join("config.json"))
}
#[tauri::command]
fn load_config(app: tauri::AppHandle) -> Result<AppConfig, String> {
let path = config_path(&app)?;
match fs::read_to_string(&path) {
Ok(raw) => serde_json::from_str(&raw).map_err(|e| format!("corrupt config: {e}")),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(AppConfig::default()),
Err(e) => Err(format!("could not read config: {e}")),
}
}
#[tauri::command]
fn save_config(app: tauri::AppHandle, config: AppConfig) -> Result<(), String> {
let path = config_path(&app)?;
let raw = serde_json::to_string_pretty(&config).map_err(|e| e.to_string())?;
fs::write(&path, raw).map_err(|e| format!("could not write config: {e}"))
}
#[cfg(target_os = "macos")]
const SAFARI_USER_AGENT: &str = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) \
AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.6 Safari/605.1.15";
const JITSI_PUBLIC_HOST: &str = "meet.jit.si";
const JITSI_SIGNIN_URL: &str =
"https://meet.jit.si/v1/_cdn/auth-static/meet-jit-si/v1/signin.html";
const SIGNIN_DONE_ROOM: &str = "quple-signed-in";
const SIGNIN_LABEL: &str = "jitsi-signin";
const POPUP_LABEL_PREFIX: &str = "popup-";
static POPUP_COUNTER: AtomicU32 = AtomicU32::new(0);
fn percent_encode(s: &str) -> String {
s.bytes()
.map(|b| match b {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => {
(b as char).to_string()
}
_ => format!("%{b:02X}"),
})
.collect()
}
fn signed_in_marker(app: &tauri::AppHandle) -> Result<std::path::PathBuf, String> {
Ok(config_path(app)?.with_file_name("jitsi-signed-in"))
}
fn is_signed_in(app: &tauri::AppHandle) -> bool {
signed_in_marker(app).map(|p| p.exists()).unwrap_or(false)
}
fn uses_public_jitsi(jitsi_base: &str) -> bool {
Url::parse(jitsi_base)
.map(|u| u.host_str() == Some(JITSI_PUBLIC_HOST))
.unwrap_or(false)
}
fn signin_url(state: &serde_json::Value) -> Result<Url, String> {
let encoded = percent_encode(&percent_encode(&state.to_string()));
Url::parse(&format!("{JITSI_SIGNIN_URL}?state={encoded}")).map_err(|e| e.to_string())
}
fn web_window<'a>(
app: &'a tauri::AppHandle,
label: impl Into<String>,
url: Url,
) -> WebviewWindowBuilder<'a, tauri::Wry, tauri::AppHandle> {
let builder = WebviewWindowBuilder::new(app, label, WebviewUrl::External(url));
#[cfg(target_os = "macos")]
let builder = builder.user_agent(SAFARI_USER_AGENT);
let app_ = app.clone();
builder.on_new_window(move |url, features| {
let n = POPUP_COUNTER.fetch_add(1, Ordering::Relaxed);
let builder = WebviewWindowBuilder::new(
&app_,
format!("{POPUP_LABEL_PREFIX}{n}"),
WebviewUrl::External("about:blank".parse().unwrap()),
)
.window_features(features)
.title(url.host_str().unwrap_or("Sign in"));
#[cfg(target_os = "macos")]
let builder = builder.user_agent(SAFARI_USER_AGENT);
match builder.build() {
Ok(window) => NewWindowResponse::Create { window },
Err(_) => NewWindowResponse::Deny,
}
})
}
fn close_popups(app: &tauri::AppHandle) {
for (label, window) in app.webview_windows() {
if label.starts_with(POPUP_LABEL_PREFIX) {
let _ = window.close();
}
}
}
#[tauri::command]
fn jitsi_signed_in(app: tauri::AppHandle) -> bool {
is_signed_in(&app)
}
#[tauri::command]
fn jitsi_sign_in(app: tauri::AppHandle) -> Result<(), String> {
if let Some(existing) = app.get_webview_window(SIGNIN_LABEL) {
return existing.set_focus().map_err(|e| e.to_string());
}
let url = signin_url(&serde_json::json!({ "room": SIGNIN_DONE_ROOM }))?;
let app_ = app.clone();
web_window(&app, SIGNIN_LABEL, url)
.title("Sign in to meet.jit.si")
.inner_size(480.0, 640.0)
.on_navigation(move |url| {
let done = url.host_str() == Some(JITSI_PUBLIC_HOST)
&& url.path() == format!("/{SIGNIN_DONE_ROOM}");
if !done {
return true;
}
let app = app_.clone();
let _ = app_.run_on_main_thread(move || {
if let Ok(marker) = signed_in_marker(&app) {
let _ = fs::write(marker, "");
}
let _ = app.emit("jitsi-auth-changed", true);
if let Some(w) = app.get_webview_window(SIGNIN_LABEL) {
let _ = w.close();
}
close_popups(&app);
});
false
})
.build()
.map_err(|e| format!("could not open sign-in window: {e}"))?;
Ok(())
}
#[tauri::command]
fn jitsi_sign_out(app: tauri::AppHandle) -> Result<(), String> {
if let Some(main) = app.get_webview_window("main") {
main.clear_all_browsing_data().map_err(|e| e.to_string())?;
}
let marker = signed_in_marker(&app)?;
if marker.exists() {
fs::remove_file(marker).map_err(|e| e.to_string())?;
}
let _ = app.emit("jitsi-auth-changed", false);
Ok(())
}
#[tauri::command]
fn open_meeting(
app: tauri::AppHandle,
jitsi_base: String,
room: String,
display_name: String,
) -> Result<(), String> {
let room = room.trim().to_string();
if room.is_empty() {
return Err("room is required".into());
}
match Url::parse(jitsi_base.trim()) {
Ok(u) if matches!(u.scheme(), "https" | "http") && u.host_str().is_some() => {}
_ => return Err(format!("{jitsi_base} isn't a Jitsi Meet server URL")),
}
let label = format!("meeting-{room}");
if let Some(existing) = app.get_webview_window(&label) {
existing.set_focus().map_err(|e| e.to_string())?;
return Ok(());
}
let display_name = display_name.trim();
let url = if uses_public_jitsi(&jitsi_base) && is_signed_in(&app) {
signin_url(&serde_json::json!({
"room": room,
"userInfo.displayName": display_name,
"config.prejoinConfig.enabled": false,
"config.prejoinPageEnabled": false,
}))?
} else {
let name_json = serde_json::to_string(display_name).map_err(|e| e.to_string())?;
let base = jitsi_base.trim_end_matches('/');
let url = format!(
"{base}/{room}#userInfo.displayName={}\
&config.prejoinConfig.enabled=false&config.prejoinPageEnabled=false",
percent_encode(&name_json)
);
Url::parse(&url).map_err(|_| format!("invalid meeting URL for {base}/{room}"))?
};
let app_ = app.clone();
let room_path = format!("/{room}");
web_window(&app, label, url)
.title(format!("Quple — {room}"))
.inner_size(1000.0, 700.0)
.min_inner_size(480.0, 360.0)
.on_navigation(move |url| {
if url.path() == room_path {
let app = app_.clone();
let _ = app_.run_on_main_thread(move || close_popups(&app));
}
true
})
.build()
.map_err(|e| format!("could not open meeting window: {e}"))?;
Ok(())
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.manage(quine::QuineAuth::default())
.invoke_handler(tauri::generate_handler![
load_config,
save_config,
open_meeting,
jitsi_signed_in,
jitsi_sign_in,
jitsi_sign_out,
quine::quine_sign_in,
quine::quine_cancel_sign_in,
quine::quine_restore,
quine::quine_access_token,
quine::quine_sign_out
])
.run(tauri::generate_context!())
.expect("error while running Quple");
}