feat: nostr domain hosting
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2025-04-03 12:56:20 +01:00
parent a4850b4e06
commit c432f603ec
32 changed files with 724 additions and 70 deletions

View File

@ -4,4 +4,14 @@ version = "0.1.0"
edition = "2024"
[dependencies]
lnvps_db = { path = "../lnvps_db" }
lnvps_db = { path = "../lnvps_db", features = ["nostr-domain"] }
lnvps_common = { path = "../lnvps_common" }
env_logger.workspace = true
log.workspace = true
anyhow.workspace = true
tokio.workspace = true
serde.workspace = true
config.workspace = true
serde_json.workspace = true
rocket.workspace = true
hex.workspace = true

3
lnvps_nostr/README.md Normal file
View File

@ -0,0 +1,3 @@
# LNVPS Nostr Services
A simple webserver hosting various nostr based services for lnvps.net

5
lnvps_nostr/config.yaml Normal file
View File

@ -0,0 +1,5 @@
# Connection string to lnvps database
db: "mysql://root:root@localhost:3376/lnvps"
# Listen address for http server
listen: "127.0.0.1:8001"

View File

@ -1,3 +1,65 @@
fn main() {
println!("Hello, world!");
mod routes;
use crate::routes::routes;
use anyhow::Result;
use config::{Config, File};
use lnvps_common::CORS;
use lnvps_db::{LNVPSNostrDb, LNVpsDbMysql};
use log::error;
use rocket::http::Method;
use serde::Deserialize;
use std::net::{IpAddr, SocketAddr};
use std::path::PathBuf;
use std::sync::Arc;
#[derive(Clone, Deserialize)]
struct Settings {
/// Database connection string
db: String,
/// Listen address for http server
listen: Option<String>,
}
#[rocket::main]
async fn main() -> Result<()> {
env_logger::init();
let settings: Settings = Config::builder()
.add_source(File::from(PathBuf::from("config.yaml")))
.build()?
.try_deserialize()?;
// Connect database
let db = LNVpsDbMysql::new(&settings.db).await?;
let db: Arc<dyn LNVPSNostrDb> = Arc::new(db);
let mut config = rocket::Config::default();
let ip: SocketAddr = match &settings.listen {
Some(i) => i.parse()?,
None => SocketAddr::new(IpAddr::from([0, 0, 0, 0]), 8000),
};
config.address = ip.ip();
config.port = ip.port();
if let Err(e) = rocket::Rocket::custom(config)
.manage(db.clone())
.manage(settings.clone())
.attach(CORS)
.mount("/", routes())
.mount(
"/",
vec![rocket::Route::ranked(
isize::MAX,
Method::Options,
"/<catch_all_options_route..>",
CORS,
)],
)
.launch()
.await
{
error!("{}", e);
}
Ok(())
}

65
lnvps_nostr/src/routes.rs Normal file
View File

@ -0,0 +1,65 @@
use lnvps_db::LNVPSNostrDb;
use log::info;
use rocket::request::{FromRequest, Outcome};
use rocket::serde::json::Json;
use rocket::{Request, Route, State, routes};
use serde::Serialize;
use std::collections::HashMap;
use std::sync::Arc;
pub fn routes() -> Vec<Route> {
routes![nostr_address]
}
#[derive(Serialize)]
struct NostrJson {
pub names: HashMap<String, String>,
pub relays: HashMap<String, Vec<String>>,
}
struct HostInfo<'r> {
pub host: Option<&'r str>,
}
#[rocket::async_trait]
impl<'r> FromRequest<'r> for HostInfo<'r> {
type Error = ();
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
Outcome::Success(HostInfo {
host: request.host().map(|h| h.domain().as_str()),
})
}
}
#[rocket::get("/.well-known/nostr.json?<name>")]
async fn nostr_address(
host: HostInfo<'_>,
db: &State<Arc<dyn LNVPSNostrDb>>,
name: Option<&str>,
) -> Result<Json<NostrJson>, &'static str> {
let name = name.unwrap_or("_");
let host = host.host.unwrap_or("lnvps.net");
info!("Got request for {} on host {}", name, host);
let domain = db
.get_domain_by_name(host)
.await
.map_err(|_| "Domain not found")?;
let handle = db
.get_handle_by_name(domain.id, name)
.await
.map_err(|_| "Handle not found")?;
let pubkey_hex = hex::encode(handle.pubkey);
let relays = if let Some(r) = handle.relays {
r.split(",").map(|x| x.to_string()).collect()
} else if let Some(r) = domain.relays {
r.split(",").map(|x| x.to_string()).collect()
} else {
vec![]
};
Ok(Json(NostrJson {
names: HashMap::from([(name.to_string(), pubkey_hex.clone())]),
relays: HashMap::from([(pubkey_hex, relays)]),
}))
}