use app_dirs2::{app_root, AppDataType, AppInfo};
use clap::{Parser, Subcommand};
use nostralink::prelude::*;
use nostralink::util::pretty_event;
use std::fs;
use std::io;
use std::io::{Read, Write};
use std::str::Utf8Error;
const APP_INFO: AppInfo = AppInfo {
name: "nostra",
author: "cipres",
};
#[derive(Debug, Parser)]
#[command(author, version, about, long_about = None)]
struct Args {
#[command(subcommand)]
cmd: Commands,
#[arg(short, long, default_value_t = false, help = "Show events as JSON")]
json: bool,
#[arg(
short,
long,
default_value_t = false,
help = "Format events inside markdown codeblocks"
)]
code_blocks: bool,
}
#[derive(Subcommand, Debug, Clone)]
enum Commands {
Note {
text: String,
},
LDNote {
text: String,
#[arg(short, help = "Language")]
lang_tag: Option<String>,
#[arg(short, help = "Headline")]
headline: Option<String>,
},
Convert,
Metadata {
#[arg(short, help = "Name")]
name: String,
#[arg(short, help = "Display name")]
display_name: String,
#[arg(short, help = "About")]
about: String,
#[arg(short, help = "NIP05")]
nip05: String,
},
}
#[derive(Debug)]
enum Error {
InitFailed,
StoreError,
LDError,
IOError,
KeysError,
Utf8Error,
EventFormatError,
EventBuildError,
SerializeError,
}
impl From<LDError> for Error {
fn from(_e: LDError) -> Self {
Self::LDError
}
}
impl From<std::io::Error> for Error {
fn from(_e: std::io::Error) -> Self {
Self::IOError
}
}
impl From<nostr::key::Error> for Error {
fn from(_e: nostr::key::Error) -> Self {
Self::KeysError
}
}
impl From<Utf8Error> for Error {
fn from(_e: Utf8Error) -> Self {
Self::Utf8Error
}
}
impl From<serde_json::Error> for Error {
fn from(_e: serde_json::Error) -> Self {
Self::SerializeError
}
}
impl From<nostr::event::builder::Error> for Error {
fn from(_e: nostr::event::builder::Error) -> Self {
Self::EventBuildError
}
}
#[tokio::main]
async fn main() -> Result<(), Error> {
let data_dir = app_root(AppDataType::UserData, &APP_INFO)
.map_err(|_| Error::InitFailed)?;
let keys_path = data_dir.join("keys");
let keys = if !keys_path.exists() {
let k = Keys::generate();
let mut file = match fs::File::create(&keys_path.display().to_string())
{
Ok(f) => f,
Err(e) => panic!("File create error: {e:?}"),
};
match file.write_all(k.secret_key().to_secret_hex().as_bytes()) {
Ok(_) => {}
Err(e) => panic!("File write error: {e:?}"),
}
k
} else {
let key: String = fs::read_to_string(keys_path.display().to_string())?;
Keys::parse(key.as_str())?
};
let args = Args::parse();
let store = RdfEventsStore::new(
Store::open(data_dir.join("store")).expect("Cannot create store"),
)
.map_err(|_| Error::StoreError)?;
let evb = match args.cmd {
Commands::Note { text } => Some(
EventBuilder::text_note(text)
.sign(&keys)
.await
.expect("Cannot sign event"),
),
Commands::LDNote {
text,
lang_tag,
headline,
} => {
let l_tag = lang_tag.unwrap_or("en-GB".to_owned());
match Language::from_tag(&l_tag) {
Some(lang) => {
let mut note = NoteBuilder::default()
.language(lang)
.headline(headline)
.body(Some(text))
.build()
.map_err(|_| Error::EventBuildError)?;
let event = note.event_builder()?.sign(&keys).await?;
Some(event)
}
None => panic!("Invalid language"),
}
}
Commands::Metadata {
name,
display_name,
about,
nip05,
} => {
let metadata = Metadata::new()
.name(name)
.display_name(display_name)
.about(about)
.nip05(nip05);
Some(
EventBuilder::metadata(&metadata)
.sign(&keys)
.await
.expect("Cannot sign event"),
)
}
Commands::Convert => {
let mut input = Vec::new();
let stdin = io::stdin();
let mut handle = stdin.lock();
let _ = handle.read_to_end(&mut input);
let all =
std::str::from_utf8(&input).map_err(|_| Error::Utf8Error)?;
if let Ok(ev) = Event::from_json(all) {
Some(ev)
} else {
None
}
}
};
if let Some(event) = evb {
if args.json {
if args.code_blocks {
println!("```json")
}
println!(
"{}\n",
pretty_event(&event).map_err(|_| Error::EventFormatError)?
);
if args.code_blocks {
println!("```")
}
}
match ttlify_event(&event).await {
Ok(ttl) => {
if args.code_blocks {
println!("```ttl")
}
println!("{}", ttl);
if args.code_blocks {
println!("```")
}
}
Err(e) => {
eprintln!("Error converting event to RDF: {e:?}");
}
}
store.feed_event(Box::new(event))?;
}
Ok(())
}