update LICENSE and README and new hashtag filter

This commit is contained in:
2026-01-31 12:44:28 +01:00
parent 7cebc490ca
commit 7b644785ee
12 changed files with 186 additions and 205 deletions

View File

@@ -1,7 +1,13 @@
[package] [package]
name = "easy-nostr" 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" version = "0.1.0"
edition = "2024" edition = "2024"
license = "MIT"
repository = "https://github.com/Bytemalte/easy-nostr"
keywords = ["nostr", "crypto", "social", "decentralized"]
categories = ["cryptography", "network-programming", "web-programming"]
[dependencies] [dependencies]
nostr-sdk = { version = "0.44.1", features = ["all-nips", "nip44"] } nostr-sdk = { version = "0.44.1", features = ["all-nips", "nip44"] }

19
LICENSE Normal file
View File

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

209
README.md
View File

@@ -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 **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.
- **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 ## 🚀 Key Features
First, you need to create an instance of `EasyNostr` using your secret key (nsec).
* **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 ```rust
use easy_nostr::EasyNostr; use easy_nostr::EasyNostr;
@@ -20,127 +42,100 @@ use anyhow::Result;
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> Result<()> {
// Your secret key (nsec) // Initialize with your private key (nsec)
let my_secret_key = "nsec1..."; let secret_key = "nsec1...";
let en = EasyNostr::new(secret_key).await?;
// Initialize client // Add relays to connect to the network
let client = EasyNostr::new(my_secret_key).await?; en.add_relays(vec![
// Connect to Relays
client.add_relays(vec![
"wss://relay.damus.io", "wss://relay.damus.io",
"wss://nos.lol", "wss://nos.lol",
"wss://relay.primal.net" "wss://relay.primal.net"
]).await?; ]).await?;
println!("Connected and ready!"); println!("Connected to Nostr!");
Ok(()) Ok(())
} }
``` ```
--- ### 📝 Publishing Content
## Capabilities & Usage Post a simple text note (Kind 1) to the network.
### 📝 Publishing Posts
Post a simple text message to the network.
```rust ```rust
let event_id = client.post_text("Hello Nostr world!").await?; let event_id = en.post_text("Building with EasyNostr is awesome!").await?;
println!("Posted! ID: {}", event_id.to_bech32()?); println!("Successfully posted! Event ID: {}", event_id);
``` ```
### 📰 Reading Feeds ### 📰 Social Feeds
You can read posts from people you follow (Timeline) or random posts from the network.
#### Timeline (Followed Users)
Fetch posts from a specific list of public keys.
**Get Timeline (Followed Users):**
```rust ```rust
// List of public keys (npubs) you want to see posts from let follow_list = vec!["npub1...".to_string(), "npub2...".to_string()];
let following = vec!["npub1...", "npub1..."]; match en.get_timeline(follow_list).await {
Ok(posts) => {
let posts = client.get_timeline(following).await?; for post in posts {
println!("@{} says: {}", post.author, post.content);
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);
} }
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) ## 📂 Project Structure
Send and receive encrypted private chats. Supports modern GiftWraps (NIP-17).
**Send a DM:** | Directory | Purpose |
```rust | :--- | :--- |
let friend_npub = "npub1..."; | `src/lib.rs` | Main entry point and public `EasyNostr` interface. |
client.send_private_message(friend_npub, "Hey, this is secret!").await?; | `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. |
**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 ## ⚖️ License
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 Distributed under the MIT License. See `LICENSE` for more information.
[dependencies]
nostr-sdk = { version = "0.44.1", features = ["all-nips", "nip44"] }
tokio = { version = "1.48.0", features = ["full"] }
anyhow = "1.0.100"
```

View File

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

View File

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

View File

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

35
src/functions/hashtags.rs Normal file
View File

@@ -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<S>(client: &Client, hashtags: Vec<S>) -> Result<Vec<Post>>
where
S: Into<String>,
{
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<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,6 +1,7 @@
pub mod create_contact; pub mod create_contact;
pub mod feed; pub mod feed;
pub mod get_contacts; pub mod get_contacts;
pub mod hashtags;
pub mod messages; pub mod messages;
pub mod new; pub mod new;
pub mod publish; pub mod publish;

View File

@@ -5,7 +5,9 @@ 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::{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::nip01::Post;
use crate::nips::nip17::Message; use crate::nips::nip17::Message;
@@ -63,4 +65,9 @@ impl EasyNostr {
pub async fn get_random_posts(&self) -> Result<Vec<Post>> { pub async fn get_random_posts(&self) -> Result<Vec<Post>> {
feed::get_random_posts(&self.client).await 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<String>) -> Result<Vec<Post>> {
hashtags::get_posts_by_hashtags(&self.client, hashtags).await
}
} }

View File

@@ -1,3 +1,4 @@
pub mod nip01; pub mod nip01;
pub mod nip02;
pub mod nip12;
pub mod nip17; pub mod nip17;
pub mod nip02;

13
src/nips/nip12.rs Normal file
View File

@@ -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<S>(hashtags: Vec<S>) -> Filter
where
S: Into<String>,
{
let tags: Vec<String> = hashtags.into_iter().map(|s| s.into()).collect();
Filter::new().kind(Kind::TextNote).hashtags(tags).limit(50)
}

View File

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