adding mail events into sqlite
This commit is contained in:
parent
21301cc603
commit
a2deb4145b
72
Cargo.lock
generated
72
Cargo.lock
generated
|
@ -1,6 +1,6 @@
|
||||||
# This file is automatically @generated by Cargo.
|
# This file is automatically @generated by Cargo.
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 3
|
version = 4
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ab_glyph"
|
name = "ab_glyph"
|
||||||
|
@ -188,9 +188,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.82"
|
version = "1.0.96"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519"
|
checksum = "6b964d184e89d9b6b67dd2715bc8e74cf3107fb2b529990c90cf517326150bf4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arbitrary"
|
name = "arbitrary"
|
||||||
|
@ -1660,12 +1660,6 @@ version = "1.0.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "foldhash"
|
|
||||||
version = "0.1.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "foreign-types"
|
name = "foreign-types"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
|
@ -1941,7 +1935,7 @@ checksum = "cc11df1ace8e7e564511f53af41f3e42ddc95b56fd07b3f4445d2a6048bc682c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.6.0",
|
"bitflags 2.6.0",
|
||||||
"gpu-descriptor-types",
|
"gpu-descriptor-types",
|
||||||
"hashbrown 0.14.3",
|
"hashbrown",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1973,22 +1967,13 @@ dependencies = [
|
||||||
"allocator-api2",
|
"allocator-api2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "hashbrown"
|
|
||||||
version = "0.15.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
|
|
||||||
dependencies = [
|
|
||||||
"foldhash",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashlink"
|
name = "hashlink"
|
||||||
version = "0.10.0"
|
version = "0.9.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
|
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hashbrown 0.15.2",
|
"hashbrown",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -2067,17 +2052,20 @@ dependencies = [
|
||||||
name = "hoot-app"
|
name = "hoot-app"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
"eframe",
|
"eframe",
|
||||||
"egui_extras",
|
"egui_extras",
|
||||||
"egui_tabs",
|
"egui_tabs",
|
||||||
"ewebsock",
|
"ewebsock",
|
||||||
"image 0.25.1",
|
"image 0.25.1",
|
||||||
|
"include_dir",
|
||||||
"nostr",
|
"nostr",
|
||||||
"pollster",
|
"pollster",
|
||||||
"puffin",
|
"puffin",
|
||||||
"puffin_http",
|
"puffin_http",
|
||||||
"rand",
|
"rand",
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
|
"rusqlite_migration",
|
||||||
"security-framework",
|
"security-framework",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
@ -2205,6 +2193,25 @@ version = "1.10.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "44feda355f4159a7c757171a77de25daf6411e217b4cabd03bd6650690468126"
|
checksum = "44feda355f4159a7c757171a77de25daf6411e217b4cabd03bd6650690468126"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "include_dir"
|
||||||
|
version = "0.7.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd"
|
||||||
|
dependencies = [
|
||||||
|
"include_dir_macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "include_dir_macros"
|
||||||
|
version = "0.7.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.2.6"
|
version = "2.2.6"
|
||||||
|
@ -2212,7 +2219,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
|
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown 0.14.3",
|
"hashbrown",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -2418,9 +2425,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libsqlite3-sys"
|
name = "libsqlite3-sys"
|
||||||
version = "0.31.0"
|
version = "0.30.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ad8935b44e7c13394a179a438e0cebba0fe08fe01b54f152e29a93b5cf993fd4"
|
checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"openssl-sys",
|
"openssl-sys",
|
||||||
|
@ -3474,9 +3481,9 @@ checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rusqlite"
|
name = "rusqlite"
|
||||||
version = "0.33.0"
|
version = "0.32.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1c6d5e5acb6f6129fe3f7ba0a7fc77bca1942cb568535e18e7bc40262baf3110"
|
checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.6.0",
|
"bitflags 2.6.0",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
@ -3488,6 +3495,17 @@ dependencies = [
|
||||||
"smallvec",
|
"smallvec",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rusqlite_migration"
|
||||||
|
version = "1.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "923b42e802f7dc20a0a6b5e097ba7c83fe4289da07e49156fecf6af08aa9cd1c"
|
||||||
|
dependencies = [
|
||||||
|
"include_dir",
|
||||||
|
"log",
|
||||||
|
"rusqlite",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-hash"
|
name = "rustc-hash"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
|
|
22
Cargo.toml
22
Cargo.toml
|
@ -6,15 +6,16 @@ edition = "2021"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
profiling = ["dep:puffin", "dep:puffin_http", "eframe/puffin", "egui_extras/puffin"]
|
profiling = [
|
||||||
|
"dep:puffin",
|
||||||
|
"dep:puffin_http",
|
||||||
|
"eframe/puffin",
|
||||||
|
"egui_extras/puffin",
|
||||||
|
]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
eframe = { version = "0.27.2", features = ["default", "persistence"] }
|
eframe = { version = "0.27.2", features = ["default", "persistence"] }
|
||||||
egui_extras = { version = "0.27.2", features = [
|
egui_extras = { version = "0.27.2", features = ["file", "image", "svg"] }
|
||||||
"file",
|
|
||||||
"image",
|
|
||||||
"svg",
|
|
||||||
] }
|
|
||||||
egui_tabs = { git = "https://github.com/damus-io/egui-tabs", rev = "120971fc43db6ba0b6f194f4bd4a66f7e00a4e22" }
|
egui_tabs = { git = "https://github.com/damus-io/egui-tabs", rev = "120971fc43db6ba0b6f194f4bd4a66f7e00a4e22" }
|
||||||
image = { version = "0.25.1", features = ["jpeg", "png"] }
|
image = { version = "0.25.1", features = ["jpeg", "png"] }
|
||||||
tracing = "0.1.40"
|
tracing = "0.1.40"
|
||||||
|
@ -28,7 +29,14 @@ nostr = { version = "0.37.0", features = ["std", "nip59"] }
|
||||||
serde = "1.0.204"
|
serde = "1.0.204"
|
||||||
serde_json = "1.0.121"
|
serde_json = "1.0.121"
|
||||||
pollster = "0.4.0"
|
pollster = "0.4.0"
|
||||||
rusqlite = { version = "0.33.0", features = ["chrono", "serde_json", "bundled-sqlcipher-vendored-openssl"] }
|
rusqlite = { version = "0.32.1", features = [
|
||||||
|
"chrono",
|
||||||
|
"serde_json",
|
||||||
|
"bundled-sqlcipher-vendored-openssl",
|
||||||
|
] }
|
||||||
|
rusqlite_migration = { version = "1.3.1", features = ["from-directory"] }
|
||||||
|
anyhow = "1.0.96"
|
||||||
|
include_dir = "0.7.4"
|
||||||
|
|
||||||
[target.'cfg(target_os = "macos")'.dependencies]
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
security-framework = "3.0.0"
|
security-framework = "3.0.0"
|
||||||
|
|
9
migrations/001-nostr_events/up.sql
Normal file
9
migrations/001-nostr_events/up.sql
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS events (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
pubkey TEXT NOT NULL,
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
kind INTEGER NOT NULL,
|
||||||
|
tags TEXT NOT NULL CHECK (json_valid (tags)),
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
sig TEXT
|
||||||
|
);
|
151
src/db.rs
Normal file
151
src/db.rs
Normal file
|
@ -0,0 +1,151 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use include_dir::{include_dir, Dir};
|
||||||
|
use nostr::{Event, Kind};
|
||||||
|
use rusqlite::Connection;
|
||||||
|
use rusqlite_migration::{Migrations, M};
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
use crate::account_manager::AccountManager;
|
||||||
|
use crate::mail_event::MAIL_EVENT_KIND;
|
||||||
|
|
||||||
|
static MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/migrations");
|
||||||
|
|
||||||
|
static MIGRATIONS: LazyLock<Migrations<'static>> =
|
||||||
|
LazyLock::new(|| Migrations::from_directory(&MIGRATIONS_DIR).unwrap());
|
||||||
|
|
||||||
|
pub struct Db {
|
||||||
|
connection: Connection,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Db {
|
||||||
|
pub fn new(path: PathBuf) -> Result<Self> {
|
||||||
|
let mut conn = Connection::open(path)?;
|
||||||
|
|
||||||
|
// Apply migrations
|
||||||
|
MIGRATIONS.to_latest(&mut conn)?;
|
||||||
|
|
||||||
|
Ok(Self { connection: conn })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Store a mail event in the database
|
||||||
|
///
|
||||||
|
/// This function first attempts to unwrap the gift wrap if necessary,
|
||||||
|
/// and then stores the event in the database.
|
||||||
|
pub fn store_mail_event(
|
||||||
|
&self,
|
||||||
|
event: &Event,
|
||||||
|
account_manager: &mut AccountManager,
|
||||||
|
) -> Result<()> {
|
||||||
|
// Try to unwrap the gift wrap if this event is a gift wrap
|
||||||
|
let store_unwrapped =
|
||||||
|
is_gift_wrap(event) && account_manager.unwrap_gift_wrap(event).is_ok();
|
||||||
|
|
||||||
|
// Determine what event to store
|
||||||
|
if store_unwrapped {
|
||||||
|
// Unwrap succeeded, store the unwrapped event
|
||||||
|
let unwrapped = account_manager.unwrap_gift_wrap(event).unwrap();
|
||||||
|
|
||||||
|
// Get event details from the unwrapped gift
|
||||||
|
let id = match unwrapped.rumor.id {
|
||||||
|
Some(id) => id.to_string(),
|
||||||
|
None => "unknown".to_string(),
|
||||||
|
};
|
||||||
|
let pubkey = unwrapped.rumor.pubkey.to_string();
|
||||||
|
let created_at = unwrapped.rumor.created_at.as_u64();
|
||||||
|
let kind = unwrapped.rumor.kind.as_u16() as u32;
|
||||||
|
let tags_json = json!(unwrapped.rumor.tags).to_string();
|
||||||
|
let content = unwrapped.rumor.content.clone();
|
||||||
|
let sig = unwrapped.sender.to_string(); // Use sender pubkey as signature reference
|
||||||
|
|
||||||
|
// Store the unwrapped event in the database
|
||||||
|
self.connection.execute(
|
||||||
|
"INSERT OR REPLACE INTO events (id, pubkey, created_at, kind, tags, content, sig)
|
||||||
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
|
||||||
|
(id, pubkey, created_at, kind, tags_json, content, sig),
|
||||||
|
)?;
|
||||||
|
} else {
|
||||||
|
// Use original event
|
||||||
|
// Convert tags to JSON string for storage
|
||||||
|
let tags_json = json!(event.tags).to_string();
|
||||||
|
|
||||||
|
// Get event details
|
||||||
|
let id = event.id.to_string();
|
||||||
|
let pubkey = event.pubkey.to_string();
|
||||||
|
let created_at = event.created_at.as_u64();
|
||||||
|
let kind = event.kind.as_u16() as u32;
|
||||||
|
let sig = event.sig.to_string();
|
||||||
|
|
||||||
|
// Store the event in the database
|
||||||
|
self.connection.execute(
|
||||||
|
"INSERT OR REPLACE INTO events (id, pubkey, created_at, kind, tags, content, sig)
|
||||||
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
|
||||||
|
(id, pubkey, created_at, kind, tags_json, &event.content, sig),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the database contains an event with the given ID
|
||||||
|
pub fn has_event(&self, event_id: &str) -> Result<bool> {
|
||||||
|
let count: i64 = self.connection.query_row(
|
||||||
|
"SELECT COUNT(*) FROM events WHERE id = ?",
|
||||||
|
[event_id],
|
||||||
|
|row| row.get(0),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(count > 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all event IDs for mail events
|
||||||
|
pub fn get_mail_event_ids(&self) -> Result<Vec<String>> {
|
||||||
|
let mut stmt = self
|
||||||
|
.connection
|
||||||
|
.prepare("SELECT id FROM events WHERE kind = ?")?;
|
||||||
|
|
||||||
|
let mail_kind = u32::from(MAIL_EVENT_KIND as u16);
|
||||||
|
|
||||||
|
let id_iter = stmt.query_map([mail_kind], |row| {
|
||||||
|
let id: String = row.get(0)?;
|
||||||
|
Ok(id)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut ids = Vec::new();
|
||||||
|
for id_result in id_iter {
|
||||||
|
match id_result {
|
||||||
|
Ok(id) => ids.push(id),
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Error loading mail event ID: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the JSON representation of an event by its ID
|
||||||
|
pub fn get_event_json(&self, event_id: &str) -> Result<Option<String>> {
|
||||||
|
let result = self.connection.query_row(
|
||||||
|
"SELECT json_object('id', id, 'pubkey', pubkey, 'created_at', created_at,
|
||||||
|
'kind', kind, 'tags', json(tags), 'content', content,
|
||||||
|
'sig', sig)
|
||||||
|
FROM events WHERE id = ?",
|
||||||
|
[event_id],
|
||||||
|
|row| row.get::<_, String>(0),
|
||||||
|
);
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(json) => Ok(Some(json)),
|
||||||
|
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
|
||||||
|
Err(e) => Err(e.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if an event is a gift wrap
|
||||||
|
fn is_gift_wrap(event: &Event) -> bool {
|
||||||
|
event.kind == Kind::GiftWrap
|
||||||
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
use nostr::{Event, EventBuilder, Keys, Kind, PublicKey, Tag, TagKind, TagStandard};
|
use nostr::{Event, EventBuilder, Keys, Kind, PublicKey, Tag, TagKind, TagStandard};
|
||||||
use std::collections::HashMap;
|
|
||||||
use pollster::FutureExt as _;
|
use pollster::FutureExt as _;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
pub const MAIL_EVENT_KIND: u16 = 1059;
|
pub const MAIL_EVENT_KIND: u16 = 2024;
|
||||||
|
|
||||||
pub struct MailMessage {
|
pub struct MailMessage {
|
||||||
pub to: Vec<PublicKey>,
|
pub to: Vec<PublicKey>,
|
||||||
|
@ -23,19 +23,25 @@ impl MailMessage {
|
||||||
}
|
}
|
||||||
|
|
||||||
for pubkey in &self.cc {
|
for pubkey in &self.cc {
|
||||||
tags.push(Tag::custom(TagKind::p(), vec![pubkey.to_hex().as_str(), "cc"]));
|
tags.push(Tag::custom(
|
||||||
|
TagKind::p(),
|
||||||
|
vec![pubkey.to_hex().as_str(), "cc"],
|
||||||
|
));
|
||||||
pubkeys_to_send_to.push(*pubkey);
|
pubkeys_to_send_to.push(*pubkey);
|
||||||
}
|
}
|
||||||
|
|
||||||
tags.push(Tag::from_standardized(TagStandard::Subject(self.subject.clone())));
|
tags.push(Tag::from_standardized(TagStandard::Subject(
|
||||||
|
self.subject.clone(),
|
||||||
|
)));
|
||||||
|
|
||||||
let base_event = EventBuilder::new(Kind::Custom(MAIL_EVENT_KIND), &self.content)
|
let base_event = EventBuilder::new(Kind::Custom(MAIL_EVENT_KIND), &self.content).tags(tags);
|
||||||
.tags(tags);
|
|
||||||
|
|
||||||
let mut event_list: HashMap<PublicKey, Event> = HashMap::new();
|
let mut event_list: HashMap<PublicKey, Event> = HashMap::new();
|
||||||
for pubkey in pubkeys_to_send_to {
|
for pubkey in pubkeys_to_send_to {
|
||||||
let wrapped_event =
|
let wrapped_event =
|
||||||
EventBuilder::gift_wrap(sending_keys, &pubkey, base_event.clone(), None).block_on().unwrap();
|
EventBuilder::gift_wrap(sending_keys, &pubkey, base_event.clone(), None)
|
||||||
|
.block_on()
|
||||||
|
.unwrap();
|
||||||
event_list.insert(pubkey, wrapped_event);
|
event_list.insert(pubkey, wrapped_event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
200
src/main.rs
200
src/main.rs
|
@ -8,6 +8,7 @@ use std::collections::HashMap;
|
||||||
use tracing::{debug, error, info, Level};
|
use tracing::{debug, error, info, Level};
|
||||||
|
|
||||||
mod account_manager;
|
mod account_manager;
|
||||||
|
mod db;
|
||||||
mod error;
|
mod error;
|
||||||
mod keystorage;
|
mod keystorage;
|
||||||
mod mail_event;
|
mod mail_event;
|
||||||
|
@ -84,6 +85,7 @@ pub struct Hoot {
|
||||||
relays: relay::RelayPool,
|
relays: relay::RelayPool,
|
||||||
events: Vec<nostr::Event>,
|
events: Vec<nostr::Event>,
|
||||||
account_manager: account_manager::AccountManager,
|
account_manager: account_manager::AccountManager,
|
||||||
|
db: db::Db,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
|
@ -113,7 +115,17 @@ fn update_app(app: &mut Hoot, ctx: &egui::Context) {
|
||||||
if app.account_manager.loaded_keys.len() > 0 {
|
if app.account_manager.loaded_keys.len() > 0 {
|
||||||
let mut gw_sub = relay::Subscription::default();
|
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().kind(nostr::Kind::GiftWrap).custom_tag(
|
||||||
|
nostr::SingleLetterTag {
|
||||||
|
character: nostr::Alphabet::P,
|
||||||
|
uppercase: false,
|
||||||
|
},
|
||||||
|
app.account_manager
|
||||||
|
.loaded_keys
|
||||||
|
.clone()
|
||||||
|
.into_iter()
|
||||||
|
.map(|keys| keys.public_key()),
|
||||||
|
);
|
||||||
gw_sub.filter(filter);
|
gw_sub.filter(filter);
|
||||||
|
|
||||||
// TODO: fix error handling
|
// TODO: fix error handling
|
||||||
|
@ -154,13 +166,30 @@ fn process_event(app: &mut Hoot, _sub_id: &str, event_json: &str) {
|
||||||
|
|
||||||
// Parse the event using the RelayMessage type which handles the ["EVENT", subscription_id, event_json] format
|
// Parse the event using the RelayMessage type which handles the ["EVENT", subscription_id, event_json] format
|
||||||
if let Ok(event) = serde_json::from_str::<nostr::Event>(event_json) {
|
if let Ok(event) = serde_json::from_str::<nostr::Event>(event_json) {
|
||||||
// Verify the event signature
|
// Check if we already have this event
|
||||||
if event.verify().is_ok() {
|
if let Ok(has_event) = app.db.has_event(&event.id.to_string()) {
|
||||||
debug!("Verified event: {:?}", event);
|
if has_event {
|
||||||
app.events.push(event);
|
debug!("Skipping already stored event: {}", event.id);
|
||||||
} else {
|
return;
|
||||||
error!("Event verification failed");
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the event signature
|
||||||
|
if event.verify().is_ok() {
|
||||||
|
debug!("Verified event: {:?}", event);
|
||||||
|
|
||||||
|
// Store the event in memory
|
||||||
|
app.events.push(event.clone());
|
||||||
|
|
||||||
|
// Store the event in the database
|
||||||
|
if let Err(e) = app.db.store_mail_event(&event, &mut app.account_manager) {
|
||||||
|
error!("Failed to store event in database: {}", e);
|
||||||
|
} else {
|
||||||
|
debug!("Successfully stored event with id {} in database", event.id);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error!("Event verification failed for event: {}", event.id);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
error!("Failed to parse event JSON: {}", event_json);
|
error!("Failed to parse event JSON: {}", event_json);
|
||||||
}
|
}
|
||||||
|
@ -182,10 +211,13 @@ fn render_app(app: &mut Hoot, ctx: &egui::Context) {
|
||||||
ui.add_space(16.0);
|
ui.add_space(16.0);
|
||||||
|
|
||||||
// Compose button
|
// Compose button
|
||||||
if ui.add_sized(
|
if ui
|
||||||
[180.0, 36.0],
|
.add_sized(
|
||||||
egui::Button::new("✉ Compose").fill(egui::Color32::from_rgb(149, 117, 205)),
|
[180.0, 36.0],
|
||||||
).clicked() {
|
egui::Button::new("✉ Compose").fill(egui::Color32::from_rgb(149, 117, 205)),
|
||||||
|
)
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
let state = ui::compose_window::ComposeWindowState {
|
let state = ui::compose_window::ComposeWindowState {
|
||||||
subject: String::new(),
|
subject: String::new(),
|
||||||
to_field: String::new(),
|
to_field: String::new(),
|
||||||
|
@ -212,18 +244,35 @@ fn render_app(app: &mut Hoot, ctx: &egui::Context) {
|
||||||
|
|
||||||
for (label, page, count) in nav_items {
|
for (label, page, count) in nav_items {
|
||||||
let is_selected = app.page == page;
|
let is_selected = app.page == page;
|
||||||
let response = ui.selectable_label(is_selected, format!("{} {}", label, if count > 0 { count.to_string() } else { String::new() }));
|
let response = ui.selectable_label(
|
||||||
|
is_selected,
|
||||||
|
format!(
|
||||||
|
"{} {}",
|
||||||
|
label,
|
||||||
|
if count > 0 {
|
||||||
|
count.to_string()
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
);
|
||||||
if response.clicked() {
|
if response.clicked() {
|
||||||
app.page = page;
|
app.page = page;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if ui.button("onboarding").clicked() {
|
||||||
|
app.page = Page::OnboardingNew;
|
||||||
|
}
|
||||||
|
|
||||||
// Add flexible space to push profile to bottom
|
// Add flexible space to push profile to bottom
|
||||||
ui.with_layout(egui::Layout::bottom_up(egui::Align::Center), |ui| {
|
ui.with_layout(egui::Layout::bottom_up(egui::Align::Center), |ui| {
|
||||||
ui.add_space(8.0);
|
ui.add_space(8.0);
|
||||||
// Profile section
|
// Profile section
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
if ui.add_sized([32.0, 32.0], egui::Button::new("👤")).clicked() {
|
if ui
|
||||||
|
.add_sized([32.0, 32.0], egui::Button::new("👤"))
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
app.page = Page::Settings;
|
app.page = Page::Settings;
|
||||||
}
|
}
|
||||||
if let Some(key) = app.account_manager.loaded_keys.first() {
|
if let Some(key) = app.account_manager.loaded_keys.first() {
|
||||||
|
@ -245,7 +294,7 @@ fn render_app(app: &mut Hoot, ctx: &egui::Context) {
|
||||||
[search_width, 32.0],
|
[search_width, 32.0],
|
||||||
egui::TextEdit::singleline(&mut String::new())
|
egui::TextEdit::singleline(&mut String::new())
|
||||||
.hint_text("Search")
|
.hint_text("Search")
|
||||||
.margin(egui::vec2(8.0, 4.0))
|
.margin(egui::vec2(8.0, 4.0)),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -253,11 +302,11 @@ fn render_app(app: &mut Hoot, ctx: &egui::Context) {
|
||||||
|
|
||||||
// Email list using TableBuilder
|
// Email list using TableBuilder
|
||||||
TableBuilder::new(ui)
|
TableBuilder::new(ui)
|
||||||
.column(Column::auto()) // Checkbox
|
.column(Column::auto()) // Checkbox
|
||||||
.column(Column::auto()) // Star
|
.column(Column::auto()) // Star
|
||||||
.column(Column::remainder()) // Sender
|
.column(Column::remainder()) // Sender
|
||||||
.column(Column::remainder()) // Content
|
.column(Column::remainder()) // Content
|
||||||
.column(Column::remainder()) // Time
|
.column(Column::remainder()) // Time
|
||||||
.striped(true)
|
.striped(true)
|
||||||
.sense(Sense::click())
|
.sense(Sense::click())
|
||||||
.auto_shrink(Vec2b { x: false, y: false })
|
.auto_shrink(Vec2b { x: false, y: false })
|
||||||
|
@ -284,36 +333,33 @@ fn render_app(app: &mut Hoot, ctx: &egui::Context) {
|
||||||
body.rows(row_height, events.len(), |mut row| {
|
body.rows(row_height, events.len(), |mut row| {
|
||||||
let event = &events[row.index()];
|
let event = &events[row.index()];
|
||||||
|
|
||||||
// Try to unwrap the gift wrap
|
row.col(|ui| {
|
||||||
if let Ok(unwrapped) = app.account_manager.unwrap_gift_wrap(event) {
|
ui.checkbox(&mut false, "");
|
||||||
row.col(|ui| {
|
});
|
||||||
ui.checkbox(&mut false, "");
|
row.col(|ui| {
|
||||||
});
|
ui.checkbox(&mut false, "");
|
||||||
row.col(|ui| {
|
});
|
||||||
ui.checkbox(&mut false, "");
|
row.col(|ui| {
|
||||||
});
|
ui.label(event.pubkey.to_string());
|
||||||
row.col(|ui| {
|
});
|
||||||
ui.label(unwrapped.sender.to_string());
|
row.col(|ui| {
|
||||||
});
|
// Try to get subject from tags
|
||||||
row.col(|ui| {
|
let subject = match &event.tags.find(nostr::TagKind::Subject) {
|
||||||
// Try to get subject from tags
|
Some(s) => match s.content() {
|
||||||
let subject = match &unwrapped.rumor.tags.find(nostr::TagKind::Subject) {
|
Some(c) => format!("{}: {}", c.to_string(), event.content),
|
||||||
Some(s) => match s.content() {
|
None => event.content.clone(),
|
||||||
Some(c) => format!("{}: {}", c.to_string(), unwrapped.rumor.content),
|
},
|
||||||
None => unwrapped.rumor.content.clone(),
|
None => event.content.clone(),
|
||||||
},
|
};
|
||||||
None => unwrapped.rumor.content.clone(),
|
ui.label(subject);
|
||||||
};
|
});
|
||||||
ui.label(subject);
|
row.col(|ui| {
|
||||||
});
|
ui.label("2 minutes ago");
|
||||||
row.col(|ui| {
|
});
|
||||||
ui.label("2 minutes ago");
|
|
||||||
});
|
|
||||||
|
|
||||||
if row.response().clicked() {
|
if row.response().clicked() {
|
||||||
app.focused_post = event.id.to_string();
|
app.focused_post = event.id.to_string();
|
||||||
app.page = Page::Post;
|
app.page = Page::Post;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -322,14 +368,23 @@ fn render_app(app: &mut Hoot, ctx: &egui::Context) {
|
||||||
ui::settings::SettingsScreen::ui(app, ui);
|
ui::settings::SettingsScreen::ui(app, ui);
|
||||||
}
|
}
|
||||||
Page::Post => {
|
Page::Post => {
|
||||||
if let Some(event) = app.events.iter().find(|e| e.id.to_string() == app.focused_post) {
|
if let Some(event) = app
|
||||||
|
.events
|
||||||
|
.iter()
|
||||||
|
.find(|e| e.id.to_string() == app.focused_post)
|
||||||
|
{
|
||||||
if let Ok(unwrapped) = app.account_manager.unwrap_gift_wrap(event) {
|
if let Ok(unwrapped) = app.account_manager.unwrap_gift_wrap(event) {
|
||||||
// Message header section
|
// Message header section
|
||||||
ui.add_space(8.0);
|
ui.add_space(8.0);
|
||||||
ui.heading(&unwrapped.rumor.tags.find(nostr::TagKind::Subject)
|
ui.heading(
|
||||||
.and_then(|s| s.content())
|
&unwrapped
|
||||||
.map(|c| c.to_string())
|
.rumor
|
||||||
.unwrap_or_else(|| "No Subject".to_string()));
|
.tags
|
||||||
|
.find(nostr::TagKind::Subject)
|
||||||
|
.and_then(|s| s.content())
|
||||||
|
.map(|c| c.to_string())
|
||||||
|
.unwrap_or_else(|| "No Subject".to_string()),
|
||||||
|
);
|
||||||
|
|
||||||
// Metadata grid
|
// Metadata grid
|
||||||
egui::Grid::new("email_metadata")
|
egui::Grid::new("email_metadata")
|
||||||
|
@ -341,11 +396,16 @@ fn render_app(app: &mut Hoot, ctx: &egui::Context) {
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
|
|
||||||
ui.label("To");
|
ui.label("To");
|
||||||
ui.label(unwrapped.rumor.tags.iter()
|
ui.label(
|
||||||
.filter_map(|tag| tag.content())
|
unwrapped
|
||||||
.next()
|
.rumor
|
||||||
.unwrap_or_else(|| "Unknown")
|
.tags
|
||||||
.to_string());
|
.iter()
|
||||||
|
.filter_map(|tag| tag.content())
|
||||||
|
.next()
|
||||||
|
.unwrap_or_else(|| "Unknown")
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -387,8 +447,7 @@ fn render_app(app: &mut Hoot, ctx: &egui::Context) {
|
||||||
ui.label("Your draft messages will appear here");
|
ui.label("Your draft messages will appear here");
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
ui.heading("Coming Soon");
|
ui::onboarding::OnboardingScreen::ui(app, ui);
|
||||||
ui.label("This feature is under development");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -396,7 +455,25 @@ fn render_app(app: &mut Hoot, ctx: &egui::Context) {
|
||||||
|
|
||||||
impl Hoot {
|
impl Hoot {
|
||||||
fn new(cc: &eframe::CreationContext<'_>) -> Self {
|
fn new(cc: &eframe::CreationContext<'_>) -> Self {
|
||||||
let storage_dir = eframe::storage_dir("Hoot").unwrap();
|
// Create storage directory if it doesn't exist
|
||||||
|
let storage_dir = eframe::storage_dir("hoot").unwrap();
|
||||||
|
std::fs::create_dir_all(&storage_dir).unwrap();
|
||||||
|
|
||||||
|
// Create the database file path
|
||||||
|
let db_path = storage_dir.join("hoot.db");
|
||||||
|
|
||||||
|
// Initialize the database
|
||||||
|
let db = match db::Db::new(db_path) {
|
||||||
|
Ok(db) => {
|
||||||
|
info!("Database initialized successfully");
|
||||||
|
db
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to initialize database: {}", e);
|
||||||
|
panic!("Database initialization failed: {}", e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
page: Page::Inbox,
|
page: Page::Inbox,
|
||||||
focused_post: "".into(),
|
focused_post: "".into(),
|
||||||
|
@ -405,6 +482,7 @@ impl Hoot {
|
||||||
relays: relay::RelayPool::new(),
|
relays: relay::RelayPool::new(),
|
||||||
events: Vec::new(),
|
events: Vec::new(),
|
||||||
account_manager: account_manager::AccountManager::new(),
|
account_manager: account_manager::AccountManager::new(),
|
||||||
|
db,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue