All basic functions plus guide README to use

This commit is contained in:
Malte Schröder
2025-12-19 20:25:39 +01:00
parent db48f07b78
commit 397635d43e
12 changed files with 486 additions and 22 deletions

146
README.md Normal file
View File

@@ -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"
```

19
big_promts.md Normal file
View File

@@ -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.

35
production_test.log Normal file
View File

@@ -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 ===

31
production_test_v2.log Normal file
View File

@@ -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 ===

93
src/bin/testall.rs Normal file
View File

@@ -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(())
}

75
src/functions/feed.rs Normal file
View File

@@ -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<String>,
) -> Result<Vec<Post>> {
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<Post> = 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<Vec<Post>> {
// 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<Post> = 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)
}

View File

@@ -1,5 +1,7 @@
pub mod new;
pub mod messages;
pub mod relays;
pub mod get_contacts;
pub mod create_contact; pub mod create_contact;
pub mod feed;
pub mod get_contacts;
pub mod messages;
pub mod new;
pub mod publish;
pub mod relays;

13
src/functions/publish.rs Normal file
View File

@@ -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<EventId> {
// We delegate the actual protocol work to the nip01 module
nip01::publish_text(client, content).await
}

View File

@@ -1,11 +1,12 @@
pub mod nips; pub mod functions;
pub mod functions; // Das neue Modul registrieren pub mod nips; // Das neue Modul registrieren
use anyhow::Result; use anyhow::Result;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
// Wir importieren die Funktionen, um Tipparbeit zu sparen // 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; use crate::nips::nip17::Message;
pub struct EasyNostr { pub struct EasyNostr {
@@ -21,7 +22,11 @@ impl EasyNostr {
} }
/// Sendet eine verschlüsselte DM (NIP-17 Wrapper) /// Sendet eine verschlüsselte DM (NIP-17 Wrapper)
pub async fn send_private_message(&self, receiver_pubkey: &str, message: &str) -> Result<EventId> { pub async fn send_private_message(
&self,
receiver_pubkey: &str,
message: &str,
) -> Result<EventId> {
messages::send_private_message(&self.client, receiver_pubkey, message).await 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<String>) -> Result<EventId> { pub async fn create_contact(&self, npub: &str, nickname: Option<String>) -> Result<EventId> {
create_contact::create_contact(&self.client, npub, nickname).await create_contact::create_contact(&self.client, npub, nickname).await
} }
/// Veröffentlicht einen Text-Post (Kind 1)
pub async fn post_text(&self, content: &str) -> Result<EventId> {
publish::post_text_note(&self.client, content).await
}
/// Ruft den Feed der gefolgten User ab
pub async fn get_timeline(&self, followed_pubkeys: Vec<String>) -> Result<Vec<Post>> {
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<Vec<Post>> {
feed::get_random_posts(&self.client).await
}
} }

View File

@@ -1,7 +1,17 @@
use anyhow::Result;
use nostr_sdk::prelude::*; 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<EventId> { pub async fn publish_text(client: &Client, content: &str) -> Result<EventId> {
let builder = EventBuilder::text_note(content); let builder = EventBuilder::text_note(content);
let output = client.send_event_builder(builder).await?; let output = client.send_event_builder(builder).await?;

View File

@@ -1,5 +1,5 @@
use nostr_sdk::prelude::*;
use anyhow::Result; use anyhow::Result;
use nostr_sdk::prelude::*;
use std::time::Duration; use std::time::Duration;
/// UI Message Struktur /// UI Message Struktur
@@ -38,8 +38,12 @@ pub async fn get_dm_messages(client: &Client, contact_npub: &str) -> Result<Vec<
.limit(100); .limit(100);
// --- Abrufen: zwei einzelne fetches, dann Ergebnisse mergen --- // --- Abrufen: zwei einzelne fetches, dann Ergebnisse mergen ---
let mut events = client.fetch_events(filter_in, Duration::from_secs(10)).await?; let mut events = client
let more_events = client.fetch_events(filter_out, Duration::from_secs(10)).await?; .fetch_events(filter_in, Duration::from_secs(10))
.await?;
let more_events = client
.fetch_events(filter_out, Duration::from_secs(10))
.await?;
events.extend(more_events.into_iter()); events.extend(more_events.into_iter());
let mut messages: Vec<Message> = Vec::new(); let mut messages: Vec<Message> = Vec::new();
@@ -49,7 +53,10 @@ pub async fn get_dm_messages(client: &Client, contact_npub: &str) -> Result<Vec<
if event.kind == Kind::GiftWrap { if event.kind == Kind::GiftWrap {
if let Ok(unwrapped) = client.unwrap_gift_wrap(event).await { if let Ok(unwrapped) = client.unwrap_gift_wrap(event).await {
let rumor = unwrapped.rumor; let rumor = unwrapped.rumor;
if rumor.pubkey == contact_pubkey && rumor.kind == Kind::TextNote { // Allow TextNote (1) or ChatMessage (14)
if rumor.pubkey == contact_pubkey
&& (rumor.kind == Kind::TextNote || rumor.kind == Kind::from(14))
{
messages.push(Message { messages.push(Message {
id: event.id, id: event.id,
sender: rumor.pubkey, sender: rumor.pubkey,
@@ -63,18 +70,20 @@ pub async fn get_dm_messages(client: &Client, contact_npub: &str) -> Result<Vec<
// === NIP-04 (Legacy) Verarbeitung === // === NIP-04 (Legacy) Verarbeitung ===
else if event.kind == Kind::EncryptedDirectMessage { else if event.kind == Kind::EncryptedDirectMessage {
// TagStandard::PublicKey ist ein Struct-Variant: Felder mit Struct-Syntax matchen // TagStandard::PublicKey ist ein Struct-Variant: Felder mit Struct-Syntax matchen
let has_contact_tag = event.tags.iter().any(|t| { let has_contact_tag = event.tags.iter().any(|t| match t.as_standardized() {
match t.as_standardized() { Some(TagStandard::PublicKey { public_key, .. }) => *public_key == contact_pubkey,
Some(TagStandard::PublicKey { public_key, .. }) => *public_key == contact_pubkey, _ => false,
_ => false,
}
}); });
let is_incoming = event.pubkey == contact_pubkey; let is_incoming = event.pubkey == contact_pubkey;
let is_outgoing = event.pubkey == my_pubkey && has_contact_tag; let is_outgoing = event.pubkey == my_pubkey && has_contact_tag;
if is_incoming || is_outgoing { 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 { if let Ok(content) = signer.nip04_decrypt(&counterparty, &event.content).await {
messages.push(Message { messages.push(Message {
id: event.id, id: event.id,

11
test_output.txt Normal file
View File

@@ -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