diff --git a/Cargo.toml b/Cargo.toml index 3cf3413..9433b94 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,13 @@ [package] name = "easy-nostr" +authors = ["Bytemalte <[bytemalte@gmail.com]>"] +description = "A high-level, developer-friendly Rust library designed to simplify the development of Nostr applications." version = "0.1.0" edition = "2024" +license = "MIT" +repository = "https://github.com/Bytemalte/easy-nostr" +keywords = ["nostr", "crypto", "social", "decentralized"] +categories = ["cryptography", "network-programming", "web-programming"] [dependencies] nostr-sdk = { version = "0.44.1", features = ["all-nips", "nip44"] } diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9f2c809 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) [2026] [Bytemalte] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 748467c..f257c04 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,40 @@ -# EasyNostr +# 🌌 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. +[![Rust](https://img.shields.io/badge/rust-stable-brightgreen.svg)](https://www.rust-lang.org/) +[![Nostr](https://img.shields.io/badge/nostr-protocol-purple.svg)](https://nostr.com/) -## 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). +**EasyNostr** is a high-level, developer-friendly Rust library designed to simplify the development of Nostr applications. It wraps the powerful `nostr-sdk` to provide a clean, intuitive API for common operations like identity management, social feeds, and encrypted messaging. -## Getting Started +--- -### 1. Initialize the Client -First, you need to create an instance of `EasyNostr` using your secret key (nsec). +## 🚀 Key Features + +* **Identity Management**: Simple handling of keys (nsec/npub). +* **Social Connectivity**: Follow users and manage contact lists. +* **Rich Feeds**: Access follow-based timelines, global discovery, and hashtag-based searches. +* **Encrypted Messaging**: Secure NIP-17 direct messages. +* **Extensible Architecture**: Clean separation between protocol logic (NIPs) and high-level functions. + +--- + +## 📦 Installation + +Add `easy-nostr` and its core dependencies to your `Cargo.toml`: + +```toml +[dependencies] +easy-nostr = { path = "." } # Use the crate path +tokio = { version = "1.48.0", features = ["full"] } +anyhow = "1.0.100" +``` + +--- + +## 🛠️ Usage Guide + +### 1. Initialization & Connection + +Create a new instance and connect to your preferred relays. ```rust use easy_nostr::EasyNostr; @@ -20,127 +42,100 @@ use anyhow::Result; #[tokio::main] async fn main() -> Result<()> { - // Your secret key (nsec) - let my_secret_key = "nsec1..."; + // Initialize with your private key (nsec) + let secret_key = "nsec1..."; + let en = EasyNostr::new(secret_key).await?; - // Initialize client - let client = EasyNostr::new(my_secret_key).await?; - - // Connect to Relays - client.add_relays(vec![ + // Add relays to connect to the network + en.add_relays(vec![ "wss://relay.damus.io", "wss://nos.lol", "wss://relay.primal.net" ]).await?; - println!("Connected and ready!"); + println!("Connected to Nostr!"); Ok(()) } ``` ---- +### 📝 Publishing Content -## Capabilities & Usage - -### 📝 Publishing Posts -Post a simple text message to the network. +Post a simple text note (Kind 1) to the network. ```rust -let event_id = client.post_text("Hello Nostr world!").await?; -println!("Posted! ID: {}", event_id.to_bech32()?); +let event_id = en.post_text("Building with EasyNostr is awesome!").await?; +println!("Successfully posted! Event ID: {}", event_id); ``` -### 📰 Reading Feeds -You can read posts from people you follow (Timeline) or random posts from the network. +### 📰 Social Feeds + +#### Timeline (Followed Users) +Fetch posts from a specific list of public keys. -**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); +let follow_list = vec!["npub1...".to_string(), "npub2...".to_string()]; +match en.get_timeline(follow_list).await { + Ok(posts) => { + for post in posts { + println!("@{} says: {}", post.author, post.content); + } } + Err(e) => eprintln!("Failed to load timeline: {}", e), } ``` +#### Discovery (Global Feed) +Get recent random posts from connected relays. + +```rust +let global_posts = en.get_random_posts().await?; +``` + +#### Hashtag Search (NIP-12) +Search for posts containing specific hashtags. + +```rust +let tags = vec!["bitcoin".to_string(), "rust".to_string()]; +let filtered_posts = en.get_posts_by_hashtags(tags).await?; +``` + +### 👥 Contacts & Following + +#### Follow a User +```rust +en.create_contact("npub1...", Some("Alice".to_string())).await?; +``` + +#### Retrieve Contact List +```rust +let contacts = en.get_contacts().await?; +``` + +### 🔒 Private Messaging (NIP-17) + +#### Send a Message +```rust +en.send_private_message("npub1...", "Hello, this is a secret!").await?; +``` + +#### Retrieve Conversations +```rust +let history = en.get_private_messages("npub1...").await?; +``` + --- -### 🔒 Direct Messages (DMs) -Send and receive encrypted private chats. Supports modern GiftWraps (NIP-17). +## 📂 Project Structure -**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. +| Directory | Purpose | +| :--- | :--- | +| `src/lib.rs` | Main entry point and public `EasyNostr` interface. | +| `src/functions/` | Implementation of high-level features (feeds, messaging, etc.). | +| `src/nips/` | Protocol-level logic and data structures categorized by NIP. | +| `src/bin/` | Utility binaries and examples. | --- -## 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`. +## ⚖️ License -```toml -[dependencies] -nostr-sdk = { version = "0.44.1", features = ["all-nips", "nip44"] } -tokio = { version = "1.48.0", features = ["full"] } -anyhow = "1.0.100" -``` +Distributed under the MIT License. See `LICENSE` for more information. diff --git a/big_promts.md b/big_promts.md deleted file mode 100644 index 7f31a2d..0000000 --- a/big_promts.md +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index 824b5ef..0000000 --- a/production_test.log +++ /dev/null @@ -1,35 +0,0 @@ -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 deleted file mode 100644 index 69c9b21..0000000 --- a/production_test_v2.log +++ /dev/null @@ -1,31 +0,0 @@ - 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/functions/hashtags.rs b/src/functions/hashtags.rs new file mode 100644 index 0000000..55f952c --- /dev/null +++ b/src/functions/hashtags.rs @@ -0,0 +1,35 @@ +use crate::nips::nip01::Post; +use crate::nips::nip12; +use anyhow::Result; +use nostr_sdk::prelude::*; +use std::time::Duration; + +/// Fetches posts by one or more hashtags. +/// +/// # Arguments +/// * `client` - The Nostr client. +/// * `hashtags` - A list of hashtags to search for. +pub async fn get_posts_by_hashtags(client: &Client, hashtags: Vec) -> Result> +where + S: Into, +{ + let filter = nip12::create_hashtag_filter(hashtags); + + 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 01e391e..7caf6cd 100644 --- a/src/functions/mod.rs +++ b/src/functions/mod.rs @@ -1,6 +1,7 @@ pub mod create_contact; pub mod feed; pub mod get_contacts; +pub mod hashtags; pub mod messages; pub mod new; pub mod publish; diff --git a/src/lib.rs b/src/lib.rs index f2f6e88..a6fb6a7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,7 +5,9 @@ use anyhow::Result; use nostr_sdk::prelude::*; // Wir importieren die Funktionen, um Tipparbeit zu sparen -use crate::functions::{create_contact, feed, get_contacts, messages, new, publish, relays}; +use crate::functions::{ + create_contact, feed, get_contacts, hashtags, messages, new, publish, relays, +}; use crate::nips::nip01::Post; use crate::nips::nip17::Message; @@ -63,4 +65,9 @@ impl EasyNostr { pub async fn get_random_posts(&self) -> Result> { feed::get_random_posts(&self.client).await } + + /// Lädt Posts nach Hashtags (z.B. "bitcoin", "rust") + pub async fn get_posts_by_hashtags(&self, hashtags: Vec) -> Result> { + hashtags::get_posts_by_hashtags(&self.client, hashtags).await + } } diff --git a/src/nips/mod.rs b/src/nips/mod.rs index e8a4b76..6d85677 100644 --- a/src/nips/mod.rs +++ b/src/nips/mod.rs @@ -1,3 +1,4 @@ pub mod nip01; +pub mod nip02; +pub mod nip12; pub mod nip17; -pub mod nip02; \ No newline at end of file diff --git a/src/nips/nip12.rs b/src/nips/nip12.rs new file mode 100644 index 0000000..21c778e --- /dev/null +++ b/src/nips/nip12.rs @@ -0,0 +1,13 @@ +use nostr_sdk::prelude::*; + +/// Creates a filter for Kind 1 posts matching specific hashtags. +/// +/// # Arguments +/// * `hashtags` - A list of hashtags to filter for (without the #). +pub fn create_hashtag_filter(hashtags: Vec) -> Filter +where + S: Into, +{ + let tags: Vec = hashtags.into_iter().map(|s| s.into()).collect(); + Filter::new().kind(Kind::TextNote).hashtags(tags).limit(50) +} diff --git a/test_output.txt b/test_output.txt deleted file mode 100644 index 3900ed0..0000000 --- a/test_output.txt +++ /dev/null @@ -1,11 +0,0 @@ - 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