Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# build
/target
*log
# ide
.cursorrules
.cursor/
.vscode/
.cursor/commands


118 changes: 97 additions & 21 deletions src/settings.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
use crate::SETTINGS;
use nostr_sdk::prelude::*;
use serde::{Deserialize, Serialize};
use std::{
env, fs,
path::{Path, PathBuf},
};
use std::{env, fs, path::PathBuf};

/// Embedded default `settings.toml` used to bootstrap configuration on first run.
/// This is generated at compile time from the repository root `settings.toml`.
const DEFAULT_SETTINGS_TOML: &str = include_str!("../settings.toml");

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Settings {
Expand Down Expand Up @@ -101,39 +103,113 @@ fn validate_currencies_config(settings_path: &PathBuf) -> Result<(), anyhow::Err

/// Internal helper: ensure settings file exists and load it from disk
fn init_or_load_settings_from_disk() -> Result<Settings, anyhow::Error> {
// HOME and package name at compile time
// Legacy location: ~/.mostrix/settings.toml (kept for backwards compatibility).
let home_dir =
dirs::home_dir().ok_or_else(|| anyhow::anyhow!("Could not find home directory"))?;
let package_name = env!("CARGO_PKG_NAME");
let hidden_dir = home_dir.join(format!(".{package_name}"));
let hidden_file = hidden_dir.join("settings.toml");

// Path to the settings.toml included in the repo (next to Cargo.toml)
let default_file: PathBuf = Path::new(env!("CARGO_MANIFEST_DIR")).join("settings.toml");
// Helper: load a settings file from the given path.
fn load_settings_from_path(path: &PathBuf) -> Result<Settings, anyhow::Error> {
validate_currencies_config(path)?;

let cfg = config::Config::builder()
.add_source(config::File::from(path.as_path()))
.build()
.map_err(|e| anyhow::anyhow!("settings.toml malformed: {}", e))?;

let settings: Settings = cfg
.try_deserialize()
.map_err(|e| anyhow::anyhow!("Error deserializing settings.toml: {}", e))?;

Ok(settings)
}

// Case B: legacy ~/.mostrix/settings.toml exists -> load with the old placeholder guard.
if hidden_file.exists() {
let settings = load_settings_from_path(&hidden_file)?;

if settings.mostro_pubkey == "mostro_pubkey_hex_format"
|| settings.nsec_privkey == "nsec1_privkey_format"
{
let path_display = hidden_file.display();
anyhow::bail!(
"Default settings.toml already exists at {} but still contains placeholder values.\n\
Please edit this file and replace placeholder values (mostro_pubkey, nsec_privkey, etc.) \
with your real keys before running Mostrix again.",
path_display
);
}

return Ok(settings);
}

// Create ~/.mostrix if it doesn't exist
// Case C: Truly first run: no config anywhere.
// Auto-generate in HOME/.mostrix with sensible defaults as per
// https://github.com/MostroP2P/mostrix/issues/40.
if !hidden_dir.exists() {
fs::create_dir(&hidden_dir).map_err(|e| {
fs::create_dir_all(&hidden_dir).map_err(|e| {
anyhow::anyhow!("The configuration directory could not be created: {}", e)
})?;
}

// Copy settings.toml if it isn't already in ~/.mostrix
if !hidden_file.exists() {
fs::copy(&default_file, &hidden_file)
.map_err(|e| anyhow::anyhow!("Could not copy default settings.toml: {}", e))?;
// Start from the embedded default template, then override fields.
let mut settings: Settings = toml::from_str(DEFAULT_SETTINGS_TOML)
.map_err(|e| anyhow::anyhow!("Embedded DEFAULT_SETTINGS_TOML is malformed: {}", e))?;

// Generate a fresh Nostr keypair for the user.
let keys = Keys::generate();
let sk = keys.secret_key();
let nsec = sk
.to_bech32()
.map_err(|e| anyhow::anyhow!("Failed to encode generated Nostr secret key: {}", e))?;
let npub = keys
.public_key()
.to_bech32()
.map_err(|e| anyhow::anyhow!("Failed to encode generated Nostr public key: {}", e))?;

// Apply sensible defaults from the issue.
settings.nsec_privkey = nsec;
settings.relays = vec!["wss://relay.mostro.network".to_string()];
settings.user_mode = "user".to_string();
settings.pow = 0;
settings.currencies_filter = Vec::new();
settings.mostro_pubkey =
"82fa8cb978b43c79b2156585bac2c022276a21d2aead6d9f7c575c005be88390".to_string();

// Serialize to TOML.
let toml_string = toml::to_string_pretty(&settings)
.map_err(|e| anyhow::anyhow!("Failed to serialize generated settings: {}", e))?;

#[cfg(unix)]
{
use std::io::Write;
use std::os::unix::fs::OpenOptionsExt;

let mut file = fs::OpenOptions::new()
.write(true)
.create_new(true)
.mode(0o600)
.open(&hidden_file)
.map_err(|e| anyhow::anyhow!("Could not write generated settings.toml: {}", e))?;
file.write_all(toml_string.as_bytes())
.map_err(|e| anyhow::anyhow!("Could not write generated settings.toml: {}", e))?;
}

validate_currencies_config(&hidden_file)?;
#[cfg(not(unix))]
{
fs::write(&hidden_file, toml_string)
.map_err(|e| anyhow::anyhow!("Could not write generated settings.toml: {}", e))?;
}

// Use the `config` crate to deserialize to the Settings struct
let cfg = config::Config::builder()
.add_source(config::File::from(hidden_file.as_path()))
.build()
.map_err(|e| anyhow::anyhow!("settings.toml malformed: {}", e))?;
println!(
"First run: generated settings.toml at {}.\nYour Nostr public key (npub) is: {}",
hidden_file.display(),
npub
);

cfg.try_deserialize::<Settings>()
.map_err(|e| anyhow::anyhow!("Error deserializing settings.toml: {}", e))
Ok(settings)
}

/// Public helper: reload current settings from disk (reflects all previous saves)
Expand Down
Loading