diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index bc546f6a86af..f4bc49c01317 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -37,7 +37,7 @@ jobs: - name: Build Rust docs run: | - cargo doc -p acter -p acter-core -p matrix-sdk-store-file-event-cache + cargo doc -p acter -p acter-matrix -p acter-core -p matrix-sdk-store-file-event-cache echo --- Moving docs mv target/doc/ docs/api/main/rust diff --git a/.github/workflows/check-native.yml b/.github/workflows/check-native.yml index 0b1973e2d3e2..d4e4c55dd2c7 100644 --- a/.github/workflows/check-native.yml +++ b/.github/workflows/check-native.yml @@ -23,6 +23,9 @@ jobs: - name: Core package: acter-core clippy_extras: --tests + - name: Matrix Core + package: acter-matrix + clippy_extras: --tests - name: SDK package: acter needs_flutter: true @@ -51,7 +54,8 @@ jobs: include: - name: Core package: acter-core - clippy_extras: --tests + - name: Matrix Core + package: acter-matrix # - name: file-event-cache # package: matrix-sdk-store-file-event-cache steps: @@ -81,9 +85,13 @@ jobs: strategy: matrix: include: - - name: SDK + - name: Matrix SDK package: acter needs_flutter: true + - name: Core on WASM + package: acter-core + cargo_flags: --target wasm32-unknown-unknown + needs_flutter: false steps: - uses: actions/checkout@v4 - name: Install rust toolchain @@ -94,7 +102,7 @@ jobs: flutter-version: '3.32' channel: 'stable' - name: Release Build - run: cargo build -p ${{matrix.package}} --release + run: cargo build -p ${{matrix.package}} --release ${{matrix.cargo_flags}} ffigen: diff --git a/Cargo.lock b/Cargo.lock index eb444e20c59d..745e18bf17c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,6 +18,7 @@ dependencies = [ name = "acter" version = "0.1.0" dependencies = [ + "acter-core", "acter-matrix", "android_logger", "anyhow", @@ -93,11 +94,26 @@ dependencies = [ [[package]] name = "acter-core" version = "0.1.0" +dependencies = [ + "anyhow", + "async-recursion", + "env_logger", + "eyeball-im", + "futures", + "scc", + "serde", + "serde_json", + "strum", + "thiserror 2.0.12", + "tokio", + "tracing", +] [[package]] name = "acter-matrix" version = "0.1.0" dependencies = [ + "acter-core", "anyhow", "async-recursion", "async-stream", @@ -136,6 +152,7 @@ name = "acter-test" version = "0.1.0" dependencies = [ "acter", + "acter-core", "acter-matrix", "anyhow", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 7550a118f7b9..8f451c99da36 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ default-members = ["native/acter"] [workspace.package] -rust-version = "1.85" +rust-version = "1.88" [workspace.dependencies.ffi-gen] git = "https://github.com/acterglobal/ffi-gen" @@ -101,6 +101,8 @@ env_logger = "0.11.7" urlencoding = "2.1.3" eyeball-im = "0.7.0" +tracing = { version = "0.1.40", features = ["log"] } + [profile.release] panic = "unwind" opt-level = 2 diff --git a/native/acter/Cargo.toml b/native/acter/Cargo.toml index 7b0e2c84ffe3..f38869167dc0 100644 --- a/native/acter/Cargo.toml +++ b/native/acter/Cargo.toml @@ -2,7 +2,7 @@ name = "acter" version = "0.1.0" edition = "2021" -rust-version = "1.81" +rust-version = "1.88" description = "SDK of the acter app" homepage = "https://acter.global" repository = "https://github.com/acterglobal/a3" @@ -81,6 +81,10 @@ ruma-common = { workspace = true, features = ["client"] } path = "../matrix" features = ["templates", "client"] +[dependencies.acter-core] +path = "../core" +features = ["strum"] + [dependencies.matrix-sdk] workspace = true features = [ diff --git a/native/acter/src/api/activities.rs b/native/acter/src/api/activities.rs index adf5d02fcee4..956f2cbea8ac 100644 --- a/native/acter/src/api/activities.rs +++ b/native/acter/src/api/activities.rs @@ -43,7 +43,7 @@ impl Activity { } pub fn origin_server_ts(&self) -> u64 { - self.inner.event_meta().origin_server_ts.get().into() + self.inner.event_meta().timestamp.get().into() } pub fn room_id_str(&self) -> String { diff --git a/native/acter/src/api/attachments.rs b/native/acter/src/api/attachments.rs index 4a2407b8bead..35b6628025e4 100644 --- a/native/acter/src/api/attachments.rs +++ b/native/acter/src/api/attachments.rs @@ -87,7 +87,7 @@ impl Attachment { } pub fn origin_server_ts(&self) -> u64 { - self.inner.meta.origin_server_ts.get().into() + self.inner.meta.timestamp.get().into() } pub fn ref_details(&self) -> Option { @@ -141,7 +141,7 @@ impl Attachment { .and_then(|info| info.mimetype.as_deref()) .and_then(|mimetype| { mime2ext::mime2ext(mimetype).map(|ext| { - format!("{}-thumbnail.{}", evt_id, ext) + format!("{evt_id}-thumbnail.{ext}") }) }); (request, filename) @@ -161,7 +161,7 @@ impl Attachment { .and_then(|info| info.mimetype.as_deref()) .and_then(|mimetype| { mime2ext::mime2ext(mimetype).map(|ext| { - format!("{}-thumbnail.{}", evt_id, ext) + format!("{evt_id}-thumbnail.{ext}") }) }); (request, filename) @@ -181,7 +181,7 @@ impl Attachment { .and_then(|info| info.mimetype.as_deref()) .and_then(|mimetype| { mime2ext::mime2ext(mimetype).map(|ext| { - format!("{}-thumbnail.{}", evt_id, ext) + format!("{evt_id}-thumbnail.{ext}") }) }); (request, filename) @@ -202,7 +202,7 @@ impl Attachment { .and_then(|info| info.mimetype.as_deref()) .and_then(|mimetype| { mime2ext::mime2ext(mimetype).map(|ext| { - format!("{}-thumbnail.{}", evt_id, ext) + format!("{evt_id}-thumbnail.{ext}") }) }); (request, filename) @@ -221,7 +221,7 @@ impl Attachment { .and_then(|info| info.mimetype.as_deref()) .and_then(|mimetype| { mime2ext::mime2ext(mimetype) - .map(|ext| format!("{}.{}", evt_id, ext)) + .map(|ext| format!("{evt_id}.{ext}")) }); (Some(request), filename) } @@ -236,7 +236,7 @@ impl Attachment { .and_then(|info| info.mimetype.as_deref()) .and_then(|mimetype| { mime2ext::mime2ext(mimetype) - .map(|ext| format!("{}.{}", evt_id, ext)) + .map(|ext| format!("{evt_id}.{ext}")) }); (Some(request), filename) } @@ -251,7 +251,7 @@ impl Attachment { .and_then(|info| info.mimetype.as_deref()) .and_then(|mimetype| { mime2ext::mime2ext(mimetype) - .map(|ext| format!("{}.{}",evt_id, ext)) + .map(|ext| format!("{evt_id}.{ext}")) }); (Some(request), filename) } @@ -266,7 +266,7 @@ impl Attachment { .and_then(|info| info.mimetype.as_deref()) .and_then(|mimetype| { mime2ext::mime2ext(mimetype) - .map(|ext| format!("{}.{}", evt_id, ext)) + .map(|ext| format!("{evt_id}.{ext}")) }); (Some(request), filename) } diff --git a/native/acter/src/api/client.rs b/native/acter/src/api/client.rs index 8a454606b86a..604887220b99 100644 --- a/native/acter/src/api/client.rs +++ b/native/acter/src/api/client.rs @@ -3,7 +3,8 @@ use acter_matrix::{ executor::Executor, models::AnyActerModel, referencing::{ - ExecuteReference, IndexKey, ModelParam, ObjectListIndex, RoomParam, SectionIndex, + ExecuteReference, IndexKey, IntoExecuteReference, ModelParam, ObjectListIndex, RoomParam, + SectionIndex, }, store::Store, templates::Engine, @@ -400,7 +401,7 @@ impl Client { pub fn subscribe_model_stream(&self, key: String) -> Result> { let model_id = EventId::parse(key)?; - Ok(BroadcastStream::new(self.subscribe(model_id)).map(|_| true)) + Ok(BroadcastStream::new(self.subscribe(model_id.to_owned())).map(|_| true)) } pub fn subscribe_model_param_stream( @@ -433,7 +434,7 @@ impl Client { pub fn subscribe_room_stream(&self, key: String) -> Result> { let model_id = RoomId::parse(key)?; - Ok(BroadcastStream::new(self.subscribe(model_id)).map(|_| true)) + Ok(BroadcastStream::new(self.subscribe(model_id.to_owned())).map(|_| true)) } pub fn subscribe_room_param_stream( @@ -486,8 +487,8 @@ impl Client { ) } - pub fn subscribe>(&self, key: K) -> Receiver<()> { - self.executor().subscribe(key) + pub fn subscribe(&self, key: K) -> Receiver<()> { + self.executor().subscribe(key.into()) } pub async fn wait_for(&self, key: String, timeout: Option) -> Result { diff --git a/native/acter/src/api/client/sync.rs b/native/acter/src/api/client/sync.rs index 3d30e938b82d..14bef4009707 100644 --- a/native/acter/src/api/client/sync.rs +++ b/native/acter/src/api/client/sync.rs @@ -1,6 +1,9 @@ use acter_matrix::{ - events::AnySyncActerEvent, executor::Executor, models::AnyActerModel, - referencing::ExecuteReference, spaces::is_acter_space, + events::AnySyncActerEvent, + executor::Executor, + models::{event_meta_for_redacted_source, AnyActerModel, EventMeta}, + referencing::ExecuteReference, + spaces::is_acter_space, }; use anyhow::Result; use core::time::Duration; @@ -235,7 +238,12 @@ impl Client { if let RoomRedactionEvent::Original(t) = ev.into_full_event(room_id.to_owned()) { trace!(?room_id, "received redaction"); - if let Err(error) = executor.live_redact(t).await { + let Some(meta) = event_meta_for_redacted_source(&t) else { + warn!(?room_id, "redaction event has no meta"); + return; + }; + + if let Err(error) = executor.live_redact(meta, Some(t.into())).await { error!(?room_id, ?error, "redaction failed"); } } else { diff --git a/native/acter/src/api/comments.rs b/native/acter/src/api/comments.rs index b3775068c499..d33f4583e77c 100644 --- a/native/acter/src/api/comments.rs +++ b/native/acter/src/api/comments.rs @@ -107,7 +107,7 @@ impl Comment { } pub fn origin_server_ts(&self) -> u64 { - self.inner.meta.origin_server_ts.get().into() + self.inner.meta.timestamp.get().into() } pub fn msg_content(&self) -> MsgContent { diff --git a/native/acter/src/api/invitations/manager.rs b/native/acter/src/api/invitations/manager.rs index 961154622b27..fb95fbc12eb3 100644 --- a/native/acter/src/api/invitations/manager.rs +++ b/native/acter/src/api/invitations/manager.rs @@ -62,7 +62,7 @@ impl InvitationsManager { // Process each room invitation match RoomInvitation::parse(&core, room).await { Ok(invitation) => invites.push(invitation), - Err(err) => log::error!("Failed to parse room invitation: {}", err), + Err(err) => log::error!("Failed to parse room invitation: {err}"), } } invites diff --git a/native/acter/src/api/news.rs b/native/acter/src/api/news.rs index affa94d1f783..8fbaebcf1343 100644 --- a/native/acter/src/api/news.rs +++ b/native/acter/src/api/news.rs @@ -444,7 +444,7 @@ impl NewsEntry { } pub fn origin_server_ts(&self) -> u64 { - self.content.meta.origin_server_ts.get().into() + self.content.meta.timestamp.get().into() } pub async fn ref_details(&self) -> Result { diff --git a/native/acter/src/api/reactions.rs b/native/acter/src/api/reactions.rs index 8666212838ad..0e6bcdee69bb 100644 --- a/native/acter/src/api/reactions.rs +++ b/native/acter/src/api/reactions.rs @@ -55,7 +55,7 @@ impl Reaction { } pub fn origin_server_ts(&self) -> u64 { - self.inner.meta.origin_server_ts.get().into() + self.inner.meta.timestamp.get().into() } pub fn relates_to(&self) -> String { diff --git a/native/acter/src/api/rsvp.rs b/native/acter/src/api/rsvp.rs index c4cb218ea541..4d2c706ba2b0 100644 --- a/native/acter/src/api/rsvp.rs +++ b/native/acter/src/api/rsvp.rs @@ -195,7 +195,7 @@ impl Rsvp { } pub fn origin_server_ts(&self) -> u64 { - self.inner.meta.origin_server_ts.get().into() + self.inner.meta.timestamp.get().into() } pub fn status(&self) -> String { diff --git a/native/acter/src/api/stories.rs b/native/acter/src/api/stories.rs index befd5c29ab91..587a67a8a580 100644 --- a/native/acter/src/api/stories.rs +++ b/native/acter/src/api/stories.rs @@ -418,7 +418,7 @@ impl Story { } pub fn origin_server_ts(&self) -> u64 { - self.content.meta.origin_server_ts.get().into() + self.content.meta.timestamp.get().into() } pub async fn ref_details(&self) -> Result { diff --git a/native/acter/src/platform/native.rs b/native/acter/src/platform/native.rs index 47871a560964..ef8a7b8048fe 100644 --- a/native/acter/src/platform/native.rs +++ b/native/acter/src/platform/native.rs @@ -282,10 +282,10 @@ impl Display for MakeStoreConfigError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { MakeStoreConfigError::OpenStoreError(i) => { - write!(f, "MakeStoreConfigError::OpenStoreError {}", i) + write!(f, "MakeStoreConfigError::OpenStoreError {i}") } MakeStoreConfigError::EventCacheStoreError(i) => { - write!(f, "MakeStoreConfigError::EventCacheStoreError {}", i) + write!(f, "MakeStoreConfigError::EventCacheStoreError {i}") } } } diff --git a/native/core/Cargo.toml b/native/core/Cargo.toml index 20c1cd198f44..d64930ade53c 100644 --- a/native/core/Cargo.toml +++ b/native/core/Cargo.toml @@ -9,4 +9,26 @@ repository = "https://github.com/acterglobal/a3" license-file = "../../LICENSE.txt" rust-version.workspace = true +[features] +testing = [ "tokio/full", "thiserror"] +strum = ["dep:strum"] + [dependencies] +strum = { workspace = true, optional = true } +serde = { version = "1", features = ["derive"] } +tracing = { workspace = true } +async-recursion = "1" +eyeball-im = { workspace = true } +futures = { version = "0.3.30" } +scc = "2.3.4" +tokio = { version = "1" } + +# for testing-feature only +thiserror = { version = "2", optional = true } + +[dev-dependencies] +serde_json = "1" +tokio = { version = "1", features = ["full"] } +thiserror = "2" +env_logger = "0.11" +anyhow = "1" \ No newline at end of file diff --git a/native/core/src/execution.rs b/native/core/src/execution.rs new file mode 100644 index 000000000000..1782d2d25c96 --- /dev/null +++ b/native/core/src/execution.rs @@ -0,0 +1,475 @@ +use async_recursion::async_recursion; +use tracing::trace; + +use crate::{ + referencing::ExecuteReference, + traits::{ModelT, StoreT, TypeConfig}, +}; + +#[async_recursion] +pub async fn transition_tree( + store: &S, + parents: I, + model: &M, +) -> Result, C::Error> +where + C: TypeConfig, + M: ModelT + Sync, + S: StoreT + Sync, + S::Model: ModelT, + I: Iterator + Send, +{ + let mut models = vec![]; + for p in parents { + let mut parent = store.get(&p).await?; + if parent.transition(model)? { + if let Some(grandparents) = parent.belongs_to() { + let mut parent_models = transition_tree::< + C, + M, + S, + std::vec::IntoIter<::ObjectId>, + >(store, grandparents.into_iter(), &parent) + .await?; + if !parent_models.is_empty() { + models.append(&mut parent_models); + } + } + models.push(parent); + } + } + Ok(models) +} + +pub async fn default_model_execute( + store: &S, + model: M, +) -> Result>, C::Error> +where + M: ModelT + Sync, + S: StoreT + Sync, + S::Model: ModelT, +{ + trace!(object_id=?model.object_id(), ?model, "handling"); + let Some(belongs_to) = model.belongs_to() else { + trace!(object_id=?model.object_id(), "saving simple model"); + return store.save(model).await; + }; + + trace!(object_id=?model.object_id(), ?belongs_to, "transitioning tree"); + let mut models = transition_tree::::ObjectId>>( + store, + belongs_to.into_iter(), + &model, + ) + .await?; + models.push(model); + store.save_many(models.into_iter()).await +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::mocks::{MockError, MockModel, MockObjectId, MockStore, MockTypeConfig}; + + #[tokio::test] + async fn test_transition_tree_simple_no_transitions() { + let store = MockStore::new(); + + // Create a parent model + let parent = MockModel::new("parent1", 10, None); + store.insert(parent.clone()).await; + + // Create a child model that won't trigger transitions + let child = MockModel::new("child1", 5, Some(vec!["parent1"])); + + let parents = vec![MockObjectId("parent1".to_string())]; + let result = transition_tree::( + &store, + parents.into_iter(), + &child, + ) + .await; + + assert!(result.is_ok()); + let models = result.unwrap(); + assert_eq!(models.len(), 0); // No transitions occurred + } + + #[tokio::test] + async fn test_transition_tree_single_transition() { + let store = MockStore::new(); + + // Create a parent model + let parent = MockModel::new("parent1", 5, None); + store.insert(parent.clone()).await; + + // Create a child model that will trigger transition + let child = MockModel::new("child1", 10, Some(vec!["parent1"])); + + let parents = vec![MockObjectId("parent1".to_string())]; + let result = transition_tree::( + &store, + parents.into_iter(), + &child, + ) + .await; + + assert!(result.is_ok()); + let models = result.unwrap(); + assert_eq!(models.len(), 1); + + let transitioned_parent = &models[0]; + assert_eq!(transitioned_parent.value, 10); // Should have been updated + assert_eq!(transitioned_parent.transition_count, 1); + } + + #[tokio::test] + async fn test_transition_tree_multiple_parents() { + let store = MockStore::new(); + + // Create parent models + let parent1 = MockModel::new("parent1", 5, None); + let parent2 = MockModel::new("parent2", 3, None); + store.insert(parent1.clone()).await; + store.insert(parent2.clone()).await; + + // Create a child model that will trigger transitions + let child = MockModel::new("child1", 10, Some(vec!["parent1", "parent2"])); + + let parents = vec![ + MockObjectId("parent1".to_string()), + MockObjectId("parent2".to_string()), + ]; + let result = transition_tree::( + &store, + parents.into_iter(), + &child, + ) + .await; + + assert!(result.is_ok()); + let models = result.unwrap(); + assert_eq!(models.len(), 2); + + // Both parents should have been updated + let parent1_updated = models.iter().find(|m| m.id.0 == "parent1").unwrap(); + let parent2_updated = models.iter().find(|m| m.id.0 == "parent2").unwrap(); + + assert_eq!(parent1_updated.value, 10); + assert_eq!(parent1_updated.transition_count, 1); + assert_eq!(parent2_updated.value, 10); + assert_eq!(parent2_updated.transition_count, 1); + } + + #[tokio::test] + async fn test_transition_tree_nested_hierarchy() { + let store = MockStore::new(); + + // Create a grandparent model + let grandparent = MockModel::new("grandparent", 2, None); + store.insert(grandparent.clone()).await; + + // Create a parent model that belongs to grandparent + let parent = MockModel::new("parent", 5, Some(vec!["grandparent"])); + store.insert(parent.clone()).await; + + // Create a child model that will trigger transitions up the chain + let child = MockModel::new("child", 15, Some(vec!["parent"])); + + let parents = vec![MockObjectId("parent".to_string())]; + let result = transition_tree::( + &store, + parents.into_iter(), + &child, + ) + .await; + + assert!(result.is_ok()); + let models = result.unwrap(); + assert_eq!(models.len(), 2); // Both parent and grandparent should transition + + // Check that parent was updated + let parent_updated = models.iter().find(|m| m.id.0 == "parent").unwrap(); + assert_eq!(parent_updated.value, 15); + assert_eq!(parent_updated.transition_count, 1); + + // Check that grandparent was also updated (through recursive call) + let grandparent_updated = models.iter().find(|m| m.id.0 == "grandparent").unwrap(); + assert_eq!(grandparent_updated.value, 15); + assert_eq!(grandparent_updated.transition_count, 1); + } + + #[tokio::test] + async fn test_transition_tree_no_parents() { + let store = MockStore::new(); + + let child = MockModel::new("child1", 10, None); + let parents: Vec = vec![]; + + let result = transition_tree::( + &store, + parents.into_iter(), + &child, + ) + .await; + + assert!(result.is_ok()); + let models = result.unwrap(); + assert_eq!(models.len(), 0); + } + + #[tokio::test] + async fn test_transition_tree_parent_not_found() { + let store = MockStore::new(); + + let child = MockModel::new("child1", 10, Some(vec!["nonexistent"])); + let parents = vec![MockObjectId("nonexistent".to_string())]; + + let result = transition_tree::( + &store, + parents.into_iter(), + &child, + ) + .await; + + assert!(result.is_err()); + assert_eq!( + result.unwrap_err(), + MockError::NotFound("nonexistent".to_string()) + ); + } + + #[tokio::test] + async fn test_default_model_execute_simple_model() { + let store = MockStore::new(); + + // Create a model with no parents + let model = MockModel::new("simple", 10, None); + + let result = + default_model_execute::(&store, model).await; + + assert!(result.is_ok()); + let references = result.unwrap(); + assert_eq!(references.len(), 1); + assert!(matches!(references[0], ExecuteReference::Model(_))); + } + + #[tokio::test] + async fn test_default_model_execute_with_parents() { + let store = MockStore::new(); + + // Create parent models + let parent1 = MockModel::new("parent1", 5, None); + let parent2 = MockModel::new("parent2", 3, None); + store.insert(parent1.clone()).await; + store.insert(parent2.clone()).await; + + // Create a model with parents that will trigger transitions + let model = MockModel::new("child", 15, Some(vec!["parent1", "parent2"])); + + let result = + default_model_execute::(&store, model).await; + + assert!(result.is_ok()); + let references = result.unwrap(); + assert_eq!(references.len(), 3); // child + 2 updated parents + + // Verify that parents were updated in the store + let updated_parent1 = store + .get_model(&MockObjectId("parent1".to_string())) + .await + .unwrap(); + let updated_parent2 = store + .get_model(&MockObjectId("parent2".to_string())) + .await + .unwrap(); + + assert_eq!(updated_parent1.value, 15); + assert_eq!(updated_parent1.transition_count, 1); + assert_eq!(updated_parent2.value, 15); + assert_eq!(updated_parent2.transition_count, 1); + } + + #[tokio::test] + async fn test_default_model_execute_nested_hierarchy() { + let store = MockStore::new(); + + // Create a grandparent + let grandparent = MockModel::new("grandparent", 2, None); + store.insert(grandparent.clone()).await; + + // Create a parent that belongs to grandparent + let parent = MockModel::new("parent", 5, Some(vec!["grandparent"])); + store.insert(parent.clone()).await; + + // Create a child that will trigger the full chain + let child = MockModel::new("child", 20, Some(vec!["parent"])); + + let result = + default_model_execute::(&store, child).await; + + assert!(result.is_ok()); + let references = result.unwrap(); + assert_eq!(references.len(), 3); // child + parent + grandparent + + // Verify the entire chain was updated + let updated_grandparent = store + .get_model(&MockObjectId("grandparent".to_string())) + .await + .unwrap(); + let updated_parent = store + .get_model(&MockObjectId("parent".to_string())) + .await + .unwrap(); + + assert_eq!(updated_grandparent.value, 20); + assert_eq!(updated_grandparent.transition_count, 1); + assert_eq!(updated_parent.value, 20); + assert_eq!(updated_parent.transition_count, 1); + } + + #[tokio::test] + async fn test_default_model_execute_partial_transitions() { + let store = MockStore::new(); + + // Create parent models with different values + let parent1 = MockModel::new("parent1", 5, None); // Will transition + let parent2 = MockModel::new("parent2", 25, None); // Won't transition (higher value) + store.insert(parent1.clone()).await; + store.insert(parent2.clone()).await; + + // Create a model with value between the parents + let model = MockModel::new("child", 15, Some(vec!["parent1", "parent2"])); + + let result = + default_model_execute::(&store, model).await; + + assert!(result.is_ok()); + let references = result.unwrap(); + assert_eq!(references.len(), 2); // child + only parent1 (parent2 didn't transition) + + // Verify only parent1 was updated + let updated_parent1 = store + .get_model(&MockObjectId("parent1".to_string())) + .await + .unwrap(); + let updated_parent2 = store + .get_model(&MockObjectId("parent2".to_string())) + .await + .unwrap(); + + assert_eq!(updated_parent1.value, 15); + assert_eq!(updated_parent1.transition_count, 1); + assert_eq!(updated_parent2.value, 25); // Unchanged + assert_eq!(updated_parent2.transition_count, 0); + } + + #[tokio::test] + async fn test_default_model_execute_parent_not_found() { + let store = MockStore::new(); + + // Create a model with a non-existent parent + let model = MockModel::new("child", 15, Some(vec!["nonexistent"])); + + let result = + default_model_execute::(&store, model).await; + + assert!(result.is_err()); + assert_eq!( + result.unwrap_err(), + MockError::NotFound("nonexistent".to_string()) + ); + } + + #[tokio::test] + async fn test_transition_tree_multiple_transitions_same_parent() { + let store = MockStore::new(); + + // Create a parent model + let parent = MockModel::new("parent", 5, None); + store.insert(parent.clone()).await; + + // Create multiple children that will trigger transitions + let child1 = MockModel::new("child1", 10, Some(vec!["parent"])); + let child2 = MockModel::new("child2", 25, Some(vec!["parent"])); + + // First transition + let parents = vec![MockObjectId("parent".to_string())]; + let result1 = transition_tree::( + &store, + parents.clone().into_iter(), + &child1, + ) + .await; + + assert!(result1.is_ok()); + let models1 = result1.unwrap(); + assert_eq!(models1.len(), 1); + let current_parent = models1.into_iter().next().unwrap(); + assert_eq!(current_parent.value, 10); + assert_eq!(current_parent.transition_count, 1); + + // this updated the parent, save it to the store for further processing + store.save(current_parent).await.unwrap(); + + // Second transition should update the already-transitioned parent + let result2 = transition_tree::( + &store, + parents.clone().into_iter(), + &child2, + ) + .await; + + assert!(result2.is_ok()); + let models2 = result2.unwrap(); + assert_eq!(models2.len(), 1); + + let final_parent = &models2[0]; + assert_eq!(final_parent.value, 25); // Should have been updated + assert_eq!(final_parent.transition_count, 2); // Should have transitioned twice + } + + #[tokio::test] + async fn test_transition_tree_complex_hierarchy() { + let store = MockStore::new(); + + // Create a complex hierarchy: root -> level1 -> level2 -> level3 + let root = MockModel::new("root", 1, None); + let level1 = MockModel::new("level1", 2, Some(vec!["root"])); + let level2 = MockModel::new("level2", 3, Some(vec!["level1"])); + + store.insert(root.clone()).await; + store.insert(level1.clone()).await; + store.insert(level2.clone()).await; + + // Create a leaf node that will trigger the entire chain + let leaf = MockModel::new("leaf", 100, Some(vec!["level2"])); + + let parents = vec![MockObjectId("level2".to_string())]; + let result = transition_tree::( + &store, + parents.into_iter(), + &leaf, + ) + .await; + + assert!(result.is_ok()); + let models = result.unwrap(); + assert_eq!(models.len(), 3); // level2, level1, root + + // Verify all levels were updated + let level2_updated = models.iter().find(|m| m.id.0 == "level2").unwrap(); + let level1_updated = models.iter().find(|m| m.id.0 == "level1").unwrap(); + let root_updated = models.iter().find(|m| m.id.0 == "root").unwrap(); + + assert_eq!(level2_updated.value, 100); + assert_eq!(level2_updated.transition_count, 1); + assert_eq!(level1_updated.value, 100); + assert_eq!(level1_updated.transition_count, 1); + assert_eq!(root_updated.value, 100); + assert_eq!(root_updated.transition_count, 1); + } +} diff --git a/native/core/src/executor.rs b/native/core/src/executor.rs new file mode 100644 index 000000000000..e683513d75ef --- /dev/null +++ b/native/core/src/executor.rs @@ -0,0 +1,186 @@ +use scc::hash_map::{Entry, HashMap}; +use std::sync::Arc; +use tokio::sync::broadcast::{Receiver, Sender, channel}; +use tracing::{error, info, trace, trace_span}; + +use crate::{ + meta::EventMeta, + referencing::ExecuteReference, + traits::{ModelT, StoreError, StoreT, TypeConfig}, +}; + +#[derive(Clone, Debug)] +pub struct Executor> { + store: S, + notifiers: Arc, Sender<()>>>, +} + +impl + 'static> Executor { + pub fn new(store: S) -> Self { + Executor { + store, + notifiers: Default::default(), + } + } + + pub fn store(&self) -> &S { + &self.store + } + + pub fn subscribe>>(&self, key: K) -> Receiver<()> { + match self.notifiers.entry(key.into()) { + Entry::Occupied(mut o) => { + let sender = o.get_mut(); + if sender.receiver_count() == 0 { + // replace the existing channel to reopen + let (sender, receiver) = channel(1); + o.insert(sender); + receiver + } else { + sender.subscribe() + } + } + Entry::Vacant(v) => { + let (sender, receiver) = channel(1); + v.insert_entry(sender); + receiver + } + } + } + + pub async fn wait_for(&self, key: C::ObjectId) -> Result { + let mut subscribe = self.subscribe(ExecuteReference::Model(key.clone())); + let Ok(model) = self.store.get(&key).await else { + if let Err(e) = subscribe.recv().await { + error!(event_id=?key, "Receiving pong failed: {e}"); + } + return self.store.get(&key).await; + }; + + Ok(model) + } + + pub fn notify(&self, mut keys: Vec>) -> u32 { + let mut counter = 0u32; + keys.dedup(); + trace!(?keys, "notify"); + for key in keys { + let span = trace_span!("Asked to notify", key = ?key); + let _enter = span.enter(); + if let Entry::Occupied(o) = self.notifiers.entry(key) { + let v = o.get(); + if v.receiver_count() == 0 { + trace!("No listeners. removing"); + let _ = o.remove(); + continue; + } + trace!("Broadcasting"); + if let Err(error) = v.send(()) { + trace!(?error, "Notifying failed. No receivers. Clearing"); + // we have overflow activated, this only fails because it has been closed + let _ = o.remove(); + } else { + counter = counter.saturating_add(1); + } + } else { + trace!("No one to notify"); + } + } + counter + } + + pub async fn handle(&self, model: S::Model) -> Result<(), C::Error> { + let object_id = model.object_id(); + trace!(?object_id, ?model, "handle"); + match model.execute(&self.store).await { + Err(error) => { + error!(?object_id, ?error, "handling failed"); + Err(error) + } + Ok(keys) => { + trace!(?object_id, "handling done"); + self.notify(keys); + Ok(()) + } + } + } + + pub async fn clear_room(&self, room_id: &C::RoomId) -> Result<(), C::Error> { + let keys = self.store.clear_room(room_id).await?; + self.notify(keys); + Ok(()) + } + + pub async fn redact( + &self, + model_type: C::ModelType, + event_meta: EventMeta, + reason: Option, + ) -> Result<(), C::Error> { + let event_id = event_meta.event_id.clone(); + trace!(event_id=?event_id, ?model_type, "asked to redact"); + + match self.store.get(&event_id).await { + Ok(model) if model.is_redacted() => { + info!(?event_id, "live redacted: Already redacted"); + } + Ok(model) => { + trace!("previous model found. overwriting"); + self.notify(model.redact(&self.store, reason).await?); + } + Err(error) if error.is_not_found() => { + trace!("no model found, storing redaction model"); + self.notify(vec![ExecuteReference::Model(event_id)]); + } + Err(error) => return Err(error), + } + Ok(()) + } + + pub async fn live_redact( + &self, + meta: EventMeta, + reason: Option, + ) -> Result<(), C::Error> { + let event_id = meta.event_id.clone(); + + match self.store.get(&event_id).await { + Ok(model) if model.is_redacted() => { + info!(?event_id, "live redacted: Already redacted"); + } + Ok(model) => { + trace!("live redacted: model found"); + let keys = model.redact(&self.store, reason).await?; + info!(?event_id, "live redacted: {:?}", &keys); + self.notify(keys); + } + Err(error) if error.is_not_found() => { + info!(?event_id, "live redaction: not found"); + self.notify(vec![ExecuteReference::Model(event_id)]); + } + Err(error) => return Err(error), + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + + use crate::mocks::{MockStore, MockTypeConfig}; + + use super::*; + + async fn fresh_executor() -> Result> { + let store = MockStore::new(); + Ok(Executor::new(store)) + } + use anyhow::Result; + + #[tokio::test] + async fn smoke_test() -> Result<()> { + let _ = env_logger::try_init(); + let _ = fresh_executor().await?; + Ok(()) + } +} diff --git a/native/core/src/lib.rs b/native/core/src/lib.rs index b93cf3ffd9cc..82ed1bfe645a 100644 --- a/native/core/src/lib.rs +++ b/native/core/src/lib.rs @@ -1,14 +1,13 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right -} +#![feature(trait_alias)] +#![feature(associated_type_defaults)] -#[cfg(test)] -mod tests { - use super::*; +pub mod execution; +pub mod meta; +pub mod referencing; +pub mod store; +pub mod traits; - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} +pub mod executor; + +#[cfg(any(test, feature = "testing"))] +pub mod mocks; diff --git a/native/core/src/meta.rs b/native/core/src/meta.rs new file mode 100644 index 000000000000..59a54e5b8406 --- /dev/null +++ b/native/core/src/meta.rs @@ -0,0 +1,231 @@ +use crate::traits::TypeConfig; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Eq))] +pub struct EventMeta +where + C: TypeConfig, +{ + /// The globally unique event identifier attached to this event + pub event_id: C::ObjectId, + + /// The fully-qualified ID of the user who sent created this event + pub sender: C::UserId, + + /// Timestamp in milliseconds on originating homeserver when the event was created + #[serde(alias = "origin_server_ts")] + pub timestamp: C::Timestamp, + + /// The ID of the room of this event + pub room_id: C::RoomId, + + /// Optional redacted event identifier + #[serde(default)] + pub redacted: Option, +} + +#[cfg(test)] +mod tests { + use crate::mocks::MockError; + + use super::*; + use serde_json; + use std::hash::Hash; + + // Test implementation of TypeConfig for testing + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] + struct TestConfig; + + impl TypeConfig for TestConfig { + type RoomId = String; + type ObjectId = String; + type ModelType = String; + type AccountData = String; + type UserId = String; + type Timestamp = u64; + type RedactionReason = String; + type Error = MockError; + } + + #[test] + fn test_event_meta_serialization() { + let event_meta = EventMeta:: { + event_id: "$event123".to_string(), + sender: "@user:example.com".to_string(), + timestamp: 1640995200000, // 2022-01-01 00:00:00 UTC + room_id: "!room123:example.com".to_string(), + redacted: None, + }; + + let serialized = serde_json::to_string(&event_meta).unwrap(); + let expected = r#"{"event_id":"$event123","sender":"@user:example.com","timestamp":1640995200000,"room_id":"!room123:example.com","redacted":null}"#; + + assert_eq!(serialized, expected); + } + + #[test] + fn test_event_meta_deserialization() { + let json = r#"{ + "event_id": "$event456", + "sender": "@alice:matrix.org", + "timestamp": 1640995200000, + "room_id": "!room456:matrix.org", + "redacted": null + }"#; + + let deserialized: EventMeta = serde_json::from_str(json).unwrap(); + + assert_eq!(deserialized.event_id, "$event456"); + assert_eq!(deserialized.sender, "@alice:matrix.org"); + assert_eq!(deserialized.timestamp, 1640995200000); + assert_eq!(deserialized.room_id, "!room456:matrix.org"); + assert_eq!(deserialized.redacted, None); + } + + #[test] + fn test_event_meta_with_redacted_event() { + let event_meta = EventMeta:: { + event_id: "$event789".to_string(), + sender: "@bob:example.com".to_string(), + timestamp: 1640995200000, + room_id: "!room789:example.com".to_string(), + redacted: Some("$redacted_event".to_string()), + }; + + let serialized = serde_json::to_string(&event_meta).unwrap(); + let deserialized: EventMeta = serde_json::from_str(&serialized).unwrap(); + + assert_eq!(event_meta, deserialized); + assert_eq!(deserialized.redacted, Some("$redacted_event".to_string())); + } + + #[test] + fn test_event_meta_timestamp_alias() { + // Test that the "origin_server_ts" alias works for deserialization + let json = r#"{ + "event_id": "$event123", + "sender": "@user:example.com", + "origin_server_ts": 1640995200000, + "room_id": "!room123:example.com" + }"#; + + let deserialized: EventMeta = serde_json::from_str(json).unwrap(); + + assert_eq!(deserialized.timestamp, 1640995200000); + } + + #[test] + fn test_event_meta_roundtrip() { + let original = EventMeta:: { + event_id: "$event_roundtrip".to_string(), + sender: "@test:example.com".to_string(), + timestamp: 1640995200000, + room_id: "!room_roundtrip:example.com".to_string(), + redacted: Some("$redacted_roundtrip".to_string()), + }; + + let serialized = serde_json::to_string(&original).unwrap(); + let deserialized: EventMeta = serde_json::from_str(&serialized).unwrap(); + + assert_eq!(original, deserialized); + } + + #[test] + fn test_event_meta_missing_redacted_field() { + // Test that missing redacted field defaults to None + let json = r#"{ + "event_id": "$event123", + "sender": "@user:example.com", + "timestamp": 1640995200000, + "room_id": "!room123:example.com" + }"#; + + let deserialized: EventMeta = serde_json::from_str(json).unwrap(); + + assert_eq!(deserialized.redacted, None); + } + + #[test] + fn test_event_meta_empty_redacted_field() { + // Test that empty redacted field is handled correctly + let json = r#"{ + "event_id": "$event123", + "sender": "@user:example.com", + "timestamp": 1640995200000, + "room_id": "!room123:example.com", + "redacted": "" + }"#; + + let deserialized: EventMeta = serde_json::from_str(json).unwrap(); + + assert_eq!(deserialized.redacted, Some("".to_string())); + } + + #[test] + fn test_event_meta_serialization_pretty() { + let event_meta = EventMeta:: { + event_id: "$event_pretty".to_string(), + sender: "@user:example.com".to_string(), + timestamp: 1640995200000, + room_id: "!room_pretty:example.com".to_string(), + redacted: Some("$redacted_pretty".to_string()), + }; + + let serialized = serde_json::to_string_pretty(&event_meta).unwrap(); + + // Verify it can be deserialized back + let deserialized: EventMeta = serde_json::from_str(&serialized).unwrap(); + assert_eq!(event_meta, deserialized); + } + + #[test] + fn test_event_meta_with_special_characters() { + let event_meta = EventMeta:: { + event_id: "$event_with_special_chars_!@#$%^&*()".to_string(), + sender: "@user_with_underscores:example.com".to_string(), + timestamp: 1640995200000, + room_id: "!room_with_dashes:example.com".to_string(), + redacted: Some("$redacted_with_spaces".to_string()), + }; + + let serialized = serde_json::to_string(&event_meta).unwrap(); + let deserialized: EventMeta = serde_json::from_str(&serialized).unwrap(); + + assert_eq!(event_meta, deserialized); + } + + #[test] + fn test_event_meta_zero_timestamp() { + let event_meta = EventMeta:: { + event_id: "$event_zero_time".to_string(), + sender: "@user:example.com".to_string(), + timestamp: 0, + room_id: "!room:example.com".to_string(), + redacted: None, + }; + + let serialized = serde_json::to_string(&event_meta).unwrap(); + let deserialized: EventMeta = serde_json::from_str(&serialized).unwrap(); + + assert_eq!(event_meta, deserialized); + assert_eq!(deserialized.timestamp, 0); + } + + #[test] + fn test_event_meta_large_timestamp() { + let event_meta = EventMeta:: { + event_id: "$event_large_time".to_string(), + sender: "@user:example.com".to_string(), + timestamp: u64::MAX, + room_id: "!room:example.com".to_string(), + redacted: None, + }; + + let serialized = serde_json::to_string(&event_meta).unwrap(); + let deserialized: EventMeta = serde_json::from_str(&serialized).unwrap(); + + assert_eq!(event_meta, deserialized); + assert_eq!(deserialized.timestamp, u64::MAX); + } +} diff --git a/native/core/src/mocks/mod.rs b/native/core/src/mocks/mod.rs new file mode 100644 index 000000000000..0461262b3c6b --- /dev/null +++ b/native/core/src/mocks/mod.rs @@ -0,0 +1,2 @@ +mod store; +pub use store::*; diff --git a/native/core/src/mocks/store.rs b/native/core/src/mocks/store.rs new file mode 100644 index 000000000000..4a8b6ce52596 --- /dev/null +++ b/native/core/src/mocks/store.rs @@ -0,0 +1,228 @@ +use core::hash::Hash; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::RwLock; + +use crate::referencing::ExecuteReference; +use crate::traits::{ModelT, StoreError, StoreT, TypeConfig}; + +// Mock types for testing +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct MockRoomId(pub String); + +impl AsRef for MockRoomId { + fn as_ref(&self) -> &str { + &self.0 + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct MockObjectId(pub String); + +impl AsRef for MockObjectId { + fn as_ref(&self) -> &str { + &self.0 + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct MockModelType(pub String); + +impl AsRef for MockModelType { + fn as_ref(&self) -> &str { + &self.0 + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] +pub struct MockTypeConfig; + +impl TypeConfig for MockTypeConfig { + type RoomId = MockRoomId; + type ObjectId = MockObjectId; + type ModelType = MockModelType; + type AccountData = String; + type UserId = String; + type Timestamp = String; + type RedactionReason = String; + type Error = MockError; +} + +// Mock model for testing +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct MockModel { + pub id: MockObjectId, + pub value: i32, + pub parent_ids: Option>, + pub transition_count: i32, +} + +impl MockModel { + pub fn new(id: &str, value: i32, parent_ids: Option>) -> Self { + Self { + id: MockObjectId(id.to_string()), + value, + parent_ids: parent_ids.map(|ids| { + ids.into_iter() + .map(|id| MockObjectId(id.to_string())) + .collect() + }), + transition_count: 0, + } + } +} + +impl ModelT for MockModel { + fn belongs_to(&self) -> Option> { + self.parent_ids.clone() + } + + fn object_id(&self) -> MockObjectId { + self.id.clone() + } + + async fn execute + Sync + 'static>( + self, + store: &T, + ) -> Result>, ::Error> { + store.save(self).await + } + + fn transition(&mut self, model: &Self) -> Result::Error> { + // Simulate transition logic: if the incoming model has a higher value, transition occurs + if model.value > self.value { + self.value = model.value; + self.transition_count += 1; + Ok(true) + } else { + Ok(false) + } + } + + fn is_redacted(&self) -> bool { + false + } + + async fn redact + Sync + 'static>( + &self, + _store: &T, + _reason: Option<::RedactionReason>, + ) -> Result>, ::Error> { + Ok(vec![]) + } +} + +// Mock error type +#[derive(Debug, thiserror::Error, PartialEq, Eq)] +pub enum MockError { + #[error("Model not found: {0}")] + NotFound(String), +} + +impl StoreError for MockError { + fn is_not_found(&self) -> bool { + matches!(self, MockError::NotFound(_)) + } +} + +// Mock store for testing +#[derive(Debug, Clone)] +pub struct MockStore { + pub models: Arc>>, +} + +impl Default for MockStore { + fn default() -> Self { + Self::new() + } +} + +impl MockStore { + pub fn new() -> Self { + Self { + models: Arc::new(RwLock::new(HashMap::new())), + } + } + + pub async fn insert(&self, model: MockModel) { + let mut models = self.models.write().await; + models.insert(model.object_id(), model); + } + + pub async fn get_model(&self, id: &MockObjectId) -> Option { + let models = self.models.read().await; + models.get(id).cloned() + } +} + +impl StoreT for MockStore { + type Model = MockModel; + + async fn get( + &self, + id: &MockObjectId, + ) -> Result::Error> { + self.get_model(id) + .await + .ok_or_else(|| MockError::NotFound(id.0.clone())) + } + + async fn save( + &self, + model: Self::Model, + ) -> Result>, ::Error> { + self.insert(model.clone()).await; + Ok(vec![ExecuteReference::Model(model.object_id())]) + } + + async fn save_many + Send>( + &self, + models: I, + ) -> Result>, ::Error> { + let mut references = Vec::new(); + for model in models { + self.insert(model.clone()).await; + references.push(ExecuteReference::Model(model.object_id())); + } + Ok(references) + } + + async fn clear_room( + &self, + _room_id: &MockRoomId, + ) -> Result>, ::Error> { + Ok(vec![]) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[tokio::test] + async fn test_mock_implementations() { + let room_id = MockRoomId("!room1:example.com".to_string()); + let object_id = MockObjectId("obj1".to_string()); + let model_type = MockModelType("model1".to_string()); + let account_data = "account1".to_string(); + let user_id = "user1".to_string(); + let timestamp = "2021-01-01T00:00:00Z".to_string(); + + assert_eq!(room_id.as_ref(), "!room1:example.com"); + assert_eq!(object_id.as_ref(), "obj1"); + assert_eq!(model_type.as_ref(), "model1"); + assert_eq!(account_data, "account1"); + assert_eq!(user_id, "user1"); + assert_eq!(timestamp, "2021-01-01T00:00:00Z"); + + let store = MockStore::new(); + + let model = MockModel::new("obj1", 10, None); + assert_eq!(model.object_id(), object_id); + assert_eq!(model.value, 10); + assert_eq!(model.parent_ids, None); + assert_eq!(model.transition_count, 0); + model.execute(&store).await.unwrap(); + } +} diff --git a/native/core/src/referencing/execute_reference.rs b/native/core/src/referencing/execute_reference.rs new file mode 100644 index 000000000000..32c603de8639 --- /dev/null +++ b/native/core/src/referencing/execute_reference.rs @@ -0,0 +1,548 @@ +use super::{IndexKey, ModelParam, RoomParam, SectionIndex, SpecialListsIndex}; +use crate::traits::TypeConfig; +use serde::{Deserialize, Serialize, de::DeserializeOwned}; + +#[derive(Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Clone, Serialize, Deserialize)] +#[serde( + bound = "C: TypeConfig, C::RoomId: Serialize + DeserializeOwned, C::ObjectId: Serialize + DeserializeOwned", + rename_all = "snake_case" +)] +pub enum ExecuteReference +where + C: TypeConfig, +{ + Index(IndexKey), + Model(C::ObjectId), + Room(C::RoomId), + RoomAccountData(C::RoomId, C::AccountData), + ModelParam(C::ObjectId, ModelParam), + RoomParam(C::RoomId, RoomParam), + AccountData(C::AccountData), + ModelType(C::ModelType), +} + +impl ExecuteReference +where + C: TypeConfig, +{ + pub fn as_storage_key(&self) -> String { + match self { + ExecuteReference::Model(owned_event_id) => { + format!("acter::{}", owned_event_id.as_ref()) + } + ExecuteReference::ModelParam(owned_event_id, model_param) => { + format!("{}::{}", owned_event_id.as_ref(), model_param) + } + ExecuteReference::RoomParam(owned_room_id, room_param) => { + format!("{}::{}", owned_room_id.as_ref(), room_param) + } + ExecuteReference::ModelType(model_type) => model_type.as_ref().to_string(), + ExecuteReference::Index(IndexKey::Special(SpecialListsIndex::InvitedTo)) => { + "global_invited".to_owned() // this is a special case, we actively store and manage + } + // not actually supported + ExecuteReference::Index(_index_key) => todo!(), + ExecuteReference::Room(_owned_room_id) => todo!(), + ExecuteReference::RoomAccountData(_owned_room_id, _cow) => todo!(), + ExecuteReference::AccountData(_cow) => todo!(), + } + } +} + +impl From> for ExecuteReference +where + C: TypeConfig, +{ + fn from(value: IndexKey) -> Self { + ExecuteReference::Index(value) + } +} + +impl From for ExecuteReference +where + C: TypeConfig, +{ + fn from(value: SectionIndex) -> Self { + ExecuteReference::Index(IndexKey::Section(value)) + } +} + +#[cfg(test)] +mod tests { + use std::borrow::Cow; + use std::hash::Hash; + + use crate::mocks::{MockError, MockModelType, MockObjectId, MockRoomId}; + + use super::super::ObjectListIndex; + + use super::*; + use serde_json; + + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] + pub struct MockTypeConfig; + + impl TypeConfig for MockTypeConfig { + type RoomId = MockRoomId; + type ObjectId = MockObjectId; + type ModelType = MockModelType; + type AccountData = Cow<'static, str>; + type UserId = String; + type Timestamp = String; + type RedactionReason = String; + type Error = MockError; + } + + // Helper function to test round-trip serialization/deserialization + fn test_round_trip(value: &T) -> Result<(), Box> + where + T: Serialize + for<'de> Deserialize<'de> + PartialEq + std::fmt::Debug, + { + let serialized = serde_json::to_string(value)?; + let deserialized: T = serde_json::from_str(&serialized)?; + assert_eq!(value, &deserialized); + Ok(()) + } + + type TestExecuteReference = ExecuteReference; + + #[test] + fn test_index_variant_serialization() -> Result<(), Box> { + // Test RoomHistory variant + let room_id = MockRoomId("!test:example.org".to_string()); + let index_key = IndexKey::RoomHistory(room_id.clone()); + let execute_ref = TestExecuteReference::Index(index_key); + test_round_trip(&execute_ref)?; + + // Test RoomModels variant + let index_key = IndexKey::RoomModels(room_id.clone()); + let execute_ref = TestExecuteReference::Index(index_key); + test_round_trip(&execute_ref)?; + + // Test ObjectHistory variant + let object_id = MockObjectId("$test_event_id".to_string()); + let index_key = IndexKey::ObjectHistory(object_id.clone()); + let execute_ref = TestExecuteReference::Index(index_key); + test_round_trip(&execute_ref)?; + + // Test Section variant + let index_key = IndexKey::Section(SectionIndex::Boosts); + let execute_ref = TestExecuteReference::Index(index_key); + test_round_trip(&execute_ref)?; + + // Test RoomSection variant + let index_key = IndexKey::RoomSection(room_id.clone(), SectionIndex::Tasks); + let execute_ref = TestExecuteReference::Index(index_key); + test_round_trip(&execute_ref)?; + + // Test ObjectList variant + let index_key = IndexKey::ObjectList(object_id.clone(), ObjectListIndex::Comments); + let execute_ref = TestExecuteReference::Index(index_key); + test_round_trip(&execute_ref)?; + + // Test Special variant + let index_key = IndexKey::Special(SpecialListsIndex::InvitedTo); + let execute_ref = TestExecuteReference::Index(index_key); + test_round_trip(&execute_ref)?; + + // Test Redacted variant + let index_key = IndexKey::Redacted; + let execute_ref = TestExecuteReference::Index(index_key); + test_round_trip(&execute_ref)?; + + // Test AllHistory variant + let index_key = IndexKey::AllHistory; + let execute_ref = TestExecuteReference::Index(index_key); + test_round_trip(&execute_ref)?; + + Ok(()) + } + + #[test] + fn test_model_variant_serialization() -> Result<(), Box> { + let object_id = MockObjectId("$test_event_id".to_string()); + let execute_ref = TestExecuteReference::Model(object_id); + test_round_trip(&execute_ref)?; + Ok(()) + } + + #[test] + fn test_room_variant_serialization() -> Result<(), Box> { + let room_id = MockRoomId("!test:example.org".to_string()); + let execute_ref = TestExecuteReference::Room(room_id); + test_round_trip(&execute_ref)?; + Ok(()) + } + + #[test] + fn test_room_account_data_variant_serialization() -> Result<(), Box> { + let room_id = MockRoomId("!test:example.org".to_string()); + let account_data = Cow::Borrowed("test_account_data"); + let execute_ref = TestExecuteReference::RoomAccountData(room_id, account_data); + test_round_trip(&execute_ref)?; + + // Test with owned string + let room_id = MockRoomId("!test2:example.org".to_string()); + let account_data = Cow::Owned("owned_account_data".to_string()); + let execute_ref = TestExecuteReference::RoomAccountData(room_id, account_data); + test_round_trip(&execute_ref)?; + Ok(()) + } + + #[test] + fn test_model_param_variant_serialization() -> Result<(), Box> { + let object_id = MockObjectId("$test_event_id".to_string()); + + // Test all ModelParam variants + for model_param in [ + ModelParam::CommentsStats, + ModelParam::AttachmentsStats, + ModelParam::ReactionStats, + ModelParam::RsvpStats, + ModelParam::ReadReceiptsStats, + ModelParam::InviteStats, + ] { + let execute_ref = TestExecuteReference::ModelParam(object_id.clone(), model_param); + test_round_trip(&execute_ref)?; + } + Ok(()) + } + + #[test] + fn test_room_param_variant_serialization() -> Result<(), Box> { + let room_id = MockRoomId("!test:example.org".to_string()); + + // Test all RoomParam variants + { + let room_param = RoomParam::LatestMessage; + let execute_ref = TestExecuteReference::RoomParam(room_id.clone(), room_param); + test_round_trip(&execute_ref)?; + } + Ok(()) + } + + #[test] + fn test_account_data_variant_serialization() -> Result<(), Box> { + // Test with borrowed string + let account_data = Cow::Borrowed("test_account_data"); + let execute_ref = TestExecuteReference::AccountData(account_data); + test_round_trip(&execute_ref)?; + + // Test with owned string + let account_data = Cow::Owned("owned_account_data".to_string()); + let execute_ref = TestExecuteReference::AccountData(account_data); + test_round_trip(&execute_ref)?; + Ok(()) + } + + #[test] + fn test_model_type_variant_serialization() -> Result<(), Box> { + let model_type = MockModelType("test_model_type".to_string()); + let execute_ref = TestExecuteReference::ModelType(model_type); + test_round_trip(&execute_ref)?; + Ok(()) + } + + #[test] + fn test_all_variants_serialization() -> Result<(), Box> { + let room_id = MockRoomId("!test:example.org".to_string()); + let object_id = MockObjectId("$test_event_id".to_string()); + let model_type = MockModelType("test_model_type".to_string()); + + // Test all variants in one comprehensive test + let variants = vec![ + TestExecuteReference::Index(IndexKey::RoomHistory(room_id.clone())), + TestExecuteReference::Index(IndexKey::RoomModels(room_id.clone())), + TestExecuteReference::Index(IndexKey::ObjectHistory(object_id.clone())), + TestExecuteReference::Index(IndexKey::Section(SectionIndex::Boosts)), + TestExecuteReference::Index(IndexKey::RoomSection( + room_id.clone(), + SectionIndex::Tasks, + )), + TestExecuteReference::Index(IndexKey::ObjectList( + object_id.clone(), + ObjectListIndex::Comments, + )), + TestExecuteReference::Index(IndexKey::Special(SpecialListsIndex::InvitedTo)), + TestExecuteReference::Index(IndexKey::Redacted), + TestExecuteReference::Index(IndexKey::AllHistory), + TestExecuteReference::Model(object_id.clone()), + TestExecuteReference::Room(room_id.clone()), + TestExecuteReference::RoomAccountData(room_id.clone(), Cow::Borrowed("test_data")), + TestExecuteReference::ModelParam(object_id.clone(), ModelParam::CommentsStats), + TestExecuteReference::RoomParam(room_id.clone(), RoomParam::LatestMessage), + TestExecuteReference::AccountData(Cow::Borrowed("test_account_data")), + TestExecuteReference::ModelType(model_type), + ]; + + for variant in variants { + test_round_trip(&variant)?; + } + Ok(()) + } + + #[test] + fn test_from_parsers() { + let room_id = MockRoomId("!test:example.org".to_string()); + let object_id = MockObjectId("$test_event_id".to_string()); + + // Test From> implementation + let index_key = IndexKey::::RoomHistory(room_id.clone()); + let execute_ref: TestExecuteReference = index_key.clone().into(); + assert!(matches!(execute_ref, TestExecuteReference::Index(_))); + if let TestExecuteReference::Index(ik) = execute_ref { + assert_eq!(ik, index_key); + } + + // Test From implementation + let section_index = SectionIndex::Boosts; + let execute_ref: TestExecuteReference = section_index.into(); + assert!(matches!( + execute_ref, + TestExecuteReference::Index(IndexKey::Section(_)) + )); + if let TestExecuteReference::Index(IndexKey::Section(si)) = execute_ref { + assert_eq!(si, SectionIndex::Boosts); + } + + // Test From with different variants + for section_index in [ + SectionIndex::Boosts, + SectionIndex::Calendar, + SectionIndex::Pins, + SectionIndex::Stories, + SectionIndex::Tasks, + ] { + let execute_ref: TestExecuteReference = section_index.clone().into(); + assert!(matches!( + execute_ref, + TestExecuteReference::Index(IndexKey::Section(_)) + )); + if let TestExecuteReference::Index(IndexKey::Section(si)) = execute_ref { + assert_eq!(si, section_index); + } + } + + // Test From> with different variants + let index_variants = vec![ + IndexKey::::RoomHistory(room_id.clone()), + IndexKey::::RoomModels(room_id.clone()), + IndexKey::::ObjectHistory(object_id.clone()), + IndexKey::::Section(SectionIndex::Boosts), + IndexKey::::RoomSection(room_id.clone(), SectionIndex::Tasks), + IndexKey::::ObjectList(object_id.clone(), ObjectListIndex::Comments), + IndexKey::::Special(SpecialListsIndex::InvitedTo), + IndexKey::::Redacted, + IndexKey::::AllHistory, + ]; + + for index_key in index_variants { + let execute_ref: TestExecuteReference = index_key.clone().into(); + assert!(matches!(execute_ref, TestExecuteReference::Index(_))); + if let TestExecuteReference::Index(ik) = execute_ref { + assert_eq!(ik, index_key); + } + } + } + + #[test] + fn test_as_storage_key() { + let room_id = MockRoomId("!test:example.org".to_string()); + let object_id = MockObjectId("$test_event_id".to_string()); + let model_type = MockModelType("test_model_type".to_string()); + + // Test Model variant + let execute_ref = TestExecuteReference::Model(object_id.clone()); + let storage_key = execute_ref.as_storage_key(); + assert_eq!(storage_key, "acter::$test_event_id"); + + // Test ModelParam variants + for model_param in [ + ModelParam::CommentsStats, + ModelParam::AttachmentsStats, + ModelParam::ReactionStats, + ModelParam::RsvpStats, + ModelParam::ReadReceiptsStats, + ModelParam::InviteStats, + ] { + let execute_ref = + TestExecuteReference::ModelParam(object_id.clone(), model_param.clone()); + let storage_key = execute_ref.as_storage_key(); + assert_eq!(storage_key, format!("$test_event_id::{model_param}")); + } + + // Test RoomParam variants + { + let room_param = RoomParam::LatestMessage; + let execute_ref = TestExecuteReference::RoomParam(room_id.clone(), room_param.clone()); + let storage_key = execute_ref.as_storage_key(); + assert_eq!(storage_key, format!("!test:example.org::{room_param}")); + } + + // Test ModelType variant + let execute_ref = TestExecuteReference::ModelType(model_type.clone()); + let storage_key = execute_ref.as_storage_key(); + assert_eq!(storage_key, "test_model_type"); + + // Test Index with SpecialListsIndex::InvitedTo + let index_key = IndexKey::Special(SpecialListsIndex::InvitedTo); + let execute_ref = TestExecuteReference::Index(index_key); + let storage_key = execute_ref.as_storage_key(); + assert_eq!(storage_key, "global_invited"); + + // Test that other Index variants panic (as they are marked as todo!) + let index_key = IndexKey::RoomHistory(room_id.clone()); + let execute_ref = TestExecuteReference::Index(index_key); + let result = std::panic::catch_unwind(|| execute_ref.as_storage_key()); + assert!(result.is_err()); + + // Test that Room variant panics (as it is marked as todo!) + let execute_ref = TestExecuteReference::Room(room_id.clone()); + let result = std::panic::catch_unwind(|| execute_ref.as_storage_key()); + assert!(result.is_err()); + + // Test that RoomAccountData variant panics (as it is marked as todo!) + let execute_ref = + TestExecuteReference::RoomAccountData(room_id.clone(), Cow::Borrowed("test")); + let result = std::panic::catch_unwind(|| execute_ref.as_storage_key()); + assert!(result.is_err()); + + // Test that AccountData variant panics (as it is marked as todo!) + let execute_ref = TestExecuteReference::AccountData(Cow::Borrowed("test")); + let result = std::panic::catch_unwind(|| execute_ref.as_storage_key()); + assert!(result.is_err()); + } + + #[test] + fn test_as_storage_key_edge_cases() { + // Test with empty strings + let room_id = MockRoomId("".to_string()); + let object_id = MockObjectId("".to_string()); + let model_type = MockModelType("".to_string()); + + let execute_ref = TestExecuteReference::Model(object_id.clone()); + let storage_key = execute_ref.as_storage_key(); + assert_eq!(storage_key, "acter::"); + + let execute_ref = + TestExecuteReference::ModelParam(object_id.clone(), ModelParam::CommentsStats); + let storage_key = execute_ref.as_storage_key(); + assert_eq!(storage_key, "::comments_stats"); + + let execute_ref = + TestExecuteReference::RoomParam(room_id.clone(), RoomParam::LatestMessage); + let storage_key = execute_ref.as_storage_key(); + assert_eq!(storage_key, "::latest_message"); + + let execute_ref = TestExecuteReference::ModelType(model_type.clone()); + let storage_key = execute_ref.as_storage_key(); + assert_eq!(storage_key, ""); + + // Test with special characters + let room_id = MockRoomId("!test@#$%^&*():example.org".to_string()); + let object_id = MockObjectId("$test@#$%^&*()_event_id".to_string()); + let model_type = MockModelType("test@#$%^&*()_model_type".to_string()); + + let execute_ref = TestExecuteReference::Model(object_id.clone()); + let storage_key = execute_ref.as_storage_key(); + assert_eq!(storage_key, "acter::$test@#$%^&*()_event_id"); + + let execute_ref = + TestExecuteReference::ModelParam(object_id.clone(), ModelParam::CommentsStats); + let storage_key = execute_ref.as_storage_key(); + assert_eq!(storage_key, "$test@#$%^&*()_event_id::comments_stats"); + + let execute_ref = + TestExecuteReference::RoomParam(room_id.clone(), RoomParam::LatestMessage); + let storage_key = execute_ref.as_storage_key(); + assert_eq!(storage_key, "!test@#$%^&*():example.org::latest_message"); + + let execute_ref = TestExecuteReference::ModelType(model_type.clone()); + let storage_key = execute_ref.as_storage_key(); + assert_eq!(storage_key, "test@#$%^&*()_model_type"); + } + + #[test] + fn test_from_parsers_comprehensive() { + let room_id = MockRoomId("!test:example.org".to_string()); + let object_id = MockObjectId("$test_event_id".to_string()); + + // Test all From implementations with different data types + let test_cases = vec![ + // From + // + // From> - RoomHistory + ( + IndexKey::::RoomHistory(room_id.clone()), + TestExecuteReference::Index(IndexKey::RoomHistory(room_id.clone())), + ), + // From> - RoomModels + ( + IndexKey::::RoomModels(room_id.clone()), + TestExecuteReference::Index(IndexKey::RoomModels(room_id.clone())), + ), + // From> - ObjectHistory + ( + IndexKey::::ObjectHistory(object_id.clone()), + TestExecuteReference::Index(IndexKey::ObjectHistory(object_id.clone())), + ), + // From> - Section + ( + IndexKey::::Section(SectionIndex::Boosts), + TestExecuteReference::Index(IndexKey::Section(SectionIndex::Boosts)), + ), + // From> - RoomSection + ( + IndexKey::::RoomSection(room_id.clone(), SectionIndex::Tasks), + TestExecuteReference::Index(IndexKey::RoomSection( + room_id.clone(), + SectionIndex::Tasks, + )), + ), + // From> - ObjectList + ( + IndexKey::::ObjectList( + object_id.clone(), + ObjectListIndex::Comments, + ), + TestExecuteReference::Index(IndexKey::ObjectList( + object_id.clone(), + ObjectListIndex::Comments, + )), + ), + // From> - Special + ( + IndexKey::::Special(SpecialListsIndex::InvitedTo), + TestExecuteReference::Index(IndexKey::Special(SpecialListsIndex::InvitedTo)), + ), + // From> - Redacted + ( + IndexKey::::Redacted, + TestExecuteReference::Index(IndexKey::Redacted), + ), + // From> - AllHistory + ( + IndexKey::::AllHistory, + TestExecuteReference::Index(IndexKey::AllHistory), + ), + ]; + + for (input, expected) in test_cases { + let result: TestExecuteReference = input.into(); + assert_eq!(result, expected); + } + + // Test From specifically + for section_index in [ + SectionIndex::Boosts, + SectionIndex::Calendar, + SectionIndex::Pins, + SectionIndex::Stories, + SectionIndex::Tasks, + ] { + let result: TestExecuteReference = section_index.clone().into(); + let expected = TestExecuteReference::Index(IndexKey::Section(section_index)); + assert_eq!(result, expected); + } + } +} diff --git a/native/core/src/referencing/index_key.rs b/native/core/src/referencing/index_key.rs new file mode 100644 index 000000000000..1a44945a7f5c --- /dev/null +++ b/native/core/src/referencing/index_key.rs @@ -0,0 +1,314 @@ +use super::{ObjectListIndex, SectionIndex, SpecialListsIndex}; +use crate::traits::TypeConfig; +use serde::{Deserialize, Serialize, de::DeserializeOwned}; + +// We organize our Index by typed keys +#[derive(Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Clone, Serialize, Deserialize)] +#[serde( + bound = "C: TypeConfig, C::RoomId: Serialize + DeserializeOwned, C::ObjectId: Serialize + DeserializeOwned", + rename_all = "snake_case" +)] +pub enum IndexKey +where + C: TypeConfig, +{ + RoomHistory(C::RoomId), + RoomModels(C::RoomId), + ObjectHistory(C::ObjectId), + Section(SectionIndex), + RoomSection(C::RoomId, SectionIndex), + ObjectList(C::ObjectId, ObjectListIndex), + Special(SpecialListsIndex), + Redacted, + AllHistory, +} + +#[cfg(test)] +mod tests { + use crate::mocks::MockError; + + use super::*; + use serde_json; + use std::hash::Hash; + + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] + struct MockTypeConfig; + + impl TypeConfig for MockTypeConfig { + type RoomId = String; + type ObjectId = String; + type ModelType = String; + type AccountData = String; + type UserId = String; + type Timestamp = String; + type RedactionReason = String; + type Error = MockError; + } + + type TestIndexKey = IndexKey; + + // Helper function to test round-trip serialization/deserialization + fn test_round_trip(value: &T) -> T + where + T: Serialize + DeserializeOwned + PartialEq + std::fmt::Debug, + { + let serialized = serde_json::to_string(value).expect("Failed to serialize"); + let deserialized: T = serde_json::from_str(&serialized).expect("Failed to deserialize"); + assert_eq!(value, &deserialized); + deserialized + } + + #[test] + fn test_room_history_serialization() { + let room_id = "room123".to_string(); + let index_key = TestIndexKey::RoomHistory(room_id.clone()); + + let result = test_round_trip(&index_key); + assert!(matches!(result, IndexKey::RoomHistory(r) if r == room_id)); + } + + #[test] + fn test_room_models_serialization() { + let room_id = "room456".to_string(); + let index_key = TestIndexKey::RoomModels(room_id.clone()); + + let result = test_round_trip(&index_key); + assert!(matches!(result, IndexKey::RoomModels(r) if r == room_id)); + } + + #[test] + fn test_object_history_serialization() { + let object_id = "object789".to_string(); + let index_key = TestIndexKey::ObjectHistory(object_id.clone()); + + let result = test_round_trip(&index_key); + assert!(matches!(result, IndexKey::ObjectHistory(o) if o == object_id)); + } + + #[test] + fn test_section_serialization() { + let section = SectionIndex::Boosts; + let index_key = TestIndexKey::Section(section); + + let result = test_round_trip(&index_key); + assert!(matches!(result, IndexKey::Section(SectionIndex::Boosts))); + } + + #[test] + fn test_section_all_variants() { + let sections = vec![ + SectionIndex::Boosts, + SectionIndex::Calendar, + SectionIndex::Pins, + SectionIndex::Stories, + SectionIndex::Tasks, + ]; + + for section in sections { + let index_key = TestIndexKey::Section(section); + let result = test_round_trip(&index_key); + assert!(matches!(result, IndexKey::Section(_))); + } + } + + #[test] + fn test_room_section_serialization() { + let room_id = "room101".to_string(); + let section = SectionIndex::Calendar; + let index_key = TestIndexKey::RoomSection(room_id.clone(), section); + + let result = test_round_trip(&index_key); + assert!(matches!(result, IndexKey::RoomSection(r, SectionIndex::Calendar) if r == room_id)); + } + + #[test] + fn test_object_list_serialization() { + let object_id = "object202".to_string(); + let list_index = ObjectListIndex::Comments; + let index_key = TestIndexKey::ObjectList(object_id.clone(), list_index); + + let result = test_round_trip(&index_key); + assert!( + matches!(result, IndexKey::ObjectList(o, ObjectListIndex::Comments) if o == object_id) + ); + } + + #[test] + fn test_object_list_all_variants() { + let object_id = "object303".to_string(); + let list_indices = vec![ + ObjectListIndex::Attachments, + ObjectListIndex::Comments, + ObjectListIndex::Reactions, + ObjectListIndex::ReadReceipt, + ObjectListIndex::Rsvp, + ObjectListIndex::Tasks, + ObjectListIndex::Invites, + ]; + + for list_index in list_indices { + let index_key = TestIndexKey::ObjectList(object_id.clone(), list_index); + let result = test_round_trip(&index_key); + assert!(matches!(result, IndexKey::ObjectList(_, _))); + } + } + + #[test] + fn test_special_serialization() { + let special_index = SpecialListsIndex::MyOpenTasks; + let index_key = TestIndexKey::Special(special_index); + + let result = test_round_trip(&index_key); + assert!(matches!( + result, + IndexKey::Special(SpecialListsIndex::MyOpenTasks) + )); + } + + #[test] + fn test_special_all_variants() { + let special_indices = vec![ + SpecialListsIndex::MyOpenTasks, + SpecialListsIndex::MyDoneTasks, + SpecialListsIndex::InvitedTo, + ]; + + for special_index in special_indices { + let index_key = TestIndexKey::Special(special_index); + let result = test_round_trip(&index_key); + assert!(matches!(result, IndexKey::Special(_))); + } + } + + #[test] + fn test_redacted_serialization() { + let index_key = TestIndexKey::Redacted; + + let result = test_round_trip(&index_key); + assert!(matches!(result, IndexKey::Redacted)); + } + + #[test] + fn test_all_history_serialization() { + let index_key = TestIndexKey::AllHistory; + + let result = test_round_trip(&index_key); + assert!(matches!(result, IndexKey::AllHistory)); + } + + #[test] + fn test_all_variants_round_trip() { + let room_id = "test_room".to_string(); + let object_id = "test_object".to_string(); + + let test_cases = vec![ + TestIndexKey::RoomHistory(room_id.clone()), + TestIndexKey::RoomModels(room_id.clone()), + TestIndexKey::ObjectHistory(object_id.clone()), + TestIndexKey::Section(SectionIndex::Boosts), + TestIndexKey::RoomSection(room_id.clone(), SectionIndex::Calendar), + TestIndexKey::ObjectList(object_id.clone(), ObjectListIndex::Comments), + TestIndexKey::Special(SpecialListsIndex::MyOpenTasks), + TestIndexKey::Redacted, + TestIndexKey::AllHistory, + ]; + + for (i, test_case) in test_cases.into_iter().enumerate() { + let result = test_round_trip(&test_case); + assert_eq!(test_case, result, "failed to round trip variant {i}"); + } + } + + #[test] + fn test_serialization_format() { + let room_id = "test_room".to_string(); + + // Test that the serialized format is as expected + let room_history = TestIndexKey::RoomHistory(room_id.clone()); + let serialized = serde_json::to_string(&room_history).expect("Failed to serialize"); + assert!(serialized.contains("room_history")); + assert!(serialized.contains(&room_id)); + + let section = TestIndexKey::Section(SectionIndex::Boosts); + let serialized = serde_json::to_string(§ion).expect("Failed to serialize"); + assert!(serialized.contains("section")); + assert!(serialized.contains("boosts")); + + let redacted = TestIndexKey::Redacted; + let serialized = serde_json::to_string(&redacted).expect("Failed to serialize"); + assert!(serialized.contains("redacted")); + } + + #[test] + fn test_deserialization_errors() { + // Test invalid JSON + let invalid_json = r#"{"invalid": "data"}"#; + let result: Result = serde_json::from_str(invalid_json); + assert!(result.is_err()); + + // Test missing variant + let invalid_json = r#"{"NonExistentVariant": "data"}"#; + let result: Result = serde_json::from_str(invalid_json); + assert!(result.is_err()); + } + + #[test] + fn test_with_custom_types() { + // Test with custom types that implement the required traits + #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] + struct CustomRoomId(String); + + #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] + struct CustomObjectId(String); + + impl AsRef for CustomRoomId { + fn as_ref(&self) -> &str { + &self.0 + } + } + + impl AsRef for CustomObjectId { + fn as_ref(&self) -> &str { + &self.0 + } + } + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] + struct CustomTypeConfig; + + impl TypeConfig for CustomTypeConfig { + type RoomId = CustomRoomId; + type ObjectId = CustomObjectId; + type ModelType = String; + type AccountData = String; + type UserId = String; + type Timestamp = String; + type RedactionReason = String; + type Error = MockError; + } + + type CustomIndexKey = IndexKey; + + let room_id = CustomRoomId("custom_room".to_string()); + let object_id = CustomObjectId("custom_object".to_string()); + + assert_eq!(room_id.as_ref(), "custom_room"); + assert_eq!(object_id.as_ref(), "custom_object"); + + let test_cases = vec![ + CustomIndexKey::RoomHistory(room_id.clone()), + CustomIndexKey::RoomModels(room_id.clone()), + CustomIndexKey::ObjectHistory(object_id.clone()), + CustomIndexKey::Section(SectionIndex::Tasks), + CustomIndexKey::RoomSection(room_id.clone(), SectionIndex::Pins), + CustomIndexKey::ObjectList(object_id.clone(), ObjectListIndex::Reactions), + CustomIndexKey::Special(SpecialListsIndex::MyDoneTasks), + CustomIndexKey::Redacted, + CustomIndexKey::AllHistory, + ]; + + for test_case in test_cases { + let result = test_round_trip(&test_case); + assert_eq!(test_case, result); + } + } +} diff --git a/native/core/src/referencing/mod.rs b/native/core/src/referencing/mod.rs new file mode 100644 index 000000000000..6f617a9a729b --- /dev/null +++ b/native/core/src/referencing/mod.rs @@ -0,0 +1,15 @@ +mod execute_reference; +mod index_key; +mod model_param; +mod object_list_index; +mod room_param; +mod section_index; +mod special_list_index; + +pub use execute_reference::*; +pub use index_key::*; +pub use model_param::*; +pub use object_list_index::*; +pub use room_param::*; +pub use section_index::*; +pub use special_list_index::*; diff --git a/native/core/src/referencing/model_param.rs b/native/core/src/referencing/model_param.rs new file mode 100644 index 000000000000..af88f7026ff6 --- /dev/null +++ b/native/core/src/referencing/model_param.rs @@ -0,0 +1,33 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "strum", derive(strum::Display, strum::EnumString))] +#[cfg_attr(feature = "strum", strum(serialize_all = "snake_case"))] +pub enum ModelParam { + CommentsStats, + AttachmentsStats, + ReactionStats, + RsvpStats, + #[cfg_attr(feature = "strum", strum(to_string = "read_receipts"))] + ReadReceiptsStats, + #[cfg_attr(feature = "strum", strum(to_string = "invites"))] + InviteStats, +} + +#[cfg(not(feature = "strum"))] +impl core::fmt::Display for ModelParam { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + ModelParam::CommentsStats => "comments_stats", + ModelParam::AttachmentsStats => "attachments_stats", + ModelParam::ReactionStats => "reaction_stats", + ModelParam::RsvpStats => "rsvp_stats", + ModelParam::ReadReceiptsStats => "read_receipts_stats", + ModelParam::InviteStats => "invite_stats", + } + ) + } +} diff --git a/native/core/src/referencing/object_list_index.rs b/native/core/src/referencing/object_list_index.rs new file mode 100644 index 000000000000..84f2de452b09 --- /dev/null +++ b/native/core/src/referencing/object_list_index.rs @@ -0,0 +1,15 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "strum", derive(strum::Display, strum::EnumString))] +#[cfg_attr(feature = "strum", strum(serialize_all = "snake_case"))] +#[repr(u8)] +pub enum ObjectListIndex { + Attachments, + Comments, + Reactions, + ReadReceipt, + Rsvp, + Tasks, + Invites, +} diff --git a/native/core/src/referencing/room_param.rs b/native/core/src/referencing/room_param.rs new file mode 100644 index 000000000000..bd52dc1fcfa9 --- /dev/null +++ b/native/core/src/referencing/room_param.rs @@ -0,0 +1,21 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "strum", derive(strum::Display, strum::EnumString))] +#[cfg_attr(feature = "strum", strum(serialize_all = "snake_case"))] +pub enum RoomParam { + LatestMessage, +} + +#[cfg(not(feature = "strum"))] +impl core::fmt::Display for RoomParam { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + RoomParam::LatestMessage => "latest_message", + } + ) + } +} diff --git a/native/core/src/referencing/section_index.rs b/native/core/src/referencing/section_index.rs new file mode 100644 index 000000000000..4af9ae84887b --- /dev/null +++ b/native/core/src/referencing/section_index.rs @@ -0,0 +1,15 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "strum", derive(strum::Display, strum::EnumString))] +#[cfg_attr(feature = "strum", strum(serialize_all = "snake_case"))] +#[repr(u8)] +#[serde(rename_all = "snake_case")] +pub enum SectionIndex { + #[cfg_attr(feature = "strum", strum(serialize = "news", serialize = "boosts"))] + Boosts = 0, + Calendar, + Pins, + Stories, + Tasks, +} diff --git a/native/core/src/referencing/special_list_index.rs b/native/core/src/referencing/special_list_index.rs new file mode 100644 index 000000000000..f54c5dc72f27 --- /dev/null +++ b/native/core/src/referencing/special_list_index.rs @@ -0,0 +1,17 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "strum", derive(strum::Display, strum::EnumString))] +#[cfg_attr(feature = "strum", strum(serialize_all = "snake_case"))] +#[repr(u8)] +pub enum SpecialListsIndex { + MyOpenTasks, + MyDoneTasks, + InvitedTo, + #[cfg(any(test, feature = "testing"))] + Test1, + #[cfg(any(test, feature = "testing"))] + Test2, + #[cfg(any(test, feature = "testing"))] + Test3, +} diff --git a/native/core/src/store.rs b/native/core/src/store.rs new file mode 100644 index 000000000000..575962f27f65 --- /dev/null +++ b/native/core/src/store.rs @@ -0,0 +1,3 @@ +mod index; + +pub use index::*; diff --git a/native/core/src/store/index.rs b/native/core/src/store/index.rs new file mode 100644 index 000000000000..4c4ff49b59b2 --- /dev/null +++ b/native/core/src/store/index.rs @@ -0,0 +1,767 @@ +use std::{fmt::Debug, ops::Deref}; + +use eyeball_im::{ObservableVector, ObservableVectorTransactionEntry, VectorDiff}; +use futures::{Stream, StreamExt}; + +use crate::{ + meta::EventMeta, + referencing::{IndexKey, ObjectListIndex, SectionIndex}, + traits::TypeConfig, +}; + +/// Keeps an index of items sorted by the given rank, highest rank first +pub struct RankedIndex +where + K: Ord + Clone + 'static, + T: Clone + Eq + 'static, +{ + vector: ObservableVector<(K, T)>, +} +impl Default for RankedIndex +where + K: Ord + Clone + 'static, + T: Clone + Eq + 'static, +{ + fn default() -> Self { + Self { + vector: Default::default(), + } + } +} + +impl Deref for RankedIndex +where + K: Ord + Clone + 'static, + T: Clone + Eq + 'static, +{ + type Target = ObservableVector<(K, T)>; + + fn deref(&self) -> &Self::Target { + &self.vector + } +} + +impl RankedIndex +where + K: Ord + Clone + 'static, + T: Clone + Eq + 'static, +{ + pub fn new_with(rank: K, value: T) -> Self { + let mut m = RankedIndex::default(); + m.insert(rank, value); + m + } + /// Insert the value T at the position of rank + /// + /// Will add at first position if a value of the same rank is found + pub fn insert(&mut self, rank: K, value: T) { + let mut pos = self.vector.len(); + for (idx, (k, _v)) in self.vector.iter().enumerate() { + if k <= &rank { + pos = idx; + break; + } + } + self.vector.insert(pos, (rank, value)); + } + + /// Remove all instances in the vector having the specific value + pub fn remove(&mut self, value: &T) { + let mut t = self.vector.transaction(); + let mut entries = t.entries(); + while let Some(entry) = entries.next() { + if &entry.1 == value { + ObservableVectorTransactionEntry::remove(entry); + } + } + t.commit(); + } + + /// Returns the current list of values in order of their rank + pub fn values(&self) -> Vec<&T> { + self.vector.iter().map(|(_k, v)| v).collect() + } + + pub fn update_stream(&self) -> impl Stream> + use { + self.vector.subscribe().into_stream().map(|v| match v { + VectorDiff::Append { values } => VectorDiff::Append { + values: values.into_iter().map(|(_k, v)| v).collect(), + }, + VectorDiff::Clear => VectorDiff::Clear, + VectorDiff::PushFront { value } => VectorDiff::PushFront { value: value.1 }, + VectorDiff::PushBack { value } => VectorDiff::PushBack { value: value.1 }, + VectorDiff::PopFront => VectorDiff::PopFront, + VectorDiff::PopBack => VectorDiff::PopBack, + VectorDiff::Insert { index, value } => VectorDiff::Insert { + index, + value: value.1, + }, + VectorDiff::Set { index, value } => VectorDiff::Set { + index, + value: value.1, + }, + VectorDiff::Remove { index } => VectorDiff::Remove { index }, + VectorDiff::Truncate { length } => VectorDiff::Truncate { length }, + VectorDiff::Reset { values } => VectorDiff::Reset { + values: values.into_iter().map(|(_k, v)| v).collect(), + }, + }) + } +} + +struct GenericIndexVectorHandler(); + +impl GenericIndexVectorHandler { + /// All instances of this element from the vector + pub fn remove(vector: &mut ObservableVector, value: &T) + where + T: Clone + Eq + 'static, + { + let mut t = vector.transaction(); + let mut entries = t.entries(); + while let Some(entry) = entries.next() { + if &*entry == value { + ObservableVectorTransactionEntry::remove(entry); + } + } + t.commit(); + } + + /// Returns the current list of values in order of when they were added + pub fn values(vector: &ObservableVector) -> Vec<&T> + where + T: Clone + Eq + 'static, + { + vector.iter().collect() + } + + pub fn update_stream( + vector: &ObservableVector, + ) -> impl Stream> + use + where + T: Clone + Eq + 'static, + { + vector.subscribe().into_stream() + } +} + +/// Keeps an index of items sorted by when they were added +/// latest first +pub struct LifoIndex +where + T: Clone + Eq + 'static, +{ + vector: ObservableVector, +} + +impl LifoIndex +where + T: Clone + Eq + 'static, +{ + pub fn new_with(value: T) -> Self { + let mut m = LifoIndex::default(); + m.insert(value); + m + } + /// Insert the element at the front + pub fn insert(&mut self, value: T) { + self.vector.push_front(value); + } + + /// All instances of this element from the vector + pub fn remove(&mut self, value: &T) { + GenericIndexVectorHandler::remove(&mut self.vector, value) + } + + /// Returns the current list of values in order of when they were added + pub fn values(&self) -> Vec<&T> { + GenericIndexVectorHandler::values(&self.vector) + } + + pub fn update_stream(&self) -> impl Stream> + use { + GenericIndexVectorHandler::update_stream(&self.vector) + } +} + +impl Default for LifoIndex +where + T: Clone + Eq + 'static, +{ + fn default() -> Self { + Self { + vector: Default::default(), + } + } +} + +impl Deref for LifoIndex +where + T: Clone + Eq + 'static, +{ + type Target = ObservableVector; + + fn deref(&self) -> &Self::Target { + &self.vector + } +} + +/// Keeps an index of items sorted by when they were added +/// latest last +pub struct FiloIndex +where + T: Clone + Eq + 'static, +{ + vector: ObservableVector, +} + +impl FiloIndex +where + T: Clone + Eq + 'static, +{ + pub fn new_with(value: T) -> Self { + let mut m = FiloIndex::default(); + m.insert(value); + m + } + /// Insert the element at the front + pub fn insert(&mut self, value: T) { + self.vector.push_back(value); + } + + /// All instances of this element from the vector + pub fn remove(&mut self, value: &T) { + GenericIndexVectorHandler::remove(&mut self.vector, value) + } + + /// Returns the current list of values in order of when they were added + pub fn values(&self) -> Vec<&T> { + GenericIndexVectorHandler::values(&self.vector) + } + + pub fn update_stream(&self) -> impl Stream> + use { + GenericIndexVectorHandler::update_stream(&self.vector) + } +} + +impl Default for FiloIndex +where + T: Clone + Eq + 'static, +{ + fn default() -> Self { + Self { + vector: Default::default(), + } + } +} + +impl Deref for FiloIndex +where + T: Clone + Eq + 'static, +{ + type Target = ObservableVector; + + fn deref(&self) -> &Self::Target { + &self.vector + } +} + +pub enum StoreIndex +where + C::ObjectId: 'static, + C::Timestamp: 'static, +{ + Lifo(LifoIndex), + Filo(FiloIndex), + Ranked(RankedIndex), +} + +impl StoreIndex { + pub fn new_for(key: &IndexKey, meta: &EventMeta) -> StoreIndex { + match key { + IndexKey::AllHistory | IndexKey::ObjectHistory(_) | IndexKey::RoomHistory(_) => { + StoreIndex::Ranked(RankedIndex::new_with( + meta.timestamp.clone(), + meta.event_id.clone(), + )) + } + //RSVPs are latest first for collection + IndexKey::ObjectList(_, ObjectListIndex::Rsvp) => StoreIndex::Ranked( + RankedIndex::new_with(meta.timestamp.clone(), meta.event_id.clone()), + ), + IndexKey::Section(SectionIndex::Boosts) + | IndexKey::Section(SectionIndex::Stories) + | IndexKey::RoomSection(_, SectionIndex::Boosts) + | IndexKey::RoomSection(_, SectionIndex::Stories) => StoreIndex::Ranked( + RankedIndex::new_with(meta.timestamp.clone(), meta.event_id.clone()), + ), + IndexKey::ObjectList(_, ObjectListIndex::Tasks) => { + StoreIndex::Filo(FiloIndex::new_with(meta.event_id.clone())) + } + _ => StoreIndex::Lifo(LifoIndex::new_with(meta.event_id.clone())), + } + } + + pub fn insert(&mut self, meta: &EventMeta) { + match self { + StoreIndex::Lifo(l) => l.insert(meta.event_id.clone()), + StoreIndex::Filo(l) => l.insert(meta.event_id.clone()), + StoreIndex::Ranked(r) => r.insert(meta.timestamp.clone(), meta.event_id.clone()), + } + } + + /// All instances of this element from the vector + pub fn remove(&mut self, value: &C::ObjectId) { + match self { + StoreIndex::Lifo(idx) => idx.remove(value), + StoreIndex::Filo(idx) => idx.remove(value), + StoreIndex::Ranked(ranked_index) => ranked_index.remove(value), + } + } + + /// Returns the current list of values in order of when they were added + pub fn values(&self) -> Vec<&C::ObjectId> { + match self { + StoreIndex::Lifo(idx) => idx.values(), + StoreIndex::Filo(idx) => idx.values(), + StoreIndex::Ranked(ranked_index) => ranked_index.values(), + } + } + + // pub fn update_stream(&self) -> impl Stream> { + // match self { + // StoreIndex::Lifo(lifo_index) => lifo_index.update_stream(), + // StoreIndex::Filo(lifo_index) => lifo_index.update_stream(), + // StoreIndex::Ranked(ranked_index) => ranked_index.update_stream(), + // } + // } +} + +impl Debug for StoreIndex { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Lifo(_) => f.debug_tuple("Lifo").finish(), + Self::Filo(_) => f.debug_tuple("Filo").finish(), + Self::Ranked(_) => f.debug_tuple("Ranked").finish(), + } + } +} + +#[cfg(test)] +mod tests { + + use crate::mocks::MockError; + + use super::*; + use futures::pin_mut; + + #[tokio::test] + async fn test_ranked_index_for_u64() { + let mut index = RankedIndex::::default(); + index.insert(18, "18"); + index.insert(20, "20"); + index.insert(5, "5"); + index.insert(8, "8"); + + assert_eq!(index.values(), [&"20", &"18", &"8", &"5",]); + + let stream = index.update_stream(); + + index.remove(&"8"); + index.remove(&"18"); + + assert_eq!(index.values(), [&"20", &"5"]); + pin_mut!(stream); + // ensure the right types and values + assert!(matches!( + stream.next().await.unwrap(), + VectorDiff::Remove { index: 2 } + )); + assert!(matches!( + stream.next().await.unwrap(), + VectorDiff::Remove { index: 1 } + )); + } + + #[tokio::test] + async fn test_lifo_index_for_u64() { + let mut index = LifoIndex::<&'static str>::default(); + index.insert("18"); + index.insert("20"); + index.insert("5"); + index.insert("8"); + + assert_eq!(index.values(), [&"8", &"5", &"20", &"18",]); + + let stream = index.update_stream(); + + index.remove(&"18"); + index.remove(&"8"); + + assert_eq!(index.values(), [&"5", &"20",]); + pin_mut!(stream); + // ensure the right types and values + assert!(matches!( + stream.next().await.unwrap(), + VectorDiff::Remove { index: 3 } + )); + assert!(matches!( + stream.next().await.unwrap(), + VectorDiff::Remove { index: 0 } + )); + } + + #[test] + fn test_ranked_index_deref() { + let mut index = RankedIndex::::default(); + index.insert(10, "10"); + index.insert(20, "20"); + + // Test Deref implementation + let vector_ref: &ObservableVector<(u64, &'static str)> = &index; + assert_eq!(vector_ref.len(), 2); + assert_eq!(vector_ref[0], (20, "20")); + assert_eq!(vector_ref[1], (10, "10")); + } + + #[test] + fn test_lifo_index_deref() { + let mut index = LifoIndex::<&'static str>::default(); + index.insert("first"); + index.insert("second"); + + // Test Deref implementation + let vector_ref: &ObservableVector<&'static str> = &index; + assert_eq!(vector_ref.len(), 2); + assert_eq!(vector_ref[0], "second"); + assert_eq!(vector_ref[1], "first"); + } + + #[test] + fn test_filo_index_deref() { + let mut index = FiloIndex::<&'static str>::default(); + index.insert("first"); + index.insert("second"); + + // Test Deref implementation + let vector_ref: &ObservableVector<&'static str> = &index; + assert_eq!(vector_ref.len(), 2); + assert_eq!(vector_ref[0], "first"); + assert_eq!(vector_ref[1], "second"); + } + + #[test] + fn test_store_index_initialization() { + use crate::meta::EventMeta; + use crate::referencing::{IndexKey, ObjectListIndex, SectionIndex}; + use crate::traits::TypeConfig; + use core::hash::Hash; + + // Mock TypeConfig for testing + #[derive(Debug, Clone, PartialEq, Eq, Hash)] + struct MockConfig; + + impl TypeConfig for MockConfig { + type RoomId = String; + type ObjectId = String; + type ModelType = String; + type AccountData = String; + type UserId = String; + type Timestamp = u64; + type RedactionReason = String; + type Error = MockError; + } + + let meta = EventMeta:: { + event_id: "test_event".to_string(), + sender: "@user:example.com".to_string(), + timestamp: 12345, + room_id: "!room:example.com".to_string(), + redacted: None, + }; + + // Test AllHistory + let key = IndexKey::AllHistory; + let index = StoreIndex::::new_for(&key, &meta); + assert!(matches!(index, StoreIndex::Ranked(_))); + + // Test ObjectHistory + let key = IndexKey::ObjectHistory("obj1".to_string()); + let index = StoreIndex::::new_for(&key, &meta); + assert!(matches!(index, StoreIndex::Ranked(_))); + + // Test RoomHistory + let key = IndexKey::RoomHistory("room1".to_string()); + let index = StoreIndex::::new_for(&key, &meta); + assert!(matches!(index, StoreIndex::Ranked(_))); + + // Test ObjectList with Rsvp + let key = IndexKey::ObjectList("obj1".to_string(), ObjectListIndex::Rsvp); + let index = StoreIndex::::new_for(&key, &meta); + assert!(matches!(index, StoreIndex::Ranked(_))); + + // Test ObjectList with Tasks + let key = IndexKey::ObjectList("obj1".to_string(), ObjectListIndex::Tasks); + let index = StoreIndex::::new_for(&key, &meta); + assert!(matches!(index, StoreIndex::Filo(_))); + + // Test Section Boosts + let key = IndexKey::Section(SectionIndex::Boosts); + let index = StoreIndex::::new_for(&key, &meta); + assert!(matches!(index, StoreIndex::Ranked(_))); + + // Test Section Stories + let key = IndexKey::Section(SectionIndex::Stories); + let index = StoreIndex::::new_for(&key, &meta); + assert!(matches!(index, StoreIndex::Ranked(_))); + + // Test RoomSection Boosts + let key = IndexKey::RoomSection("room1".to_string(), SectionIndex::Boosts); + let index = StoreIndex::::new_for(&key, &meta); + assert!(matches!(index, StoreIndex::Ranked(_))); + + // Test RoomSection Stories + let key = IndexKey::RoomSection("room1".to_string(), SectionIndex::Stories); + let index = StoreIndex::::new_for(&key, &meta); + assert!(matches!(index, StoreIndex::Ranked(_))); + + // Test default case (should be Lifo) + let key = IndexKey::ObjectList("obj1".to_string(), ObjectListIndex::Comments); + let index = StoreIndex::::new_for(&key, &meta); + assert!(matches!(index, StoreIndex::Lifo(_))); + } + + #[test] + fn test_store_index_operations() { + use crate::meta::EventMeta; + use crate::referencing::{IndexKey, ObjectListIndex}; + use crate::traits::TypeConfig; + use core::hash::Hash; + + // Mock TypeConfig for testing + #[derive(Debug, Clone, PartialEq, Eq, Hash)] + struct MockConfig; + + impl TypeConfig for MockConfig { + type RoomId = String; + type ObjectId = String; + type ModelType = String; + type AccountData = String; + type UserId = String; + type Timestamp = u64; + type RedactionReason = String; + type Error = MockError; + } + + let meta1 = EventMeta:: { + event_id: "event1".to_string(), + sender: "@user1:example.com".to_string(), + timestamp: 100, + room_id: "!room1:example.com".to_string(), + redacted: None, + }; + let meta2 = EventMeta:: { + event_id: "event2".to_string(), + sender: "@user2:example.com".to_string(), + timestamp: 200, + room_id: "!room2:example.com".to_string(), + redacted: None, + }; + + // Test Lifo index operations + let key = IndexKey::ObjectList("obj1".to_string(), ObjectListIndex::Comments); + let mut index = StoreIndex::::new_for(&key, &meta1); + + index.insert(&meta2); + + assert_eq!(index.values(), [&"event2", &"event1"]); + + index.remove(&"event1".to_string()); + assert_eq!(index.values(), [&"event2"]); + + // Test Filo index operations + let key = IndexKey::ObjectList("obj1".to_string(), ObjectListIndex::Tasks); + let mut index = StoreIndex::::new_for(&key, &meta1); + + index.insert(&meta2); + + assert_eq!(index.values(), [&"event1", &"event2"]); + + index.remove(&"event1".to_string()); + assert_eq!(index.values(), [&"event2"]); + + // Test Ranked index operations + let key = IndexKey::AllHistory; + let mut index = StoreIndex::::new_for(&key, &meta1); + + index.insert(&meta2); + + assert_eq!(index.values(), [&"event2", &"event1"]); // Higher timestamp first + + index.remove(&"event1".to_string()); + assert_eq!(index.values(), [&"event2"]); + } + + #[test] + fn test_store_index_debug() { + use crate::meta::EventMeta; + use crate::referencing::{IndexKey, ObjectListIndex}; + use crate::traits::TypeConfig; + use core::hash::Hash; + + // Mock TypeConfig for testing + #[derive(Debug, Clone, PartialEq, Eq, Hash)] + struct MockConfig; + + impl TypeConfig for MockConfig { + type RoomId = String; + type ObjectId = String; + type ModelType = String; + type AccountData = String; + type UserId = String; + type Timestamp = u64; + type RedactionReason = String; + type Error = MockError; + } + + let meta = EventMeta:: { + event_id: "test_event".to_string(), + sender: "@user:example.com".to_string(), + timestamp: 12345, + room_id: "!room:example.com".to_string(), + redacted: None, + }; + + // Test Debug for Lifo + let key = IndexKey::ObjectList("obj1".to_string(), ObjectListIndex::Comments); + let index = StoreIndex::::new_for(&key, &meta); + assert_eq!(format!("{index:?}"), "Lifo"); + + // Test Debug for Filo + let key = IndexKey::ObjectList("obj1".to_string(), ObjectListIndex::Tasks); + let index = StoreIndex::::new_for(&key, &meta); + assert_eq!(format!("{index:?}"), "Filo"); + + // Test Debug for Ranked + let key = IndexKey::AllHistory; + let index = StoreIndex::::new_for(&key, &meta); + assert_eq!(format!("{index:?}"), "Ranked"); + } + + #[tokio::test] + async fn test_ranked_index_update_stream_all_cases() { + let mut index = RankedIndex::::default(); + let stream = index.update_stream(); + pin_mut!(stream); + + // Test Append + index.insert(10, "10"); + let next = stream.next().await.unwrap(); + assert!(matches!( + next, + VectorDiff::Insert { index: 0, value } if value == "10" + )); + + // we do internal calls to make sure all these are going through properly + index.vector.push_back((20, "20")); + let next = stream.next().await.unwrap(); + assert!(matches!( + next, + VectorDiff::PushBack { value } if value == "20" + )); + + index.vector.push_front((5, "5")); + + let next = stream.next().await.unwrap(); + assert!(matches!( + next, + VectorDiff::PushFront { value } if value == "5" + )); + + index.vector.append([(25, "25"), (35, "35")].into()); + + let next = stream.next().await.unwrap(); + assert!(matches!( + next, + VectorDiff::Append { ref values } if *values == vec!["25", "35"].into() + )); + + // // Test Set + // // Note: ObservableVector doesn't have a direct set method, so we'll test other cases + + // Test Remove + index.remove(&"10"); + let next = stream.next().await.unwrap(); + assert!(matches!(next, VectorDiff::Remove { index: 1 }), "{next:?}"); + + // Test Clear + index.remove(&"5"); + index.remove(&"20"); + let next = stream.next().await.unwrap(); + assert!(matches!(next, VectorDiff::Remove { index: 0 }), "{next:?}"); + let next = stream.next().await.unwrap(); + assert!(matches!(next, VectorDiff::Remove { index: 0 }), "{next:?}"); + } + + #[tokio::test] + async fn test_lifo_index_update_stream_all_cases() { + let mut index = LifoIndex::<&'static str>::default(); + let stream = index.update_stream(); + pin_mut!(stream); + + // Test PushFront + index.insert("first"); + index.insert("second"); + + let next = stream.next().await.unwrap(); + assert!(matches!( + next, + VectorDiff::PushFront { value } if value == "first" + )); + let next = stream.next().await.unwrap(); + assert!(matches!( + next, + VectorDiff::PushFront { value } if value == "second" + )); + + // Test Remove + index.remove(&"first"); + let next = stream.next().await.unwrap(); + assert!(matches!(next, VectorDiff::Remove { index: 1 }), "{next:?}"); + + // Test Clear + index.remove(&"second"); + let next = stream.next().await.unwrap(); + assert!(matches!(next, VectorDiff::Remove { index: 0 }), "{next:?}"); + } + + #[tokio::test] + async fn test_filo_index_update_stream_all_cases() { + let mut index = FiloIndex::<&'static str>::default(); + let stream = index.update_stream(); + pin_mut!(stream); + + // Test PushBack + index.insert("first"); + index.insert("second"); + let next = stream.next().await.unwrap(); + assert!(matches!( + next, + VectorDiff::PushBack { value } if value == "first" + )); + let next = stream.next().await.unwrap(); + assert!(matches!( + next, + VectorDiff::PushBack { value } if value == "second" + )); + + // Test Remove + index.remove(&"first"); + let next = stream.next().await.unwrap(); + assert!(matches!(next, VectorDiff::Remove { index: 0 }), "{next:?}"); + + // Test Clear + index.remove(&"second"); + let next = stream.next().await.unwrap(); + assert!(matches!(next, VectorDiff::Remove { index: 0 }), "{next:?}"); + } +} diff --git a/native/core/src/traits/config.rs b/native/core/src/traits/config.rs new file mode 100644 index 000000000000..1054490e3b0f --- /dev/null +++ b/native/core/src/traits/config.rs @@ -0,0 +1,16 @@ +use crate::traits::{Error, StoreError}; + +use super::{AccountData, ModelType, ObjectId, RedactionReason, RoomId, Timestamp, UserId}; +use std::hash::Hash; + +// Configure the types related as one +pub trait TypeConfig: core::fmt::Debug + PartialEq + Eq + Hash { + type RoomId: RoomId; + type ObjectId: ObjectId; + type ModelType: ModelType; + type AccountData: AccountData; + type UserId: UserId; + type Timestamp: Timestamp; + type RedactionReason: RedactionReason; + type Error: Error + StoreError; +} diff --git a/native/core/src/traits/error.rs b/native/core/src/traits/error.rs new file mode 100644 index 000000000000..47ba12941308 --- /dev/null +++ b/native/core/src/traits/error.rs @@ -0,0 +1 @@ +pub trait Error = core::error::Error + Send; diff --git a/native/core/src/traits/mod.rs b/native/core/src/traits/mod.rs new file mode 100644 index 000000000000..3d467126e70f --- /dev/null +++ b/native/core/src/traits/mod.rs @@ -0,0 +1,11 @@ +mod config; +mod error; +mod model; +mod primitives; +mod store; + +pub use config::*; +pub use error::*; +pub use model::*; +pub use primitives::*; +pub use store::*; diff --git a/native/core/src/traits/model.rs b/native/core/src/traits/model.rs new file mode 100644 index 000000000000..8c03b92c3836 --- /dev/null +++ b/native/core/src/traits/model.rs @@ -0,0 +1,24 @@ +use crate::{ + referencing::ExecuteReference, + traits::{StoreT, TypeConfig}, +}; + +pub trait ModelT: core::fmt::Debug + Clone + Send { + fn belongs_to(&self) -> Option>; + fn object_id(&self) -> C::ObjectId; + + fn execute + Sync + 'static>( + self, + store: &T, + ) -> impl core::future::Future>, C::Error>> + Send; + + fn transition(&mut self, model: &Self) -> Result; + + fn is_redacted(&self) -> bool; + + fn redact + Sync + 'static>( + &self, + store: &T, + reason: Option, + ) -> impl core::future::Future>, C::Error>> + Send; +} diff --git a/native/core/src/traits/primitives.rs b/native/core/src/traits/primitives.rs new file mode 100644 index 000000000000..e582451b52b0 --- /dev/null +++ b/native/core/src/traits/primitives.rs @@ -0,0 +1,17 @@ +use core::hash::Hash; +use serde::{Deserialize, Serialize}; + +pub trait ActerKeyable = AsRef; +pub trait ActerCoreSerial = + Serialize + for<'de> Deserialize<'de> + std::fmt::Debug + Clone + Sized + Send + Sync; + +pub trait ActerCoreHash = Hash + Eq + PartialEq + Send + Sync; + +pub trait ObjectId = ActerCoreSerial + ActerKeyable + ActerCoreHash; +pub trait RoomId = ActerCoreSerial + ActerKeyable + ActerCoreHash; +pub trait ModelType = ActerCoreSerial + ActerKeyable + ActerCoreHash; +pub trait AccountData = ActerCoreSerial + ActerKeyable + ActerCoreHash; +pub trait UserId = ActerCoreSerial + ActerKeyable + ActerCoreHash; +pub trait Timestamp = ActerCoreSerial + Ord + PartialOrd; + +pub trait RedactionReason = ActerCoreSerial; diff --git a/native/core/src/traits/store.rs b/native/core/src/traits/store.rs new file mode 100644 index 000000000000..78d6cf20d0a6 --- /dev/null +++ b/native/core/src/traits/store.rs @@ -0,0 +1,32 @@ +use crate::{ + referencing::ExecuteReference, + traits::{Error, ModelT, TypeConfig}, +}; + +pub trait StoreError: Error { + fn is_not_found(&self) -> bool; +} + +pub trait StoreT: Send + Sync { + type Model: ModelT; + + fn get( + &self, + id: &C::ObjectId, + ) -> impl core::future::Future> + Send; + + fn save( + &self, + model: Self::Model, + ) -> impl core::future::Future>, C::Error>> + Send; + + fn save_many + Send>( + &self, + models: I, + ) -> impl core::future::Future>, C::Error>> + Send; + + fn clear_room( + &self, + room_id: &C::RoomId, + ) -> impl core::future::Future>, C::Error>> + Send; +} diff --git a/native/matrix/Cargo.toml b/native/matrix/Cargo.toml index bbf3c150fbb0..3cd0128d9079 100644 --- a/native/matrix/Cargo.toml +++ b/native/matrix/Cargo.toml @@ -3,7 +3,7 @@ name = "acter-matrix" authors = [ "ben@acter.global" ] version = "0.1.0" edition = "2021" -rust-version = "1.70" +rust-version = "1.88" description = "Matrix core transport implemenation of acter app" homepage = "https://acter.global" repository = "https://github.com/acterglobal/a3" @@ -22,6 +22,7 @@ unstable-exhaustive-types = [] testing = ['dep:uuid'] [dependencies] +acter-core = { path = "../core" } tokio = { version = "1", features = ["rt", "macros"] } async-recursion = "1" chrono = { version = "0.4", default-features = false, features = ["serde", "clock"] } @@ -39,7 +40,7 @@ serde_json = "1" serde_repr = "0.1" strum = { workspace = true } thiserror = "2" -tracing = { version = "0.1.40", features = ["log"] } +tracing = { workspace = true } url = { workspace = true } urlencoding = { workspace = true } uuid = { version = "1.6.1", features = ["v4"], optional = true } @@ -77,6 +78,7 @@ anyhow = "1.0.79" env_logger = { workspace = true } tracing = { version = "0.1.40", default-features = false, features = ["log"] } uuid = { version = "1.6.1", features = ["v4"] } +acter-core = { path = "../core", features = ["testing"] } [dev-dependencies.matrix-sdk-base] diff --git a/native/matrix/src/activities/object.rs b/native/matrix/src/activities/object.rs index a7b49137f719..c5159c96b970 100644 --- a/native/matrix/src/activities/object.rs +++ b/native/matrix/src/activities/object.rs @@ -79,15 +79,15 @@ impl ActivityObject { pub fn target_url(&self) -> String { match self { - ActivityObject::News { object_id } => format!("/updates/{}", object_id), - ActivityObject::Story { object_id } => format!("/updates/{}", object_id), - ActivityObject::Pin { object_id, .. } => format!("/pins/{}", object_id), - ActivityObject::TaskList { object_id, .. } => format!("/tasks/{}", object_id), + ActivityObject::News { object_id } => format!("/updates/{object_id}"), + ActivityObject::Story { object_id } => format!("/updates/{object_id}"), + ActivityObject::Pin { object_id, .. } => format!("/pins/{object_id}"), + ActivityObject::TaskList { object_id, .. } => format!("/tasks/{object_id}"), ActivityObject::Task { object_id, tl_id, .. } => format!("/tasks/{tl_id}/{object_id}"), ActivityObject::CalendarEvent { object_id, .. } => { - format!("/events/{}", object_id) + format!("/events/{object_id}") } ActivityObject::Unknown { object_id } => { format!("/forward?eventId={}", encode(object_id.as_str()),) diff --git a/native/matrix/src/client.rs b/native/matrix/src/client.rs index f5d70188c1bf..9091227bac2b 100644 --- a/native/matrix/src/client.rs +++ b/native/matrix/src/client.rs @@ -15,7 +15,7 @@ pub struct CoreClient { impl CoreClient { pub async fn new(client: Client) -> Result { let store = Store::new(client.clone()).await?; - let executor = Executor::new(store.clone()).await?; + let executor = Executor::new(store.clone()); client.add_event_handler_context(executor.clone()); Ok(CoreClient { diff --git a/native/matrix/src/config.rs b/native/matrix/src/config.rs new file mode 100644 index 000000000000..d5d3dfdc92c0 --- /dev/null +++ b/native/matrix/src/config.rs @@ -0,0 +1,24 @@ +use crate::models::RedactionContent; +pub use acter_core::referencing::{ + ModelParam, ObjectListIndex, RoomParam, SectionIndex, SpecialListsIndex, +}; +use acter_core::traits::TypeConfig; +use matrix_sdk::ruma::{MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedUserId}; +use std::borrow::Cow; + +pub type ModelType = Cow<'static, str>; +pub type AccountData = Cow<'static, str>; + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] +pub struct MatrixCoreTypeConfig; + +impl TypeConfig for MatrixCoreTypeConfig { + type RoomId = OwnedRoomId; + type ObjectId = OwnedEventId; + type ModelType = ModelType; + type AccountData = AccountData; + type UserId = OwnedUserId; + type Timestamp = MilliSecondsSinceUnixEpoch; + type RedactionReason = RedactionContent; + type Error = crate::Error; +} diff --git a/native/matrix/src/error.rs b/native/matrix/src/error.rs index 378ddf40a07c..ede2acb179db 100644 --- a/native/matrix/src/error.rs +++ b/native/matrix/src/error.rs @@ -2,6 +2,7 @@ use matrix_sdk_base::ruma::events::UnsignedRoomRedactionEvent; use std::sync::PoisonError; use crate::models::EventMeta; +use acter_core::traits::StoreError; #[derive(Debug)] pub struct ModelRedactedDetails { @@ -39,7 +40,7 @@ pub enum Error { #[error("Index not found.")] IndexNotFound, - #[error("Your Homeserver doesnโ€™t have a hostname, that is required for this action.")] + #[error("Your Homeserver doesn't have a hostname, that is required for this action.")] HomeserverMissesHostname, #[error("The client must be logged in for this interaction.")] @@ -77,3 +78,9 @@ impl From for Error { Self::HttpError(Box::new(err)) } } + +impl StoreError for Error { + fn is_not_found(&self) -> bool { + matches!(self, Error::ModelNotFound(_)) + } +} diff --git a/native/matrix/src/executor.rs b/native/matrix/src/executor.rs index 061e7028fb88..d7556d53f659 100644 --- a/native/matrix/src/executor.rs +++ b/native/matrix/src/executor.rs @@ -1,194 +1,19 @@ -use matrix_sdk::ruma::OwnedEventId; -use matrix_sdk_base::ruma::{ - events::{room::redaction::OriginalRoomRedactionEvent, UnsignedRoomRedactionEvent}, - OwnedRoomId, -}; -use scc::hash_map::{Entry, HashMap}; -use std::sync::Arc; -use tokio::sync::broadcast::{channel, Receiver, Sender}; -use tracing::{error, info, trace, trace_span, warn}; - -use crate::{ - models::{ActerModel, AnyActerModel, EventMeta, RedactedActerModel}, - referencing::ExecuteReference, - store::Store, - Error, Result, -}; - -#[derive(Clone, Debug)] -pub struct Executor { - store: Store, - notifiers: Arc>>, -} - -impl Executor { - pub async fn new(store: Store) -> Result { - Ok(Executor { - store, - notifiers: Default::default(), - }) - } - - pub fn store(&self) -> &Store { - &self.store - } - - pub fn subscribe>(&self, key: K) -> Receiver<()> { - match self.notifiers.entry(key.into()) { - Entry::Occupied(mut o) => { - let sender = o.get_mut(); - if sender.receiver_count() == 0 { - // replace the existing channel to reopen - let (sender, receiver) = channel(1); - o.insert(sender); - receiver - } else { - sender.subscribe() - } - } - Entry::Vacant(v) => { - let (sender, receiver) = channel(1); - v.insert_entry(sender); - receiver - } - } - } - - pub async fn wait_for(&self, key: OwnedEventId) -> Result { - let mut subscribe = self.subscribe(ExecuteReference::Model(key.clone())); - let Ok(model) = self.store.get(&key).await else { - if let Err(e) = subscribe.recv().await { - error!(event_id=?key, "Receiving pong failed: {e}"); - } - return self.store.get(&key).await; - }; - - Ok(model) - } - - pub fn notify(&self, mut keys: Vec) -> u32 { - let mut counter = 0u32; - keys.dedup(); - trace!(?keys, "notify"); - for key in keys { - let span = trace_span!("Asked to notify", key = ?key); - let _enter = span.enter(); - if let Entry::Occupied(o) = self.notifiers.entry(key) { - let v = o.get(); - if v.receiver_count() == 0 { - trace!("No listeners. removing"); - let _ = o.remove(); - continue; - } - trace!("Broadcasting"); - if let Err(error) = v.send(()) { - trace!(?error, "Notifying failed. No receivers. Clearing"); - // we have overflow activated, this only fails because it has been closed - let _ = o.remove(); - } else { - counter = counter.checked_add(1).unwrap_or(u32::MAX); - } - } else { - trace!("No one to notify"); - } - } - counter - } - - pub async fn handle(&self, model: AnyActerModel) -> Result<()> { - let event_id = model.event_id().to_string(); - trace!(?event_id, ?model, "handle"); - match model.execute(&self.store).await { - Err(error) => { - error!(?event_id, ?error, "handling failed"); - Err(error) - } - Ok(keys) => { - trace!(?event_id, "handling done"); - info!("******************** executor handled: {:?}", &keys); - self.notify(keys); - Ok(()) - } - } - } - - pub async fn clear_room(&self, room_id: &OwnedRoomId) -> Result<()> { - let keys = self.store.clear_room(room_id).await?; - self.notify(keys); - Ok(()) - } - - pub async fn redact( - &self, - model_type: String, - event_meta: EventMeta, - reason: UnsignedRoomRedactionEvent, - ) -> Result<()> { - let event_id = event_meta.event_id.clone(); - trace!(event_id=?event_id, ?model_type, "asked to redact"); - - match self.store.get(&event_id).await { - Ok(AnyActerModel::RedactedActerModel(_)) => { - info!(?event_id, "live redacted: Already redacted"); - } - Ok(model) => { - trace!("previous model found. overwriting"); - let redacted = RedactedActerModel::new(model_type, event_meta, reason.into()); - let keys = model.redact(&self.store, redacted).await?; - info!("******************** found model redacted: {:?}", &keys); - self.notify(keys); - } - Err(Error::ModelNotFound(_)) => { - trace!("no model found, storing redaction model"); - let redacted = RedactedActerModel::new(model_type, event_meta, reason.into()); - let keys = redacted.execute(&self.store).await?; - info!("******************** not found redacted: {:?}", &keys); - self.notify(keys); - } - Err(error) => return Err(error), - } - Ok(()) - } +use crate::{config::MatrixCoreTypeConfig, store::Store}; - pub async fn live_redact(&self, event: OriginalRoomRedactionEvent) -> Result<()> { - let Some(meta) = EventMeta::for_redacted_source(&event) else { - warn!(?event, "Redaction didnโ€™t contain any target. skipping."); - return Ok(()); - }; - - let event_id = meta.event_id.clone(); - - match self.store.get(&event_id).await { - Ok(AnyActerModel::RedactedActerModel(_)) => { - info!(?event_id, "live redacted: Already redacted"); - } - Ok(model) => { - trace!("live redacted: model found"); - let redacted = - RedactedActerModel::new(model.model_type().to_owned(), meta, event.into()); - let keys = model.redact(&self.store, redacted).await?; - info!(?event_id, "live redacted: {:?}", &keys); - self.notify(keys); - } - Err(Error::ModelNotFound(_)) => { - info!(?event_id, "live redaction: not found"); - self.notify(vec![ExecuteReference::Model(event_id)]); - } - Err(error) => return Err(error), - } - Ok(()) - } -} +pub type Executor = acter_core::executor::Executor; #[cfg(test)] mod tests { + use std::borrow::Cow; use super::*; use crate::{ events::{comments::CommentEventContent, BelongsTo}, - models::{Comment, TestModelBuilder}, - referencing::{IndexKey, ObjectListIndex}, + models::{ActerModel, AnyActerModel, Comment, TestModelBuilder}, + referencing::{IndexKey, IntoExecuteReference, ObjectListIndex}, + store::Store, }; + use anyhow::Result; use matrix_sdk::Client; use matrix_sdk_base::{ ruma::{ @@ -196,7 +21,6 @@ mod tests { }, store::{MemoryStore, StoreConfig}, }; - use serde_json::{from_value, json}; async fn fresh_executor() -> Result { let config = StoreConfig::new("tests".to_owned()).state_store(MemoryStore::new()); @@ -209,7 +33,7 @@ mod tests { .unwrap(); let store = Store::new_with_auth(client, user_id!("@test:example.org").to_owned()).await?; - Executor::new(store).await + Ok(Executor::new(store)) } #[tokio::test] @@ -225,7 +49,7 @@ mod tests { let executor = fresh_executor().await?; let model = TestModelBuilder::default().simple().build().unwrap(); let model_id = model.event_id(); - let sub = executor.subscribe(model_id); + let sub = executor.subscribe(IntoExecuteReference::into(model_id)); assert!(sub.is_empty(), "Already received an event"); executor.handle(model.into()).await?; @@ -240,7 +64,7 @@ mod tests { let executor = fresh_executor().await?; let model = TestModelBuilder::default().simple().build().unwrap(); let model_id = model.event_id().to_owned(); - let mut sub = executor.subscribe(model_id.clone()); + let mut sub = executor.subscribe(IntoExecuteReference::into(model_id.clone())); assert!(sub.is_empty()); executor.handle(model.into()).await?; @@ -268,7 +92,7 @@ mod tests { let model = TestModelBuilder::default().simple().build().unwrap(); let parent_id = model.event_id().to_owned(); let parent_idx = IndexKey::ObjectList(parent_id.clone(), ObjectListIndex::Attachments); - let mut sub = executor.subscribe(parent_idx.clone()); + let mut sub = executor.subscribe(IntoExecuteReference::into(parent_idx.clone())); assert!(sub.is_empty()); executor.handle(model.into()).await?; @@ -296,7 +120,7 @@ mod tests { let model = TestModelBuilder::default().simple().build().unwrap(); let parent_id = model.event_id().to_owned(); let parent_idx = Comment::index_for(parent_id.clone()); - let mut sub = executor.subscribe(parent_idx.clone()); + let mut sub = executor.subscribe(IntoExecuteReference::into(parent_idx.clone())); assert!(sub.is_empty()); executor.handle(model.into()).await?; @@ -364,18 +188,11 @@ mod tests { // now letโ€™s redact this model; - let redaction: UnsignedRoomRedactionEvent = from_value(json!({ - "event_id" : format!("{model_id}:redacted"), - "sender": "@someone:example.org", - "origin_server_ts": 123456, - "content": { "redacts" : model_id, }, - }))?; - executor .redact( - "test_model".to_owned(), + Cow::Owned("test_model".to_owned()), model.event_meta().clone(), - redaction, + None, ) .await?; @@ -387,18 +204,11 @@ mod tests { // redacting again - let redaction: UnsignedRoomRedactionEvent = from_value(json!({ - "event_id" : format!("{model_id}:redacted2"), - "sender": "@someone:example.org", - "origin_server_ts": 123456, - "content": { "redacts" : model_id, }, - }))?; - executor .redact( - "test_model".to_owned(), + Cow::Owned("test_model".to_owned()), model.event_meta().clone(), - redaction, + None, ) .await?; diff --git a/native/matrix/src/lib.rs b/native/matrix/src/lib.rs index 2aced25b7263..9b5a3d24c294 100644 --- a/native/matrix/src/lib.rs +++ b/native/matrix/src/lib.rs @@ -1,13 +1,14 @@ #![warn(clippy::all)] -#![feature(slice_as_chunks)] #![allow(refining_impl_trait)] // we are using enum_dispatch and need this for now #![allow(async_fn_in_trait)] +#![feature(trait_alias)] pub use matrix_sdk; pub use matrix_sdk::ruma; pub mod activities; pub mod client; +pub mod config; pub mod error; pub mod events; pub mod executor; diff --git a/native/matrix/src/models.rs b/native/matrix/src/models.rs index 1693f5364ed7..924bcdea1571 100644 --- a/native/matrix/src/models.rs +++ b/native/matrix/src/models.rs @@ -30,12 +30,12 @@ pub use common::*; pub use core::fmt::Debug; pub(crate) use execution::default_model_execute; pub use invites::{InvitationsManager, InviteStats, MyInvitesManager}; -pub use meta::{can_redact, EventMeta}; +pub use meta::{can_redact, event_meta_for_redacted_source, EventMeta}; pub use news::{NewsEntry, NewsEntryUpdate}; pub use pins::{Pin, PinUpdate}; pub use reactions::{Reaction, ReactionManager, ReactionStats}; pub use read_receipts::{ReadReceipt, ReadReceiptStats, ReadReceiptsManager}; -pub use redaction::RedactedActerModel; +pub use redaction::{RedactedActerModel, RedactionContent}; pub use rsvp::{Rsvp, RsvpManager, RsvpStats}; pub use status::{ActerSupportedRoomStatusEvents, RoomStatus}; pub use stories::{Story, StoryUpdate}; diff --git a/native/matrix/src/models/any.rs b/native/matrix/src/models/any.rs index e7970eff8bc9..036214a5d440 100644 --- a/native/matrix/src/models/any.rs +++ b/native/matrix/src/models/any.rs @@ -3,7 +3,6 @@ use super::calendar::{CalendarEvent, CalendarEventUpdate}; use super::capabilities::Capability; use super::comments::{Comment, CommentUpdate}; use super::conversion::ParseError; -pub(crate) use super::execution::transition_tree; use super::invites::ExplicitInvite; use super::meta::EventMeta; use super::news::{NewsEntry, NewsEntryUpdate}; @@ -22,12 +21,14 @@ use matrix_sdk_base::ruma::{ EventId, OwnedEventId, RoomId, UserId, }; use serde::{Deserialize, Serialize}; +use std::borrow::Cow; use tracing::{error, trace, warn}; #[cfg(any(test, feature = "testing"))] use super::test::TestModel; use crate::error::ModelRedactedDetails; +use crate::models::execution::transition_tree; use crate::store::Store; use crate::{ events::{ @@ -97,10 +98,10 @@ pub trait ActerModel: Debug { return store.save(redaction_model.into()).await; }; let model: AnyActerModel = redaction_model.into(); - trace!(event_id=?model.event_id(), ?belongs_to, "transitioning tree"); + trace!(event_id=?ActerModel::event_id(&model), ?belongs_to, "transitioning tree"); let mut models = transition_tree(store, belongs_to, &model).await?; models.push(model); - store.save_many(models).await + store.save_many(models.into_iter()).await } } @@ -199,7 +200,10 @@ impl AnyActerModel { reason, } = *details; trace!(?meta.room_id, model_type, ?meta.event_id, "redacted event"); - if let Err(e) = executor.redact(model_type, meta, reason).await { + if let Err(e) = executor + .redact(Cow::Owned(model_type), meta, Some(reason.into())) + .await + { error!("Failure redacting {:}", e); } } diff --git a/native/matrix/src/models/attachments.rs b/native/matrix/src/models/attachments.rs index 0f8e634e1ab1..e5c455b2643e 100644 --- a/native/matrix/src/models/attachments.rs +++ b/native/matrix/src/models/attachments.rs @@ -262,7 +262,7 @@ impl From> for Attachment { room_id, event_id, sender, - origin_server_ts, + timestamp: origin_server_ts, redacted: None, }, } @@ -320,7 +320,7 @@ impl From> for Attachment room_id, event_id, sender, - origin_server_ts, + timestamp: origin_server_ts, redacted: None, }, } diff --git a/native/matrix/src/models/calendar/event.rs b/native/matrix/src/models/calendar/event.rs index d6bf07ca8d96..c5e889103d03 100644 --- a/native/matrix/src/models/calendar/event.rs +++ b/native/matrix/src/models/calendar/event.rs @@ -145,7 +145,7 @@ impl From> for CalendarEvent room_id, event_id, sender, - origin_server_ts, + timestamp: origin_server_ts, redacted: None, }, } @@ -203,7 +203,7 @@ impl From> for Calenda room_id, event_id, sender, - origin_server_ts, + timestamp: origin_server_ts, redacted: None, }, } diff --git a/native/matrix/src/models/comments.rs b/native/matrix/src/models/comments.rs index 9b40263f34cf..0bf9a20e1ac6 100644 --- a/native/matrix/src/models/comments.rs +++ b/native/matrix/src/models/comments.rs @@ -234,7 +234,7 @@ impl From> for Comment { room_id, event_id, sender, - origin_server_ts, + timestamp: origin_server_ts, redacted: None, }, } @@ -291,7 +291,7 @@ impl From> for CommentUpdate room_id, event_id, sender, - origin_server_ts, + timestamp: origin_server_ts, redacted: None, }, } diff --git a/native/matrix/src/models/conversion.rs b/native/matrix/src/models/conversion.rs index 22e618edbf0b..be27d910cefb 100644 --- a/native/matrix/src/models/conversion.rs +++ b/native/matrix/src/models/conversion.rs @@ -44,7 +44,7 @@ impl TryFrom for AnyActerModel { room_id: r.room_id, event_id: r.event_id, sender: r.sender, - origin_server_ts: r.origin_server_ts, + timestamp: r.origin_server_ts, redacted: None, }, reason: r.unsigned.redacted_because, @@ -60,7 +60,7 @@ impl TryFrom for AnyActerModel { room_id: r.room_id, event_id: r.event_id, sender: r.sender, - origin_server_ts: r.origin_server_ts, + timestamp: r.origin_server_ts, redacted: None, }, reason: r.unsigned.redacted_because, @@ -76,7 +76,7 @@ impl TryFrom for AnyActerModel { room_id: r.room_id, event_id: r.event_id, sender: r.sender, - origin_server_ts: r.origin_server_ts, + timestamp: r.origin_server_ts, redacted: None, }, reason: r.unsigned.redacted_because, @@ -92,7 +92,7 @@ impl TryFrom for AnyActerModel { room_id: r.room_id, event_id: r.event_id, sender: r.sender, - origin_server_ts: r.origin_server_ts, + timestamp: r.origin_server_ts, redacted: None, }, reason: r.unsigned.redacted_because, @@ -108,7 +108,7 @@ impl TryFrom for AnyActerModel { room_id: r.room_id, event_id: r.event_id, sender: r.sender, - origin_server_ts: r.origin_server_ts, + timestamp: r.origin_server_ts, redacted: None, }, reason: r.unsigned.redacted_because, @@ -124,7 +124,7 @@ impl TryFrom for AnyActerModel { room_id: r.room_id, event_id: r.event_id, sender: r.sender, - origin_server_ts: r.origin_server_ts, + timestamp: r.origin_server_ts, redacted: None, }, reason: r.unsigned.redacted_because, @@ -141,7 +141,7 @@ impl TryFrom for AnyActerModel { room_id: r.room_id, event_id: r.event_id, sender: r.sender, - origin_server_ts: r.origin_server_ts, + timestamp: r.origin_server_ts, redacted: None, }, reason: r.unsigned.redacted_because, @@ -157,7 +157,7 @@ impl TryFrom for AnyActerModel { room_id: r.room_id, event_id: r.event_id, sender: r.sender, - origin_server_ts: r.origin_server_ts, + timestamp: r.origin_server_ts, redacted: None, }, reason: r.unsigned.redacted_because, @@ -173,7 +173,7 @@ impl TryFrom for AnyActerModel { room_id: r.room_id, event_id: r.event_id, sender: r.sender, - origin_server_ts: r.origin_server_ts, + timestamp: r.origin_server_ts, redacted: None, }, reason: r.unsigned.redacted_because, @@ -189,7 +189,7 @@ impl TryFrom for AnyActerModel { room_id: r.room_id, event_id: r.event_id, sender: r.sender, - origin_server_ts: r.origin_server_ts, + timestamp: r.origin_server_ts, redacted: None, }, reason: r.unsigned.redacted_because, @@ -205,7 +205,7 @@ impl TryFrom for AnyActerModel { room_id: r.room_id, event_id: r.event_id, sender: r.sender, - origin_server_ts: r.origin_server_ts, + timestamp: r.origin_server_ts, redacted: None, }, reason: r.unsigned.redacted_because, @@ -221,7 +221,7 @@ impl TryFrom for AnyActerModel { room_id: r.room_id, event_id: r.event_id, sender: r.sender, - origin_server_ts: r.origin_server_ts, + timestamp: r.origin_server_ts, redacted: None, }, reason: r.unsigned.redacted_because, @@ -237,7 +237,7 @@ impl TryFrom for AnyActerModel { room_id: r.room_id, event_id: r.event_id, sender: r.sender, - origin_server_ts: r.origin_server_ts, + timestamp: r.origin_server_ts, redacted: None, }, reason: r.unsigned.redacted_because, @@ -253,7 +253,7 @@ impl TryFrom for AnyActerModel { room_id: r.room_id, event_id: r.event_id, sender: r.sender, - origin_server_ts: r.origin_server_ts, + timestamp: r.origin_server_ts, redacted: None, }, reason: r.unsigned.redacted_because, @@ -269,7 +269,7 @@ impl TryFrom for AnyActerModel { room_id: r.room_id, event_id: r.event_id, sender: r.sender, - origin_server_ts: r.origin_server_ts, + timestamp: r.origin_server_ts, redacted: None, }, reason: r.unsigned.redacted_because, @@ -285,7 +285,7 @@ impl TryFrom for AnyActerModel { room_id: r.room_id, event_id: r.event_id, sender: r.sender, - origin_server_ts: r.origin_server_ts, + timestamp: r.origin_server_ts, redacted: None, }, reason: r.unsigned.redacted_because, @@ -301,7 +301,7 @@ impl TryFrom for AnyActerModel { room_id: r.room_id, event_id: r.event_id, sender: r.sender, - origin_server_ts: r.origin_server_ts, + timestamp: r.origin_server_ts, redacted: None, }, reason: r.unsigned.redacted_because, @@ -317,7 +317,7 @@ impl TryFrom for AnyActerModel { room_id: r.room_id, event_id: r.event_id, sender: r.sender, - origin_server_ts: r.origin_server_ts, + timestamp: r.origin_server_ts, redacted: None, }, reason: r.unsigned.redacted_because, @@ -333,7 +333,7 @@ impl TryFrom for AnyActerModel { room_id: r.room_id, event_id: r.event_id, sender: r.sender, - origin_server_ts: r.origin_server_ts, + timestamp: r.origin_server_ts, redacted: None, }, reason: r.unsigned.redacted_because, @@ -350,7 +350,7 @@ impl TryFrom for AnyActerModel { room_id: r.room_id, event_id: r.event_id, sender: r.sender, - origin_server_ts: r.origin_server_ts, + timestamp: r.origin_server_ts, redacted: None, }, reason: r.unsigned.redacted_because, @@ -367,7 +367,7 @@ impl TryFrom for AnyActerModel { room_id: r.room_id, event_id: r.event_id, sender: r.sender, - origin_server_ts: r.origin_server_ts, + timestamp: r.origin_server_ts, redacted: None, }, reason: r.unsigned.redacted_because, @@ -383,7 +383,7 @@ impl TryFrom for AnyActerModel { room_id: r.room_id, event_id: r.event_id, sender: r.sender, - origin_server_ts: r.origin_server_ts, + timestamp: r.origin_server_ts, redacted: None, }, reason: r.unsigned.redacted_because, diff --git a/native/matrix/src/models/execution.rs b/native/matrix/src/models/execution.rs index 924da5c8f1c6..2ca8833a6e4c 100644 --- a/native/matrix/src/models/execution.rs +++ b/native/matrix/src/models/execution.rs @@ -1,46 +1,157 @@ -use async_recursion::async_recursion; -use matrix_sdk_base::ruma::OwnedEventId; -use tracing::trace; +use acter_core::{ + execution::{ + default_model_execute as core_default_model_execute, + transition_tree as core_transition_tree, + }, + traits::{ModelT, StoreT, TypeConfig}, +}; +use matrix_sdk::ruma::{events::room::redaction::RoomRedactionEventContent, OwnedEventId}; +use crate::config::MatrixCoreTypeConfig; +use crate::models::{ActerModel, AnyActerModel, RedactedActerModel, RedactionContent}; use crate::referencing::ExecuteReference; -pub use crate::store::Store; +use crate::store::Store; -use super::{ActerModel, AnyActerModel}; +pub async fn default_model_execute( + store: &Store, + model: AnyActerModel, +) -> Result, crate::Error> { + core_default_model_execute::(store, model).await +} -#[async_recursion] pub async fn transition_tree( store: &Store, parents: Vec, model: &AnyActerModel, -) -> crate::Result> { - let mut models = vec![]; - for p in parents { - let mut parent = store.get(&p).await?; - if parent.transition(model)? { - if let Some(grandparents) = parent.belongs_to() { - let mut parent_models = transition_tree(store, grandparents, &parent).await?; - if !parent_models.is_empty() { - models.append(&mut parent_models); +) -> Result, crate::Error> { + core_transition_tree::< + MatrixCoreTypeConfig, + AnyActerModel, + Store, + std::vec::IntoIter, + >(store, parents.into_iter(), model) + .await +} + +impl ModelT for AnyActerModel { + fn object_id(&self) -> OwnedEventId { + ActerModel::event_id(self).to_owned() + } + + fn belongs_to(&self) -> Option> { + ActerModel::belongs_to(self) + } + + async fn execute + Sync + 'static>( + self, + store: &T, + ) -> Result, ::Error> { + // For now, we'll use a simple approach that works with our Store type + // In the future, we should make default_model_execute generic + if let Some(store_ref) = (store as &dyn std::any::Any).downcast_ref::() { + match default_model_execute(store_ref, self).await { + Ok(result) => Ok(result), + Err(_) => { + // If the conversion fails, we need to handle it differently + // For now, we'll return an empty result + Ok(vec![]) } } - models.push(parent); + } else { + // If it's not our Store type, we can't handle it yet + // This is a limitation of the current implementation + Ok(vec![]) + } + } + + fn transition( + &mut self, + model: &Self, + ) -> Result::Error> { + ActerModel::transition(self, model) + } + + fn is_redacted(&self) -> bool { + matches!(self, AnyActerModel::RedactedActerModel(_)) + } + + async fn redact + Sync + 'static>( + &self, + store: &T, + reason: Option, + ) -> Result, ::Error> { + let redacted_model = RedactedActerModel::new( + self.model_type().to_owned(), + self.event_meta().to_owned(), + reason.unwrap_or_else(|| { + // Create a default redaction content if none provided + RedactionContent { + content: RoomRedactionEventContent::new_v1(), + event_id: self.object_id(), + sender: "unknown".to_string().try_into().unwrap_or_else(|_| { + matrix_sdk_base::ruma::OwnedUserId::try_from("@unknown:example.com") + .unwrap_or_else(|_| { + matrix_sdk_base::ruma::OwnedUserId::try_from( + "@fallback:example.com", + ) + .expect("Hardcoded user ID will always work") + }) + }), + origin_server_ts: matrix_sdk_base::ruma::MilliSecondsSinceUnixEpoch::now(), + } + }), + ); + + // For now, we'll use a simple approach that works with our Store type + if let Some(store_ref) = (store as &dyn std::any::Any).downcast_ref::() { + match ::redact(self, store_ref, redacted_model).await { + Ok(result) => Ok(result), + Err(_) => Ok(vec![]), + } + } else { + // If it's not our Store type, we can't handle it yet + // This is a limitation of the current implementation + Ok(vec![]) } } - Ok(models) } -pub async fn default_model_execute( - store: &Store, - model: AnyActerModel, -) -> crate::Result> { - trace!(event_id=?model.event_id(), ?model, "handling"); - let Some(belongs_to) = model.belongs_to() else { - trace!(event_id=?model.event_id(), "saving simple model"); - return store.save(model).await; - }; - - trace!(event_id=?model.event_id(), ?belongs_to, "transitioning tree"); - let mut models = transition_tree(store, belongs_to, &model).await?; - models.push(model); - store.save_many(models).await +impl StoreT for Store { + type Model = AnyActerModel; + + fn get( + &self, + id: &OwnedEventId, + ) -> impl core::future::Future< + Output = Result::Error>, + > + Send { + self.get(id) + } + + fn save( + &self, + model: Self::Model, + ) -> impl core::future::Future< + Output = Result, ::Error>, + > + Send { + self.save(model.clone()) + } + + fn save_many + Send>( + &self, + models: I, + ) -> impl core::future::Future< + Output = Result, ::Error>, + > + Send { + self.save_many(models) + } + + fn clear_room( + &self, + room_id: &::RoomId, + ) -> impl core::future::Future< + Output = Result, ::Error>, + > + Send { + self.clear_room(room_id) + } } diff --git a/native/matrix/src/models/invites.rs b/native/matrix/src/models/invites.rs index ffbadf6d5613..368119f16ce7 100644 --- a/native/matrix/src/models/invites.rs +++ b/native/matrix/src/models/invites.rs @@ -262,7 +262,7 @@ impl From> for ExplicitInvi room_id, event_id, sender, - origin_server_ts, + timestamp: origin_server_ts, redacted: None, }, } diff --git a/native/matrix/src/models/meta.rs b/native/matrix/src/models/meta.rs index 57058496ec2b..d690af62b2a4 100644 --- a/native/matrix/src/models/meta.rs +++ b/native/matrix/src/models/meta.rs @@ -1,49 +1,27 @@ -use matrix_sdk::room::Room; -use matrix_sdk_base::ruma::{ - events::room::redaction::OriginalRoomRedactionEvent, MilliSecondsSinceUnixEpoch, OwnedEventId, - OwnedRoomId, OwnedUserId, UserId, +use crate::config::MatrixCoreTypeConfig; +use acter_core::meta::EventMeta as CoreEventMeta; +use matrix_sdk::{ + ruma::{events::room::redaction::OriginalRoomRedactionEvent, UserId}, + Room, }; +pub type EventMeta = CoreEventMeta; -use serde::{Deserialize, Serialize}; +pub fn event_meta_for_redacted_source(value: &OriginalRoomRedactionEvent) -> Option { + let target_event_id: matrix_sdk::ruma::OwnedEventId = value.redacts.clone()?; -#[derive(Serialize, Deserialize, Debug, Clone)] -#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Eq))] -pub struct EventMeta { - /// The globally unique event identifier attached to this event - pub event_id: OwnedEventId, - - /// The fully-qualified ID of the user who sent created this event - pub sender: OwnedUserId, - - /// Timestamp in milliseconds on originating homeserver when the event was created - pub origin_server_ts: MilliSecondsSinceUnixEpoch, - - /// The ID of the room of this event - pub room_id: OwnedRoomId, - - /// Optional redacted event identifier - #[serde(default)] - pub(crate) redacted: Option, -} - -impl EventMeta { - pub fn for_redacted_source(value: &OriginalRoomRedactionEvent) -> Option { - let target_event_id = value.redacts.clone()?; - - Some(EventMeta { - event_id: target_event_id, - sender: value.sender.clone(), - room_id: value.room_id.clone(), - origin_server_ts: value.origin_server_ts, - redacted: None, - }) - } + Some(EventMeta { + event_id: target_event_id, + sender: value.sender.clone(), + room_id: value.room_id.clone(), + timestamp: value.origin_server_ts, + redacted: None, + }) } pub async fn can_redact(room: &Room, sender_id: &UserId) -> crate::error::Result { let client = room.client(); let Some(user_id) = client.user_id() else { - // not logged in means we canโ€™t redact + // not logged in means we can't redact return Ok(false); }; Ok(if sender_id == user_id { @@ -52,3 +30,406 @@ pub async fn can_redact(room: &Room, sender_id: &UserId) -> crate::error::Result room.can_user_redact_other(user_id).await? }) } + +#[cfg(test)] +mod tests { + use super::*; + use matrix_sdk_base::ruma::{event_id, room_id, user_id, MilliSecondsSinceUnixEpoch, UInt}; + use serde_json::{json, Value}; + + fn create_matrix_style_event_meta() -> EventMeta { + EventMeta { + event_id: event_id!("$143273582443PhrSn:example.org").to_owned(), + sender: user_id!("@alice:example.org").to_owned(), + timestamp: MilliSecondsSinceUnixEpoch(UInt::new(1432735824653u64).unwrap()), + room_id: room_id!("!636q39766251:example.org").to_owned(), + redacted: None, + } + } + + fn create_matrix_style_event_json() -> Value { + json!({ + "event_id": "$143273582443PhrSn:example.org", + "sender": "@alice:example.org", + "origin_server_ts": 1432735824653u64, + "room_id": "!636q39766251:example.org", + "redacted": null + }) + } + + #[test] + fn test_event_meta_serialization_matrix_style() { + let event_meta = create_matrix_style_event_meta(); + let serialized = serde_json::to_string(&event_meta).unwrap(); + + // Verify the serialized format matches matrix.org style + let expected = r#"{"event_id":"$143273582443PhrSn:example.org","sender":"@alice:example.org","timestamp":1432735824653,"room_id":"!636q39766251:example.org","redacted":null}"#; + assert_eq!(serialized, expected); + } + + #[test] + fn test_event_meta_deserialization_matrix_style() { + let json = create_matrix_style_event_json(); + let deserialized: EventMeta = serde_json::from_value(json).unwrap(); + + let expected = create_matrix_style_event_meta(); + assert_eq!(deserialized, expected); + } + + #[test] + fn test_event_meta_roundtrip_matrix_style() { + let original = create_matrix_style_event_meta(); + let serialized = serde_json::to_string(&original).unwrap(); + let deserialized: EventMeta = serde_json::from_str(&serialized).unwrap(); + + assert_eq!(original, deserialized); + } + + #[test] + fn test_event_meta_with_redacted_event_matrix_style() { + let event_meta = EventMeta { + event_id: event_id!("$143273582443PhrSn:example.org").to_owned(), + sender: user_id!("@alice:example.org").to_owned(), + timestamp: MilliSecondsSinceUnixEpoch(UInt::new(1432735824653u64).unwrap()), + room_id: room_id!("!636q39766251:example.org").to_owned(), + redacted: Some(event_id!("$redacted_event:example.org").to_owned()), + }; + + let serialized = serde_json::to_string(&event_meta).unwrap(); + let deserialized: EventMeta = serde_json::from_str(&serialized).unwrap(); + + assert_eq!(event_meta, deserialized); + assert_eq!( + deserialized.redacted, + Some(event_id!("$redacted_event:example.org").to_owned()) + ); + } + + #[test] + fn test_event_meta_timestamp_alias_matrix_style() { + // Test that the "origin_server_ts" alias works for deserialization + let json = json!({ + "event_id": "$143273582443PhrSn:example.org", + "sender": "@alice:example.org", + "origin_server_ts": 1432735824653u64, + "room_id": "!636q39766251:example.org" + }); + + let deserialized: EventMeta = serde_json::from_value(json).unwrap(); + + assert_eq!( + deserialized.timestamp, + MilliSecondsSinceUnixEpoch(UInt::new(1432735824653u64).unwrap()) + ); + } + + #[test] + fn test_event_meta_missing_redacted_field_matrix_style() { + // Test that missing redacted field defaults to None + let json = json!({ + "event_id": "$143273582443PhrSn:example.org", + "sender": "@alice:example.org", + "origin_server_ts": 1432735824653u64, + "room_id": "!636q39766251:example.org" + }); + + let deserialized: EventMeta = serde_json::from_value(json).unwrap(); + + assert_eq!(deserialized.redacted, None); + } + + #[test] + fn test_event_meta_real_matrix_org_example() { + // Real matrix.org style event meta + let json = json!({ + "event_id": "$143273582443PhrSn:matrix.org", + "sender": "@alice:matrix.org", + "origin_server_ts": 1432735824653u64, + "room_id": "!636q39766251:matrix.org", + "redacted": null + }); + + let deserialized: EventMeta = serde_json::from_value(json).unwrap(); + + assert_eq!( + deserialized.event_id, + event_id!("$143273582443PhrSn:matrix.org").to_owned() + ); + assert_eq!( + deserialized.sender, + user_id!("@alice:matrix.org").to_owned() + ); + assert_eq!( + deserialized.timestamp, + MilliSecondsSinceUnixEpoch(UInt::new(1432735824653u64).unwrap()) + ); + assert_eq!( + deserialized.room_id, + room_id!("!636q39766251:matrix.org").to_owned() + ); + assert_eq!(deserialized.redacted, None); + } + + #[test] + fn test_event_meta_with_redacted_because_matrix_style() { + // Matrix.org style event with redacted_because field + let json = json!({ + "event_id": "$143273582443PhrSn:matrix.org", + "sender": "@alice:matrix.org", + "origin_server_ts": 1432735824653u64, + "room_id": "!636q39766251:matrix.org", + "redacted": "$redacted_event:matrix.org", + "redacted_because": { + "type": "m.room.redaction", + "room_id": "!636q39766251:matrix.org", + "sender": "@bob:matrix.org", + "content": { + "reason": "Spam" + }, + "redacts": "$143273582443PhrSn:matrix.org", + "origin_server_ts": 1432735825000u64, + "event_id": "$redacted_event:matrix.org" + } + }); + + let deserialized: EventMeta = serde_json::from_value(json).unwrap(); + + assert_eq!( + deserialized.event_id, + event_id!("$143273582443PhrSn:matrix.org").to_owned() + ); + assert_eq!( + deserialized.redacted, + Some(event_id!("$redacted_event:matrix.org").to_owned()) + ); + } + + #[test] + fn test_event_meta_with_unsigned_data_matrix_style() { + // Matrix.org style event with unsigned data (should be ignored for EventMeta) + let json = json!({ + "event_id": "$143273582443PhrSn:matrix.org", + "sender": "@alice:matrix.org", + "origin_server_ts": 1432735824653u64, + "room_id": "!636q39766251:matrix.org", + "unsigned": { + "age": 123456, + "transaction_id": "txn123" + } + }); + + let deserialized: EventMeta = serde_json::from_value(json).unwrap(); + + assert_eq!( + deserialized.event_id, + event_id!("$143273582443PhrSn:matrix.org").to_owned() + ); + assert_eq!( + deserialized.sender, + user_id!("@alice:matrix.org").to_owned() + ); + assert_eq!( + deserialized.timestamp, + MilliSecondsSinceUnixEpoch(UInt::new(1432735824653u64).unwrap()) + ); + assert_eq!( + deserialized.room_id, + room_id!("!636q39766251:matrix.org").to_owned() + ); + } + + #[test] + fn test_event_meta_with_special_characters_matrix_style() { + // Test with special characters in IDs (common in matrix.org) + let json = json!({ + "event_id": "$143273582443PhrSn_!@#$%^&*():matrix.org", + "sender": "@alice_with_underscores:matrix.org", + "origin_server_ts": 1432735824653u64, + "room_id": "!636q39766251_with-dashes:matrix.org", + "redacted": "$redacted_with_spaces:matrix.org" + }); + + let deserialized: EventMeta = serde_json::from_value(json).unwrap(); + + assert_eq!( + deserialized.event_id, + event_id!("$143273582443PhrSn_!@#$%^&*():matrix.org").to_owned() + ); + assert_eq!( + deserialized.sender, + user_id!("@alice_with_underscores:matrix.org").to_owned() + ); + assert_eq!( + deserialized.room_id, + room_id!("!636q39766251_with-dashes:matrix.org").to_owned() + ); + assert_eq!( + deserialized.redacted, + Some(event_id!("$redacted_with_spaces:matrix.org").to_owned()) + ); + } + + #[test] + fn test_event_meta_zero_timestamp_matrix_style() { + let event_meta = EventMeta { + event_id: event_id!("$zero_time:matrix.org").to_owned(), + sender: user_id!("@alice:matrix.org").to_owned(), + timestamp: MilliSecondsSinceUnixEpoch(UInt::new(0u64).unwrap()), + room_id: room_id!("!room:matrix.org").to_owned(), + redacted: None, + }; + + let serialized = serde_json::to_string(&event_meta).unwrap(); + let deserialized: EventMeta = serde_json::from_str(&serialized).unwrap(); + + assert_eq!(event_meta, deserialized); + assert_eq!( + deserialized.timestamp, + MilliSecondsSinceUnixEpoch(UInt::new(0u64).unwrap()) + ); + } + + #[test] + fn test_event_meta_large_timestamp_matrix_style() { + // Test with a large timestamp (future date) + let large_timestamp = UInt::new(1735689600000u64).unwrap(); // 2025-01-01 00:00:00 UTC + let event_meta = EventMeta { + event_id: event_id!("$large_time:matrix.org").to_owned(), + sender: user_id!("@alice:matrix.org").to_owned(), + timestamp: MilliSecondsSinceUnixEpoch(large_timestamp), + room_id: room_id!("!room:matrix.org").to_owned(), + redacted: None, + }; + + let serialized = serde_json::to_string(&event_meta).unwrap(); + let deserialized: EventMeta = serde_json::from_str(&serialized).unwrap(); + + assert_eq!(event_meta, deserialized); + assert_eq!( + deserialized.timestamp, + MilliSecondsSinceUnixEpoch(large_timestamp) + ); + } + + #[test] + fn test_event_meta_serialization_pretty_matrix_style() { + let event_meta = create_matrix_style_event_meta(); + let serialized = serde_json::to_string_pretty(&event_meta).unwrap(); + + // Verify it can be deserialized back + let deserialized: EventMeta = serde_json::from_str(&serialized).unwrap(); + assert_eq!(event_meta, deserialized); + } + + #[test] + fn test_event_meta_from_acter_event_style() { + // Extract just the EventMeta fields + let event_meta_json = json!({ + "event_id": "$KwumA4L3M-duXu0I3UA886LvN-BDCKAyxR1skNfnh3c", + "sender": "@odo:ds9.acter.global", + "origin_server_ts": 1672407531453u64, + "room_id": "!euhIDqDVvVXulrhWgN:ds9.acter.global", + "redacted": null + }); + + let deserialized: EventMeta = serde_json::from_value(event_meta_json).unwrap(); + + assert_eq!( + deserialized.event_id, + event_id!("$KwumA4L3M-duXu0I3UA886LvN-BDCKAyxR1skNfnh3c").to_owned() + ); + assert_eq!( + deserialized.sender, + user_id!("@odo:ds9.acter.global").to_owned() + ); + assert_eq!( + deserialized.timestamp, + MilliSecondsSinceUnixEpoch(UInt::new(1672407531453u64).unwrap()) + ); + assert_eq!( + deserialized.room_id, + room_id!("!euhIDqDVvVXulrhWgN:ds9.acter.global").to_owned() + ); + } + + #[test] + fn test_event_meta_redacted_acter_event_style() { + // Test parsing from redacted acter event style JSON + let event_meta_json = json!({ + "event_id": "$WAfv0heG198eXRIRPVVuli2Guc9pI2PB_spOcS8NXco", + "sender": "@emilvincentz:effektio.org", + "origin_server_ts": 1689158713657u64, + "room_id": "!uUufOaBOZwafrtxhoO:effektio.org", + "redacted": "$2_k7NsG2GOGfyeNOvV55OovysVl7WGKgGEY2hv6VosY" + }); + + let deserialized: EventMeta = serde_json::from_value(event_meta_json).unwrap(); + + assert_eq!( + deserialized.event_id, + event_id!("$WAfv0heG198eXRIRPVVuli2Guc9pI2PB_spOcS8NXco").to_owned() + ); + assert_eq!( + deserialized.sender, + user_id!("@emilvincentz:effektio.org").to_owned() + ); + assert_eq!( + deserialized.timestamp, + MilliSecondsSinceUnixEpoch(UInt::new(1689158713657u64).unwrap()) + ); + assert_eq!( + deserialized.room_id, + room_id!("!uUufOaBOZwafrtxhoO:effektio.org").to_owned() + ); + assert_eq!( + deserialized.redacted, + Some(event_id!("$2_k7NsG2GOGfyeNOvV55OovysVl7WGKgGEY2hv6VosY").to_owned()) + ); + } + + #[test] + fn test_event_meta_error_handling() { + // Test missing required fields + let json = json!({ + "sender": "@alice:matrix.org", + "origin_server_ts": 1432735824653u64, + "room_id": "!636q39766251:matrix.org" + }); + + let result: Result = serde_json::from_value(json); + assert!(result.is_err()); + + // Test invalid event_id format + let json = json!({ + "event_id": "invalid_event_id", + "sender": "@alice:matrix.org", + "origin_server_ts": 1432735824653u64, + "room_id": "!636q39766251:matrix.org" + }); + + let result: Result = serde_json::from_value(json); + assert!(result.is_err()); + + // Test invalid room_id format + let json = json!({ + "event_id": "$143273582443PhrSn:matrix.org", + "sender": "@alice:matrix.org", + "origin_server_ts": 1432735824653u64, + "room_id": "invalid_room_id" + }); + + let result: Result = serde_json::from_value(json); + assert!(result.is_err()); + + // Test invalid sender format + let json = json!({ + "event_id": "$143273582443PhrSn:matrix.org", + "sender": "invalid_sender", + "origin_server_ts": 1432735824653u64, + "room_id": "!636q39766251:matrix.org" + }); + + let result: Result = serde_json::from_value(json); + assert!(result.is_err()); + } +} diff --git a/native/matrix/src/models/news.rs b/native/matrix/src/models/news.rs index ebdc890d1d9c..d165089ca724 100644 --- a/native/matrix/src/models/news.rs +++ b/native/matrix/src/models/news.rs @@ -96,7 +96,7 @@ impl From> for NewsEntry { room_id, event_id, sender, - origin_server_ts, + timestamp: origin_server_ts, redacted: None, }, } @@ -154,7 +154,7 @@ impl From> for NewsEntryUp room_id, event_id, sender, - origin_server_ts, + timestamp: origin_server_ts, redacted: None, }, } diff --git a/native/matrix/src/models/pins.rs b/native/matrix/src/models/pins.rs index bed12eedc062..2084ae5e63bd 100644 --- a/native/matrix/src/models/pins.rs +++ b/native/matrix/src/models/pins.rs @@ -105,7 +105,7 @@ impl From> for Pin { room_id, event_id, sender, - origin_server_ts, + timestamp: origin_server_ts, redacted: None, }, } @@ -163,7 +163,7 @@ impl From> for PinUpdate { room_id, event_id, sender, - origin_server_ts, + timestamp: origin_server_ts, redacted: None, }, } diff --git a/native/matrix/src/models/reactions.rs b/native/matrix/src/models/reactions.rs index d55cd63a28f8..50d55673f36f 100644 --- a/native/matrix/src/models/reactions.rs +++ b/native/matrix/src/models/reactions.rs @@ -312,7 +312,7 @@ impl From> for Reaction { room_id, event_id, sender, - origin_server_ts, + timestamp: origin_server_ts, redacted: None, }, } diff --git a/native/matrix/src/models/read_receipts.rs b/native/matrix/src/models/read_receipts.rs index a559115eb37d..19e1e9734019 100644 --- a/native/matrix/src/models/read_receipts.rs +++ b/native/matrix/src/models/read_receipts.rs @@ -202,7 +202,7 @@ impl From> for ReadReceipt { room_id, event_id, sender, - origin_server_ts, + timestamp: origin_server_ts, redacted: None, }, } diff --git a/native/matrix/src/models/redaction.rs b/native/matrix/src/models/redaction.rs index 7b9fe6dc78ff..df1bf6165665 100644 --- a/native/matrix/src/models/redaction.rs +++ b/native/matrix/src/models/redaction.rs @@ -64,7 +64,6 @@ impl From for RedactionContent { } } } - #[derive(Serialize, Deserialize, Clone, Debug)] pub struct RedactedActerModel { orig_type: String, @@ -114,7 +113,7 @@ impl ActerModel for RedactedActerModel { } fn transition(&mut self, model: &AnyActerModel) -> crate::Result { - // Transitions arenโ€™t possible anymore when the source has been redacted + // Transitions aren't possible anymore when the source has been redacted // so we eat up the content and just log that we had to do that. info!(?self, ?model, "Transition on Redaction Swallowed"); Ok(false) diff --git a/native/matrix/src/models/rsvp.rs b/native/matrix/src/models/rsvp.rs index 8d0a2f7beffd..36c50a7a701a 100644 --- a/native/matrix/src/models/rsvp.rs +++ b/native/matrix/src/models/rsvp.rs @@ -187,7 +187,7 @@ impl From> for Rsvp { room_id, event_id, sender, - origin_server_ts, + timestamp: origin_server_ts, redacted: None, }, } diff --git a/native/matrix/src/models/status/mod.rs b/native/matrix/src/models/status/mod.rs index 63853d296b76..25c2408fe147 100644 --- a/native/matrix/src/models/status/mod.rs +++ b/native/matrix/src/models/status/mod.rs @@ -71,7 +71,7 @@ impl TryFrom for RoomStatus { event_id: event.event_id().to_owned(), room_id: event.room_id().to_owned(), sender: event.sender().to_owned(), - origin_server_ts: event.origin_server_ts(), + timestamp: event.origin_server_ts(), redacted: None, }; let make_err = |event| { diff --git a/native/matrix/src/models/stories.rs b/native/matrix/src/models/stories.rs index 3491a2f4a061..1877f3b16679 100644 --- a/native/matrix/src/models/stories.rs +++ b/native/matrix/src/models/stories.rs @@ -96,7 +96,7 @@ impl From> for Story { room_id, event_id, sender, - origin_server_ts, + timestamp: origin_server_ts, redacted: None, }, } @@ -154,7 +154,7 @@ impl From> for StoryUpdate { room_id, event_id, sender, - origin_server_ts, + timestamp: origin_server_ts, redacted: None, }, } diff --git a/native/matrix/src/models/tasks/task.rs b/native/matrix/src/models/tasks/task.rs index 8d3819770381..7cb1ea97607e 100644 --- a/native/matrix/src/models/tasks/task.rs +++ b/native/matrix/src/models/tasks/task.rs @@ -176,7 +176,7 @@ impl From> for Task { room_id, event_id, sender, - origin_server_ts, + timestamp: origin_server_ts, redacted: None, }, } @@ -234,7 +234,7 @@ impl From> for TaskUpdate { room_id, event_id, sender, - origin_server_ts, + timestamp: origin_server_ts, redacted: None, }, } @@ -311,7 +311,7 @@ impl From> for TaskSelfAssi room_id, event_id, sender, - origin_server_ts, + timestamp: origin_server_ts, redacted: None, }, } @@ -386,7 +386,7 @@ impl From> for TaskSelfUn room_id, event_id, sender, - origin_server_ts, + timestamp: origin_server_ts, redacted: None, }, } diff --git a/native/matrix/src/models/tasks/task_list.rs b/native/matrix/src/models/tasks/task_list.rs index bd4dbb6c243b..8c99b9391fe4 100644 --- a/native/matrix/src/models/tasks/task_list.rs +++ b/native/matrix/src/models/tasks/task_list.rs @@ -78,7 +78,7 @@ impl From> for TaskList { room_id, event_id, sender, - origin_server_ts, + timestamp: origin_server_ts, redacted: None, }, task_stats: Default::default(), @@ -182,7 +182,7 @@ impl From> for TaskListUpda room_id, event_id, sender, - origin_server_ts, + timestamp: origin_server_ts, redacted: None, }, } diff --git a/native/matrix/src/models/test.rs b/native/matrix/src/models/test.rs index d1907e672cff..edbd0edc46d2 100644 --- a/native/matrix/src/models/test.rs +++ b/native/matrix/src/models/test.rs @@ -45,7 +45,7 @@ impl TestModelBuilder { EventMeta { event_id: OwnedEventId::try_from(format!("${ev}")).unwrap(), sender: user_id!("@test:example.org").to_owned(), - origin_server_ts: MilliSecondsSinceUnixEpoch::from_system_time(SystemTime::now()) + timestamp: MilliSecondsSinceUnixEpoch::from_system_time(SystemTime::now()) .expect("We can parse system time"), room_id: OwnedRoomId::try_from(format!("!{room_id}:example.org")).unwrap(), redacted: None, @@ -74,7 +74,7 @@ impl TestModelBuilder { self.event_meta = Some(EventMeta { event_id, sender: user_id!("@test:example.org").to_owned(), - origin_server_ts, + timestamp: origin_server_ts, room_id, redacted: None, }); diff --git a/native/matrix/src/referencing.rs b/native/matrix/src/referencing.rs index 49ed3c1b0c46..66ebef6ee73c 100644 --- a/native/matrix/src/referencing.rs +++ b/native/matrix/src/referencing.rs @@ -1,165 +1,66 @@ -use std::borrow::Cow; - +use crate::config::{MatrixCoreTypeConfig, ModelType}; +use acter_core::referencing::{ExecuteReference as CoreExecuteReference, IndexKey as CoreIndexKey}; +pub use acter_core::referencing::{ + ModelParam, ObjectListIndex, RoomParam, SectionIndex, SpecialListsIndex, +}; use matrix_sdk::ruma::{EventId, OwnedEventId, OwnedRoomId, RoomId}; -use serde::{Deserialize, Serialize}; -use strum::{Display, EnumString}; - -#[derive( - Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Clone, Display, EnumString, Serialize, Deserialize, -)] -#[strum(serialize_all = "snake_case")] -#[repr(u8)] -pub enum SectionIndex { - #[strum(serialize = "news", serialize = "boosts")] - Boosts = 0, - Calendar, - Pins, - Stories, - Tasks, -} - -#[derive( - Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Clone, Display, EnumString, Serialize, Deserialize, -)] -#[strum(serialize_all = "snake_case")] -#[repr(u8)] -pub enum ObjectListIndex { - Attachments, - Comments, - Reactions, - ReadReceipt, - Rsvp, - Tasks, - Invites, -} - -#[derive( - Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Clone, Display, EnumString, Serialize, Deserialize, -)] -#[strum(serialize_all = "snake_case")] -#[repr(u8)] -pub enum SpecialListsIndex { - MyOpenTasks, - MyDoneTasks, - InvitedTo, - #[cfg(any(test, feature = "testing"))] - Test1, - #[cfg(any(test, feature = "testing"))] - Test2, - #[cfg(any(test, feature = "testing"))] - Test3, -} - -// We organize our Index by typed keys -#[derive(Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Clone, Serialize, Deserialize)] -pub enum IndexKey { - RoomHistory(OwnedRoomId), - RoomModels(OwnedRoomId), - ObjectHistory(OwnedEventId), - Section(SectionIndex), - RoomSection(OwnedRoomId, SectionIndex), - ObjectList(OwnedEventId, ObjectListIndex), - Special(SpecialListsIndex), - Redacted, - AllHistory, -} -#[derive( - Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Clone, Display, EnumString, Serialize, Deserialize, -)] -#[strum(serialize_all = "snake_case")] -pub enum ModelParam { - CommentsStats, - AttachmentsStats, - ReactionStats, - RsvpStats, - #[strum(to_string = "read_receipts")] - ReadReceiptsStats, - #[strum(to_string = "invites")] - InviteStats, +pub type ExecuteReference = CoreExecuteReference; +pub type IndexKey = CoreIndexKey; +pub trait IntoExecuteReference { + fn into(self) -> ExecuteReference; } -#[derive( - Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Clone, Display, EnumString, Serialize, Deserialize, -)] -#[strum(serialize_all = "snake_case")] -pub enum RoomParam { - LatestMessage, -} - -#[derive(Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Clone, Serialize, Deserialize)] -pub enum ExecuteReference { - Index(IndexKey), - Model(OwnedEventId), - Room(OwnedRoomId), - RoomAccountData(OwnedRoomId, Cow<'static, str>), - ModelParam(OwnedEventId, ModelParam), - RoomParam(OwnedRoomId, RoomParam), - AccountData(Cow<'static, str>), - ModelType(Cow<'static, str>), +impl IntoExecuteReference for OwnedRoomId { + fn into(self) -> ExecuteReference { + ExecuteReference::Room(self) + } } -impl ExecuteReference { - pub fn as_storage_key(&self) -> String { - match self { - ExecuteReference::Model(owned_event_id) => format!("acter::{owned_event_id}"), - ExecuteReference::ModelParam(owned_event_id, model_param) => { - format!("{owned_event_id}::{model_param}") - } - ExecuteReference::RoomParam(owned_room_id, room_param) => { - format!("{owned_room_id}::{room_param}") - } - ExecuteReference::ModelType(model_type) => model_type.to_string(), - ExecuteReference::Index(IndexKey::Special(SpecialListsIndex::InvitedTo)) => { - "global_invited".to_owned() // this is a special case, we actively store and manage - } - //ย not actually supported - ExecuteReference::Index(_index_key) => todo!(), - ExecuteReference::Room(_owned_room_id) => todo!(), - ExecuteReference::RoomAccountData(_owned_room_id, _cow) => todo!(), - ExecuteReference::AccountData(_cow) => todo!(), - } +impl IntoExecuteReference for &RoomId { + fn into(self) -> ExecuteReference { + ExecuteReference::Room(self.to_owned()) } } -impl From<&'static str> for ExecuteReference { - fn from(value: &'static str) -> Self { - ExecuteReference::ModelType(Cow::Borrowed(value)) +impl IntoExecuteReference for OwnedEventId { + fn into(self) -> ExecuteReference { + ExecuteReference::Model(self) } } -impl From<&EventId> for ExecuteReference { - fn from(value: &EventId) -> Self { - ExecuteReference::Model(value.to_owned()) +impl IntoExecuteReference for &EventId { + fn into(self) -> ExecuteReference { + ExecuteReference::Model(self.to_owned()) } } -impl From for ExecuteReference { - fn from(value: OwnedEventId) -> Self { - ExecuteReference::Model(value) +impl IntoExecuteReference for ModelType { + fn into(self) -> ExecuteReference { + ExecuteReference::ModelType(self) } } -impl From<&RoomId> for ExecuteReference { - fn from(value: &RoomId) -> Self { - ExecuteReference::Room(value.to_owned()) +impl IntoExecuteReference for ExecuteReference { + fn into(self) -> ExecuteReference { + self } } -impl From for ExecuteReference { - fn from(value: OwnedRoomId) -> Self { - ExecuteReference::Room(value) +impl IntoExecuteReference for IndexKey { + fn into(self) -> ExecuteReference { + ExecuteReference::Index(self) } } -impl From for ExecuteReference { - fn from(value: IndexKey) -> Self { - ExecuteReference::Index(value) +impl IntoExecuteReference for SectionIndex { + fn into(self) -> ExecuteReference { + ExecuteReference::Index(IndexKey::Section(self)) } } -impl From for ExecuteReference { - fn from(value: SectionIndex) -> Self { - ExecuteReference::Index(IndexKey::Section(value)) +impl IntoExecuteReference for SpecialListsIndex { + fn into(self) -> ExecuteReference { + ExecuteReference::Index(IndexKey::Special(self)) } } diff --git a/native/matrix/src/store.rs b/native/matrix/src/store.rs index e2f5838cecde..f7148cc16dd7 100644 --- a/native/matrix/src/store.rs +++ b/native/matrix/src/store.rs @@ -6,9 +6,9 @@ use std::collections::HashSet; use std::sync::{Arc, Mutex}; use tracing::{debug, error, info, instrument, trace, warn}; -mod index; -pub use index::{LifoIndex, RankedIndex, StoreIndex}; +pub use acter_core::store::{LifoIndex, RankedIndex, StoreIndex}; +use crate::config::MatrixCoreTypeConfig; use crate::referencing::{ExecuteReference, IndexKey}; use crate::{ models::{ActerModel, AnyActerModel}, @@ -20,7 +20,7 @@ pub struct Store { pub(crate) client: Client, user_id: OwnedUserId, models: Arc>, - indizes: Arc>, + indizes: Arc>>, dirty: Arc>>, // our key mutex; } @@ -143,7 +143,7 @@ impl Store { vec![] }; - let indizes: HashMap = HashMap::new(); + let indizes: HashMap> = HashMap::new(); let models: HashMap = HashMap::new(); for m in models_vec { let Some(m) = m else { @@ -273,12 +273,15 @@ impl Store { )) } - pub async fn save_many(&self, models: Vec) -> Result> { + pub async fn save_many + Send>( + &self, + models: I, + ) -> Result> { let mut total_keys = Vec::new(); let mut total_indizes = Vec::new(); { let mut dirty = self.dirty.lock()?; // hold the lock - for mdl in models.into_iter() { + for mdl in models { let (keys, indizes) = self.model_inner_under_lock(mdl)?; dirty.extend(keys.clone()); total_keys.extend(keys); @@ -421,7 +424,7 @@ mod tests { use super::*; use crate::{ models::{TestModel, TestModelBuilder}, - referencing::{SectionIndex, SpecialListsIndex}, + referencing::{IntoExecuteReference, SectionIndex, SpecialListsIndex}, }; use anyhow::bail; use matrix_sdk::ruma::MilliSecondsSinceUnixEpoch; @@ -487,12 +490,7 @@ mod tests { }) .collect(); let res_keys = store - .save_many( - models - .iter() - .map(|m| AnyActerModel::TestModel(m.clone())) - .collect(), - ) + .save_many(models.iter().map(|m| AnyActerModel::TestModel(m.clone()))) .await?; assert_eq!( models @@ -545,9 +543,9 @@ mod tests { let res_keys = store.save(AnyActerModel::TestModel(model.clone())).await?; assert_eq!( vec![ - ExecuteReference::from(key.clone()), - IndexKey::Special(SpecialListsIndex::Test1).into(), - IndexKey::Special(SpecialListsIndex::Test2).into() + IntoExecuteReference::into(key.clone()), + IntoExecuteReference::into(IndexKey::Special(SpecialListsIndex::Test1)), + IntoExecuteReference::into(IndexKey::Special(SpecialListsIndex::Test2)) ], res_keys ); @@ -597,8 +595,8 @@ mod tests { let res_keys = store.save(AnyActerModel::TestModel(model.clone())).await?; assert_eq!( vec![ - ExecuteReference::from(key.clone()), - IndexKey::Special(SpecialListsIndex::Test1).into() + IntoExecuteReference::into(key.clone()), + IntoExecuteReference::into(IndexKey::Special(SpecialListsIndex::Test1)) ], res_keys ); @@ -624,8 +622,8 @@ mod tests { .await?; assert_eq!( vec![ - ExecuteReference::from(key.clone()), - IndexKey::Special(SpecialListsIndex::Test1).into() + IntoExecuteReference::into(key.clone()), + IntoExecuteReference::into(IndexKey::Special(SpecialListsIndex::Test1)) ], res_keys ); @@ -674,8 +672,8 @@ mod tests { let res_keys = store.save(AnyActerModel::TestModel(model.clone())).await?; assert_eq!( vec![ - ExecuteReference::from(key.clone()), - IndexKey::RoomHistory(room_id.clone()).into() + IntoExecuteReference::into(key.clone()), + IntoExecuteReference::into(IndexKey::RoomHistory(room_id.clone())) ], res_keys ); @@ -702,8 +700,8 @@ mod tests { .await?; assert_eq!( vec![ - ExecuteReference::from(key.clone()), - IndexKey::RoomHistory(room_id.clone()).into() + IntoExecuteReference::into(key.clone()), + IntoExecuteReference::into(IndexKey::RoomHistory(room_id.clone())) ], res_keys ); @@ -752,8 +750,8 @@ mod tests { let res_keys = store.save(AnyActerModel::TestModel(model.clone())).await?; assert_eq!( vec![ - ExecuteReference::from(key.clone()), - IndexKey::Section(SectionIndex::Boosts).into() + IntoExecuteReference::into(key.clone()), + IntoExecuteReference::into(IndexKey::Section(SectionIndex::Boosts)) ], res_keys ); @@ -780,8 +778,8 @@ mod tests { .await?; assert_eq!( vec![ - ExecuteReference::from(key.clone()), - IndexKey::Section(SectionIndex::Boosts).into() + IntoExecuteReference::into(key.clone()), + IntoExecuteReference::into(IndexKey::Section(SectionIndex::Boosts)) ], res_keys ); @@ -823,9 +821,9 @@ mod tests { let res_keys = store.save(AnyActerModel::TestModel(model.clone())).await?; assert_eq!( vec![ - ExecuteReference::from(key.clone()), - IndexKey::Special(SpecialListsIndex::Test1).into(), - IndexKey::Special(SpecialListsIndex::Test2).into() + IntoExecuteReference::into(key.clone()), + IntoExecuteReference::into(IndexKey::Special(SpecialListsIndex::Test1)), + IntoExecuteReference::into(IndexKey::Special(SpecialListsIndex::Test2)), ], res_keys ); @@ -872,10 +870,10 @@ mod tests { let res_keys = store.save(AnyActerModel::TestModel(model.clone())).await?; assert_eq!( vec![ - ExecuteReference::from(key.clone()), - IndexKey::Special(SpecialListsIndex::Test1).into(), - IndexKey::Special(SpecialListsIndex::Test2).into(), - IndexKey::Special(SpecialListsIndex::Test3).into(), + IntoExecuteReference::into(key.clone()), + IntoExecuteReference::into(IndexKey::Special(SpecialListsIndex::Test1)), + IntoExecuteReference::into(IndexKey::Special(SpecialListsIndex::Test2)), + IntoExecuteReference::into(IndexKey::Special(SpecialListsIndex::Test3)), ], res_keys ); @@ -917,8 +915,8 @@ mod tests { let res_keys = store.save(AnyActerModel::TestModel(model.clone())).await?; assert_eq!( vec![ - ExecuteReference::from(key.clone()), - IndexKey::Special(SpecialListsIndex::Test2).into() + IntoExecuteReference::into(key.clone()), + IntoExecuteReference::into(IndexKey::Special(SpecialListsIndex::Test2)) ], res_keys ); @@ -1027,8 +1025,7 @@ mod tests { first_room_models .iter() .chain(second_room_models.iter()) - .map(|m| AnyActerModel::TestModel(m.clone())) - .collect(), + .map(|m| AnyActerModel::TestModel(m.clone())), ) .await?; diff --git a/native/matrix/src/store/index.rs b/native/matrix/src/store/index.rs deleted file mode 100644 index db8235acd39e..000000000000 --- a/native/matrix/src/store/index.rs +++ /dev/null @@ -1,404 +0,0 @@ -use std::{fmt::Debug, ops::Deref}; - -use eyeball_im::{ObservableVector, ObservableVectorTransactionEntry, VectorDiff}; -use futures::{Stream, StreamExt}; -use matrix_sdk::ruma::{MilliSecondsSinceUnixEpoch, OwnedEventId}; - -use crate::{ - models::EventMeta, - referencing::{IndexKey, ObjectListIndex, SectionIndex}, -}; - -/// Keeps an index of items sorted by the given rank, highest rank first -pub struct RankedIndex -where - K: 'static + Ord + Clone, - T: 'static + Clone + Eq, -{ - vector: ObservableVector<(K, T)>, -} -impl Default for RankedIndex -where - K: 'static + Ord + Clone, - T: 'static + Clone + Eq, -{ - fn default() -> Self { - Self { - vector: Default::default(), - } - } -} - -impl Deref for RankedIndex -where - K: 'static + Ord + Clone, - T: 'static + Clone + Eq, -{ - type Target = ObservableVector<(K, T)>; - - fn deref(&self) -> &Self::Target { - &self.vector - } -} - -impl RankedIndex -where - K: 'static + Ord + Clone, - T: 'static + Clone + Eq, -{ - pub fn new_with(rank: K, value: T) -> Self { - let mut m = RankedIndex::default(); - m.insert(rank, value); - m - } - /// Insert the value T at the position of rank - /// - /// Will add at first position if a value of the same rank is found - pub fn insert(&mut self, rank: K, value: T) { - let mut pos = self.vector.len(); - for (idx, (k, _v)) in self.vector.iter().enumerate() { - if k <= &rank { - pos = idx; - break; - } - } - self.vector.insert(pos, (rank, value)); - } - - /// Remove all instances in the vector having the specific value - pub fn remove(&mut self, value: &T) { - let mut t = self.vector.transaction(); - let mut entries = t.entries(); - while let Some(entry) = entries.next() { - if &entry.1 == value { - ObservableVectorTransactionEntry::remove(entry); - } - } - t.commit(); - } - - /// Returns the current list of values in order of their rank - pub fn values(&self) -> Vec<&T> { - self.vector.iter().map(|(_k, v)| v).collect() - } - - pub fn update_stream(&self) -> impl Stream> { - self.vector.subscribe().into_stream().map(|v| match v { - VectorDiff::Append { values } => VectorDiff::Append { - values: values.into_iter().map(|(_k, v)| v).collect(), - }, - VectorDiff::Clear => VectorDiff::Clear, - VectorDiff::PushFront { value } => VectorDiff::PushFront { value: value.1 }, - VectorDiff::PushBack { value } => VectorDiff::PushBack { value: value.1 }, - VectorDiff::PopFront => VectorDiff::PopFront, - VectorDiff::PopBack => VectorDiff::PopBack, - VectorDiff::Insert { index, value } => VectorDiff::Insert { - index, - value: value.1, - }, - VectorDiff::Set { index, value } => VectorDiff::Set { - index, - value: value.1, - }, - VectorDiff::Remove { index } => VectorDiff::Remove { index }, - VectorDiff::Truncate { length } => VectorDiff::Truncate { length }, - VectorDiff::Reset { values } => VectorDiff::Reset { - values: values.into_iter().map(|(_k, v)| v).collect(), - }, - }) - } -} - -struct GenericIndexVectorHandler(); - -impl GenericIndexVectorHandler { - /// All instances of this element from the vector - pub fn remove(vector: &mut ObservableVector, value: &T) - where - T: 'static + Clone + Eq, - { - let mut t = vector.transaction(); - let mut entries = t.entries(); - while let Some(entry) = entries.next() { - if &*entry == value { - ObservableVectorTransactionEntry::remove(entry); - } - } - t.commit(); - } - - /// Returns the current list of values in order of when they were added - pub fn values(vector: &ObservableVector) -> Vec<&T> - where - T: 'static + Clone + Eq, - { - vector.iter().collect() - } - - pub fn update_stream(vector: &ObservableVector) -> impl Stream> - where - T: 'static + Clone + Eq, - { - vector.subscribe().into_stream() - } -} - -/// Keeps an index of items sorted by when they were added -/// latest first -pub struct LifoIndex -where - T: 'static + Clone + Eq, -{ - vector: ObservableVector, -} - -impl LifoIndex -where - T: 'static + Clone + Eq, -{ - pub fn new_with(value: T) -> Self { - let mut m = LifoIndex::default(); - m.insert(value); - m - } - /// Insert the element at the front - pub fn insert(&mut self, value: T) { - self.vector.push_front(value); - } - - /// All instances of this element from the vector - pub fn remove(&mut self, value: &T) { - GenericIndexVectorHandler::remove(&mut self.vector, value) - } - - /// Returns the current list of values in order of when they were added - pub fn values(&self) -> Vec<&T> { - GenericIndexVectorHandler::values(&self.vector) - } - - pub fn update_stream(&self) -> impl Stream> { - GenericIndexVectorHandler::update_stream(&self.vector) - } -} - -impl Default for LifoIndex -where - T: 'static + Clone + Eq, -{ - fn default() -> Self { - Self { - vector: Default::default(), - } - } -} - -impl Deref for LifoIndex -where - T: 'static + Clone + Eq, -{ - type Target = ObservableVector; - - fn deref(&self) -> &Self::Target { - &self.vector - } -} - -/// Keeps an index of items sorted by when they were added -/// latest last -pub struct FiloIndex -where - T: 'static + Clone + Eq, -{ - vector: ObservableVector, -} - -impl FiloIndex -where - T: 'static + Clone + Eq, -{ - pub fn new_with(value: T) -> Self { - let mut m = FiloIndex::default(); - m.insert(value); - m - } - /// Insert the element at the front - pub fn insert(&mut self, value: T) { - self.vector.push_back(value); - } - - /// All instances of this element from the vector - pub fn remove(&mut self, value: &T) { - GenericIndexVectorHandler::remove(&mut self.vector, value) - } - - /// Returns the current list of values in order of when they were added - pub fn values(&self) -> Vec<&T> { - GenericIndexVectorHandler::values(&self.vector) - } - - pub fn update_stream(&self) -> impl Stream> { - GenericIndexVectorHandler::update_stream(&self.vector) - } -} - -impl Default for FiloIndex -where - T: 'static + Clone + Eq, -{ - fn default() -> Self { - Self { - vector: Default::default(), - } - } -} - -impl Deref for FiloIndex -where - T: 'static + Clone + Eq, -{ - type Target = ObservableVector; - - fn deref(&self) -> &Self::Target { - &self.vector - } -} - -pub enum StoreIndex { - Lifo(LifoIndex), - Filo(FiloIndex), - Ranked(RankedIndex), -} - -impl StoreIndex { - pub fn new_for(key: &IndexKey, meta: &EventMeta) -> StoreIndex { - match key { - IndexKey::AllHistory | IndexKey::ObjectHistory(_) | IndexKey::RoomHistory(_) => { - StoreIndex::Ranked(RankedIndex::new_with( - meta.origin_server_ts, - meta.event_id.clone(), - )) - } - //RSVPs are latest first for collection - IndexKey::ObjectList(_, ObjectListIndex::Rsvp) => StoreIndex::Ranked( - RankedIndex::new_with(meta.origin_server_ts, meta.event_id.clone()), - ), - IndexKey::Section(SectionIndex::Boosts) - | IndexKey::Section(SectionIndex::Stories) - | IndexKey::RoomSection(_, SectionIndex::Boosts) - | IndexKey::RoomSection(_, SectionIndex::Stories) => StoreIndex::Ranked( - RankedIndex::new_with(meta.origin_server_ts, meta.event_id.clone()), - ), - IndexKey::ObjectList(_, ObjectListIndex::Tasks) => { - StoreIndex::Filo(FiloIndex::new_with(meta.event_id.clone())) - } - _ => StoreIndex::Lifo(LifoIndex::new_with(meta.event_id.clone())), - } - } - - pub fn insert(&mut self, meta: &EventMeta) { - match self { - StoreIndex::Lifo(l) => l.insert(meta.event_id.clone()), - StoreIndex::Filo(l) => l.insert(meta.event_id.clone()), - StoreIndex::Ranked(r) => r.insert(meta.origin_server_ts, meta.event_id.clone()), - } - } - - /// All instances of this element from the vector - pub fn remove(&mut self, value: &OwnedEventId) { - match self { - StoreIndex::Lifo(idx) => idx.remove(value), - StoreIndex::Filo(idx) => idx.remove(value), - StoreIndex::Ranked(ranked_index) => ranked_index.remove(value), - } - } - - /// Returns the current list of values in order of when they were added - pub fn values(&self) -> Vec<&OwnedEventId> { - match self { - StoreIndex::Lifo(idx) => idx.values(), - StoreIndex::Filo(idx) => idx.values(), - StoreIndex::Ranked(ranked_index) => ranked_index.values(), - } - } - - // pub fn update_stream(&self) -> impl Stream> { - // match self { - // StoreIndex::Lifo(lifo_index) => lifo_index.update_stream(), - // StoreIndex::Filo(lifo_index) => lifo_index.update_stream(), - // StoreIndex::Ranked(ranked_index) => ranked_index.update_stream(), - // } - // } -} - -impl Debug for StoreIndex { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Lifo(_) => f.debug_tuple("Lifo").finish(), - Self::Filo(_) => f.debug_tuple("Filo").finish(), - Self::Ranked(_) => f.debug_tuple("Ranked").finish(), - } - } -} - -#[cfg(test)] -mod tests { - - use super::*; - use futures::pin_mut; - - #[tokio::test] - async fn test_ranked_index_for_u64() { - let mut index = RankedIndex::::default(); - index.insert(18, "18"); - index.insert(20, "20"); - index.insert(5, "5"); - index.insert(8, "8"); - - assert_eq!(index.values(), [&"20", &"18", &"8", &"5",]); - - let stream = index.update_stream(); - - index.remove(&"8"); - index.remove(&"18"); - - assert_eq!(index.values(), [&"20", &"5"]); - pin_mut!(stream); - // ensure the right types and values - assert!(matches!( - stream.next().await.unwrap(), - VectorDiff::Remove { index: 2 } - )); - assert!(matches!( - stream.next().await.unwrap(), - VectorDiff::Remove { index: 1 } - )); - } - - #[tokio::test] - async fn test_lifo_index_for_u64() { - let mut index = LifoIndex::<&'static str>::default(); - index.insert("18"); - index.insert("20"); - index.insert("5"); - index.insert("8"); - - assert_eq!(index.values(), [&"8", &"5", &"20", &"18",]); - - let stream = index.update_stream(); - - index.remove(&"18"); - index.remove(&"8"); - - assert_eq!(index.values(), [&"5", &"20",]); - pin_mut!(stream); - // ensure the right types and values - assert!(matches!( - stream.next().await.unwrap(), - VectorDiff::Remove { index: 3 } - )); - assert!(matches!( - stream.next().await.unwrap(), - VectorDiff::Remove { index: 0 } - )); - } -} diff --git a/native/test/Cargo.toml b/native/test/Cargo.toml index c44adeb35dd5..7bc0e95edc62 100644 --- a/native/test/Cargo.toml +++ b/native/test/Cargo.toml @@ -10,6 +10,11 @@ path = "../acter" default-features = false features = ["testing"] +[dependencies.acter-core] +path = "../core" +default-features = false +features = ["testing"] + [dependencies.acter-matrix] path = "../matrix" features = ["testing"] diff --git a/native/test/src/tests/activities/calendar.rs b/native/test/src/tests/activities/calendar.rs index 60716f5b4ec7..60860064ad22 100644 --- a/native/test/src/tests/activities/calendar.rs +++ b/native/test/src/tests/activities/calendar.rs @@ -110,7 +110,7 @@ async fn calendar_update_start_activity() -> Result<()> { activity .date_time_range_content() .and_then(|c| c.start_new_val()), - Some(utc_start.clone()) + Some(utc_start) ); assert_eq!( activity @@ -183,7 +183,7 @@ async fn calendar_update_end_activity() -> Result<()> { activity .date_time_range_content() .and_then(|c| c.end_new_val()), - Some(utc_end.clone()) + Some(utc_end) ); let object = activity.object().expect("we have an object"); @@ -246,13 +246,13 @@ async fn calendar_update_start_end_activity() -> Result<()> { activity .date_time_range_content() .and_then(|c| c.start_new_val()), - Some(utc_start.clone()) + Some(utc_start) ); assert_eq!( activity .date_time_range_content() .and_then(|c| c.end_new_val()), - Some(utc_end.clone()) + Some(utc_end) ); let object = activity.object().expect("we have an object"); diff --git a/native/test/src/tests/activities/policy_rule_room.rs b/native/test/src/tests/activities/policy_rule_room.rs index daf7fb4074f3..ac0a428e6dee 100644 --- a/native/test/src/tests/activities/policy_rule_room.rs +++ b/native/test/src/tests/activities/policy_rule_room.rs @@ -54,7 +54,7 @@ async fn test_policy_rule_room() -> Result<()> { assert_eq!(activity.event_id_str(), meta.event_id); assert_eq!(activity.room_id_str(), room_id); assert_eq!(activity.type_str(), "policyRuleRoom"); - let ts: u64 = meta.origin_server_ts.get().into(); + let ts: u64 = meta.timestamp.get().into(); assert_eq!(activity.origin_server_ts(), ts); // check the content of activity diff --git a/native/test/src/tests/activities/policy_rule_server.rs b/native/test/src/tests/activities/policy_rule_server.rs index 4f92b5f1d465..e5810b8c6d99 100644 --- a/native/test/src/tests/activities/policy_rule_server.rs +++ b/native/test/src/tests/activities/policy_rule_server.rs @@ -54,7 +54,7 @@ async fn test_policy_rule_server() -> Result<()> { assert_eq!(activity.event_id_str(), meta.event_id); assert_eq!(activity.room_id_str(), room.room_id_str()); assert_eq!(activity.type_str(), "policyRuleServer"); - let ts: u64 = meta.origin_server_ts.get().into(); + let ts: u64 = meta.timestamp.get().into(); assert_eq!(activity.origin_server_ts(), ts); // check the content of activity diff --git a/native/test/src/tests/activities/policy_rule_user.rs b/native/test/src/tests/activities/policy_rule_user.rs index 2b68dd096b2e..43ff3afd2537 100644 --- a/native/test/src/tests/activities/policy_rule_user.rs +++ b/native/test/src/tests/activities/policy_rule_user.rs @@ -54,7 +54,7 @@ async fn test_policy_rule_user() -> Result<()> { assert_eq!(activity.event_id_str(), meta.event_id); assert_eq!(activity.room_id_str(), room.room_id_str()); assert_eq!(activity.type_str(), "policyRuleUser"); - let ts: u64 = meta.origin_server_ts.get().into(); + let ts: u64 = meta.timestamp.get().into(); assert_eq!(activity.origin_server_ts(), ts); // check the content of activity diff --git a/native/test/src/tests/activities/room_avatar.rs b/native/test/src/tests/activities/room_avatar.rs index e31cffb5da81..0f3e62d798b8 100644 --- a/native/test/src/tests/activities/room_avatar.rs +++ b/native/test/src/tests/activities/room_avatar.rs @@ -50,7 +50,7 @@ async fn test_room_avatar() -> Result<()> { assert_eq!(activity.event_id_str(), meta.event_id); assert_eq!(activity.room_id_str(), room.room_id_str()); assert_eq!(activity.type_str(), "roomAvatar"); - let ts: u64 = meta.origin_server_ts.get().into(); + let ts: u64 = meta.timestamp.get().into(); assert_eq!(activity.origin_server_ts(), ts); // check the content of activity diff --git a/native/test/src/tests/activities/room_create.rs b/native/test/src/tests/activities/room_create.rs index 51a610a58508..ec7c8a8709c8 100644 --- a/native/test/src/tests/activities/room_create.rs +++ b/native/test/src/tests/activities/room_create.rs @@ -38,7 +38,7 @@ async fn test_room_create() -> Result<()> { assert_eq!(activity.sender_id_str(), user.user_id()?); assert_eq!(activity.event_id_str(), meta.event_id); assert_eq!(activity.room_id_str(), room.room_id_str()); - let ts: u64 = meta.origin_server_ts.get().into(); + let ts: u64 = meta.timestamp.get().into(); assert_eq!(activity.origin_server_ts(), ts); // check the content of activity diff --git a/native/test/src/tests/activities/room_encryption.rs b/native/test/src/tests/activities/room_encryption.rs index b9a8879e9ef9..3816151ca54a 100644 --- a/native/test/src/tests/activities/room_encryption.rs +++ b/native/test/src/tests/activities/room_encryption.rs @@ -50,7 +50,7 @@ async fn test_room_encryption() -> Result<()> { assert_eq!(activity.event_id_str(), meta.event_id); assert_eq!(activity.room_id_str(), room.room_id_str()); assert_eq!(activity.type_str(), "roomEncryption"); - let ts: u64 = meta.origin_server_ts.get().into(); + let ts: u64 = meta.timestamp.get().into(); assert_eq!(activity.origin_server_ts(), ts); // check the content of activity diff --git a/native/test/src/tests/activities/room_guest_access.rs b/native/test/src/tests/activities/room_guest_access.rs index 738fb3b21cee..67e721f8b757 100644 --- a/native/test/src/tests/activities/room_guest_access.rs +++ b/native/test/src/tests/activities/room_guest_access.rs @@ -29,7 +29,7 @@ async fn test_room_guest_access() -> Result<()> { assert_eq!(activity.event_id_str(), meta.event_id); assert_eq!(activity.room_id_str(), room_id); assert_eq!(activity.type_str(), "roomGuestAccess"); - let ts: u64 = meta.origin_server_ts.get().into(); + let ts: u64 = meta.timestamp.get().into(); assert_eq!(activity.origin_server_ts(), ts); // check the content of activity diff --git a/native/test/src/tests/activities/room_history_visibility.rs b/native/test/src/tests/activities/room_history_visibility.rs index 623b790a1bd7..4696954dddfa 100644 --- a/native/test/src/tests/activities/room_history_visibility.rs +++ b/native/test/src/tests/activities/room_history_visibility.rs @@ -53,7 +53,7 @@ async fn test_room_history_visibility() -> Result<()> { assert_eq!(activity.event_id_str(), meta.event_id); assert_eq!(activity.room_id_str(), room_id); assert_eq!(activity.type_str(), "roomHistoryVisibility"); - let ts: u64 = meta.origin_server_ts.get().into(); + let ts: u64 = meta.timestamp.get().into(); assert_eq!(activity.origin_server_ts(), ts); // check the content of activity diff --git a/native/test/src/tests/activities/room_join_rules.rs b/native/test/src/tests/activities/room_join_rules.rs index 2a307427c5b6..030b015e4519 100644 --- a/native/test/src/tests/activities/room_join_rules.rs +++ b/native/test/src/tests/activities/room_join_rules.rs @@ -53,7 +53,7 @@ async fn test_room_join_rules() -> Result<()> { assert_eq!(activity.event_id_str(), meta.event_id); assert_eq!(activity.room_id_str(), room_id); assert_eq!(activity.type_str(), "roomJoinRules"); - let ts: u64 = meta.origin_server_ts.get().into(); + let ts: u64 = meta.timestamp.get().into(); assert_eq!(activity.origin_server_ts(), ts); // check the content of activity diff --git a/native/test/src/tests/activities/room_name.rs b/native/test/src/tests/activities/room_name.rs index adfc2b728c27..825f33bd9519 100644 --- a/native/test/src/tests/activities/room_name.rs +++ b/native/test/src/tests/activities/room_name.rs @@ -47,7 +47,7 @@ async fn test_room_name() -> Result<()> { assert_eq!(activity.event_id_str(), meta.event_id); assert_eq!(activity.room_id_str(), room_id); assert_eq!(activity.type_str(), "roomName"); - let ts: u64 = meta.origin_server_ts.get().into(); + let ts: u64 = meta.timestamp.get().into(); assert_eq!(activity.origin_server_ts(), ts); // check the content of activity diff --git a/native/test/src/tests/activities/room_pinned_events.rs b/native/test/src/tests/activities/room_pinned_events.rs index d198847e119b..85ce32ba9e3c 100644 --- a/native/test/src/tests/activities/room_pinned_events.rs +++ b/native/test/src/tests/activities/room_pinned_events.rs @@ -55,7 +55,7 @@ async fn test_room_pinned_events() -> Result<()> { assert_eq!(activity.event_id_str(), meta.event_id); assert_eq!(activity.room_id_str(), room_id); assert_eq!(activity.type_str(), "roomPinnedEvents"); - let ts: u64 = meta.origin_server_ts.get().into(); + let ts: u64 = meta.timestamp.get().into(); assert_eq!(activity.origin_server_ts(), ts); // check the content of activity diff --git a/native/test/src/tests/activities/room_power_levels.rs b/native/test/src/tests/activities/room_power_levels.rs index 8cce5ead45d6..6704caf940a2 100644 --- a/native/test/src/tests/activities/room_power_levels.rs +++ b/native/test/src/tests/activities/room_power_levels.rs @@ -47,7 +47,7 @@ async fn test_room_power_levels_ban() -> Result<()> { assert_eq!(activity.event_id_str(), meta.event_id); assert_eq!(activity.room_id_str(), room_id); assert_eq!(activity.type_str(), "roomPowerLevels"); - let ts: u64 = meta.origin_server_ts.get().into(); + let ts: u64 = meta.timestamp.get().into(); assert_eq!(activity.origin_server_ts(), ts); // check the content of activity @@ -121,7 +121,7 @@ async fn test_room_power_levels_events() -> Result<()> { assert_eq!(activity.event_id_str(), meta.event_id); assert_eq!(activity.room_id_str(), room_id); assert_eq!(activity.type_str(), "roomPowerLevels"); - let ts: u64 = meta.origin_server_ts.get().into(); + let ts: u64 = meta.timestamp.get().into(); assert_eq!(activity.origin_server_ts(), ts); // check the content of activity @@ -194,7 +194,7 @@ async fn test_room_power_levels_events_default() -> Result<()> { assert_eq!(activity.event_id_str(), meta.event_id); assert_eq!(activity.room_id_str(), room_id); assert_eq!(activity.type_str(), "roomPowerLevels"); - let ts: u64 = meta.origin_server_ts.get().into(); + let ts: u64 = meta.timestamp.get().into(); assert_eq!(activity.origin_server_ts(), ts); // check the content of activity @@ -265,7 +265,7 @@ async fn test_room_power_levels_invite() -> Result<()> { assert_eq!(activity.event_id_str(), meta.event_id); assert_eq!(activity.room_id_str(), room_id); assert_eq!(activity.type_str(), "roomPowerLevels"); - let ts: u64 = meta.origin_server_ts.get().into(); + let ts: u64 = meta.timestamp.get().into(); assert_eq!(activity.origin_server_ts(), ts); // check the content of activity @@ -336,7 +336,7 @@ async fn test_room_power_levels_kick() -> Result<()> { assert_eq!(activity.event_id_str(), meta.event_id); assert_eq!(activity.room_id_str(), room_id); assert_eq!(activity.type_str(), "roomPowerLevels"); - let ts: u64 = meta.origin_server_ts.get().into(); + let ts: u64 = meta.timestamp.get().into(); assert_eq!(activity.origin_server_ts(), ts); // check the content of activity @@ -407,7 +407,7 @@ async fn test_room_power_levels_redact() -> Result<()> { assert_eq!(activity.event_id_str(), meta.event_id); assert_eq!(activity.room_id_str(), room_id); assert_eq!(activity.type_str(), "roomPowerLevels"); - let ts: u64 = meta.origin_server_ts.get().into(); + let ts: u64 = meta.timestamp.get().into(); assert_eq!(activity.origin_server_ts(), ts); // check the content of activity @@ -482,7 +482,7 @@ async fn test_room_power_levels_state_default() -> Result<()> { assert_eq!(activity.type_str(), "roomPowerLevels"); assert_eq!( activity.origin_server_ts(), - Into::::into(meta.origin_server_ts.get()) + Into::::into(meta.timestamp.get()) ); // check the content of activity @@ -555,7 +555,7 @@ async fn test_room_power_levels_users_default() -> Result<()> { assert_eq!(activity.event_id_str(), meta.event_id); assert_eq!(activity.room_id_str(), room_id); assert_eq!(activity.type_str(), "roomPowerLevels"); - let ts: u64 = meta.origin_server_ts.get().into(); + let ts: u64 = meta.timestamp.get().into(); assert_eq!(activity.origin_server_ts(), ts); // check the content of activity @@ -628,7 +628,7 @@ async fn test_room_power_levels_notifications() -> Result<()> { assert_eq!(activity.event_id_str(), meta.event_id); assert_eq!(activity.room_id_str(), room_id); assert_eq!(activity.type_str(), "roomPowerLevels"); - let ts: u64 = meta.origin_server_ts.get().into(); + let ts: u64 = meta.timestamp.get().into(); assert_eq!(activity.origin_server_ts(), ts); // check the content of activity diff --git a/native/test/src/tests/activities/room_server_acl.rs b/native/test/src/tests/activities/room_server_acl.rs index aa0b5bdc0621..b4caf98d1434 100644 --- a/native/test/src/tests/activities/room_server_acl.rs +++ b/native/test/src/tests/activities/room_server_acl.rs @@ -53,7 +53,7 @@ async fn test_room_server_acl() -> Result<()> { assert_eq!(activity.event_id_str(), meta.event_id); assert_eq!(activity.room_id_str(), room_id); assert_eq!(activity.type_str(), "roomServerAcl"); - let ts: u64 = meta.origin_server_ts.get().into(); + let ts: u64 = meta.timestamp.get().into(); assert_eq!(activity.origin_server_ts(), ts); // check the content of activity diff --git a/native/test/src/tests/activities/room_tombstone.rs b/native/test/src/tests/activities/room_tombstone.rs index 29698cf14a04..60f0a112f177 100644 --- a/native/test/src/tests/activities/room_tombstone.rs +++ b/native/test/src/tests/activities/room_tombstone.rs @@ -26,7 +26,7 @@ async fn test_room_tombstone() -> Result<()> { // ensure it was sent let body = "This room was upgraded to the other version"; let id = gen_id(18); - let replacement_room_id = format!("!{}:localhost", id); + let replacement_room_id = format!("!{id}:localhost"); let tombstone_event_id = room .set_tombstone(body.to_owned(), replacement_room_id.clone()) .await?; @@ -54,7 +54,7 @@ async fn test_room_tombstone() -> Result<()> { assert_eq!(activity.event_id_str(), meta.event_id); assert_eq!(activity.room_id_str(), room_id); assert_eq!(activity.type_str(), "roomTombstone"); - let ts: u64 = meta.origin_server_ts.get().into(); + let ts: u64 = meta.timestamp.get().into(); assert_eq!(activity.origin_server_ts(), ts); // check the content of activity diff --git a/native/test/src/tests/activities/room_topic.rs b/native/test/src/tests/activities/room_topic.rs index ddc60536aa26..580886bd4940 100644 --- a/native/test/src/tests/activities/room_topic.rs +++ b/native/test/src/tests/activities/room_topic.rs @@ -47,7 +47,7 @@ async fn test_room_topic() -> Result<()> { assert_eq!(activity.event_id_str(), meta.event_id); assert_eq!(activity.room_id_str(), room_id); assert_eq!(activity.type_str(), "roomTopic"); - let ts: u64 = meta.origin_server_ts.get().into(); + let ts: u64 = meta.timestamp.get().into(); assert_eq!(activity.origin_server_ts(), ts); // check the content of activity diff --git a/native/test/src/tests/activities/space_child.rs b/native/test/src/tests/activities/space_child.rs index c1f3268adb8e..31bdbc8197e4 100644 --- a/native/test/src/tests/activities/space_child.rs +++ b/native/test/src/tests/activities/space_child.rs @@ -53,7 +53,7 @@ async fn test_space_child() -> Result<()> { assert_eq!(activity.event_id_str(), meta.event_id); assert_eq!(activity.room_id_str(), room_id); assert_eq!(activity.type_str(), "spaceChild"); - let ts: u64 = meta.origin_server_ts.get().into(); + let ts: u64 = meta.timestamp.get().into(); assert_eq!(activity.origin_server_ts(), ts); // check the content of activity diff --git a/native/test/src/tests/activities/space_parent.rs b/native/test/src/tests/activities/space_parent.rs index bcd834c9468b..ae62017897d7 100644 --- a/native/test/src/tests/activities/space_parent.rs +++ b/native/test/src/tests/activities/space_parent.rs @@ -52,7 +52,7 @@ async fn test_space_parent() -> Result<()> { assert_eq!(activity.event_id_str(), meta.event_id); assert_eq!(activity.room_id_str(), room_id); assert_eq!(activity.type_str(), "spaceParent"); - let ts: u64 = meta.origin_server_ts.get().into(); + let ts: u64 = meta.timestamp.get().into(); assert_eq!(activity.origin_server_ts(), ts); // check the content of activity diff --git a/native/test/src/tests/activities/status.rs b/native/test/src/tests/activities/status.rs index 902a9b2378c9..d5a18d3ea22a 100644 --- a/native/test/src/tests/activities/status.rs +++ b/native/test/src/tests/activities/status.rs @@ -345,7 +345,7 @@ async fn left() -> Result<()> { assert_eq!(activity.type_str(), "left"); assert_eq!( activity.origin_server_ts(), - Into::::into(meta.origin_server_ts.get()) + Into::::into(meta.timestamp.get()) ); assert_triggered_with_latest_activity(&mut act_obs, activity.event_id_str()).await?; Ok(()) @@ -379,7 +379,7 @@ async fn display_name() -> Result<()> { assert_eq!(activity.room_id_str(), room_id); assert_eq!( activity.origin_server_ts(), - Into::::into(meta.origin_server_ts.get()) + Into::::into(meta.timestamp.get()) ); assert_triggered_with_latest_activity(&mut act_obs, activity.event_id_str()).await?; Ok(()) @@ -420,7 +420,7 @@ async fn avatar_url() -> Result<()> { assert_eq!(activity.room_id_str(), room_id); assert_eq!( activity.origin_server_ts(), - Into::::into(meta.origin_server_ts.get()) + Into::::into(meta.timestamp.get()) ); assert_triggered_with_latest_activity(&mut act_obs, activity.event_id_str()).await?; Ok(()) diff --git a/native/test/src/tests/auth.rs b/native/test/src/tests/auth.rs index a5e5c3587e38..11d92fa64e83 100644 --- a/native/test/src/tests/auth.rs +++ b/native/test/src/tests/auth.rs @@ -207,7 +207,7 @@ async fn user_changes_password() -> Result<()> { let account = client.account()?; let user_id = client.user_id().expect("we just logged in"); let password = default_user_password(user_id.localpart()); - let new_password = format!("new_{:?}", password); + let new_password = format!("new_{password:?}"); let result = account .change_password(password.clone(), new_password.clone()) diff --git a/native/test/src/tests/news.rs b/native/test/src/tests/news.rs index ac55532905e3..ae3acca5b5ea 100644 --- a/native/test/src/tests/news.rs +++ b/native/test/src/tests/news.rs @@ -97,7 +97,7 @@ async fn news_smoketest() -> Result<()> { .add_slide(Box::new(text_draft.into())) .send() .await?; - print!("draft sent event id: {}", event_id); + print!("draft sent event id: {event_id}"); Ok(()) } diff --git a/native/test/src/tests/notifications/attachments.rs b/native/test/src/tests/notifications/attachments.rs index 6d74ea9e8511..b85bddfcefbd 100644 --- a/native/test/src/tests/notifications/attachments.rs +++ b/native/test/src/tests/notifications/attachments.rs @@ -116,7 +116,7 @@ async fn image_attachment_on_pin() -> Result<()> { *obj_entry.event_id() ); - assert_eq!(notification_item.title(), format!("๐Ÿ–ผ๏ธ \"{}\"", filename)); + assert_eq!(notification_item.title(), format!("๐Ÿ–ผ๏ธ \"{filename}\"")); let parent = notification_item.parent().expect("parent was found"); assert_eq!( notification_item.target_url(), @@ -208,7 +208,7 @@ async fn file_attachment_on_event() -> Result<()> { ); // notification_item.body().expect("found content"); - assert_eq!(notification_item.title(), format!("๐Ÿ“„ \"{}\"", filename)); + assert_eq!(notification_item.title(), format!("๐Ÿ“„ \"{filename}\"")); let parent = notification_item.parent().expect("parent was found"); assert_eq!( notification_item.target_url(), @@ -300,7 +300,7 @@ async fn video_attachment_on_tasklist() -> Result<()> { ); // notification_item.body().expect("found content"); - assert_eq!(notification_item.title(), format!("๐ŸŽฅ \"{}\"", filename)); + assert_eq!(notification_item.title(), format!("๐ŸŽฅ \"{filename}\"")); let parent = notification_item.parent().expect("parent was found"); assert_eq!( notification_item.target_url(), diff --git a/native/test/src/tests/notifications/event.rs b/native/test/src/tests/notifications/event.rs index 87f5cd25f954..2bcfa2d9706c 100644 --- a/native/test/src/tests/notifications/event.rs +++ b/native/test/src/tests/notifications/event.rs @@ -248,8 +248,8 @@ async fn event_rescheduled() -> Result<()> { obj_entry.event_id() ); - assert_eq!(notification_item.utc_start(), Some(utc_start.clone())); - assert_eq!(notification_item.utc_end(), Some(utc_end.clone())); + assert_eq!(notification_item.utc_start(), Some(utc_start)); + assert_eq!(notification_item.utc_end(), Some(utc_end)); let title = format!( "From: {}, To: {}", utc_start.to_rfc3339(), diff --git a/native/test/src/tests/notifications/pins.rs b/native/test/src/tests/notifications/pins.rs index 306439ebb482..d89377ef322b 100644 --- a/native/test/src/tests/notifications/pins.rs +++ b/native/test/src/tests/notifications/pins.rs @@ -125,7 +125,7 @@ async fn pin_title_update() -> Result<()> { assert_eq!(notification_item.title(), title); // old title let parent = notification_item.parent().expect("parent was found"); - assert_eq!(notification_item.target_url(), format!("/pins/{}", obj_id)); + assert_eq!(notification_item.target_url(), format!("/pins/{obj_id}")); assert_eq!(parent.type_str(), "pin"); // assert_eq!(parent.title().as_deref(), Some("Acter Website")); assert_eq!(parent.emoji(), "๐Ÿ“Œ"); // pin icon @@ -181,7 +181,7 @@ async fn pin_desc_update() -> Result<()> { let content = notification_item.body().expect("found content"); assert_eq!(content.body(), body); // new description let parent = notification_item.parent().expect("parent was found"); - assert_eq!(notification_item.target_url(), format!("/pins/{}", obj_id)); + assert_eq!(notification_item.target_url(), format!("/pins/{obj_id}")); assert_eq!(parent.type_str(), "pin"); assert_eq!(parent.title().as_deref(), Some("Acter Website")); assert_eq!(parent.emoji(), "๐Ÿ“Œ"); // pin icon diff --git a/native/test/src/tests/room/room_tombstone.rs b/native/test/src/tests/room/room_tombstone.rs index 07986b1d7daf..ba51ee5c58ee 100644 --- a/native/test/src/tests/room/room_tombstone.rs +++ b/native/test/src/tests/room/room_tombstone.rs @@ -34,7 +34,7 @@ async fn test_room_tombstone() -> Result<()> { let body = "This room was upgraded to the other version"; let id = gen_id(18); - let replacement_room_id = format!("!{}:localhost", id); + let replacement_room_id = format!("!{id}:localhost"); let tombstone_event_id = convo .set_tombstone(body.to_owned(), replacement_room_id.clone()) .await?; diff --git a/native/test/src/tests/stories.rs b/native/test/src/tests/stories.rs index 045aef050901..2bc9faef17c6 100644 --- a/native/test/src/tests/stories.rs +++ b/native/test/src/tests/stories.rs @@ -97,7 +97,7 @@ async fn story_smoketest() -> Result<()> { .add_slide(Box::new(text_draft.into())) .send() .await?; - print!("draft sent event id: {}", event_id); + print!("draft sent event id: {event_id}"); Ok(()) } diff --git a/rust-toolchain b/rust-toolchain index a2296b7c6f00..985588c01d37 100644 --- a/rust-toolchain +++ b/rust-toolchain @@ -1,5 +1,5 @@ [toolchain] -channel = "nightly-2025-04-22" +channel = "nightly-2025-06-25" components = ["rustfmt", "clippy", "rust-src"] targets = [ "wasm32-unknown-unknown",