diff --git a/Cargo.lock b/Cargo.lock index 0990313..beab2ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -677,7 +677,7 @@ dependencies = [ [[package]] name = "nostr-commander" -version = "0.0.3" +version = "0.0.4" dependencies = [ "anyhow", "clap", @@ -1416,9 +1416,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" [[package]] name = "unicase" diff --git a/Cargo.toml b/Cargo.toml index 9f6466e..07585a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "nostr-commander" -version = "0.0.3" +version = "0.0.4" edition = "2021" description = "simple but convenient CLI-based Nostr client app for publishing,sending and subscribing" documentation = "https://docs.rs/nostr-commander" diff --git a/README.md b/README.md index d05cfc2..1287224 100644 --- a/README.md +++ b/README.md @@ -185,7 +185,10 @@ Options: -l, --listen Listen to events, notifications and messages. This option listens to events and messages forever. To stop, type Control-C on your - keyboard. E.g. this helps you get event ids for published notices + keyboard. You want to listen if you want to get the event ids for + published notices. Subscriptions do not automatically turn listening + on. If you want to listen to your subscriptions, you must use + --listen --add-contact Add one or more contacts. Must be used in combination with --alias, --key, --relay. If you want to add N new contacts, use --add-contact @@ -208,6 +211,14 @@ Options: --relay [...] Provide one or multiple relays for argument --add-contact. They have the form 'wss://some.relay.org' + --subscribe-author [...] + Subscribe to one or more authors. Specify each author by its public + key in form of 'npub1SomePublicKey'. Alternatively you can use the + Hex form of the private key + --subscribe-pubkey [...] + Subscribe to one or more public keys. Specify each public key in form + of 'npub1SomePublicKey'. Alternatively you can use the Hex form of + the private key -h, --help Print help information (use `--help` for more detail) ``` diff --git a/VERSION b/VERSION index bcab45a..81340c7 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.3 +0.0.4 diff --git a/help.txt b/help.txt index f748bab..bbe8ec3 100644 --- a/help.txt +++ b/help.txt @@ -144,7 +144,10 @@ Options: -l, --listen Listen to events, notifications and messages. This option listens to events and messages forever. To stop, type Control-C on your - keyboard. E.g. this helps you get event ids for published notices + keyboard. You want to listen if you want to get the event ids for + published notices. Subscriptions do not automatically turn listening + on. If you want to listen to your subscriptions, you must use + --listen --add-contact Add one or more contacts. Must be used in combination with --alias, --key, --relay. If you want to add N new contacts, use --add-contact @@ -167,5 +170,13 @@ Options: --relay [...] Provide one or multiple relays for argument --add-contact. They have the form 'wss://some.relay.org' + --subscribe-author [...] + Subscribe to one or more authors. Specify each author by its public + key in form of 'npub1SomePublicKey'. Alternatively you can use the + Hex form of the private key + --subscribe-pubkey [...] + Subscribe to one or more public keys. Specify each public key in form + of 'npub1SomePublicKey'. Alternatively you can use the Hex form of + the private key -h, --help Print help information (use `--help` for more detail) diff --git a/src/main.rs b/src/main.rs index cda1964..92bed29 100644 --- a/src/main.rs +++ b/src/main.rs @@ -44,11 +44,14 @@ use url::Url; use nostr_sdk::{ nostr::contact::Contact, + nostr::key::XOnlyPublicKey, nostr::key::{FromBech32, KeyError, Keys, ToBech32}, nostr::message::relay::RelayMessage, + nostr::message::subscription::SubscriptionFilter, // nostr::util::nips::nip04::Error as Nip04Error, nostr::Metadata, relay::pool::RelayPoolNotifications, + subscription::Subscription, Client, RelayPoolNotifications::ReceivedEvent, RelayPoolNotifications::ReceivedMessage, @@ -121,9 +124,15 @@ pub enum Error { #[error("Listen Failed")] ListenFailed, + #[error("Subscription Failed")] + SubscriptionFailed, + #[error("Invalid Client Connection")] InvalidClientConnection, + #[error("Invalid Key")] + InvalidKey, + #[error("Unknown CLI parameter")] UnknownCliParameter, @@ -543,8 +552,11 @@ pub struct Args { /// Listen to events, notifications and messages. /// This option listens to events and messages forever. To stop, type - /// Control-C on your keyboard. E.g. this helps you get event ids for - /// published notices. + /// Control-C on your keyboard. You want to listen if you want + /// to get the event ids for published notices. + /// Subscriptions do not automatically turn listening on. + /// If you want to listen to your subscriptions, you must use + /// --listen. #[arg(short, long, default_value_t = false)] listen: bool, @@ -574,6 +586,7 @@ pub struct Args { /// Provide one or multiple public keys for argument /// --add-contact. They have the form 'npub1SomeStrangeString'. + // todo: allow Hex keys #[arg(long, value_name = "KEY", num_args(0..), )] key: Vec, @@ -581,6 +594,19 @@ pub struct Args { /// --add-contact. They have the form 'wss://some.relay.org'. #[arg(long, value_name = "RELAY", num_args(0..), )] relay: Vec, + + /// Subscribe to one or more authors. Specify each author by its + /// public key in form of 'npub1SomePublicKey'. + /// Alternatively you can use the Hex form of the private key. + #[arg(long, value_name = "KEY", num_args(0..), )] + subscribe_author: Vec, + + /// Subscribe to one or more public keys. Specify each + /// public key in form of 'npub1SomePublicKey'. + /// Alternatively you can use the Hex form of the private key. + #[arg(long, value_name = "KEY", num_args(0..), )] + subscribe_pubkey: Vec, + // todo: unsubscribe } impl Default for Args { @@ -627,6 +653,8 @@ impl Args { alias: Vec::new(), key: Vec::new(), relay: Vec::new(), + subscribe_author: Vec::new(), + subscribe_pubkey: Vec::new(), } } } @@ -641,6 +669,8 @@ pub struct Credentials { relays: Vec, metadata: Metadata, contacts: Vec, + subscribed_authors: Vec, + subscribed_pubkeys: Vec, } impl AsRef for Credentials { @@ -665,6 +695,8 @@ impl Credentials { relays: Vec::new(), metadata: Metadata::new(), contacts: Vec::new(), + subscribed_authors: Vec::new(), + subscribed_pubkeys: Vec::new(), } } @@ -1374,18 +1406,115 @@ pub(crate) async fn cli_add_contact(client: &Client, ap: &mut Args) -> Result<() Ok(()) } -/// Handle the --add-conect CLI argument, write contacts from CLI args into creds data structure +/// Handle the --add-conect CLI argument, remove CLI args contacts from creds data structure pub(crate) async fn cli_remove_contact(client: &Client, ap: &mut Args) -> Result<(), Error> { // Todo - let anum = ap.alias.len(); + let num = ap.alias.len(); let mut i = 0; - while i < anum { + while i < num { ap.creds.contacts.retain(|x| x.alias != ap.alias[i].trim()); i += 1; } Ok(()) } +/// Handle the --subscribe-author CLI argument, moving authors from CLI args into creds data structure +pub(crate) async fn cli_subscribe_author(client: &mut Client, ap: &mut Args) -> Result<(), Error> { + let mut err_count = 0usize; + let num = ap.subscribe_author.len(); + let mut authors = Vec::new(); + let mut i = 0; + while i < num { + match str_to_pubkey(&ap.subscribe_author[i]) { + Ok(pkey) => { + authors.push(pkey); + debug!( + "Valid key added to subscription filter. Key {:?}, {:?}.", + &ap.subscribe_author[i], pkey + ); + } + Err(ref e) => { + error!( + "Error: Invalid key {:?}. Not added to subscription filter.", + &ap.subscribe_author[i] + ); + err_count += 1; + } + } + i += 1; + } + ap.creds.subscribed_authors.append(&mut authors); + ap.creds.subscribed_authors.dedup_by(|a, b| a == b); + if err_count != 0 { + Err(Error::SubscriptionFailed) + } else { + Ok(()) + } +} + +/// Convert npub1... Bech32 key or Hex key into a XOnlyPublicKey +pub(crate) fn str_to_pubkey(s: &str) -> Result { + match Keys::from_bech32_public_key(s) { + Ok(keys) => { + debug!( + "Valid key in Bech32 format: Npub {:?}, Hex {:?}", + s, + keys.public_key().to_string() + ); + return Ok(keys.public_key()); + } + Err(ref e) => match XOnlyPublicKey::from_str(s) { + Ok(pkey) => { + debug!( + "Valid key in Hex format: Hex {:?}, Npub {:?}", + s, + pkey.to_bech32().unwrap() + ); + return Ok(pkey); + } + Err(ref e) => { + error!("Error: Invalid key {:?}. Reported error: {:?}.", s, e); + return Err(Error::InvalidKey); + } + }, + } +} + +/// Handle the --subscribe-pubkey CLI argument, moving pkeys from CLI args into creds data structure +pub(crate) async fn cli_subscribe_pubkey(client: &mut Client, ap: &mut Args) -> Result<(), Error> { + let mut err_count = 0usize; + let num = ap.subscribe_pubkey.len(); + let mut pubkeys = Vec::new(); + let mut i = 0; + while i < num { + match str_to_pubkey(&ap.subscribe_pubkey[i]) { + Ok(pkey) => { + pubkeys.push(pkey); + debug!( + "Valid key added to subscription filter. Key {:?}, Hex: {:?}.", + &ap.subscribe_pubkey[i], + pkey.to_string() + ); + } + Err(ref e) => { + error!( + "Error: Invalid key {:?}. Not added to subscription filter.", + &ap.subscribe_pubkey[i] + ); + err_count += 1; + } + } + i += 1; + } + ap.creds.subscribed_pubkeys.append(&mut pubkeys); + ap.creds.subscribed_pubkeys.dedup_by(|a, b| a == b); + if err_count != 0 { + Err(Error::SubscriptionFailed) + } else { + Ok(()) + } +} + /// Utility function to print JSON object as JSON or as plain text pub(crate) fn print_json(json_data: &json::JsonValue, output: Output) { debug!("{:?}", json_data); @@ -1640,6 +1769,7 @@ async fn main() -> Result<(), Error> { if ap.show_contacts { println!("Contacts: {:?}", ap.creds.contacts); } + // ap.creds.save(get_credentials_actual_path(&ap))?; // do it later // Publish a text note if !ap.publish.is_empty() { @@ -1675,8 +1805,62 @@ async fn main() -> Result<(), Error> { } } + // Subscribe authors + if !ap.subscribe_author.is_empty() { + match crate::cli_subscribe_author(&mut client, &mut ap).await { + Ok(()) => { + info!("subscribe_author successful."); + } + Err(ref e) => { + error!("subscribe_author failed. Reported error is: {:?}", e); + } + } + } + match client + .subscribe(vec![ + SubscriptionFilter::new().authors(ap.creds.subscribed_authors.clone()) + ]) + .await + { + Ok(()) => { + info!("subscribe to authors successful."); + } + Err(ref e) => { + error!("subscribe to authors failed. Reported error is: {:?}", e); + } + } + // Subscribe keys + if !ap.subscribe_pubkey.is_empty() { + match crate::cli_subscribe_pubkey(&mut client, &mut ap).await { + Ok(()) => { + info!("subscribe_pubkey successful."); + } + Err(ref e) => { + error!("subscribe_pubkey failed. Reported error is: {:?}", e); + } + } + } + match client + .subscribe(vec![ + SubscriptionFilter::new().pubkeys(ap.creds.subscribed_pubkeys.clone()) + ]) + .await + { + Ok(()) => { + info!("subscribe to pubkeys successful."); + } + Err(ref e) => { + error!("subscribe to pubkeys failed. Reported error is: {:?}", e); + } + } + ap.creds.save(get_credentials_actual_path(&ap))?; + // notices will be published even if we do not go into handle_notification event loop - if ap.listen { + // Do not automatically listen when subscriptions exist, only listen to subscriptions if --listen is set. + if ap.listen + // || !ap.creds.subscribed_authors.is_empty() + // || !ap.creds.subscribed_pubkeys.is_empty() + { let num = ap.publish.len() + ap.publish_pow.len(); info!( "You should be receiving {:?} 'OK' messages with event ids, one for each notice that has been relayed.",