Rename the app to Keynectr
- Crate/binary: nostr-manager-backend -> keynectr - Data directory: nost-feed-manager -> keynectr, migrated automatically on first data_dir() call (existing vaults, settings and backups move) - Electron extraResources/spawn path, executableName, productName, window title and CLI usage strings updated to match - Deliberately unchanged: crypto.rs KDF verifier string, so previously encrypted vault backups remain decryptable Verified live: existing vault with two profiles migrated to ~/.local/share/keynectr and loads correctly.
This commit is contained in:
parent
1116dfdbb4
commit
7c6a085bf1
13 changed files with 108 additions and 264 deletions
38
Cargo.lock
generated
38
Cargo.lock
generated
|
|
@ -683,6 +683,25 @@ dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "keynectr"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"aes-gcm",
|
||||||
|
"argon2",
|
||||||
|
"base64",
|
||||||
|
"getrandom 0.2.17",
|
||||||
|
"hex",
|
||||||
|
"nostr",
|
||||||
|
"nostr-sdk",
|
||||||
|
"rpassword",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tokio",
|
||||||
|
"uuid",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.189"
|
version = "0.2.189"
|
||||||
|
|
@ -781,25 +800,6 @@ dependencies = [
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "nostr-manager-backend"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"aes-gcm",
|
|
||||||
"argon2",
|
|
||||||
"base64",
|
|
||||||
"getrandom 0.2.17",
|
|
||||||
"hex",
|
|
||||||
"nostr",
|
|
||||||
"nostr-sdk",
|
|
||||||
"rpassword",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"tokio",
|
|
||||||
"uuid",
|
|
||||||
"zeroize",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-relay-pool"
|
name = "nostr-relay-pool"
|
||||||
version = "0.40.1"
|
version = "0.40.1"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
[package]
|
[package]
|
||||||
name = "nostr-manager-backend"
|
name = "keynectr"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -76,10 +76,10 @@ let nextId = 1;
|
||||||
|
|
||||||
function resolveBackendPath(): string {
|
function resolveBackendPath(): string {
|
||||||
if (app.isPackaged) {
|
if (app.isPackaged) {
|
||||||
return path.join(process.resourcesPath, 'nostr-manager-backend');
|
return path.join(process.resourcesPath, 'keynectr');
|
||||||
}
|
}
|
||||||
// Development: the crate builds to <project>/target/release.
|
// Development: the crate builds to <project>/target/release.
|
||||||
return path.join(app.getAppPath(), '..', 'target', 'release', 'nostr-manager-backend');
|
return path.join(app.getAppPath(), '..', 'target', 'release', 'keynectr');
|
||||||
}
|
}
|
||||||
|
|
||||||
function startBackend(): void {
|
function startBackend(): void {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Nostr Feed Manager</title>
|
<title>Keynectr</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|
|
||||||
4
frontend/package-lock.json
generated
4
frontend/package-lock.json
generated
|
|
@ -1,11 +1,11 @@
|
||||||
{
|
{
|
||||||
"name": "nost-feed-manager",
|
"name": "keynectr",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "nost-feed-manager",
|
"name": "keynectr",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "nost-feed-manager",
|
"name": "keynectr",
|
||||||
|
"productName": "Keynectr",
|
||||||
"productName": "Nostr Feed Manager",
|
"productName": "Nostr Feed Manager",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "A friendly Linux desktop app for managing Nostr profiles and publishing text notes.",
|
"description": "A friendly Linux desktop app for managing Nostr profiles and publishing text notes.",
|
||||||
|
|
@ -58,8 +59,8 @@
|
||||||
],
|
],
|
||||||
"extraResources": [
|
"extraResources": [
|
||||||
{
|
{
|
||||||
"from": "../target/release/nostr-manager-backend",
|
"from": "../target/release/keynectr",
|
||||||
"to": "nostr-manager-backend"
|
"to": "keynectr"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"linux": {
|
"linux": {
|
||||||
|
|
@ -67,7 +68,7 @@
|
||||||
"dir"
|
"dir"
|
||||||
],
|
],
|
||||||
"category": "Network",
|
"category": "Network",
|
||||||
"executableName": "nost-feed-manager"
|
"executableName": "keynectr"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ describe('SettingsScreen', () => {
|
||||||
renderWithApp(<SettingsScreen />);
|
renderWithApp(<SettingsScreen />);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
await screen.findByText('/home/user/.local/share/nost-feed-manager/profiles_vault.json'),
|
await screen.findByText('/home/user/.local/share/keynectr/profiles_vault.json'),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
expect(screen.getByText('Storage is not encrypted')).toBeInTheDocument();
|
expect(screen.getByText('Storage is not encrypted')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -35,8 +35,8 @@ export function makeState(overrides?: Partial<AppState>): AppState {
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
version: '0.1.0',
|
version: '0.1.0',
|
||||||
vault_path: '/home/user/.local/share/nost-feed-manager/profiles_vault.json',
|
vault_path: '/home/user/.local/share/keynectr/profiles_vault.json',
|
||||||
settings_path: '/home/user/.local/share/nost-feed-manager/settings.json',
|
settings_path: '/home/user/.local/share/keynectr/settings.json',
|
||||||
encrypted_storage: false,
|
encrypted_storage: false,
|
||||||
vault_locked: false,
|
vault_locked: false,
|
||||||
migrated_from: null,
|
migrated_from: null,
|
||||||
|
|
@ -188,7 +188,7 @@ export function createApiMock(initial: AppState = makeState()): ApiMock {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
backupNow: vi.fn(async () => ({
|
backupNow: vi.fn(async () => ({
|
||||||
backup_path: '/home/user/.local/share/nost-feed-manager/profiles_vault.json.backup-1',
|
backup_path: '/home/user/.local/share/keynectr/profiles_vault.json.backup-1',
|
||||||
})),
|
})),
|
||||||
setVaultPassword: vi.fn(async () => ({
|
setVaultPassword: vi.fn(async () => ({
|
||||||
...state,
|
...state,
|
||||||
|
|
|
||||||
|
|
@ -151,7 +151,7 @@ pub struct ReplyEnvelope {
|
||||||
|
|
||||||
/// Run the JSON-lines IPC server on stdin/stdout.
|
/// Run the JSON-lines IPC server on stdin/stdout.
|
||||||
///
|
///
|
||||||
/// The Electron main process spawns `nostr-manager-backend serve` and
|
/// The Electron main process spawns `keynectr serve` and
|
||||||
/// exchanges one JSON object per line. Requests are processed sequentially so
|
/// exchanges one JSON object per line. Requests are processed sequentially so
|
||||||
/// the shared state never sees concurrent mutations.
|
/// the shared state never sees concurrent mutations.
|
||||||
pub async fn serve() -> Result<(), AppError> {
|
pub async fn serve() -> Result<(), AppError> {
|
||||||
|
|
|
||||||
|
|
@ -14,4 +14,7 @@ pub mod vault;
|
||||||
pub use errors::AppError;
|
pub use errors::AppError;
|
||||||
|
|
||||||
/// Stable application-data directory name.
|
/// Stable application-data directory name.
|
||||||
pub const APP_DIR_NAME: &str = "nost-feed-manager";
|
pub const APP_DIR_NAME: &str = "keynectr";
|
||||||
|
|
||||||
|
/// Previous application-data directory name, migrated automatically on first use.
|
||||||
|
pub const LEGACY_APP_DIR_NAME: &str = "nost-feed-manager";
|
||||||
|
|
|
||||||
52
src/main.rs
52
src/main.rs
|
|
@ -1,18 +1,18 @@
|
||||||
use std::process::ExitCode;
|
use std::process::ExitCode;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
use nostr_manager_backend::app::App;
|
use keynectr::app::App;
|
||||||
use nostr_manager_backend::errors::{AppError, ErrorKind};
|
use keynectr::errors::{AppError, ErrorKind};
|
||||||
use nostr_manager_backend::ipc;
|
use keynectr::ipc;
|
||||||
use nostr_manager_backend::profiles::{self, ProfileSummary};
|
use keynectr::profiles::{self, ProfileSummary};
|
||||||
use nostr_manager_backend::publish;
|
use keynectr::publish;
|
||||||
use nostr_manager_backend::relays;
|
use keynectr::relays;
|
||||||
use nostr_manager_backend::settings::Theme;
|
use keynectr::settings::Theme;
|
||||||
use nostr_manager_backend::signer::Signer;
|
use keynectr::signer::Signer;
|
||||||
use nostr_manager_backend::vault::{self, StoredProfile, Vault};
|
use keynectr::vault::{self, StoredProfile, Vault};
|
||||||
|
|
||||||
const USAGE: &str = "\
|
const USAGE: &str = "\
|
||||||
nostr-manager-backend <command> [args...]
|
keynectr <command> [args...]
|
||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
create <label> Create a new profile
|
create <label> Create a new profile
|
||||||
|
|
@ -133,7 +133,7 @@ fn cli_list() -> Result<String, AppError> {
|
||||||
fn cli_switch(args: &[String]) -> Result<String, AppError> {
|
fn cli_switch(args: &[String]) -> Result<String, AppError> {
|
||||||
let npub = args
|
let npub = args
|
||||||
.get(2)
|
.get(2)
|
||||||
.ok_or_else(|| AppError::config("Usage: nostr-manager-backend switch <npub>"))?;
|
.ok_or_else(|| AppError::config("Usage: keynectr switch <npub>"))?;
|
||||||
|
|
||||||
let mut app = App::load()?;
|
let mut app = App::load()?;
|
||||||
profiles::set_active(&mut app.vault, npub)?;
|
profiles::set_active(&mut app.vault, npub)?;
|
||||||
|
|
@ -143,9 +143,7 @@ fn cli_switch(args: &[String]) -> Result<String, AppError> {
|
||||||
|
|
||||||
async fn cli_publish(args: &[String]) -> Result<String, AppError> {
|
async fn cli_publish(args: &[String]) -> Result<String, AppError> {
|
||||||
if args.len() < 4 {
|
if args.len() < 4 {
|
||||||
return Err(AppError::config(
|
return Err(AppError::config("Usage: keynectr publish <npub> <content>"));
|
||||||
"Usage: nostr-manager-backend publish <npub> <content>",
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let npub = &args[2];
|
let npub = &args[2];
|
||||||
|
|
@ -169,9 +167,7 @@ async fn cli_publish(args: &[String]) -> Result<String, AppError> {
|
||||||
|
|
||||||
fn cli_set_picture(args: &[String]) -> Result<String, AppError> {
|
fn cli_set_picture(args: &[String]) -> Result<String, AppError> {
|
||||||
let [_, _, npub, url] = args else {
|
let [_, _, npub, url] = args else {
|
||||||
return Err(AppError::config(
|
return Err(AppError::config("Usage: keynectr set-picture <npub> <url>"));
|
||||||
"Usage: nostr-manager-backend set-picture <npub> <url>",
|
|
||||||
));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut app = load_app_with_unlock()?;
|
let mut app = load_app_with_unlock()?;
|
||||||
|
|
@ -194,7 +190,7 @@ fn cli_set_picture(args: &[String]) -> Result<String, AppError> {
|
||||||
fn cli_publish_name(args: &[String]) -> Result<String, AppError> {
|
fn cli_publish_name(args: &[String]) -> Result<String, AppError> {
|
||||||
let npub = args
|
let npub = args
|
||||||
.get(2)
|
.get(2)
|
||||||
.ok_or_else(|| AppError::config("Usage: nostr-manager-backend publish-name <npub>"))?;
|
.ok_or_else(|| AppError::config("Usage: keynectr publish-name <npub>"))?;
|
||||||
|
|
||||||
let app = load_app_with_unlock()?;
|
let app = load_app_with_unlock()?;
|
||||||
let key = app.vault_key().copied();
|
let key = app.vault_key().copied();
|
||||||
|
|
@ -227,7 +223,7 @@ async fn cli_feed(args: &[String]) -> Result<String, AppError> {
|
||||||
let limit = rest
|
let limit = rest
|
||||||
.first()
|
.first()
|
||||||
.and_then(|raw| raw.parse::<usize>().ok().filter(|n| *n > 0))
|
.and_then(|raw| raw.parse::<usize>().ok().filter(|n| *n > 0))
|
||||||
.unwrap_or(nostr_manager_backend::feed::DEFAULT_LIMIT);
|
.unwrap_or(keynectr::feed::DEFAULT_LIMIT);
|
||||||
|
|
||||||
let app = App::load()?;
|
let app = App::load()?;
|
||||||
let items = if contacts {
|
let items = if contacts {
|
||||||
|
|
@ -236,10 +232,10 @@ async fn cli_feed(args: &[String]) -> Result<String, AppError> {
|
||||||
.active_profile
|
.active_profile
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.ok_or_else(AppError::no_active_profile)?;
|
.ok_or_else(AppError::no_active_profile)?;
|
||||||
let pubkey = nostr_manager_backend::feed::owner_pubkey(npub)?;
|
let pubkey = keynectr::feed::owner_pubkey(npub)?;
|
||||||
nostr_manager_backend::feed::contact_feed(&app.settings, limit, &pubkey.to_hex()).await?
|
keynectr::feed::contact_feed(&app.settings, limit, &pubkey.to_hex()).await?
|
||||||
} else {
|
} else {
|
||||||
nostr_manager_backend::feed::aggregate_feed(&app.settings, limit).await?
|
keynectr::feed::aggregate_feed(&app.settings, limit).await?
|
||||||
};
|
};
|
||||||
if items.is_empty() {
|
if items.is_empty() {
|
||||||
return Ok(if contacts {
|
return Ok(if contacts {
|
||||||
|
|
@ -274,9 +270,7 @@ fn shorten_note(content: &str) -> String {
|
||||||
|
|
||||||
async fn cli_relays(args: &[String]) -> Result<String, AppError> {
|
async fn cli_relays(args: &[String]) -> Result<String, AppError> {
|
||||||
let sub = args.get(2).ok_or_else(|| {
|
let sub = args.get(2).ok_or_else(|| {
|
||||||
AppError::config(
|
AppError::config("Usage: keynectr relays <list|add|remove|enable|disable|test> [...]")
|
||||||
"Usage: nostr-manager-backend relays <list|add|remove|enable|disable|test> [...]",
|
|
||||||
)
|
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let mut app = App::load()?;
|
let mut app = App::load()?;
|
||||||
|
|
@ -332,7 +326,7 @@ async fn cli_relays(args: &[String]) -> Result<String, AppError> {
|
||||||
fn cli_settings(args: &[String]) -> Result<String, AppError> {
|
fn cli_settings(args: &[String]) -> Result<String, AppError> {
|
||||||
let sub = args
|
let sub = args
|
||||||
.get(2)
|
.get(2)
|
||||||
.ok_or_else(|| AppError::config("Usage: nostr-manager-backend settings <get|set>"))?;
|
.ok_or_else(|| AppError::config("Usage: keynectr settings <get|set>"))?;
|
||||||
|
|
||||||
let mut app = App::load()?;
|
let mut app = App::load()?;
|
||||||
|
|
||||||
|
|
@ -447,7 +441,7 @@ fn cli_unlock() -> Result<String, AppError> {
|
||||||
fn cli_show_secret(args: &[String]) -> Result<String, AppError> {
|
fn cli_show_secret(args: &[String]) -> Result<String, AppError> {
|
||||||
let npub = args
|
let npub = args
|
||||||
.get(2)
|
.get(2)
|
||||||
.ok_or_else(|| AppError::config("Usage: nostr-manager-backend show-secret <npub>"))?;
|
.ok_or_else(|| AppError::config("Usage: keynectr show-secret <npub>"))?;
|
||||||
|
|
||||||
let app = load_app_with_unlock()?;
|
let app = load_app_with_unlock()?;
|
||||||
let revealed = profiles::reveal_secret_key(&app.vault, npub, app.vault_key())?;
|
let revealed = profiles::reveal_secret_key(&app.vault, npub, app.vault_key())?;
|
||||||
|
|
@ -462,7 +456,7 @@ fn cli_show_secret(args: &[String]) -> Result<String, AppError> {
|
||||||
async fn cli_signer(args: &[String]) -> Result<String, AppError> {
|
async fn cli_signer(args: &[String]) -> Result<String, AppError> {
|
||||||
let sub = args
|
let sub = args
|
||||||
.get(2)
|
.get(2)
|
||||||
.ok_or_else(|| AppError::config("Usage: nostr-manager-backend signer <status|connect>"))?;
|
.ok_or_else(|| AppError::config("Usage: keynectr signer <status|connect>"))?;
|
||||||
|
|
||||||
match sub.as_str() {
|
match sub.as_str() {
|
||||||
"status" => {
|
"status" => {
|
||||||
|
|
@ -483,7 +477,7 @@ async fn cli_signer(args: &[String]) -> Result<String, AppError> {
|
||||||
),
|
),
|
||||||
"In the GUI, the signer listens as long as the app is running. From here, run:"
|
"In the GUI, the signer listens as long as the app is running. From here, run:"
|
||||||
.to_string(),
|
.to_string(),
|
||||||
" nostr-manager-backend signer connect <nostrconnect://…>".to_string(),
|
" keynectr signer connect <nostrconnect://…>".to_string(),
|
||||||
"Sign/decrypt requests must be approved in the GUI signer screen.".to_string(),
|
"Sign/decrypt requests must be approved in the GUI signer screen.".to_string(),
|
||||||
]
|
]
|
||||||
.join("\n"))
|
.join("\n"))
|
||||||
|
|
|
||||||
|
|
@ -1,194 +0,0 @@
|
||||||
use nostr_sdk::prelude::*;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::env;
|
|
||||||
use std::fs;
|
|
||||||
use std::path::Path;
|
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
|
||||||
struct Profile {
|
|
||||||
label: String,
|
|
||||||
public_key: String,
|
|
||||||
secret_key: String, // Stored as a plaintext hex string
|
|
||||||
created_at: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
const VAULT_FILE: &str = "profiles_vault.json";
|
|
||||||
|
|
||||||
fn load_vault() -> Result<Vec<Profile>, String> {
|
|
||||||
if !Path::new(VAULT_FILE).exists() {
|
|
||||||
return Ok(Vec::new());
|
|
||||||
}
|
|
||||||
|
|
||||||
let content = fs::read_to_string(VAULT_FILE)
|
|
||||||
.map_err(|e| format!("Failed to read {VAULT_FILE}: {e}"))?;
|
|
||||||
|
|
||||||
if content.trim().is_empty() {
|
|
||||||
return Ok(Vec::new());
|
|
||||||
}
|
|
||||||
|
|
||||||
serde_json::from_str(&content)
|
|
||||||
.map_err(|e| format!("Failed to parse {VAULT_FILE}: {e}"))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn save_vault(profiles: &[Profile]) -> Result<(), String> {
|
|
||||||
let content = serde_json::to_string_pretty(profiles)
|
|
||||||
.map_err(|e| format!("Failed to serialize profiles: {e}"))?;
|
|
||||||
|
|
||||||
fs::write(VAULT_FILE, content)
|
|
||||||
.map_err(|e| format!("Failed to write {VAULT_FILE}: {e}"))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn unix_timestamp() -> Result<u64, String> {
|
|
||||||
SystemTime::now()
|
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.map(|duration| duration.as_secs())
|
|
||||||
.map_err(|e| format!("System clock error: {e}"))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() {
|
|
||||||
if let Err(error) = run().await {
|
|
||||||
eprintln!("Error: {error}");
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn run() -> Result<(), String> {
|
|
||||||
let args: Vec<String> = env::args().collect();
|
|
||||||
|
|
||||||
let command = args.get(1).ok_or_else(|| {
|
|
||||||
[
|
|
||||||
"Usage: nostr-manager-backend <command> [args...]",
|
|
||||||
"Commands:",
|
|
||||||
" create <label>",
|
|
||||||
" list",
|
|
||||||
" switch <npub>",
|
|
||||||
" publish <npub> <content>",
|
|
||||||
]
|
|
||||||
.join("\n")
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let result = match command.as_str() {
|
|
||||||
"create" => {
|
|
||||||
let label = args
|
|
||||||
.get(2)
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or_else(|| "New Profile".to_string());
|
|
||||||
|
|
||||||
let keys = Keys::generate();
|
|
||||||
|
|
||||||
// Explicit ::hex prevents conflict with nostr_sdk::prelude::*.
|
|
||||||
let secret_hex =
|
|
||||||
::hex::encode(keys.secret_key().to_secret_bytes());
|
|
||||||
|
|
||||||
let public_key = keys
|
|
||||||
.public_key()
|
|
||||||
.to_bech32()
|
|
||||||
.map_err(|e| format!("Failed to encode public key: {e}"))?;
|
|
||||||
|
|
||||||
let profile = Profile {
|
|
||||||
label,
|
|
||||||
public_key: public_key.clone(),
|
|
||||||
secret_key: secret_hex,
|
|
||||||
created_at: unix_timestamp()?,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut profiles = load_vault()?;
|
|
||||||
profiles.push(profile);
|
|
||||||
save_vault(&profiles)?;
|
|
||||||
|
|
||||||
format!("Created profile: {public_key}")
|
|
||||||
}
|
|
||||||
|
|
||||||
"list" => {
|
|
||||||
let profiles = load_vault()?;
|
|
||||||
|
|
||||||
serde_json::to_string_pretty(&profiles)
|
|
||||||
.map_err(|e| format!("Failed to serialize profiles: {e}"))?
|
|
||||||
}
|
|
||||||
|
|
||||||
"switch" => {
|
|
||||||
let npub = args
|
|
||||||
.get(2)
|
|
||||||
.ok_or("Usage: nostr-manager-backend switch <npub>")?;
|
|
||||||
|
|
||||||
let profiles = load_vault()?;
|
|
||||||
|
|
||||||
if !profiles.iter().any(|profile| profile.public_key == *npub) {
|
|
||||||
return Err(format!("No stored profile found for {npub}"));
|
|
||||||
}
|
|
||||||
|
|
||||||
format!("Switched context to: {npub}")
|
|
||||||
}
|
|
||||||
|
|
||||||
"publish" => {
|
|
||||||
if args.len() < 4 {
|
|
||||||
return Err(
|
|
||||||
"Usage: nostr-manager-backend publish <npub> <content>"
|
|
||||||
.to_string(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let npub = &args[2];
|
|
||||||
let content = args[3..].join(" ");
|
|
||||||
|
|
||||||
let profiles = load_vault()?;
|
|
||||||
|
|
||||||
let secret_hex = profiles
|
|
||||||
.iter()
|
|
||||||
.find(|profile| profile.public_key == *npub)
|
|
||||||
.map(|profile| profile.secret_key.clone())
|
|
||||||
.ok_or_else(|| format!("No stored profile found for {npub}"))?;
|
|
||||||
|
|
||||||
let secret_bytes = ::hex::decode(&secret_hex)
|
|
||||||
.map_err(|e| format!("Stored secret key is not valid hex: {e}"))?;
|
|
||||||
|
|
||||||
let secret_key = SecretKey::from_slice(&secret_bytes)
|
|
||||||
.map_err(|e| format!("Stored secret key is invalid: {e}"))?;
|
|
||||||
|
|
||||||
// Keys::from(secret_key) is incorrect for this nostr-sdk API.
|
|
||||||
let keys = Keys::new(secret_key);
|
|
||||||
|
|
||||||
let client = Client::new(keys.clone());
|
|
||||||
|
|
||||||
client
|
|
||||||
.add_relay("wss://relay.damus.io")
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Failed to add Damus relay: {e}"))?;
|
|
||||||
|
|
||||||
client
|
|
||||||
.add_relay("wss://relay.nostr.band")
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Failed to add nostr.band relay: {e}"))?;
|
|
||||||
|
|
||||||
client.connect().await;
|
|
||||||
|
|
||||||
let builder = EventBuilder::new(Kind::TextNote, content);
|
|
||||||
|
|
||||||
let event = builder
|
|
||||||
.sign(&keys)
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Failed to sign event: {e}"))?;
|
|
||||||
|
|
||||||
client
|
|
||||||
.send_event(&event)
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Failed to publish event: {e}"))?;
|
|
||||||
|
|
||||||
let event_id = event
|
|
||||||
.id
|
|
||||||
.to_bech32()
|
|
||||||
.map_err(|e| format!("Failed to encode event ID: {e}"))?;
|
|
||||||
|
|
||||||
format!("Published: {event_id}")
|
|
||||||
}
|
|
||||||
|
|
||||||
_ => {
|
|
||||||
return Err(format!("Unknown command: {command}"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
println!("{result}");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
50
src/vault.rs
50
src/vault.rs
|
|
@ -3,6 +3,7 @@ use std::fs;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
|
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::Once;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
use base64::engine::general_purpose::STANDARD as B64;
|
use base64::engine::general_purpose::STANDARD as B64;
|
||||||
|
|
@ -10,7 +11,6 @@ use base64::Engine;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::errors::AppError;
|
use crate::errors::AppError;
|
||||||
use crate::APP_DIR_NAME;
|
|
||||||
|
|
||||||
/// Current vault schema version.
|
/// Current vault schema version.
|
||||||
pub const VAULT_VERSION: u32 = 2;
|
pub const VAULT_VERSION: u32 = 2;
|
||||||
|
|
@ -114,17 +114,57 @@ pub fn unix_timestamp() -> Result<u64, AppError> {
|
||||||
/// Stable application-data directory for this app.
|
/// Stable application-data directory for this app.
|
||||||
///
|
///
|
||||||
/// Uses `$XDG_DATA_HOME` when set, otherwise `~/.local/share`.
|
/// Uses `$XDG_DATA_HOME` when set, otherwise `~/.local/share`.
|
||||||
|
///
|
||||||
|
/// The first call also migrates a previous-named data directory (from before
|
||||||
|
/// the app was renamed) by moving it to the new location, so existing vaults,
|
||||||
|
/// settings and backups are preserved without any user action.
|
||||||
pub fn data_dir() -> PathBuf {
|
pub fn data_dir() -> PathBuf {
|
||||||
if let Ok(dir) = env::var("XDG_DATA_HOME") {
|
let dir = if let Ok(dir) = env::var("XDG_DATA_HOME") {
|
||||||
if !dir.trim().is_empty() {
|
if !dir.trim().is_empty() {
|
||||||
return PathBuf::from(dir).join(APP_DIR_NAME);
|
PathBuf::from(dir).join(crate::APP_DIR_NAME)
|
||||||
|
} else {
|
||||||
|
default_app_dir()
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
default_app_dir()
|
||||||
|
};
|
||||||
|
static MIGRATED: Once = Once::new();
|
||||||
|
MIGRATED.call_once(|| migrate_legacy_app_dir(&dir));
|
||||||
|
dir
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_app_dir() -> PathBuf {
|
||||||
let home = env::var("HOME").unwrap_or_else(|_| ".".to_string());
|
let home = env::var("HOME").unwrap_or_else(|_| ".".to_string());
|
||||||
PathBuf::from(home)
|
PathBuf::from(home)
|
||||||
.join(".local")
|
.join(".local")
|
||||||
.join("share")
|
.join("share")
|
||||||
.join(APP_DIR_NAME)
|
.join(crate::APP_DIR_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move the legacy application-data directory to the new name, once.
|
||||||
|
///
|
||||||
|
/// No-op when the target already exists or there is nothing to move; a failed
|
||||||
|
/// move is reported rather than silently losing the data.
|
||||||
|
fn migrate_legacy_app_dir(target: &Path) {
|
||||||
|
if target.exists() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let legacy = match &target.parent() {
|
||||||
|
Some(parent) => parent.join(crate::LEGACY_APP_DIR_NAME),
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
if !legacy.exists() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Err(e) = fs::rename(&legacy, target) {
|
||||||
|
// Leave both in place: the app starts fresh at `target` and the old
|
||||||
|
// data stays untouched for manual recovery.
|
||||||
|
eprintln!(
|
||||||
|
"Warning: could not move {} to {}: {e}",
|
||||||
|
legacy.display(),
|
||||||
|
target.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn vault_path() -> PathBuf {
|
pub fn vault_path() -> PathBuf {
|
||||||
|
|
@ -402,7 +442,7 @@ mod tests {
|
||||||
|
|
||||||
fn temp_vault_path() -> PathBuf {
|
fn temp_vault_path() -> PathBuf {
|
||||||
let dir = env::temp_dir().join(format!(
|
let dir = env::temp_dir().join(format!(
|
||||||
"nost-feed-manager-test-{}-{}",
|
"keynectr-test-{}-{}",
|
||||||
std::process::id(),
|
std::process::id(),
|
||||||
COUNTER.fetch_add(1, Ordering::SeqCst)
|
COUNTER.fetch_add(1, Ordering::SeqCst)
|
||||||
));
|
));
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue