use crate::config::{self, ConfigState};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SendNotificationResult {
pub success: bool,
pub message: String,
pub notification: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConnectionTestResult {
pub ok: bool,
pub message: String,
}
#[tauri::command]
pub fn get_config() -> Result<ConfigState, String> {
Ok(config::load_config())
}
#[tauri::command]
pub fn set_active_profile(name: String) -> Result<ConfigState, String> {
config::set_active_profile(&name)
}
#[tauri::command]
pub fn save_server_config(
name: String,
url: String,
pin: String,
make_active: bool,
) -> Result<ConfigState, String> {
config::save_profile(&name, &url, &pin, make_active)
}
#[tauri::command]
pub fn test_connection(
target_url: Option<String>,
target_pin: Option<String>,
) -> Result<ConnectionTestResult, String> {
let cfg = config::load_config();
let url = target_url
.filter(|s| !s.trim().is_empty())
.unwrap_or(cfg.active_server.url)
.trim()
.trim_end_matches('/')
.to_string();
let pin = target_pin
.filter(|s| !s.trim().is_empty())
.unwrap_or(cfg.active_server.pin)
.trim()
.to_string();
let up_url = format!("{url}/up");
let resp = ureq::get(&up_url)
.timeout(std::time::Duration::from_secs(5))
.call()
.map_err(|e| format!("Server unreachable at {url}: {e}"))?;
if resp.status() != 200 {
return Ok(ConnectionTestResult {
ok: false,
message: format!("Server returned HTTP {}", resp.status()),
});
}
// Also check PIN auth against /notifications/scan
let scan_url = format!("{url}/notifications/scan");
let scan_resp = ureq::post(&scan_url)
.set("X-PIN", &pin)
.timeout(std::time::Duration::from_secs(5))
.send_json(serde_json::json!({ "limit": 1 }));
match scan_resp {
Ok(r) => {
if r.status() == 200 || r.status() == 204 {
Ok(ConnectionTestResult {
ok: true,
message: "Connected and authenticated successfully".to_string(),
})
} else {
Ok(ConnectionTestResult {
ok: false,
message: format!("Unexpected status: {}", r.status()),
})
}
}
Err(ureq::Error::Status(401, _)) => Ok(ConnectionTestResult {
ok: false,
message: "Authentication failed: Invalid PIN".to_string(),
}),
Err(e) => Ok(ConnectionTestResult {
ok: false,
message: format!("Scan check failed: {e}"),
}),
}
}
#[tauri::command]
pub fn send_notification(
kind: Option<String>,
message: Option<String>,
target_url: Option<String>,
target_pin: Option<String>,
) -> Result<SendNotificationResult, String> {
let cfg = config::load_config();
let url = target_url
.filter(|s| !s.trim().is_empty())
.unwrap_or(cfg.active_server.url)
.trim()
.trim_end_matches('/')
.to_string();
let pin = target_pin
.filter(|s| !s.trim().is_empty())
.unwrap_or(cfg.active_server.pin)
.trim()
.to_string();
let resolved_kind = kind.as_deref().unwrap_or("notification").trim();
let default_msg = match resolved_kind {
"heart" => "Look at your phone",
"cat" => "Missing you",
"emergency" => "Emergency",
_ => "Notification",
};
let resolved_msg = message
.as_deref()
.filter(|s| !s.trim().is_empty())
.unwrap_or(default_msg)
.to_string();
// 1. Try modern JSON endpoint: POST /api/notify
let api_url = format!("{url}/api/notify");
let payload = serde_json::json!({
"kind": resolved_kind,
"message": resolved_msg
});
let json_req = ureq::post(&api_url)
.set("X-PIN", &pin)
.timeout(std::time::Duration::from_secs(10))
.send_json(&payload);
match json_req {
Ok(resp) => {
let notification: Option<serde_json::Value> = resp.into_json().ok();
Ok(SendNotificationResult {
success: true,
message: format!("Notification '{resolved_kind}' sent successfully"),
notification,
})
}
Err(ureq::Error::Status(404, _)) => {
// Fallback to legacy path endpoint: POST /notify/{kind}?message=...
let encoded_msg = urlencoding_simple(&resolved_msg);
let legacy_url = format!("{url}/notify/{resolved_kind}?message={encoded_msg}");
let legacy_resp = ureq::post(&legacy_url)
.set("X-PIN", &pin)
.timeout(std::time::Duration::from_secs(10))
.call()
.map_err(|e| format!("Failed to send notification: {e}"))?;
if legacy_resp.status() == 200 || legacy_resp.status() == 204 {
Ok(SendNotificationResult {
success: true,
message: format!(
"Notification '{resolved_kind}' sent successfully (legacy route)"
),
notification: None,
})
} else {
Err(format!("Server returned HTTP {}", legacy_resp.status()))
}
}
Err(ureq::Error::Status(401, _)) => {
Err("Unauthorized: Server rejected the PIN".to_string())
}
Err(e) => Err(format!("Network error: {e}")),
}
}
pub fn urlencoding_simple(s: &str) -> String {
let mut out = String::new();
for b in s.bytes() {
match b {
b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
out.push(b as char);
}
b' ' => out.push('+'),
_ => out.push_str(&format!("%{:02X}", b)),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_urlencoding_simple() {
assert_eq!(urlencoding_simple("hello world"), "hello+world");
assert_eq!(urlencoding_simple("a/b?c"), "a%2Fb%3Fc");
}
}