Skip to content
190 linesCodeBlameRaw
1//! What the site may ask of the app: the commands behind `window.g1tDesktop`
2//! (bridge.js), and the launcher's own.
3//!
4//! Every command checks who is asking. The site's commands answer only a
5//! page of the configured server; the launcher's only the page bundled
6//! with the app. A page from anywhere else gets nothing, whatever it
7//! loads.
8
9use serde::Serialize;
10use tauri::{AppHandle, Manager, UserAttentionType, Webview};
11use tauri_plugin_notification::NotificationExt;
12use tauri_plugin_opener::OpenerExt;
13use url::Url;
14
15use crate::settings::{self, Settings};
16use crate::{WINDOW, badge, config, updates};
17
18const SCRIPT: &str = include_str!("bridge.js");
19
20/// The bridge, run at the start of every page the window loads.
21pub fn script() -> String {
22 SCRIPT
23 .replace("__VERSION__", env!("CARGO_PKG_VERSION"))
24 .replace("__PLATFORM__", std::env::consts::OS)
25 .replace("\"__OUTSIDE__\"", &serde_json::to_string(crate::navigation::OUTSIDE).expect("a list of paths is JSON"))
26}
27
28/// What the site and the launcher learn about the app.
29#[derive(Serialize)]
30pub struct Info {
31 pub version: String,
32 pub platform: String,
33 pub server: String,
34 /// The version downloaded and waiting for a restart, if one is.
35 pub update_ready: Option<String>,
36}
37
38fn caller(webview: &Webview) -> Result<Url, String> {
39 webview.url().map_err(|error| error.to_string())
40}
41
42/// The page asking is one of the server's.
43fn from_site(app: &AppHandle, webview: &Webview) -> Result<(), String> {
44 let url = caller(webview)?;
45 if config::is_server(app, &url) { Ok(()) } else { Err("only g1t's own pages may ask that".into()) }
46}
47
48/// The page asking is the launcher.
49fn from_launcher(webview: &Webview) -> Result<(), String> {
50 let url = caller(webview)?;
51 if config::is_launcher(&url) { Ok(()) } else { Err("only the launcher may ask that".into()) }
52}
53
54/// The page asking is the settings window's, bundled with the app.
55fn from_settings(webview: &Webview) -> Result<(), String> {
56 from_launcher(webview)?;
57 if webview.label() == settings::SETTINGS { Ok(()) } else { Err("only the settings window may ask that".into()) }
58}
59
60fn from_either(app: &AppHandle, webview: &Webview) -> Result<(), String> {
61 from_site(app, webview).or_else(|_| from_launcher(webview))
62}
63
64#[tauri::command]
65pub fn desktop_info(app: AppHandle, webview: Webview) -> Result<Info, String> {
66 from_either(&app, &webview)?;
67 Ok(Info {
68 version: env!("CARGO_PKG_VERSION").into(),
69 platform: std::env::consts::OS.into(),
70 server: config::server(&app).to_string(),
71 update_ready: updates::ready(&app),
72 })
73}
74
75/// A notification from the system: the site asks for one when a message
76/// is for the person and the window is behind another.
77#[tauri::command]
78pub fn notify(app: AppHandle, webview: Webview, title: String, body: String) -> Result<(), String> {
79 from_site(&app, &webview)?;
80 let settings = config::settings(&app);
81 if let Some(window) = app.get_webview_window(WINDOW).filter(|window| settings.flash && !window.is_focused().unwrap_or(false)) {
82 let _ = window.request_user_attention(Some(UserAttentionType::Informational));
83 }
84 if !settings.notifications {
85 return Ok(());
86 }
87 app.notification().builder().title(title).body(body).show().map_err(|error| error.to_string())
88}
89
90/// What needs the person, on the icon and the tray; 0 clears it.
91#[tauri::command]
92pub fn set_badge(app: AppHandle, webview: Webview, count: i64) -> Result<(), String> {
93 from_site(&app, &webview)?;
94 badge::set(&app, count.max(0));
95 Ok(())
96}
97
98/// A link to somewhere else, opened in the browser the system uses.
99#[tauri::command]
100pub fn open_url(app: AppHandle, webview: Webview, url: String) -> Result<(), String> {
101 from_either(&app, &webview)?;
102 let parsed = Url::parse(&url).map_err(|error| error.to_string())?;
103 if !matches!(parsed.scheme(), "http" | "https" | "mailto") {
104 return Err(format!("{} links stay in the app", parsed.scheme()));
105 }
106 app.opener().open_url(parsed.as_str(), None::<&str>).map_err(|error| error.to_string())
107}
108
109/// Whether a server answers at all: the launcher asks before the window
110/// goes there, so a machine with no network sees a page of ours, not the
111/// web view's own error.
112#[tauri::command]
113pub async fn reachable(app: AppHandle, webview: Webview, url: Option<String>) -> Result<bool, String> {
114 from_launcher(&webview)?;
115 let target = match url {
116 Some(text) => config::parse(&text).ok_or("That is not a web address.")?,
117 None => config::server(&app),
118 };
119 let client = reqwest::Client::builder()
120 .timeout(std::time::Duration::from_secs(6))
121 .build()
122 .map_err(|error| error.to_string())?;
123 Ok(client.head(target).send().await.is_ok())
124}
125
126/// The launcher chose a server: it is kept, and the window goes there next.
127#[tauri::command]
128pub fn set_server(app: AppHandle, webview: Webview, url: String) -> Result<String, String> {
129 from_launcher(&webview)?;
130 config::set_server(&app, &url).map(|server| server.to_string())
131}
132
133/// Looks for an update now: `current`, `ready:<version>`, or an error.
134#[tauri::command]
135pub async fn check_for_updates(app: AppHandle, webview: Webview) -> Result<String, String> {
136 from_either(&app, &webview)?;
137 updates::check(&app, true).await
138}
139
140/// Opens the app's settings window: the site's account menu asks for it.
141#[tauri::command]
142pub fn open_settings(app: AppHandle, webview: Webview) -> Result<(), String> {
143 from_either(&app, &webview)?;
144 settings::open(&app);
145 Ok(())
146}
147
148/// What the settings window shows.
149#[tauri::command]
150pub fn get_settings(app: AppHandle, webview: Webview) -> Result<settings::View, String> {
151 from_settings(&webview)?;
152 Ok(settings::view(&app))
153}
154
155/// The settings window changed something: everything but opening at sign-in.
156#[tauri::command]
157pub fn save_settings(app: AppHandle, webview: Webview, settings: Settings) -> Result<settings::View, String> {
158 from_settings(&webview)?;
159 settings::apply(&app, settings)?;
160 Ok(settings::view(&app))
161}
162
163/// Opening at sign-in, which the system keeps.
164#[tauri::command]
165pub fn set_open_at_login(app: AppHandle, webview: Webview, on: bool) -> Result<settings::View, String> {
166 from_settings(&webview)?;
167 settings::open_at_login(&app, on)?;
168 Ok(settings::view(&app))
169}
170
171/// The settings window's "Change…": the main window goes to the launcher's choice of server.
172#[tauri::command]
173pub fn change_server(app: AppHandle, webview: Webview) -> Result<(), String> {
174 from_settings(&webview)?;
175 if let Some(window) = app.get_webview_window(settings::SETTINGS) {
176 let _ = window.close();
177 }
178 crate::tray::launcher(&app, "change");
179 Ok(())
180}
181
182#[cfg(test)]
183mod tests {
184 #[test]
185 fn the_bridge_has_nothing_left_to_fill_in() {
186 let script = super::script();
187 assert!(!script.contains("__VERSION__") && !script.contains("__PLATFORM__") && !script.contains("__OUTSIDE__"));
188 assert!(script.contains(r#"var OUTSIDE = ["/pricing""#));
189 }
190}