diff --git a/pubky-common/src/timestamp.rs b/pubky-common/src/timestamp.rs index 4c546d5..174c7e3 100644 --- a/pubky-common/src/timestamp.rs +++ b/pubky-common/src/timestamp.rs @@ -1,5 +1,6 @@ //! Monotonic unix timestamp in microseconds +use serde::{Deserialize, Serialize}; use std::fmt::Display; use std::{ ops::{Add, Sub}, @@ -83,6 +84,12 @@ impl Timestamp { } } +impl Default for Timestamp { + fn default() -> Self { + Timestamp::now() + } +} + impl Display for Timestamp { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let bytes: [u8; 8] = self.into(); @@ -155,6 +162,26 @@ impl Sub for &Timestamp { } } +impl Serialize for Timestamp { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let bytes = self.to_bytes(); + bytes.serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for Timestamp { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let bytes: [u8; 8] = Deserialize::deserialize(deserializer)?; + Ok(Timestamp(u64::from_be_bytes(bytes))) + } +} + #[cfg(not(target_arch = "wasm32"))] /// Return the number of microseconds since [SystemTime::UNIX_EPOCH] fn system_time() -> u64 { diff --git a/pubky-homeserver/src/database.rs b/pubky-homeserver/src/database.rs index f7174ee..4adc73d 100644 --- a/pubky-homeserver/src/database.rs +++ b/pubky-homeserver/src/database.rs @@ -9,6 +9,8 @@ pub mod tables; use tables::{Tables, TABLES_COUNT}; +pub const MAX_LIST_LIMIT: u16 = 100; + #[derive(Debug, Clone)] pub struct DB { pub(crate) env: Env, @@ -43,7 +45,7 @@ mod tests { .join(Timestamp::now().to_string()) .join("pubky"); - let mut db = DB::open(&storage).unwrap(); + let db = DB::open(&storage).unwrap(); let keypair = Keypair::random(); let path = "/pub/foo.txt"; diff --git a/pubky-homeserver/src/database/migrations/m0.rs b/pubky-homeserver/src/database/migrations/m0.rs index a690049..11c0e1a 100644 --- a/pubky-homeserver/src/database/migrations/m0.rs +++ b/pubky-homeserver/src/database/migrations/m0.rs @@ -1,6 +1,6 @@ use heed::{Env, RwTxn}; -use crate::database::tables::{blobs, entries, sessions, users}; +use crate::database::tables::{blobs, entries, events, sessions, users}; pub fn run(env: &Env, wtxn: &mut RwTxn) -> anyhow::Result<()> { let _: users::UsersTable = env.create_database(wtxn, Some(users::USERS_TABLE))?; @@ -11,5 +11,7 @@ pub fn run(env: &Env, wtxn: &mut RwTxn) -> anyhow::Result<()> { let _: entries::EntriesTable = env.create_database(wtxn, Some(entries::ENTRIES_TABLE))?; + let _: events::EventsTable = env.create_database(wtxn, Some(events::EVENTS_TABLE))?; + Ok(()) } diff --git a/pubky-homeserver/src/database/tables.rs b/pubky-homeserver/src/database/tables.rs index a019fbe..81a87da 100644 --- a/pubky-homeserver/src/database/tables.rs +++ b/pubky-homeserver/src/database/tables.rs @@ -1,5 +1,6 @@ pub mod blobs; pub mod entries; +pub mod events; pub mod sessions; pub mod users; @@ -8,12 +9,15 @@ use heed::{Env, RwTxn}; use blobs::{BlobsTable, BLOBS_TABLE}; use entries::{EntriesTable, ENTRIES_TABLE}; -pub const TABLES_COUNT: u32 = 4; +use self::events::{EventsTable, EVENTS_TABLE}; + +pub const TABLES_COUNT: u32 = 5; #[derive(Debug, Clone)] pub struct Tables { pub blobs: BlobsTable, pub entries: EntriesTable, + pub events: EventsTable, } impl Tables { @@ -25,6 +29,9 @@ impl Tables { entries: env .open_database(wtxn, Some(ENTRIES_TABLE))? .expect("Entries table already created"), + events: env + .open_database(wtxn, Some(EVENTS_TABLE))? + .expect("Events table already created"), }) } } diff --git a/pubky-homeserver/src/database/tables/blobs.rs b/pubky-homeserver/src/database/tables/blobs.rs index 1a02a09..25f57c0 100644 --- a/pubky-homeserver/src/database/tables/blobs.rs +++ b/pubky-homeserver/src/database/tables/blobs.rs @@ -12,7 +12,7 @@ pub const BLOBS_TABLE: &str = "blobs"; impl DB { pub fn get_blob( - &mut self, + &self, public_key: &PublicKey, path: &str, ) -> anyhow::Result> { diff --git a/pubky-homeserver/src/database/tables/entries.rs b/pubky-homeserver/src/database/tables/entries.rs index e82c309..e41a5df 100644 --- a/pubky-homeserver/src/database/tables/entries.rs +++ b/pubky-homeserver/src/database/tables/entries.rs @@ -13,15 +13,15 @@ use pubky_common::{ timestamp::Timestamp, }; -use crate::database::DB; +use crate::database::{DB, MAX_LIST_LIMIT}; + +use super::events::Event; /// full_path(pubky/*path) => Entry. pub type EntriesTable = Database; pub const ENTRIES_TABLE: &str = "entries"; -const MAX_LIST_LIMIT: u16 = 100; - impl DB { pub fn put_entry( &mut self, @@ -56,6 +56,19 @@ impl DB { .entries .put(&mut wtxn, &key, &entry.serialize())?; + if path.starts_with("pub/") { + let url = format!("pubky://{key}"); + let event = Event::put(&url); + let value = event.serialize(); + + let key = entry.timestamp.to_string(); + + self.tables.events.put(&mut wtxn, &key, &value)?; + + // TODO: delete older events. + // TODO: move to events.rs + } + wtxn.commit()?; Ok(()) @@ -74,6 +87,21 @@ impl DB { let deleted_entry = self.tables.entries.delete(&mut wtxn, &key)?; + // create DELETE event + if path.starts_with("pub/") { + let url = format!("pubky://{key}"); + + let event = Event::delete(&url); + let value = event.serialize(); + + let key = Timestamp::now().to_string(); + + self.tables.events.put(&mut wtxn, &key, &value)?; + + // TODO: delete older events. + // TODO: move to events.rs + } + deleted_entry & deleted_blobs } else { false @@ -198,7 +226,7 @@ pub struct Entry { /// Encoding version version: usize, /// Modified at - timestamp: u64, + timestamp: Timestamp, content_hash: [u8; 32], content_length: usize, content_type: String, @@ -209,10 +237,7 @@ pub struct Entry { impl Entry { pub fn new() -> Self { - Self { - timestamp: Timestamp::now().into_inner(), - ..Default::default() - } + Default::default() } // === Setters === diff --git a/pubky-homeserver/src/database/tables/events.rs b/pubky-homeserver/src/database/tables/events.rs new file mode 100644 index 0000000..cf82e18 --- /dev/null +++ b/pubky-homeserver/src/database/tables/events.rs @@ -0,0 +1,58 @@ +//! Server events (Put and Delete entries) +//! +//! Useful as a realtime sync with Indexers until +//! we implement more self-authenticated merkle data. + +use heed::{ + types::{Bytes, Str}, + Database, +}; +use postcard::{from_bytes, to_allocvec}; +use serde::{Deserialize, Serialize}; + +/// Event [Timestamp] base32 => Encoded event. +pub type EventsTable = Database; + +pub const EVENTS_TABLE: &str = "events"; + +#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)] +pub enum Event { + Put(String), + Delete(String), +} + +impl Event { + pub fn put(url: &str) -> Self { + Self::Put(url.to_string()) + } + + pub fn delete(url: &str) -> Self { + Self::Delete(url.to_string()) + } + + pub fn serialize(&self) -> Vec { + to_allocvec(self).expect("Session::serialize") + } + + pub fn deserialize(bytes: &[u8]) -> core::result::Result { + if bytes[0] > 1 { + panic!("Unknown Event version"); + } + + from_bytes(bytes) + } + + pub fn url(&self) -> &str { + match self { + Event::Put(url) => url, + Event::Delete(url) => url, + } + } + + pub fn operation(&self) -> &str { + match self { + Event::Put(_) => "PUT", + Event::Delete(_) => "DEL", + } + } +} diff --git a/pubky-homeserver/src/routes.rs b/pubky-homeserver/src/routes.rs index 35615fd..163baec 100644 --- a/pubky-homeserver/src/routes.rs +++ b/pubky-homeserver/src/routes.rs @@ -11,6 +11,7 @@ use crate::server::AppState; use self::pkarr::pkarr_router; mod auth; +mod feed; mod pkarr; mod public; mod root; @@ -25,6 +26,7 @@ fn base(state: AppState) -> Router { .route("/:pubky/*path", put(public::put)) .route("/:pubky/*path", get(public::get)) .route("/:pubky/*path", delete(public::delete)) + .route("/events/", get(feed::feed)) .layer(CookieManagerLayer::new()) // TODO: revisit if we enable streaming big payloads // TODO: maybe add to a separate router (drive router?). diff --git a/pubky-homeserver/src/routes/feed.rs b/pubky-homeserver/src/routes/feed.rs new file mode 100644 index 0000000..bd426f3 --- /dev/null +++ b/pubky-homeserver/src/routes/feed.rs @@ -0,0 +1,71 @@ +use std::collections::HashMap; + +use axum::{ + body::Body, + extract::{Query, State}, + http::{header, Response, StatusCode}, + response::IntoResponse, +}; + +use crate::{ + database::{tables::events::Event, MAX_LIST_LIMIT}, + error::Result, + server::AppState, +}; + +pub async fn feed( + State(state): State, + Query(params): Query>, +) -> Result { + let txn = state.db.env.read_txn()?; + + let limit = params + .get("limit") + .and_then(|l| l.parse::().ok()) + .unwrap_or(MAX_LIST_LIMIT) + .min(MAX_LIST_LIMIT); + + let mut cursor = params + .get("cursor") + .map(|c| c.as_str()) + .unwrap_or("0000000000000"); + + // Guard against bad cursor + if cursor.len() < 13 { + cursor = "0000000000000" + } + + let mut result: Vec = vec![]; + let mut next_cursor = cursor.to_string(); + + for _ in 0..limit { + match state + .db + .tables + .events + .get_greater_than(&txn, &next_cursor)? + { + Some((timestamp, event_bytes)) => { + let event = Event::deserialize(event_bytes)?; + + let line = format!("{} {}", event.operation(), event.url()); + next_cursor = timestamp.to_string(); + + result.push(line); + } + None => break, + }; + } + + if !result.is_empty() { + result.push(format!("cursor: {next_cursor}")) + } + + txn.commit()?; + + Ok(Response::builder() + .status(StatusCode::OK) + .header(header::CONTENT_TYPE, "text/plain") + .body(Body::from(result.join("\n"))) + .unwrap()) +} diff --git a/pubky-homeserver/src/routes/public.rs b/pubky-homeserver/src/routes/public.rs index 4dc1bf6..cdfb0a9 100644 --- a/pubky-homeserver/src/routes/public.rs +++ b/pubky-homeserver/src/routes/public.rs @@ -62,7 +62,7 @@ pub async fn put( } pub async fn get( - State(mut state): State, + State(state): State, pubky: Pubky, path: EntryPath, Query(params): Query>, @@ -96,7 +96,7 @@ pub async fn get( return Ok(Response::builder() .status(StatusCode::OK) - .header(header::CONTENT_TYPE, "application/json") + .header(header::CONTENT_TYPE, "text/plain") .body(Body::from(vec.join("\n"))) .unwrap()); } diff --git a/pubky/src/shared/public.rs b/pubky/src/shared/public.rs index dabfdd8..10d8d91 100644 --- a/pubky/src/shared/public.rs +++ b/pubky/src/shared/public.rs @@ -100,7 +100,7 @@ mod tests { use pkarr::{mainline::Testnet, Keypair}; use pubky_homeserver::Homeserver; - use reqwest::StatusCode; + use reqwest::{Method, StatusCode}; #[tokio::test] async fn put_get_delete() { @@ -206,25 +206,24 @@ mod tests { client.signup(&keypair, &server.public_key()).await.unwrap(); + let pubky = keypair.public_key(); + let urls = vec![ - format!("pubky://{}/pub/a.wrong/a.txt", keypair.public_key()), - format!("pubky://{}/pub/example.com/a.txt", keypair.public_key()), - format!("pubky://{}/pub/example.com/b.txt", keypair.public_key()), - format!( - "pubky://{}/pub/example.com/cc-nested/z.txt", - keypair.public_key() - ), - format!("pubky://{}/pub/example.wrong/a.txt", keypair.public_key()), - format!("pubky://{}/pub/example.com/c.txt", keypair.public_key()), - format!("pubky://{}/pub/example.com/d.txt", keypair.public_key()), - format!("pubky://{}/pub/z.wrong/a.txt", keypair.public_key()), + format!("pubky://{pubky}/pub/a.wrong/a.txt"), + format!("pubky://{pubky}/pub/example.com/a.txt"), + format!("pubky://{pubky}/pub/example.com/b.txt"), + format!("pubky://{pubky}/pub/example.com/cc-nested/z.txt"), + format!("pubky://{pubky}/pub/example.wrong/a.txt"), + format!("pubky://{pubky}/pub/example.com/c.txt"), + format!("pubky://{pubky}/pub/example.com/d.txt"), + format!("pubky://{pubky}/pub/z.wrong/a.txt"), ]; for url in urls { client.put(url.as_str(), &[0]).await.unwrap(); } - let url = format!("pubky://{}/pub/example.com/extra", keypair.public_key()); + let url = format!("pubky://{pubky}/pub/example.com/extra"); let url = url.as_str(); { @@ -233,14 +232,11 @@ mod tests { assert_eq!( list, vec![ - format!("pubky://{}/pub/example.com/a.txt", keypair.public_key()), - format!("pubky://{}/pub/example.com/b.txt", keypair.public_key()), - format!("pubky://{}/pub/example.com/c.txt", keypair.public_key()), - format!( - "pubky://{}/pub/example.com/cc-nested/z.txt", - keypair.public_key() - ), - format!("pubky://{}/pub/example.com/d.txt", keypair.public_key()), + format!("pubky://{pubky}/pub/example.com/a.txt"), + format!("pubky://{pubky}/pub/example.com/b.txt"), + format!("pubky://{pubky}/pub/example.com/c.txt"), + format!("pubky://{pubky}/pub/example.com/cc-nested/z.txt"), + format!("pubky://{pubky}/pub/example.com/d.txt"), ], "normal list with no limit or cursor" ); @@ -252,8 +248,8 @@ mod tests { assert_eq!( list, vec![ - format!("pubky://{}/pub/example.com/a.txt", keypair.public_key()), - format!("pubky://{}/pub/example.com/b.txt", keypair.public_key()), + format!("pubky://{pubky}/pub/example.com/a.txt"), + format!("pubky://{pubky}/pub/example.com/b.txt"), ], "normal list with limit but no cursor" ); @@ -272,8 +268,8 @@ mod tests { assert_eq!( list, vec![ - format!("pubky://{}/pub/example.com/b.txt", keypair.public_key()), - format!("pubky://{}/pub/example.com/c.txt", keypair.public_key()), + format!("pubky://{pubky}/pub/example.com/b.txt"), + format!("pubky://{pubky}/pub/example.com/c.txt"), ], "normal list with limit and a file cursor" ); @@ -292,11 +288,8 @@ mod tests { assert_eq!( list, vec![ - format!( - "pubky://{}/pub/example.com/cc-nested/z.txt", - keypair.public_key() - ), - format!("pubky://{}/pub/example.com/d.txt", keypair.public_key()), + format!("pubky://{pubky}/pub/example.com/cc-nested/z.txt"), + format!("pubky://{pubky}/pub/example.com/d.txt"), ], "normal list with limit and a directory cursor" ); @@ -307,10 +300,7 @@ mod tests { .list(url) .unwrap() .limit(2) - .cursor(&format!( - "pubky://{}/pub/example.com/a.txt", - keypair.public_key() - )) + .cursor(&format!("pubky://{pubky}/pub/example.com/a.txt")) .send() .await .unwrap(); @@ -318,8 +308,8 @@ mod tests { assert_eq!( list, vec![ - format!("pubky://{}/pub/example.com/b.txt", keypair.public_key()), - format!("pubky://{}/pub/example.com/c.txt", keypair.public_key()), + format!("pubky://{pubky}/pub/example.com/b.txt"), + format!("pubky://{pubky}/pub/example.com/c.txt"), ], "normal list with limit and a full url cursor" ); @@ -338,8 +328,8 @@ mod tests { assert_eq!( list, vec![ - format!("pubky://{}/pub/example.com/b.txt", keypair.public_key()), - format!("pubky://{}/pub/example.com/c.txt", keypair.public_key()), + format!("pubky://{pubky}/pub/example.com/b.txt"), + format!("pubky://{pubky}/pub/example.com/c.txt"), ], "normal list with limit and a leading / cursor" ); @@ -357,14 +347,11 @@ mod tests { assert_eq!( list, vec![ - format!("pubky://{}/pub/example.com/d.txt", keypair.public_key()), - format!( - "pubky://{}/pub/example.com/cc-nested/z.txt", - keypair.public_key() - ), - format!("pubky://{}/pub/example.com/c.txt", keypair.public_key()), - format!("pubky://{}/pub/example.com/b.txt", keypair.public_key()), - format!("pubky://{}/pub/example.com/a.txt", keypair.public_key()), + format!("pubky://{pubky}/pub/example.com/d.txt"), + format!("pubky://{pubky}/pub/example.com/cc-nested/z.txt"), + format!("pubky://{pubky}/pub/example.com/c.txt"), + format!("pubky://{pubky}/pub/example.com/b.txt"), + format!("pubky://{pubky}/pub/example.com/a.txt"), ], "reverse list with no limit or cursor" ); @@ -383,11 +370,8 @@ mod tests { assert_eq!( list, vec![ - format!("pubky://{}/pub/example.com/d.txt", keypair.public_key()), - format!( - "pubky://{}/pub/example.com/cc-nested/z.txt", - keypair.public_key() - ), + format!("pubky://{pubky}/pub/example.com/d.txt"), + format!("pubky://{pubky}/pub/example.com/cc-nested/z.txt"), ], "reverse list with limit but no cursor" ); @@ -407,11 +391,8 @@ mod tests { assert_eq!( list, vec![ - format!( - "pubky://{}/pub/example.com/cc-nested/z.txt", - keypair.public_key() - ), - format!("pubky://{}/pub/example.com/c.txt", keypair.public_key()), + format!("pubky://{pubky}/pub/example.com/cc-nested/z.txt"), + format!("pubky://{pubky}/pub/example.com/c.txt"), ], "reverse list with limit and cursor" ); @@ -429,24 +410,26 @@ mod tests { client.signup(&keypair, &server.public_key()).await.unwrap(); + let pubky = keypair.public_key(); + let urls = vec![ - format!("pubky://{}/pub/a.com/a.txt", keypair.public_key()), - format!("pubky://{}/pub/example.com/a.txt", keypair.public_key()), - format!("pubky://{}/pub/example.com/b.txt", keypair.public_key()), - format!("pubky://{}/pub/example.com/c.txt", keypair.public_key()), - format!("pubky://{}/pub/example.com/d.txt", keypair.public_key()), - format!("pubky://{}/pub/example.con/d.txt", keypair.public_key()), - format!("pubky://{}/pub/example.con", keypair.public_key()), - format!("pubky://{}/pub/file", keypair.public_key()), - format!("pubky://{}/pub/file2", keypair.public_key()), - format!("pubky://{}/pub/z.com/a.txt", keypair.public_key()), + format!("pubky://{pubky}/pub/a.com/a.txt"), + format!("pubky://{pubky}/pub/example.com/a.txt"), + format!("pubky://{pubky}/pub/example.com/b.txt"), + format!("pubky://{pubky}/pub/example.com/c.txt"), + format!("pubky://{pubky}/pub/example.com/d.txt"), + format!("pubky://{pubky}/pub/example.con/d.txt"), + format!("pubky://{pubky}/pub/example.con"), + format!("pubky://{pubky}/pub/file"), + format!("pubky://{pubky}/pub/file2"), + format!("pubky://{pubky}/pub/z.com/a.txt"), ]; for url in urls { client.put(url.as_str(), &[0]).await.unwrap(); } - let url = format!("pubky://{}/pub/", keypair.public_key()); + let url = format!("pubky://{pubky}/pub/"); let url = url.as_str(); { @@ -461,13 +444,13 @@ mod tests { assert_eq!( list, vec![ - format!("pubky://{}/pub/a.com/", keypair.public_key()), - format!("pubky://{}/pub/example.com/", keypair.public_key()), - format!("pubky://{}/pub/example.con", keypair.public_key()), - format!("pubky://{}/pub/example.con/", keypair.public_key()), - format!("pubky://{}/pub/file", keypair.public_key()), - format!("pubky://{}/pub/file2", keypair.public_key()), - format!("pubky://{}/pub/z.com/", keypair.public_key()), + format!("pubky://{pubky}/pub/a.com/"), + format!("pubky://{pubky}/pub/example.com/"), + format!("pubky://{pubky}/pub/example.con"), + format!("pubky://{pubky}/pub/example.con/"), + format!("pubky://{pubky}/pub/file"), + format!("pubky://{pubky}/pub/file2"), + format!("pubky://{pubky}/pub/z.com/"), ], "normal list shallow" ); @@ -486,8 +469,8 @@ mod tests { assert_eq!( list, vec![ - format!("pubky://{}/pub/a.com/", keypair.public_key()), - format!("pubky://{}/pub/example.com/", keypair.public_key()), + format!("pubky://{pubky}/pub/a.com/"), + format!("pubky://{pubky}/pub/example.com/"), ], "normal list shallow with limit but no cursor" ); @@ -507,8 +490,8 @@ mod tests { assert_eq!( list, vec![ - format!("pubky://{}/pub/example.com/", keypair.public_key()), - format!("pubky://{}/pub/example.con", keypair.public_key()), + format!("pubky://{pubky}/pub/example.com/"), + format!("pubky://{pubky}/pub/example.con"), ], "normal list shallow with limit and a file cursor" ); @@ -528,9 +511,9 @@ mod tests { assert_eq!( list, vec![ - format!("pubky://{}/pub/example.con", keypair.public_key()), - format!("pubky://{}/pub/example.con/", keypair.public_key()), - format!("pubky://{}/pub/file", keypair.public_key()), + format!("pubky://{pubky}/pub/example.con"), + format!("pubky://{pubky}/pub/example.con/"), + format!("pubky://{pubky}/pub/file"), ], "normal list shallow with limit and a directory cursor" ); @@ -549,13 +532,13 @@ mod tests { assert_eq!( list, vec![ - format!("pubky://{}/pub/z.com/", keypair.public_key()), - format!("pubky://{}/pub/file2", keypair.public_key()), - format!("pubky://{}/pub/file", keypair.public_key()), - format!("pubky://{}/pub/example.con/", keypair.public_key()), - format!("pubky://{}/pub/example.con", keypair.public_key()), - format!("pubky://{}/pub/example.com/", keypair.public_key()), - format!("pubky://{}/pub/a.com/", keypair.public_key()), + format!("pubky://{pubky}/pub/z.com/"), + format!("pubky://{pubky}/pub/file2"), + format!("pubky://{pubky}/pub/file"), + format!("pubky://{pubky}/pub/example.con/"), + format!("pubky://{pubky}/pub/example.con"), + format!("pubky://{pubky}/pub/example.com/"), + format!("pubky://{pubky}/pub/a.com/"), ], "reverse list shallow" ); @@ -575,8 +558,8 @@ mod tests { assert_eq!( list, vec![ - format!("pubky://{}/pub/z.com/", keypair.public_key()), - format!("pubky://{}/pub/file2", keypair.public_key()), + format!("pubky://{pubky}/pub/z.com/"), + format!("pubky://{pubky}/pub/file2"), ], "reverse list shallow with limit but no cursor" ); @@ -597,8 +580,8 @@ mod tests { assert_eq!( list, vec![ - format!("pubky://{}/pub/file", keypair.public_key()), - format!("pubky://{}/pub/example.con/", keypair.public_key()), + format!("pubky://{pubky}/pub/file"), + format!("pubky://{pubky}/pub/example.con/"), ], "reverse list shallow with limit and a file cursor" ); @@ -619,11 +602,117 @@ mod tests { assert_eq!( list, vec![ - format!("pubky://{}/pub/example.con", keypair.public_key()), - format!("pubky://{}/pub/example.com/", keypair.public_key()), + format!("pubky://{pubky}/pub/example.con"), + format!("pubky://{pubky}/pub/example.com/"), ], "reverse list shallow with limit and a directory cursor" ); } } + + #[tokio::test] + async fn list_events() { + let testnet = Testnet::new(10); + let server = Homeserver::start_test(&testnet).await.unwrap(); + + let client = PubkyClient::test(&testnet); + + let keypair = Keypair::random(); + + client.signup(&keypair, &server.public_key()).await.unwrap(); + + let pubky = keypair.public_key(); + + let urls = vec![ + format!("pubky://{pubky}/pub/a.com/a.txt"), + format!("pubky://{pubky}/pub/example.com/a.txt"), + format!("pubky://{pubky}/pub/example.com/b.txt"), + format!("pubky://{pubky}/pub/example.com/c.txt"), + format!("pubky://{pubky}/pub/example.com/d.txt"), + format!("pubky://{pubky}/pub/example.con/d.txt"), + format!("pubky://{pubky}/pub/example.con"), + format!("pubky://{pubky}/pub/file"), + format!("pubky://{pubky}/pub/file2"), + format!("pubky://{pubky}/pub/z.com/a.txt"), + ]; + + for url in urls { + client.put(url.as_str(), &[0]).await.unwrap(); + client.delete(url.as_str()).await.unwrap(); + } + + let feed_url = format!("http://localhost:{}/events/", server.port()); + let feed_url = feed_url.as_str(); + + let client = PubkyClient::test(&testnet); + + let cursor; + + { + let response = client + .request( + Method::GET, + format!("{feed_url}?limit=10").as_str().try_into().unwrap(), + ) + .send() + .await + .unwrap(); + + let text = response.text().await.unwrap(); + let lines = text.split('\n').collect::>(); + + cursor = lines.last().unwrap().split(" ").last().unwrap().to_string(); + + assert_eq!( + lines, + vec![ + format!("PUT pubky://{pubky}/pub/a.com/a.txt"), + format!("DEL pubky://{pubky}/pub/a.com/a.txt"), + format!("PUT pubky://{pubky}/pub/example.com/a.txt"), + format!("DEL pubky://{pubky}/pub/example.com/a.txt"), + format!("PUT pubky://{pubky}/pub/example.com/b.txt"), + format!("DEL pubky://{pubky}/pub/example.com/b.txt"), + format!("PUT pubky://{pubky}/pub/example.com/c.txt"), + format!("DEL pubky://{pubky}/pub/example.com/c.txt"), + format!("PUT pubky://{pubky}/pub/example.com/d.txt"), + format!("DEL pubky://{pubky}/pub/example.com/d.txt"), + format!("cursor: {cursor}",) + ] + ); + } + + { + let response = client + .request( + Method::GET, + format!("{feed_url}?limit=10&cursor={cursor}") + .as_str() + .try_into() + .unwrap(), + ) + .send() + .await + .unwrap(); + + let text = response.text().await.unwrap(); + let lines = text.split('\n').collect::>(); + + assert_eq!( + lines, + vec![ + format!("PUT pubky://{pubky}/pub/example.con/d.txt"), + format!("DEL pubky://{pubky}/pub/example.con/d.txt"), + format!("PUT pubky://{pubky}/pub/example.con"), + format!("DEL pubky://{pubky}/pub/example.con"), + format!("PUT pubky://{pubky}/pub/file"), + format!("DEL pubky://{pubky}/pub/file"), + format!("PUT pubky://{pubky}/pub/file2"), + format!("DEL pubky://{pubky}/pub/file2"), + format!("PUT pubky://{pubky}/pub/z.com/a.txt"), + format!("DEL pubky://{pubky}/pub/z.com/a.txt"), + lines.last().unwrap().to_string() + ] + ) + } + } }