Files
hermes-grocy-mcp/src/credentials/providers.rs

127 lines
4.2 KiB
Rust

use crate::{
error::{AppError, Result},
model::ProviderIdentity,
};
use glib::variant::ToVariant;
use std::{collections::HashSet, path::PathBuf};
pub const ALIASES: &[(&str, &str)] = &[
("KDE KWallet", "org.kde.secretservicecompat"),
("KDE KWallet", "org.kde.kwalletd6"),
("KDE KWallet", "org.kde.kwalletd5"),
("GNOME Keyring", "org.gnome.keyring"),
("System Secret Service", "org.freedesktop.secrets"),
];
#[derive(Clone)]
pub struct ProviderCandidate {
pub label: String,
pub identity: Option<ProviderIdentity>,
pub recommended: bool,
}
pub(crate) fn bus() -> Result<gio::DBusConnection> {
gio::bus_get_sync(gio::BusType::Session, gio::Cancellable::NONE).map_err(|_| {
AppError::new(
"WALLET_UNAVAILABLE",
"No desktop session bus. Run setup from your desktop session.",
)
})
}
pub(crate) fn call(
bus: &gio::DBusConnection,
method: &str,
params: Option<&glib::Variant>,
) -> Result<glib::Variant> {
bus.call_sync(
Some("org.freedesktop.DBus"),
"/org/freedesktop/DBus",
"org.freedesktop.DBus",
method,
params,
None,
gio::DBusCallFlags::NO_AUTO_START,
5000,
gio::Cancellable::NONE,
)
.map_err(|_| {
AppError::new(
"WALLET_UNAVAILABLE",
"The selected wallet is not running. Open its native manager and retry.",
)
})
}
pub(crate) fn owner_identity(bus: &gio::DBusConnection, name: &str) -> Result<(String, PathBuf)> {
let owner = call(bus, "GetNameOwner", Some(&(name,).to_variant()))?
.get::<(String,)>()
.ok_or_else(AppError::input)?
.0;
let pid = call(
bus,
"GetConnectionUnixProcessID",
Some(&(owner.as_str(),).to_variant()),
)?
.get::<(u32,)>()
.ok_or_else(AppError::input)?
.0;
let exe = std::fs::read_link(format!("/proc/{pid}/exe")).map_err(|_| {
AppError::new(
"WALLET_UNAVAILABLE",
"Cannot identify the selected wallet process.",
)
})?;
Ok((owner, exe))
}
pub(crate) fn bind_selected(identity: &ProviderIdentity) -> Result<String> {
let bus = bus()?;
let (owner, exe) = owner_identity(&bus, &identity.bus_name)?;
if exe != identity.executable {
return Err(AppError::new(
"WALLET_PROVIDER_CHANGED",
"The wallet provider changed. Run setup to review its selection.",
));
}
Ok(owner)
}
pub fn discover_providers() -> Result<Vec<ProviderCandidate>> {
let bus = bus()?;
let names = call(&bus, "ListNames", None)?
.get::<(Vec<String>,)>()
.ok_or_else(AppError::input)?
.0;
let inactive = call(&bus, "ListActivatableNames", None)?
.get::<(Vec<String>,)>()
.ok_or_else(AppError::input)?
.0;
let desktop = std::env::var("XDG_CURRENT_DESKTOP")
.unwrap_or_default()
.to_lowercase();
let mut seen = HashSet::new();
let mut output = Vec::new();
for (label, name) in ALIASES {
if names.iter().any(|n| n == name) {
if let Ok((owner, exe)) = owner_identity(&bus, name) {
if seen.insert(owner) {
let recommended = (desktop.contains("kde")
&& exe.to_string_lossy().contains("kwallet"))
|| (desktop.contains("gnome")
&& exe.to_string_lossy().contains("gnome-keyring"));
output.push(ProviderCandidate {
label: label.to_string(),
identity: Some(ProviderIdentity {
bus_name: name.to_string(),
executable: exe,
}),
recommended,
});
}
}
} else if inactive.iter().any(|n| n == name) && !output.iter().any(|p| p.label == *label) {
output.push(ProviderCandidate {
label: format!("{label} (not running; open its manager)"),
identity: None,
recommended: false,
})
}
}
output.sort_by_key(|p| (!p.recommended, p.identity.is_none()));
Ok(output)
}