diff --git a/README.md b/README.md new file mode 100644 index 0000000..748467c --- /dev/null +++ b/README.md @@ -0,0 +1,146 @@ +# EasyNostr + +A simple and functional Rust crate for building Nostr applications. This library handles the complexity of keys, relays, and protocol details (NIPs), giving you easy-to-use functions for a social media app. + +## Features +- **Identity**: Easy management of keys (nsec/npub). +- **Social Feed**: View timeline of friends or global random posts. +- **Posting**: Publish text notes (Kind 1). +- **Contacts**: Follow users and retrieve your contact list. +- **Direct Messages**: Send and receive encrypted private messages (NIP-17 & NIP-04). + +## Getting Started + +### 1. Initialize the Client +First, you need to create an instance of `EasyNostr` using your secret key (nsec). + +```rust +use easy_nostr::EasyNostr; +use anyhow::Result; + +#[tokio::main] +async fn main() -> Result<()> { + // Your secret key (nsec) + let my_secret_key = "nsec1..."; + + // Initialize client + let client = EasyNostr::new(my_secret_key).await?; + + // Connect to Relays + client.add_relays(vec![ + "wss://relay.damus.io", + "wss://nos.lol", + "wss://relay.primal.net" + ]).await?; + + println!("Connected and ready!"); + Ok(()) +} +``` + +--- + +## Capabilities & Usage + +### 📝 Publishing Posts +Post a simple text message to the network. + +```rust +let event_id = client.post_text("Hello Nostr world!").await?; +println!("Posted! ID: {}", event_id.to_bech32()?); +``` + +### 📰 Reading Feeds +You can read posts from people you follow (Timeline) or random posts from the network. + +**Get Timeline (Followed Users):** +```rust +// List of public keys (npubs) you want to see posts from +let following = vec!["npub1...", "npub1..."]; + +let posts = client.get_timeline(following).await?; + +for post in posts { + println!("@{} wrote: {}", post.author.to_bech32()?, post.content); +} +``` + +**Get Global Feed (Random/Recent):** +```rust +let random_posts = client.get_random_posts().await?; + +for post in random_posts { + println!("New Post: {}", post.content); +} +``` + +**The `Post` Structure:** +When you get posts, you receive a `Post` struct: +- `post.id`: The unique Event ID. +- `post.author`: The PublicKey of the author. +- `post.content`: The text content. +- `post.created_at`: The timestamp. + +--- + +### đŸ‘„ Managing Contacts (Followers) +Manage who you follow. + +**Follow a User:** +```rust +let jack_dorsey = "npub1sg6plzptd64u62a878hep2kd8856nmmhd0x479jp6bir725uqqks698teq"; +// Context: (NPub, Optional Nickname) +client.create_contact(jack_dorsey, Some("Jack".to_string())).await?; +``` + +**Get Your Contact List:** +```rust +let my_contacts = client.get_contacts().await?; + +for contact in my_contacts { + println!("Following: {:?}", contact.public_key.to_bech32()?); + if let Some(alias) = &contact.alias { + println!(" - Alias: {}", alias); + } +} +``` + +--- + +### 🔒 Direct Messages (DMs) +Send and receive encrypted private chats. Supports modern GiftWraps (NIP-17). + +**Send a DM:** +```rust +let friend_npub = "npub1..."; +client.send_private_message(friend_npub, "Hey, this is secret!").await?; +``` + +**Read DMs:** +```rust +let friend_npub = "npub1..."; +let messages = client.get_private_messages(friend_npub).await?; + +for msg in messages { + let direction = if msg.is_incoming { "Received" } else { "Sent" }; + println!("[{}] {}", direction, msg.content); +} +``` + +**The `Message` Structure:** +- `msg.sender`: PublicKey of the sender. +- `msg.content`: Decrypted text. +- `msg.is_incoming`: `true` if you received it, `false` if you sent it. +- `msg.created_at`: Use `msg.created_at.as_secs()` to get the unix timestamp. + +--- + +## Dependencies +Ensure you have the following in your `Cargo.toml`. `easy-nostr` re-exports most things you need, but you might need `anyhow` and `tokio`. + +```toml +[dependencies] +nostr-sdk = { version = "0.44.1", features = ["all-nips", "nip44"] } +tokio = { version = "1.48.0", features = ["full"] } +anyhow = "1.0.100" +``` diff --git a/big_promts.md b/big_promts.md new file mode 100644 index 0000000..7f31a2d --- /dev/null +++ b/big_promts.md @@ -0,0 +1,19 @@ +Du bist erfahrener Rust entwickler und hĂ€llst den code simpel aber funktional. Achte darauf die richtigen versionen zu verwenden: nostr-sdk = { version = "0.44.1", features = ["all-nips", "nip44"] } +tokio = { version = "1.48.0", features = ["full"] } +secp256k1 = "0.27" +anyhow = "1.0.100" + Kommentiere alles in einfachem English im code fĂŒrs verstĂ€ndnis. Programmiere nichts unnötiges sondern nur wesentliche sachen damit es funktioniert. Baue nun folgendes ein, passe dich auf meine Codestruktur an: Der Feed / Timeline (fĂŒge hinzu das einen Feed abrufen kann also posts von leuten sieht denen man folgt und eine funktion in der lib.rs mit der man zufĂ€llige posts laden kann); Erstelle noch eine funktion in der lib.rs das man Text posts selber machen kann. Teile alles wieder auf in @src/nips und @src/functions und die lib.rs @lib.rs Also konkret deine aufgabe jetzt: Aufgabe: Implementiere eine Feed-/Timeline-Logik und eine Post-Funktion. + +lib.rs: Zentrale Einstiegspunkte. + +src/functions: Logik fĂŒr get_feed (Posts von gefolgten Personen) und get_random_posts. + +src/nips: Definition der Datenstrukturen (Post-Typen). + +Features: + +Funktion zum Erstellen eines Text-Posts. + +Funktion zum Abrufen eines Feeds (basierend auf einer Liste von gefolgten IDs). + +Funktion fĂŒr zufĂ€llige Posts. \ No newline at end of file diff --git a/production_test.log b/production_test.log new file mode 100644 index 0000000..824b5ef --- /dev/null +++ b/production_test.log @@ -0,0 +1,35 @@ +warning: use of deprecated method `nostr_sdk::Timestamp::as_u64`: Use `as_secs` instead + --> src/bin/testall.rs:40:63 + | +40 | ...w().as_u64()); + | ^^^^^^ + | + = note: `#[warn(deprecated)]` on by default + +warning: `easy-nostr` (bin "testall") generated 1 warning + Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.10s + Running `target/debug/testall` +=== EasyNostr Production Test Suite === +[INFO] Identity: npub1jx2cnrlnpvy9gxgfnkqf30l0u3jkgjmp76jcvgxw5uuf5lqynduqz9n2me + +[STEP 1] Connecting to Relays... + [OK] Connected. + +[STEP 2] Following Self (for robust timeline testing)... + [OK] Followed self. + +[STEP 3] Publishing Post... + [OK] Posted: note10xlp2za0p0ug3v49ggve2gv2lzfmgltzk84a7qkueskeau9kj5nshcfu20 + +[STEP 4] verifying Timeline (expecting own post)... + [OK] Found our post in timeline after 2s! + +[STEP 5] Testing Direct Messages... + ... attempt 1/5: DM not yet visible... + ... attempt 2/5: DM not yet visible... + ... attempt 3/5: DM not yet visible... + ... attempt 4/5: DM not yet visible... + ... attempt 5/5: DM not yet visible... + [ERR] DM propagation failed. + +=== Test Complete === diff --git a/production_test_v2.log b/production_test_v2.log new file mode 100644 index 0000000..69c9b21 --- /dev/null +++ b/production_test_v2.log @@ -0,0 +1,31 @@ + Compiling easy-nostr v0.1.0 (/home/malxte/Documents/Programming/Rust/Crates/easy-nostr) +warning: use of deprecated method `nostr_sdk::Timestamp::as_u64`: Use `as_secs` instead + --> src/bin/testall.rs:40:63 + | +40 | let secret_code = format!("Test Run {}", Timestamp::now().as_u64()); + | ^^^^^^ + | + = note: `#[warn(deprecated)]` on by default + +warning: `easy-nostr` (bin "testall") generated 1 warning + Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.40s + Running `target/debug/testall` +=== EasyNostr Production Test Suite === +[INFO] Identity: npub1tfqs84ttgcw8z424dsrzzkchmee9s22hzpahxxxmyrg3z82zwmusmq4aga + +[STEP 1] Connecting to Relays... + [OK] Connected. + +[STEP 2] Following Self (for robust timeline testing)... + [OK] Followed self. + +[STEP 3] Publishing Post... + [OK] Posted: note1p79vn2myyd29adw3dk0t0vuxk50ng0fkqk4yhhq34u085j4k65rqq40c0q + +[STEP 4] verifying Timeline (expecting own post)... + [OK] Found our post in timeline after 2s! + +[STEP 5] Testing Direct Messages... + [OK] Found DM after 2s! + +=== Test Complete === diff --git a/src/bin/testall.rs b/src/bin/testall.rs new file mode 100644 index 0000000..1238864 --- /dev/null +++ b/src/bin/testall.rs @@ -0,0 +1,93 @@ +use easy_nostr::EasyNostr; +use nostr_sdk::prelude::*; +use std::time::Duration; +use tokio::time::sleep; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + println!("=== EasyNostr Production Test Suite ==="); + + // 1. Identity + let keys = Keys::generate(); + let nsec = keys.secret_key().to_bech32()?; + let npub = keys.public_key().to_bech32()?; + + println!("[INFO] Identity: {}", npub); + + // 2. Init + let ez = EasyNostr::new(&nsec).await?; + + // 3. Relays (More reliable set) + println!("\n[STEP 1] Connecting to Relays..."); + let relays = vec![ + "wss://relay.damus.io", + "wss://nos.lol", + "wss://relay.primal.net", + ]; + ez.add_relays(relays).await?; + // Give relays a moment to fully welcome us + sleep(Duration::from_secs(2)).await; + println!(" [OK] Connected."); + + // 4. Follow Self (Critical for Timeline Test reliability) + println!("\n[STEP 2] Following Self (for robust timeline testing)..."); + ez.create_contact(&npub, Some("Me Myself".to_string())) + .await?; + println!(" [OK] Followed self."); + + // 5. Publish Post + println!("\n[STEP 3] Publishing Post..."); + let secret_code = format!("Test Run {}", Timestamp::now().as_secs()); + let event_id = ez + .post_text(&format!("Hello Nostr! {}", secret_code)) + .await?; + println!(" [OK] Posted: {}", event_id.to_bech32()?); + + // 6. Verify Timeline (with Retry) + println!("\n[STEP 4] verifying Timeline (expecting own post)..."); + let mut found_post = false; + for i in 1..=5 { + sleep(Duration::from_secs(2)).await; // Wait for propagation + let posts = ez.get_timeline(vec![npub.clone()]).await?; + + // Look for our specific post + if posts.iter().any(|p| p.content.contains(&secret_code)) { + println!(" [OK] Found our post in timeline after {}s!", i * 2); + found_post = true; + break; + } else { + println!(" ... attempt {}/5: Post not yet visible...", i); + } + } + + if !found_post { + println!(" [ERR] Verified Failed: Post did not appear in timeline."); + } + + // 7. DM Test (with Retry) + println!("\n[STEP 5] Testing Direct Messages..."); + let dm_msg = format!("Secret DM {}", secret_code); + ez.send_private_message(&npub, &dm_msg).await?; + + let mut found_dm = false; + for i in 1..=5 { + sleep(Duration::from_secs(2)).await; + let dms = ez.get_private_messages(&npub).await?; + + if dms.iter().any(|m| m.content == dm_msg) { + println!(" [OK] Found DM after {}s!", i * 2); + found_dm = true; + break; + } else { + println!(" ... attempt {}/5: DM not yet visible...", i); + } + } + + if !found_dm { + println!(" [ERR] DM propagation failed."); + // Non-fatal, DMs are slower + } + + println!("\n=== Test Complete ==="); + Ok(()) +} diff --git a/src/functions/feed.rs b/src/functions/feed.rs new file mode 100644 index 0000000..550faa7 --- /dev/null +++ b/src/functions/feed.rs @@ -0,0 +1,75 @@ +use crate::nips::nip01::Post; +use anyhow::{Context, Result}; +use nostr_sdk::prelude::*; +use std::time::Duration; + +/// Fetches a timeline/feed of posts from specific users (Followed users). +/// +/// # Arguments +/// * `client` - The Nostr client. +/// * `followed_pubkeys` - A list of hex strings (bech32 also works if parsed correctly, but we assume hex or wait for parsing) representing the users to follow. +/// +/// The function parses the keys, creates a filter, and returns a list of sorted Posts. +pub async fn get_followed_feed( + client: &Client, + followed_pubkeys: Vec, +) -> Result> { + let mut keys = Vec::new(); + for key in followed_pubkeys { + // We try to parse each key. If one fails, we just ignore it or log it (here we context it). + let pk = PublicKey::parse(&key).context(format!("Invalid public key: {}", key))?; + keys.push(pk); + } + + if keys.is_empty() { + return Ok(Vec::new()); + } + + // Create a filter for Text Notes (Kind 1) from these authors + let filter = Filter::new().kind(Kind::TextNote).authors(keys).limit(50); // Limit to 50 posts for now + + let timeout = Duration::from_secs(10); + let events = client.fetch_events(filter, timeout).await?; + + // Convert Events to our Post struct + let mut posts: Vec = events + .into_iter() + .map(|e| Post { + id: e.id, + author: e.pubkey, + content: e.content.clone(), + created_at: e.created_at, + }) + .collect(); + + // Sort by created_at descending (newest first) + posts.sort_by(|a, b| b.created_at.cmp(&a.created_at)); + + Ok(posts) +} + +/// Fetches random (recent) posts from the connected Relays (Global Feed). +/// +/// This is useful for discovery. +pub async fn get_random_posts(client: &Client) -> Result> { + // Filter for any Text Note, limit 20 + let filter = Filter::new().kind(Kind::TextNote).limit(20); + + let timeout = Duration::from_secs(10); + let events = client.fetch_events(filter, timeout).await?; + + let mut posts: Vec = events + .into_iter() + .map(|e| Post { + id: e.id, + author: e.pubkey, + content: e.content.clone(), + created_at: e.created_at, + }) + .collect(); + + // Sort by newest first + posts.sort_by(|a, b| b.created_at.cmp(&a.created_at)); + + Ok(posts) +} diff --git a/src/functions/mod.rs b/src/functions/mod.rs index 910f96f..01e391e 100644 --- a/src/functions/mod.rs +++ b/src/functions/mod.rs @@ -1,5 +1,7 @@ -pub mod new; -pub mod messages; -pub mod relays; +pub mod create_contact; +pub mod feed; pub mod get_contacts; -pub mod create_contact; \ No newline at end of file +pub mod messages; +pub mod new; +pub mod publish; +pub mod relays; diff --git a/src/functions/publish.rs b/src/functions/publish.rs new file mode 100644 index 0000000..6612dd1 --- /dev/null +++ b/src/functions/publish.rs @@ -0,0 +1,13 @@ +use crate::nips::nip01; +use anyhow::Result; +use nostr_sdk::prelude::*; + +/// Publishes a text post to the network. +/// +/// # Arguments +/// * `client` - The Nostr client. +/// * `content` - The text content of the post. +pub async fn post_text_note(client: &Client, content: &str) -> Result { + // We delegate the actual protocol work to the nip01 module + nip01::publish_text(client, content).await +} diff --git a/src/lib.rs b/src/lib.rs index 8796d08..f8ce545 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,11 +1,12 @@ -pub mod nips; -pub mod functions; // Das neue Modul registrieren +pub mod functions; +pub mod nips; // Das neue Modul registrieren use anyhow::Result; use nostr_sdk::prelude::*; // Wir importieren die Funktionen, um Tipparbeit zu sparen -use crate::functions::{new, messages, relays, get_contacts, create_contact}; +use crate::functions::{create_contact, feed, get_contacts, messages, new, publish, relays}; +use crate::nips::nip01::Post; use crate::nips::nip17::Message; pub struct EasyNostr { @@ -21,7 +22,11 @@ impl EasyNostr { } /// Sendet eine verschlĂŒsselte DM (NIP-17 Wrapper) - pub async fn send_private_message(&self, receiver_pubkey: &str, message: &str) -> Result { + pub async fn send_private_message( + &self, + receiver_pubkey: &str, + message: &str, + ) -> Result { messages::send_private_message(&self.client, receiver_pubkey, message).await } @@ -43,4 +48,19 @@ impl EasyNostr { pub async fn create_contact(&self, npub: &str, nickname: Option) -> Result { create_contact::create_contact(&self.client, npub, nickname).await } -} \ No newline at end of file + + /// Veröffentlicht einen Text-Post (Kind 1) + pub async fn post_text(&self, content: &str) -> Result { + publish::post_text_note(&self.client, content).await + } + + /// Ruft den Feed der gefolgten User ab + pub async fn get_timeline(&self, followed_pubkeys: Vec) -> Result> { + feed::get_followed_feed(&self.client, followed_pubkeys).await + } + + /// Ruft zufĂ€llige (aktuelle) Posts ab (Global Feed) + pub async fn get_random_posts(&self) -> Result> { + feed::get_random_posts(&self.client).await + } +} diff --git a/src/nips/nip01.rs b/src/nips/nip01.rs index db38f4f..6b36d4a 100644 --- a/src/nips/nip01.rs +++ b/src/nips/nip01.rs @@ -1,9 +1,19 @@ +use anyhow::Result; use nostr_sdk::prelude::*; -use anyhow::Result; // <--- HinzufĂŒgen -// Das Result hier bezieht sich jetzt auf anyhow::Result +/// A simplified structure for a Text Post (Kind 1) +/// Contains the ID, Author, content, and timestamp. +#[derive(Debug, Clone)] +pub struct Post { + pub id: EventId, + pub author: PublicKey, + pub content: String, + pub created_at: Timestamp, +} + +/// Publishes a text note (Kind 1) to the connected relays. pub async fn publish_text(client: &Client, content: &str) -> Result { let builder = EventBuilder::text_note(content); let output = client.send_event_builder(builder).await?; Ok(*output.id()) -} \ No newline at end of file +} diff --git a/src/nips/nip17.rs b/src/nips/nip17.rs index 41eeb68..6ce2101 100644 --- a/src/nips/nip17.rs +++ b/src/nips/nip17.rs @@ -1,5 +1,5 @@ -use nostr_sdk::prelude::*; use anyhow::Result; +use nostr_sdk::prelude::*; use std::time::Duration; /// UI Message Struktur @@ -38,8 +38,12 @@ pub async fn get_dm_messages(client: &Client, contact_npub: &str) -> Result = Vec::new(); @@ -49,7 +53,10 @@ pub async fn get_dm_messages(client: &Client, contact_npub: &str) -> Result Result *public_key == contact_pubkey, - _ => false, - } + let has_contact_tag = event.tags.iter().any(|t| match t.as_standardized() { + Some(TagStandard::PublicKey { public_key, .. }) => *public_key == contact_pubkey, + _ => false, }); let is_incoming = event.pubkey == contact_pubkey; let is_outgoing = event.pubkey == my_pubkey && has_contact_tag; if is_incoming || is_outgoing { - let counterparty = if is_incoming { event.pubkey } else { contact_pubkey }; + let counterparty = if is_incoming { + event.pubkey + } else { + contact_pubkey + }; if let Ok(content) = signer.nip04_decrypt(&counterparty, &event.content).await { messages.push(Message { id: event.id, diff --git a/test_output.txt b/test_output.txt new file mode 100644 index 0000000..3900ed0 --- /dev/null +++ b/test_output.txt @@ -0,0 +1,11 @@ + Compiling easy-nostr v0.1.0 (/home/malxte/Documents/Programming/Rust/Crates/easy-nostr) +error[E0277]: the `?` operator can only be applied to values that implement `Try` + --> src/bin/testall.rs:12:16 + | +12 | let nsec = keys.secret_key()?.to_bech32()?; + | ^^^^^^^^^^^^^^^^^^ the `?` operator cannot be applied to type `&nostr_sdk::SecretKey` + | + = help: the trait `Try` is not implemented for `&nostr_sdk::SecretKey` + +For more information about this error, try `rustc --explain E0277`. +error: could not compile `easy-nostr` (bin "testall") due to 1 previous error