nostralink 0.2.1

Linked data library for nostr
Documentation
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 => {
            // Convert the JSON event on stdin
            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(())
}