mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co>
167 lines
6.6 KiB
Rust
167 lines
6.6 KiB
Rust
use nostr::Keys;
|
|
use serde::{Deserialize, Serialize};
|
|
use tauri::{AppHandle, Emitter, State};
|
|
|
|
use crate::app_state::AppState;
|
|
use crate::managed_agents::{
|
|
effective_repos_dir, ensure_repos_symlink, nest_dir, try_regenerate_nest,
|
|
write_persisted_repos_dir,
|
|
};
|
|
use crate::relay;
|
|
|
|
#[derive(Deserialize)]
|
|
struct RelayInfoIcon {
|
|
#[serde(default)]
|
|
icon: Option<String>,
|
|
}
|
|
|
|
/// Fetch a relay's workspace icon from its NIP-11 relay information document.
|
|
///
|
|
/// Works for any workspace (active or not) with a plain unauthenticated HTTP
|
|
/// GET — no WebSocket session needed. Returns `None` when the relay has no
|
|
/// icon set, is unreachable, or serves a malformed document: the rail falls
|
|
/// back to initials in all three cases.
|
|
#[tauri::command]
|
|
pub async fn fetch_workspace_icon(
|
|
relay_url: String,
|
|
state: State<'_, AppState>,
|
|
) -> Result<Option<String>, String> {
|
|
let http_url = relay::relay_http_base_url(&relay_url);
|
|
let Ok(response) = state
|
|
.http_client
|
|
.get(&http_url)
|
|
.header("Accept", "application/nostr+json")
|
|
.send()
|
|
.await
|
|
else {
|
|
return Ok(None);
|
|
};
|
|
if !response.status().is_success() {
|
|
return Ok(None);
|
|
}
|
|
let doc = response
|
|
.json::<RelayInfoIcon>()
|
|
.await
|
|
.unwrap_or(RelayInfoIcon { icon: None });
|
|
Ok(doc.icon.filter(|icon| !icon.is_empty()))
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub struct ActiveWorkspaceInfo {
|
|
relay_url: String,
|
|
pubkey: String,
|
|
}
|
|
|
|
/// Returns the current active workspace info (relay URL + pubkey).
|
|
#[tauri::command]
|
|
pub fn get_active_workspace(state: State<'_, AppState>) -> Result<ActiveWorkspaceInfo, String> {
|
|
let keys = state.keys.lock().map_err(|e| e.to_string())?;
|
|
let relay_url = relay::relay_ws_url_with_override(&state);
|
|
Ok(ActiveWorkspaceInfo {
|
|
relay_url,
|
|
pubkey: keys.public_key().to_hex(),
|
|
})
|
|
}
|
|
|
|
/// Validate a candidate `repos_dir` without mutating the filesystem.
|
|
///
|
|
/// The Add/Edit workspace dialogs call this on submit to block Save on a bad
|
|
/// path, so a typo never reaches `apply_workspace`. Reuses the same
|
|
/// `validate_repos_dir` the boot/apply path uses — one source of truth for
|
|
/// "what's a valid repos dir". An empty/whitespace value clears the override
|
|
/// and is valid. `Err` carries the human-readable reason for inline display.
|
|
#[tauri::command]
|
|
pub fn validate_repos_dir(dir: String) -> Result<(), String> {
|
|
let trimmed = dir.trim();
|
|
if trimmed.is_empty() {
|
|
return Ok(());
|
|
}
|
|
let nest = nest_dir().ok_or("cannot resolve home directory for nest")?;
|
|
crate::managed_agents::validate_repos_dir(&nest, trimmed).map(|_| ())
|
|
}
|
|
|
|
/// Apply a workspace's configuration to the backend session.
|
|
///
|
|
/// Called by the frontend on app init (after reload) to configure the
|
|
/// Tauri backend with the selected workspace's relay URL, keys, and repos
|
|
/// directory.
|
|
///
|
|
/// A bad `repos_dir` is non-fatal: relay/keys always apply (the relay is the
|
|
/// active workspace's own choice — orthogonal to the filesystem repos dir),
|
|
/// the bad value is NOT persisted (so the next boot starts clean), the
|
|
/// `REPOS` symlink is skipped (REPOS stays a real dir), a `repos-dir-error`
|
|
/// event surfaces the reason, and the command returns `Ok`. The dialogs
|
|
/// already block a bad path at Save (`validate_repos_dir`); this fallback only
|
|
/// catches a value that went bad after save (deleted dir, unmounted volume).
|
|
#[tauri::command]
|
|
pub fn apply_workspace(
|
|
relay_url: String,
|
|
nsec: Option<String>,
|
|
repos_dir: Option<String>,
|
|
app: AppHandle,
|
|
state: State<'_, AppState>,
|
|
) -> Result<(), String> {
|
|
// ── Validate before mutating ──────────────────────────────────────────
|
|
let parsed_keys = match nsec.as_deref().map(str::trim).filter(|s| !s.is_empty()) {
|
|
Some(nsec_trimmed) => {
|
|
Some(Keys::parse(nsec_trimmed).map_err(|e| format!("invalid nsec: {e}"))?)
|
|
}
|
|
None => None,
|
|
};
|
|
|
|
// Decide the effective repos_dir from the candidate. A bad path does NOT
|
|
// reject — it is treated as if no override were set: relay/keys still
|
|
// apply, the bad value is not persisted, and a `repos-dir-error` surfaces
|
|
// the reason. Persisting a bad path would make every later boot read it,
|
|
// fail to resolve the symlink, and silently skip agent restore. One
|
|
// validate (inside `effective_repos_dir`) drives both the emit and the
|
|
// persisted value. `nest` is resolved softly: when absent there is nothing
|
|
// to persist or symlink, and relay/keys must still apply unconditionally.
|
|
let nest = nest_dir();
|
|
let effective_repos_dir = match nest.as_deref() {
|
|
Some(nest) => match effective_repos_dir(nest, repos_dir.as_deref()) {
|
|
Ok(value) => value,
|
|
Err(error) => {
|
|
let _ = app.emit("repos-dir-error", error);
|
|
None
|
|
}
|
|
},
|
|
None => None,
|
|
};
|
|
|
|
// ── Apply all state changes (nothing below can fail) ──────────────────
|
|
{
|
|
let mut override_guard = state.relay_url_override.lock().map_err(|e| e.to_string())?;
|
|
*override_guard = Some(relay_url);
|
|
}
|
|
|
|
if let Some(keys) = parsed_keys {
|
|
let mut keys_guard = state.keys.lock().map_err(|e| e.to_string())?;
|
|
*keys_guard = keys;
|
|
}
|
|
|
|
// ── Filesystem side-effect (non-fatal) ────────────────────────────────
|
|
// Persist the *effective* repos_dir (None when the candidate failed
|
|
// validation) for the backend to read at boot, then re-point REPOS to
|
|
// match. Persisting first makes the dotfile authoritative even if the
|
|
// symlink apply fails here (e.g. a non-empty real REPOS): the next boot
|
|
// reads the persisted value and resolves the symlink before any agent can
|
|
// clone into REPOS. A bad candidate persists `None`, so the next boot is
|
|
// clean and agent restore proceeds. Failure of either must NOT fail the
|
|
// command — relay/keys are already applied. Surface symlink errors via
|
|
// `repos-dir-error`.
|
|
if let Some(nest) = nest.as_deref() {
|
|
if let Err(error) = write_persisted_repos_dir(nest, effective_repos_dir.as_deref()) {
|
|
eprintln!("buzz-desktop: persist repos dir failed: {error}");
|
|
}
|
|
if let Err(error) = ensure_repos_symlink(nest, effective_repos_dir.as_deref()) {
|
|
eprintln!("buzz-desktop: repos dir setup failed: {error}");
|
|
let _ = app.emit("repos-dir-error", error);
|
|
}
|
|
}
|
|
|
|
try_regenerate_nest(&app);
|
|
|
|
Ok(())
|
|
}
|