Compare commits

...
Sign in to create a new pull request.

5 commits

14 changed files with 554 additions and 289 deletions

287
Cargo.lock generated
View file

@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
version = 4
[[package]]
name = "ab_glyph"
@ -108,16 +108,6 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "aead"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
dependencies = [
"crypto-common",
"generic-array",
]
[[package]]
name = "ahash"
version = "0.8.11"
@ -180,6 +170,12 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04"
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
@ -536,7 +532,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c8d66485a3a2ea485c1913c4572ce0256067a5377ac8c75c4960e1cda98605f"
dependencies = [
"bitcoin-internals",
"bitcoin_hashes 0.14.0",
"bitcoin_hashes",
]
[[package]]
@ -545,18 +541,6 @@ version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "base64ct"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]]
name = "bech32"
version = "0.11.0"
@ -595,17 +579,6 @@ dependencies = [
"which",
]
[[package]]
name = "bip39"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93f2635620bf0b9d4576eb7bb9a38a55df78bd1205d26fa994b25911a69f212f"
dependencies = [
"bitcoin_hashes 0.11.0",
"serde",
"unicode-normalization",
]
[[package]]
name = "bit-set"
version = "0.5.3"
@ -638,7 +611,7 @@ dependencies = [
"bitcoin-internals",
"bitcoin-io",
"bitcoin-units",
"bitcoin_hashes 0.14.0",
"bitcoin_hashes",
"hex-conservative",
"hex_lit",
"secp256k1",
@ -670,12 +643,6 @@ dependencies = [
"serde",
]
[[package]]
name = "bitcoin_hashes"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90064b8dee6815a6470d60bad07bbbaee885c0e12d04177138fa3291a01b7bc4"
[[package]]
name = "bitcoin_hashes"
version = "0.14.0"
@ -723,15 +690,6 @@ dependencies = [
"generic-array",
]
[[package]]
name = "block-padding"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
dependencies = [
"generic-array",
]
[[package]]
name = "block-sys"
version = "0.1.0-beta.1"
@ -862,15 +820,6 @@ dependencies = [
"wayland-client",
]
[[package]]
name = "cbc"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"
dependencies = [
"cipher",
]
[[package]]
name = "cc"
version = "1.0.95"
@ -929,38 +878,18 @@ dependencies = [
]
[[package]]
name = "chacha20"
version = "0.9.1"
name = "chrono"
version = "0.4.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
]
[[package]]
name = "chacha20poly1305"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"
dependencies = [
"aead",
"chacha20",
"cipher",
"poly1305",
"zeroize",
]
[[package]]
name = "cipher"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"crypto-common",
"inout",
"zeroize",
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"wasm-bindgen",
"windows-targets 0.52.5",
]
[[package]]
@ -1194,7 +1123,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"rand_core",
"typenum",
]
@ -1244,7 +1172,6 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
"subtle",
]
[[package]]
@ -1838,10 +1765,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi",
"wasm-bindgen",
]
[[package]]
@ -2080,15 +2005,6 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
[[package]]
name = "hmac"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest",
]
[[package]]
name = "home"
version = "0.5.9"
@ -2102,12 +2018,13 @@ dependencies = [
name = "hoot-app"
version = "0.1.0"
dependencies = [
"bitcoin",
"chrono",
"eframe",
"egui_extras",
"egui_tabs",
"ewebsock",
"image 0.25.1",
"nostr",
"nostrdb",
"pollster",
"puffin",
@ -2138,6 +2055,29 @@ version = "1.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9"
[[package]]
name = "iana-time-zone"
version = "0.1.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "icrate"
version = "0.0.4"
@ -2227,16 +2167,6 @@ dependencies = [
"hashbrown",
]
[[package]]
name = "inout"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
dependencies = [
"block-padding",
"generic-array",
]
[[package]]
name = "instant"
version = "0.1.12"
@ -2244,9 +2174,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
dependencies = [
"cfg-if",
"js-sys",
"wasm-bindgen",
"web-sys",
]
[[package]]
@ -2633,18 +2560,6 @@ dependencies = [
"jni-sys",
]
[[package]]
name = "negentropy"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e664971378a3987224f7a0e10059782035e89899ae403718ee07de85bec42afe"
[[package]]
name = "negentropy"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a88da9dd148bbcdce323dd6ac47d369b4769d4a3b78c6c52389b9269f77932"
[[package]]
name = "new_debug_unreachable"
version = "1.0.6"
@ -2685,32 +2600,6 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
[[package]]
name = "nostr"
version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8aad4b767bbed24ac5eb4465bfb83bc1210522eb99d67cf4e547ec2ec7e47786"
dependencies = [
"async-trait",
"base64 0.22.1",
"bech32",
"bip39",
"bitcoin",
"cbc",
"chacha20",
"chacha20poly1305",
"getrandom",
"instant",
"negentropy 0.3.1",
"negentropy 0.4.3",
"once_cell",
"scrypt",
"serde",
"serde_json",
"unicode-normalization",
"url",
]
[[package]]
name = "nostrdb"
version = "0.3.4"
@ -2926,12 +2815,6 @@ version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
[[package]]
name = "opaque-debug"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]]
name = "orbclient"
version = "0.3.47"
@ -2995,33 +2878,12 @@ dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "password-hash"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
dependencies = [
"base64ct",
"rand_core",
"subtle",
]
[[package]]
name = "paste"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
[[package]]
name = "pbkdf2"
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
dependencies = [
"digest",
"hmac",
]
[[package]]
name = "percent-encoding"
version = "2.3.1"
@ -3113,17 +2975,6 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3"
[[package]]
name = "poly1305"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
dependencies = [
"cpufeatures",
"opaque-debug",
"universal-hash",
]
[[package]]
name = "powerfmt"
version = "0.2.0"
@ -3487,7 +3338,7 @@ version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94"
dependencies = [
"base64 0.21.7",
"base64",
"bitflags 2.6.0",
"serde",
"serde_derive",
@ -3584,15 +3435,6 @@ version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]]
name = "salsa20"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213"
dependencies = [
"cipher",
]
[[package]]
name = "same-file"
version = "1.0.6"
@ -3614,18 +3456,6 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "scrypt"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f"
dependencies = [
"password-hash",
"pbkdf2",
"salsa20",
"sha2",
]
[[package]]
name = "sctk-adwaita"
version = "0.8.1"
@ -3645,7 +3475,7 @@ version = "0.29.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113"
dependencies = [
"bitcoin_hashes 0.14.0",
"bitcoin_hashes",
"rand",
"secp256k1-sys",
"serde",
@ -3715,7 +3545,6 @@ version = "1.0.121"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ab380d7d9f22ef3f21ad3e6c1ebe8e4fc7a2000ccba2e4d71fc96f15b2cb609"
dependencies = [
"indexmap",
"itoa",
"memchr",
"ryu",
@ -3753,17 +3582,6 @@ dependencies = [
"digest",
]
[[package]]
name = "sha2"
version = "0.10.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "sharded-slab"
version = "0.1.7"
@ -4371,16 +4189,6 @@ version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
[[package]]
name = "universal-hash"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
dependencies = [
"crypto-common",
"subtle",
]
[[package]]
name = "untrusted"
version = "0.9.0"
@ -4396,7 +4204,6 @@ dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
"serde",
]
[[package]]
@ -4405,7 +4212,7 @@ version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38b0a51b72ab80ca511d126b77feeeb4fb1e972764653e61feac30adc161a756"
dependencies = [
"base64 0.21.7",
"base64",
"log",
"pico-args",
"usvg-parser",

View file

@ -25,10 +25,11 @@ puffin_http = { version = "0.16.0", optional = true }
ewebsock = { version = "0.6.0", features = ["tls"] }
nostrdb = { git = "https://github.com/damus-io/nostrdb-rs", rev = "ee8afeeb0b6695fca6d27dd0b74a8dc159e37b95" }
rand = "0.8.5"
nostr = { version = "0.37.0", features = ["std", "nip59"] }
serde = "1.0.204"
serde_json = "1.0.121"
pollster = "0.4.0"
bitcoin = { version = "0.32.5", features = ["serde", "rand-std"] }
chrono = { version = "0.4.39", features = ["serde"] }
[target.'cfg(target_os = "macos")'.dependencies]
security-framework = "3.0.0"

1
rust-toolchain Normal file
View file

@ -0,0 +1 @@
1.83.0

View file

@ -1,8 +1,9 @@
use nostr::{Event, EventBuilder, Keys, Kind, PublicKey, Tag, TagKind, TagStandard};
use crate::nostr::{Event, EventBuilder, EventKind, Tag};
use bitcoin::secp256k1::{PublicKey, Keypair};
use std::collections::HashMap;
use pollster::FutureExt as _;
pub const MAIL_EVENT_KIND: u16 = 1059;
pub const MAIL_EVENT_KIND: u32 = 1059;
pub struct MailMessage {
pub to: Vec<PublicKey>,
@ -13,24 +14,28 @@ pub struct MailMessage {
}
impl MailMessage {
pub fn to_events(&mut self, sending_keys: &Keys) -> HashMap<PublicKey, Event> {
pub fn to_events(&mut self, sending_keys: &Keypair) -> HashMap<PublicKey, Event> {
let mut pubkeys_to_send_to: Vec<PublicKey> = Vec::new();
let mut tags: Vec<Tag> = Vec::new();
for pubkey in &self.to {
tags.push(Tag::public_key(*pubkey));
tags.push(Tag::new_with_values(vec!["p".to_string(), *pubkey.to_hex().as_str()]));
pubkeys_to_send_to.push(*pubkey);
}
for pubkey in &self.cc {
tags.push(Tag::custom(TagKind::p(), vec![pubkey.to_hex().as_str(), "cc"]));
tags.push(Tag::new_with_values(vec!["p".to_string(), *pubkey.to_hex().as_str(), "cc".to_string()]));
pubkeys_to_send_to.push(*pubkey);
}
tags.push(Tag::from_standardized(TagStandard::Subject(self.subject.clone())));
tags.push(Tag::new_with_values(vec!["subject".to_string(), self.subject.clone()]));
let base_event = EventBuilder::new(Kind::Custom(MAIL_EVENT_KIND), &self.content)
.tags(tags);
let base_event = EventBuilder::new()
.kind(EventKind::MailEvent)
.content(&self.content)
.tags(tags)
.build();
base_event.pubkey = sending_keys.clone().public_key();
let mut event_list: HashMap<PublicKey, Event> = HashMap::new();
for pubkey in pubkeys_to_send_to {

View file

@ -12,6 +12,7 @@ mod keystorage;
mod mail_event;
mod relay;
mod ui;
mod nostr;
fn main() -> Result<(), eframe::Error> {
let (non_blocking, _guard) = tracing_appender::non_blocking(std::io::stdout()); // add log files in prod one day
@ -111,7 +112,7 @@ fn update_app(app: &mut Hoot, ctx: &egui::Context) {
if app.account_manager.loaded_keys.len() > 0 {
let mut gw_sub = relay::Subscription::default();
let filter = nostr::Filter::new().kind(nostr::Kind::Custom(mail_event::MAIL_EVENT_KIND)).custom_tag(nostr::SingleLetterTag { character: nostr::Alphabet::P, uppercase: false }, app.account_manager.loaded_keys.clone().into_iter().map(|keys| keys.public_key()));
let filter = nostr::Filter::new().add_kind(mail_event::MAIL_EVENT_KIND).add_tag("p", app.account_manager.loaded_keys.clone().into_iter().map(|keys| keys.public_key()));
gw_sub.filter(filter);
// TODO: fix error handling
@ -208,36 +209,6 @@ fn render_app(app: &mut Hoot, ctx: &egui::Context) {
.insert(egui::Id::new(rand::random::<u32>()), state);
}
if ui.button("Send Test Event").clicked() {
let temp_keys = nostr::Keys::generate();
// todo: lmao
let new_event = nostr::EventBuilder::text_note("GFY!")
.sign_with_keys(&temp_keys)
.unwrap();
let event_json = crate::relay::ClientMessage::Event { event: new_event };
let _ = &app
.relays
.send(ewebsock::WsMessage::Text(
serde_json::to_string(&event_json).unwrap(),
))
.unwrap();
}
if ui.button("Get kind 1 notes").clicked() {
let mut filter = nostr::types::Filter::new();
filter = filter.kind(nostr::Kind::TextNote);
let mut sub = crate::relay::Subscription::default();
sub.filter(filter);
let c_msg = crate::relay::ClientMessage::from(sub);
let _ = &app
.relays
.send(ewebsock::WsMessage::Text(
serde_json::to_string(&c_msg).unwrap(),
))
.unwrap();
}
ui.label(format!("total events rendered: {}", app.events.len()));
TableBuilder::new(ui)
@ -262,7 +233,7 @@ fn render_app(app: &mut Hoot, ctx: &egui::Context) {
ui.checkbox(&mut false, "");
});
row.col(|ui| {
ui.label(event.pubkey.to_string());
ui.label(event.pubkey.expect("I NEED PUBLIC KEY").to_string());
});
row.col(|ui| {
ui.label(event.content.clone());
@ -273,7 +244,7 @@ fn render_app(app: &mut Hoot, ctx: &egui::Context) {
if row.response().clicked() {
println!("clicked: {}", event.content.clone());
app.focused_post = event.id.to_string();
app.focused_post = event.id.expect("WE SHOULD HAVE AN EVENT ID").to_string();
app.page = Page::Post;
}
});
@ -290,14 +261,14 @@ fn render_app(app: &mut Hoot, ctx: &egui::Context) {
let gift_wrapped_event = app
.events
.iter()
.find(|&x| x.id.to_string() == app.focused_post)
.find(|&x| x.id.expect("there should be id").to_string() == app.focused_post)
.expect("event id should be present inside event list");
let event_to_display = app.account_manager.unwrap_gift_wrap(gift_wrapped_event).expect("we should be able to unwrap an event we recieved");
ui.heading("View Message");
ui.label(format!("Content: {}", event_to_display.rumor.content));
ui.label(match &event_to_display.rumor.tags.find(nostr::TagKind::Subject) {
ui.label(match &event_to_display.rumor.tags.find(nostr::event::tag::kind::TagKind::Subject) {
Some(s) => match s.content() {
Some(c) => format!("Subject: {}", c.to_string()),
None => "Subject: None".to_string(),

43
src/nostr/event/id.rs Normal file
View file

@ -0,0 +1,43 @@
use serde::{Serialize, Deserialize};
type Bytes = [u8; 32];
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct EventId {
inner: Bytes,
}
impl EventId {
#[inline]
pub fn as_bytes(&self) -> &Bytes {
&self.inner
}
#[inline]
pub fn into_bytes(&self) -> Bytes {
self.inner
}
}
impl std::fmt::Display for EventId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use bitcoin::hex::DisplayHex;
write!(f, "{}", self.inner.to_lower_hex_string())
}
}
impl From<String> for EventId {
fn from(s: String) -> Self {
let mut inner = [0u8; 32];
let bytes = s.as_bytes();
let len = bytes.len().min(32);
inner[..len].copy_from_slice(&bytes[..len]);
EventId { inner }
}
}
impl From<[u8; 32]> for EventId {
fn from(array: [u8; 32]) -> Self {
EventId { inner: array }
}
}

7
src/nostr/event/kind.rs Normal file
View file

@ -0,0 +1,7 @@
#[derive(Debug, PartialEq, Eq)]
#[repr(u32)]
pub enum EventKind {
ProfileMetadata = 0,
MailEvent = 1059,
Custom(u64),
}

191
src/nostr/event/mod.rs Normal file
View file

@ -0,0 +1,191 @@
use bitcoin::secp256k1::{Message, PublicKey, SecretKey, Keypair, schnorr::Signature, Secp256k1, hashes::{Hash, sha256}};
use serde_json::json;
pub mod tag;
pub use tag::{Tag, list::Tags};
pub mod id;
pub use id::EventId;
pub mod kind;
pub use kind::EventKind;
#[derive(Debug, PartialEq, Eq)]
pub enum EventBuilderError {
MissingFields
}
#[derive(Debug, Default)]
pub struct EventBuilder {
pub created_at: Option<i64>,
pub kind: Option<EventKind>,
pub tags: Tags,
pub content: String,
}
impl EventBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn gift_wrap(sender_keypair: &Keypair, recipient_pubkey: &PublicKey, event: Event) -> Result<Event, EventBuilderError> {
event.pubkey = Some(sender_keypair.clone().public_key());
event.id = Some(event.compute_id());
}
pub fn kind(mut self, kind: EventKind) -> Self {
self.kind = Some(kind);
self
}
pub fn created_at(mut self, created_at: i64) -> Self {
self.created_at = Some(created_at);
self
}
pub fn tag(mut self, tag: Tag) -> Self {
self.tags.push(tag);
self
}
/// Extends the current tags.
pub fn tags<I>(mut self, tags: I) -> Self
where
I: IntoIterator<Item = Tag>,
{
tags.into_iter().map(|t| self.tags.push(t));
self
}
pub fn content(mut self, content: &str) -> Self {
self.content = content.to_owned();
self
}
pub fn build(&self) -> Result<Event, EventBuilderError> {
if self.created_at.is_none() || self.kind.is_none() {
return Err(EventBuilderError::MissingFields);
}
Ok(Event {
created_at: self.created_at.unwrap(),
kind: self.kind.unwrap().into(),
tags: self.tags.clone(),
content: self.content.clone(),
id: None,
pubkey: None,
sig: None,
})
}
}
#[derive(Debug, Clone)]
pub struct Event {
pub id: Option<EventId>,
pub pubkey: Option<PublicKey>,
pub created_at: i64,
pub kind: u32,
pub tags: Tags,
pub content: String,
pub sig: Option<Signature>,
}
impl Event {
/// Verifies the signature of the event
pub fn verify(&self) -> bool {
let secp = Secp256k1::verification_only();
let message = Message::from_digest(*self.id.clone().expect("id should be present").as_bytes());
secp.verify_schnorr(&self.sig.expect("signature should be present"), &message, &self.pubkey.expect("public key should be present").into()).is_ok()
}
pub fn sign_with_seckey(&mut self, seckey: &SecretKey) -> Result<(), String> {
let secp = Secp256k1::new();
let keypair = Keypair::from_secret_key(&secp, seckey);
self.sign(&keypair)
}
/// Signs the event with the given private key
pub fn sign(&mut self, keypair: &Keypair) -> Result<(), String> {
let secp = Secp256k1::new();
if self.pubkey.is_none() {
self.pubkey = Some(keypair.public_key());
}
let id = self.compute_id();
self.id = Some(id.clone());
let message = Message::from_digest(*id.as_bytes());
self.sig = Some(secp.sign_schnorr(&message, keypair));
Ok(())
}
/// Computes the event ID
fn compute_id(&self) -> EventId {
let serialized = json!([
0,
self.pubkey,
self.created_at,
self.kind,
self.tags,
self.content
]);
sha256::Hash::hash(serialized.as_str().unwrap().as_bytes()).to_string().into()
}
}
#[cfg(test)]
mod tests {
use super::*;
use bitcoin::secp256k1::{Secp256k1, SecretKey, PublicKey};
fn create_test_event() -> Event {
Event {
id: Some(EventId::default()),
pubkey: Some(PublicKey::from_slice(&[0; 33]).unwrap()),
created_at: 1234567890,
kind: 1,
tags: vec![vec!["tag1".to_string(), "value1".to_string()]].into(),
content: "Test content".to_string(),
sig: Some(Signature::from_slice(&[0; 64]).unwrap()),
}
}
#[test]
fn test_compute_id() {
let event = create_test_event();
let id = event.compute_id();
assert_ne!(id, EventId::default());
}
#[test]
fn test_sign_and_verify() {
let secp = Secp256k1::new();
let secret_key = SecretKey::new(&mut rand::thread_rng());
let keypair = Keypair::from_secret_key(&secp, &secret_key);
let mut event = create_test_event();
assert!(event.sign(&keypair).is_ok());
assert!(event.verify());
}
#[test]
fn test_sign_with_seckey() {
let secret_key = SecretKey::new(&mut rand::thread_rng());
let mut event = create_test_event();
assert!(event.sign_with_seckey(&secret_key).is_ok());
assert!(event.verify());
}
#[test]
fn test_verify_invalid_signature() {
let mut event = create_test_event();
event.content = "Modified content".to_string();
assert!(!event.verify());
}
}

View file

@ -0,0 +1,11 @@
use serde::{Serialize, Deserialize};
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum TagKind {
#[serde(rename = "t")]
Tag,
#[serde(rename = "p")]
Pubkey,
#[serde(rename = "subject")]
Subject,
}

View file

@ -0,0 +1,3 @@
use super::Tag;
pub type Tags = Vec<Tag>;

View file

@ -0,0 +1,40 @@
use serde::{Serialize, Deserialize};
pub mod list;
pub mod kind;
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Tag(Vec<String>);
impl Tag {
#[inline]
pub fn new() -> Self {
Tag::new_with_values(Vec::new())
}
#[inline]
pub fn new_with_values(values: Vec<String>) -> Self {
Self(values)
}
#[inline]
pub fn kind(&self) -> &str {
&self.0[0]
}
#[inline]
pub fn content(&self) -> Option<&str> {
self.0.get(1).map(|s| s.as_str())
}
#[inline]
pub fn len(self) -> usize {
self.0.len()
}
}
impl From<Vec<String>> for Tag {
fn from(value: Vec<String>) -> Self {
Tag::new_with_values(value)
}
}

183
src/nostr/filter.rs Normal file
View file

@ -0,0 +1,183 @@
use std::collections::BTreeMap;
use chrono::{DateTime, Utc, serde::ts_seconds_option};
use super::EventId;
use serde::ser::{SerializeMap, Serializer};
use serde::{Serialize, Deserialize};
type GenericTags = BTreeMap<String, Vec<String>>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Filter {
#[serde(skip_serializing_if = "Option::is_none")]
pub ids: Option<Vec<EventId>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub authors: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub kinds: Option<Vec<u32>>,
#[serde(with = "ts_seconds_option", skip_serializing_if = "Option::is_none")]
pub since: Option<DateTime<Utc>>,
#[serde(with = "ts_seconds_option", skip_serializing_if = "Option::is_none")]
pub until: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<usize>,
#[serde(flatten, serialize_with = "serialize_generic_tags")]
pub generic_tags: GenericTags,
}
impl Filter {
pub fn new() -> Self {
Self::default()
}
pub fn set_ids(mut self, ids: Vec<EventId>) -> Self {
self.ids = Some(ids);
self
}
pub fn unset_ids(mut self) -> Self {
self.ids = None;
self
}
pub fn add_id(mut self, id: EventId) -> Self {
self.ids.get_or_insert_with(Vec::new).push(id);
self
}
pub fn remove_id(mut self, id: &EventId) -> Self {
if let Some(ids) = &mut self.ids {
ids.retain(|x| x != id);
}
self
}
pub fn set_authors(mut self, authors: Vec<String>) -> Self {
self.authors = Some(authors);
self
}
pub fn unset_authors(mut self) -> Self {
self.authors = None;
self
}
pub fn add_author(mut self, author: String) -> Self {
self.authors.get_or_insert_with(Vec::new).push(author);
self
}
pub fn remove_author(mut self, author: &str) -> Self {
if let Some(authors) = &mut self.authors {
authors.retain(|x| x != author);
}
self
}
pub fn set_kinds(mut self, kinds: Vec<u32>) -> Self {
self.kinds = Some(kinds);
self
}
pub fn unset_kinds(mut self) -> Self {
self.kinds = None;
self
}
pub fn add_kind(mut self, kind: u32) -> Self {
self.kinds.get_or_insert_with(Vec::new).push(kind);
self
}
pub fn remove_kind(mut self, kind: u32) -> Self {
if let Some(kinds) = &mut self.kinds {
kinds.retain(|&x| x != kind);
}
self
}
pub fn set_since(mut self, since: DateTime<Utc>) -> Self {
self.since = Some(since);
self
}
pub fn unset_since(mut self) -> Self {
self.since = None;
self
}
pub fn set_until(mut self, until: DateTime<Utc>) -> Self {
self.until = Some(until);
self
}
pub fn unset_until(mut self) -> Self {
self.until = None;
self
}
pub fn set_limit(mut self, limit: usize) -> Self {
self.limit = Some(limit);
self
}
pub fn unset_limit(mut self) -> Self {
self.limit = None;
self
}
pub fn add_tag(mut self, key: &str, value: Vec<String>) -> Self {
self.generic_tags.insert(key.to_owned(), value);
self
}
pub fn remove_tag(mut self, key: &str, value: &str) -> Self {
if let Some(values) = self.generic_tags.get_mut(key) {
values.retain(|x| x != value);
if values.is_empty() {
self.generic_tags.remove(key);
}
}
self
}
pub fn clear_tag(mut self, key: &str) -> Self {
self.generic_tags.remove(key);
self
}
pub fn clear_all_tags(mut self) -> Self {
self.generic_tags.clear();
self
}
}
impl Default for Filter {
fn default() -> Self {
Filter {
ids: None,
authors: None,
kinds: None,
since: None,
until: None,
limit: None,
generic_tags: GenericTags::new(),
}
}
}
fn serialize_generic_tags<S>(tags: &GenericTags, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut map = serializer.serialize_map(Some(tags.len()))?;
for (key, value) in tags {
map.serialize_entry(key, value)?;
}
map.end()
}

4
src/nostr/mod.rs Normal file
View file

@ -0,0 +1,4 @@
pub mod event;
pub use event::{Event, EventBuilder, EventId, Tag, Tags, EventKind};
pub mod filter;
pub use filter::Filter;

View file

@ -1,10 +1,8 @@
use ewebsock::{WsMessage, WsEvent};
use nostr::types::Filter;
use nostr::Event;
use serde::de::{SeqAccess, Visitor};
use crate::nostr::Filter;
use crate::nostr::Event;
use serde::ser::SerializeSeq;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::fmt::{self};
use serde::{Serialize, Serializer};
use crate::error;
#[derive(Debug, Eq, PartialEq)]