Compare commits
68 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9e20659d5d | |||
| 7783188769 | |||
| 514af54c4c | |||
| ad615b7612 | |||
| a1b7906a7d | |||
| 79c8d2c345 | |||
| dcf6af405d | |||
| bb598b61a5 | |||
| 1c554c4912 | |||
| 21f8b7ed31 | |||
| 23ee8e25dd | |||
| 1098095846 | |||
| 3e7d7e8a31 | |||
| 2c45316bcb | |||
| 8dc7c1f876 | |||
| db84936dcd | |||
| 75d7d07013 | |||
| d4d5f45edc | |||
| d0257d1cb2 | |||
| ecf44348cf | |||
| cc8bc05537 | |||
| 728d646ce2 | |||
| ca397dca0f | |||
| 1fbe6815c3 | |||
| c61f70727f | |||
| 2abbf58825 | |||
| b979b2ea1e | |||
| 24b968ad39 | |||
| faa8aa2b9c | |||
| db9ee9d87b | |||
| 1dbb494b94 | |||
| fe52b4cb78 | |||
| 5519442ad8 | |||
| 88363d8033 | |||
| fb5d8f29ac | |||
| 912b121d27 | |||
| 2e975d9b19 | |||
| edc93e62b4 | |||
| 9d6ffa951f | |||
| 079ec023b7 | |||
| e55a1c7e00 | |||
| ddd737e4d8 | |||
| 38a15afc9c | |||
| fa93daabd2 | |||
| 6b0987385e | |||
| 48fbda844f | |||
| bc70f3c051 | |||
| d2f255d613 | |||
| bf86b168d7 | |||
| e5ca44bb04 | |||
| 1f563c964c | |||
| 9a9730d59e | |||
| af3ce4b32b | |||
| 03f0c3a001 | |||
| 639833acf1 | |||
| 60893d2797 | |||
| 9e45111d8b | |||
| 0080f17c1f | |||
| fa47af3dd6 | |||
| c4ff07124b | |||
| 900cf5d071 | |||
| 8a6ced0e8f | |||
| f20401c657 | |||
| b987fc1de2 | |||
| efeac2ef39 | |||
| 6b80055bd2 | |||
| 0af53e99ee | |||
| bc0c2a6be2 |
@@ -17,7 +17,7 @@ jobs:
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2024-06-25
|
||||
toolchain: nightly-2024-11-26
|
||||
components: rustfmt
|
||||
|
||||
- name: Run Benchmarks
|
||||
|
||||
@@ -288,7 +288,7 @@ jobs:
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2024-06-25
|
||||
toolchain: nightly-2024-11-26
|
||||
components: rustfmt
|
||||
|
||||
- name: Cargo fmt
|
||||
@@ -323,7 +323,7 @@ jobs:
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2024-06-25
|
||||
toolchain: nightly-2024-11-26
|
||||
components: clippy
|
||||
|
||||
- name: Load cache
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2024-06-25
|
||||
toolchain: nightly-2024-11-26
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
env:
|
||||
RUSTDOCFLAGS: "--enable-index-page -Zunstable-options --cfg docsrs -Dwarnings"
|
||||
run:
|
||||
cargo doc --no-deps --workspace --features docsrs
|
||||
cargo doc --no-deps --workspace --features docsrs --exclude=xtask
|
||||
|
||||
- name: Upload artifact
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
|
||||
Generated
+5
-1
@@ -3001,10 +3001,11 @@ dependencies = [
|
||||
"eyeball-im",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"getrandom",
|
||||
"gloo-timers",
|
||||
"imbl",
|
||||
"js-sys",
|
||||
"matrix-sdk-test",
|
||||
"matrix-sdk-test-macros",
|
||||
"proptest",
|
||||
"ruma",
|
||||
"serde",
|
||||
@@ -3026,6 +3027,7 @@ version = "0.8.0"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"anyhow",
|
||||
"aquamarine",
|
||||
"as_variant",
|
||||
"assert_matches",
|
||||
"assert_matches2",
|
||||
@@ -3274,9 +3276,11 @@ dependencies = [
|
||||
name = "matrix-sdk-test"
|
||||
version = "0.7.0"
|
||||
dependencies = [
|
||||
"as_variant",
|
||||
"ctor",
|
||||
"getrandom",
|
||||
"http",
|
||||
"matrix-sdk-common",
|
||||
"matrix-sdk-test-macros",
|
||||
"once_cell",
|
||||
"ruma",
|
||||
|
||||
@@ -22,6 +22,7 @@ rust-version = "1.76"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0.68"
|
||||
aquamarine = "0.6.0"
|
||||
assert-json-diff = "2"
|
||||
assert_matches = "1.5.0"
|
||||
assert_matches2 = "0.1.1"
|
||||
|
||||
@@ -2,15 +2,16 @@ use std::{sync::Arc, time::Duration};
|
||||
|
||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
||||
use matrix_sdk::{
|
||||
config::SyncSettings,
|
||||
test_utils::{events::EventFactory, logged_in_client_with_server},
|
||||
utils::IntoRawStateEventContent,
|
||||
config::SyncSettings, test_utils::logged_in_client_with_server, utils::IntoRawStateEventContent,
|
||||
};
|
||||
use matrix_sdk_base::{
|
||||
store::StoreConfig, BaseClient, RoomInfo, RoomState, SessionMeta, StateChanges, StateStore,
|
||||
};
|
||||
use matrix_sdk_sqlite::SqliteStateStore;
|
||||
use matrix_sdk_test::{EventBuilder, JoinedRoomBuilder, StateTestEvent, SyncResponseBuilder};
|
||||
use matrix_sdk_test::{
|
||||
event_factory::EventFactory, EventBuilder, JoinedRoomBuilder, StateTestEvent,
|
||||
SyncResponseBuilder,
|
||||
};
|
||||
use matrix_sdk_ui::{timeline::TimelineFocus, Timeline};
|
||||
use ruma::{
|
||||
api::client::membership::get_member_events,
|
||||
|
||||
@@ -13,6 +13,7 @@ let package = Package(
|
||||
],
|
||||
products: [
|
||||
.library(name: "MatrixRustSDK",
|
||||
type: .dynamic,
|
||||
targets: ["MatrixRustSDK"]),
|
||||
],
|
||||
targets: [
|
||||
|
||||
@@ -13,10 +13,3 @@ interface RoomMessageEventContentWithoutRelation {
|
||||
interface ClientError {
|
||||
Generic(string msg);
|
||||
};
|
||||
|
||||
interface MediaSource {
|
||||
[Name=from_json, Throws=ClientError]
|
||||
constructor(string json);
|
||||
string to_json();
|
||||
string url();
|
||||
};
|
||||
|
||||
@@ -32,9 +32,7 @@ use matrix_sdk::{
|
||||
user_directory::search_users,
|
||||
},
|
||||
events::{
|
||||
room::{
|
||||
avatar::RoomAvatarEventContent, encryption::RoomEncryptionEventContent, MediaSource,
|
||||
},
|
||||
room::{avatar::RoomAvatarEventContent, encryption::RoomEncryptionEventContent},
|
||||
AnyInitialStateEvent, AnyToDeviceEvent, InitialStateEvent,
|
||||
},
|
||||
serde::Raw,
|
||||
@@ -55,7 +53,12 @@ use ruma::{
|
||||
},
|
||||
events::{
|
||||
ignored_user_list::IgnoredUserListEventContent,
|
||||
room::{join_rules::RoomJoinRulesEventContent, power_levels::RoomPowerLevelsEventContent},
|
||||
room::{
|
||||
join_rules::{
|
||||
AllowRule as RumaAllowRule, JoinRule as RumaJoinRule, RoomJoinRulesEventContent,
|
||||
},
|
||||
power_levels::RoomPowerLevelsEventContent,
|
||||
},
|
||||
GlobalAccountDataEventType,
|
||||
},
|
||||
push::{HttpPusherData as RumaHttpPusherData, PushFormat as RumaPushFormat},
|
||||
@@ -76,7 +79,7 @@ use crate::{
|
||||
notification_settings::NotificationSettings,
|
||||
room_directory_search::RoomDirectorySearch,
|
||||
room_preview::RoomPreview,
|
||||
ruma::AuthData,
|
||||
ruma::{AuthData, MediaSource},
|
||||
sync_service::{SyncService, SyncServiceBuilder},
|
||||
task_handle::TaskHandle,
|
||||
utils::AsyncRuntimeDropped,
|
||||
@@ -450,7 +453,7 @@ impl Client {
|
||||
.inner
|
||||
.media()
|
||||
.get_media_file(
|
||||
&MediaRequestParameters { source, format: MediaFormat::File },
|
||||
&MediaRequestParameters { source: source.media_source, format: MediaFormat::File },
|
||||
filename,
|
||||
&mime_type,
|
||||
use_cache,
|
||||
@@ -723,7 +726,7 @@ impl Client {
|
||||
&self,
|
||||
media_source: Arc<MediaSource>,
|
||||
) -> Result<Vec<u8>, ClientError> {
|
||||
let source = (*media_source).clone();
|
||||
let source = (*media_source).clone().media_source;
|
||||
|
||||
debug!(?source, "requesting media file");
|
||||
Ok(self
|
||||
@@ -739,9 +742,9 @@ impl Client {
|
||||
width: u64,
|
||||
height: u64,
|
||||
) -> Result<Vec<u8>, ClientError> {
|
||||
let source = (*media_source).clone();
|
||||
let source = (*media_source).clone().media_source;
|
||||
|
||||
debug!(source = ?media_source, width, height, "requesting media thumbnail");
|
||||
debug!(?source, width, height, "requesting media thumbnail");
|
||||
Ok(self
|
||||
.inner
|
||||
.media()
|
||||
@@ -1630,7 +1633,7 @@ impl TryFrom<Session> for AuthSession {
|
||||
user: user_session,
|
||||
};
|
||||
|
||||
Ok(AuthSession::Oidc(session))
|
||||
Ok(AuthSession::Oidc(session.into()))
|
||||
} else {
|
||||
// Create a regular Matrix Session.
|
||||
let session = matrix_sdk::matrix_auth::MatrixSession {
|
||||
@@ -1917,9 +1920,13 @@ pub enum AllowRule {
|
||||
/// Only a member of the `room_id` Room can join the one this rule is used
|
||||
/// in.
|
||||
RoomMembership { room_id: String },
|
||||
|
||||
/// A custom allow rule implementation, containing its JSON representation
|
||||
/// as a `String`.
|
||||
Custom { json: String },
|
||||
}
|
||||
|
||||
impl TryFrom<JoinRule> for ruma::events::room::join_rules::JoinRule {
|
||||
impl TryFrom<JoinRule> for RumaJoinRule {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(value: JoinRule) -> Result<Self, Self::Error> {
|
||||
@@ -1929,11 +1936,11 @@ impl TryFrom<JoinRule> for ruma::events::room::join_rules::JoinRule {
|
||||
JoinRule::Knock => Ok(Self::Knock),
|
||||
JoinRule::Private => Ok(Self::Private),
|
||||
JoinRule::Restricted { rules } => {
|
||||
let rules = allow_rules_from(rules)?;
|
||||
let rules = ruma_allow_rules_from_ffi(rules)?;
|
||||
Ok(Self::Restricted(ruma::events::room::join_rules::Restricted::new(rules)))
|
||||
}
|
||||
JoinRule::KnockRestricted { rules } => {
|
||||
let rules = allow_rules_from(rules)?;
|
||||
let rules = ruma_allow_rules_from_ffi(rules)?;
|
||||
Ok(Self::KnockRestricted(ruma::events::room::join_rules::Restricted::new(rules)))
|
||||
}
|
||||
JoinRule::Custom { repr } => Ok(serde_json::from_str(&repr)?),
|
||||
@@ -1941,12 +1948,10 @@ impl TryFrom<JoinRule> for ruma::events::room::join_rules::JoinRule {
|
||||
}
|
||||
}
|
||||
|
||||
fn allow_rules_from(
|
||||
value: Vec<AllowRule>,
|
||||
) -> Result<Vec<ruma::events::room::join_rules::AllowRule>, ClientError> {
|
||||
fn ruma_allow_rules_from_ffi(value: Vec<AllowRule>) -> Result<Vec<RumaAllowRule>, ClientError> {
|
||||
let mut ret = Vec::with_capacity(value.len());
|
||||
for rule in value {
|
||||
let rule: Result<ruma::events::room::join_rules::AllowRule, ClientError> = rule.try_into();
|
||||
let rule: Result<RumaAllowRule, ClientError> = rule.try_into();
|
||||
match rule {
|
||||
Ok(rule) => ret.push(rule),
|
||||
Err(error) => return Err(error),
|
||||
@@ -1955,7 +1960,7 @@ fn allow_rules_from(
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
impl TryFrom<AllowRule> for ruma::events::room::join_rules::AllowRule {
|
||||
impl TryFrom<AllowRule> for RumaAllowRule {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(value: AllowRule) -> Result<Self, Self::Error> {
|
||||
@@ -1966,6 +1971,54 @@ impl TryFrom<AllowRule> for ruma::events::room::join_rules::AllowRule {
|
||||
room_id,
|
||||
)))
|
||||
}
|
||||
AllowRule::Custom { json } => Ok(Self::_Custom(Box::new(serde_json::from_str(&json)?))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<RumaJoinRule> for JoinRule {
|
||||
type Error = String;
|
||||
fn try_from(value: RumaJoinRule) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
RumaJoinRule::Knock => Ok(JoinRule::Knock),
|
||||
RumaJoinRule::Public => Ok(JoinRule::Public),
|
||||
RumaJoinRule::Private => Ok(JoinRule::Private),
|
||||
RumaJoinRule::KnockRestricted(restricted) => {
|
||||
let rules = restricted.allow.into_iter().map(TryInto::try_into).collect::<Result<
|
||||
Vec<_>,
|
||||
Self::Error,
|
||||
>>(
|
||||
)?;
|
||||
Ok(JoinRule::KnockRestricted { rules })
|
||||
}
|
||||
RumaJoinRule::Restricted(restricted) => {
|
||||
let rules = restricted.allow.into_iter().map(TryInto::try_into).collect::<Result<
|
||||
Vec<_>,
|
||||
Self::Error,
|
||||
>>(
|
||||
)?;
|
||||
Ok(JoinRule::Restricted { rules })
|
||||
}
|
||||
RumaJoinRule::Invite => Ok(JoinRule::Invite),
|
||||
RumaJoinRule::_Custom(_) => Ok(JoinRule::Custom { repr: value.as_str().to_owned() }),
|
||||
_ => Err(format!("Unknown JoinRule: {:?}", value)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<RumaAllowRule> for AllowRule {
|
||||
type Error = String;
|
||||
fn try_from(value: RumaAllowRule) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
RumaAllowRule::RoomMembership(membership) => {
|
||||
Ok(AllowRule::RoomMembership { room_id: membership.room_id.to_string() })
|
||||
}
|
||||
RumaAllowRule::_Custom(repr) => {
|
||||
let json = serde_json::to_string(&repr)
|
||||
.map_err(|e| format!("Couldn't serialize custom AllowRule: {e:?}"))?;
|
||||
Ok(Self::Custom { json })
|
||||
}
|
||||
_ => Err(format!("Invalid AllowRule: {:?}", value)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,7 +202,7 @@ impl TryFrom<AnySyncMessageLikeEvent> for MessageLikeEventContent {
|
||||
_ => None,
|
||||
});
|
||||
MessageLikeEventContent::RoomMessage {
|
||||
message_type: original_content.msgtype.into(),
|
||||
message_type: original_content.msgtype.try_into()?,
|
||||
in_reply_to_event_id,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// TODO: target-os conditional would be good.
|
||||
|
||||
#![allow(unused_qualifications, clippy::new_without_default)]
|
||||
#![allow(clippy::empty_line_after_doc_comments)] // Needed because uniffi macros contain empty
|
||||
// lines after docs.
|
||||
|
||||
mod authentication;
|
||||
mod chunk_iterator;
|
||||
@@ -33,13 +35,11 @@ mod utils;
|
||||
mod widget;
|
||||
|
||||
use async_compat::TOKIO1 as RUNTIME;
|
||||
use matrix_sdk::ruma::events::room::{
|
||||
message::RoomMessageEventContentWithoutRelation, MediaSource,
|
||||
};
|
||||
use matrix_sdk::ruma::events::room::message::RoomMessageEventContentWithoutRelation;
|
||||
|
||||
use self::{
|
||||
error::ClientError,
|
||||
ruma::{MediaSourceExt, Mentions, RoomMessageEventContentWithoutRelationExt},
|
||||
ruma::{Mentions, RoomMessageEventContentWithoutRelationExt},
|
||||
task_handle::TaskHandle,
|
||||
};
|
||||
|
||||
|
||||
@@ -973,7 +973,7 @@ impl TryFrom<ImageInfo> for RumaAvatarImageInfo {
|
||||
|
||||
fn try_from(value: ImageInfo) -> Result<Self, MediaInfoError> {
|
||||
let thumbnail_url = if let Some(media_source) = value.thumbnail_source {
|
||||
match media_source.as_ref() {
|
||||
match &media_source.as_ref().media_source {
|
||||
MediaSource::Plain(mxc_uri) => Some(mxc_uri.clone()),
|
||||
MediaSource::Encrypted(_) => return Err(MediaInfoError::InvalidField),
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use matrix_sdk::RoomState;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::{
|
||||
client::JoinRule,
|
||||
notification_settings::RoomNotificationMode,
|
||||
room::{Membership, RoomHero},
|
||||
room_member::RoomMember,
|
||||
@@ -54,8 +56,10 @@ pub struct RoomInfo {
|
||||
/// Events causing mentions/highlights for the user, according to their
|
||||
/// notification settings.
|
||||
num_unread_mentions: u64,
|
||||
/// The currently pinned event ids
|
||||
/// The currently pinned event ids.
|
||||
pinned_event_ids: Vec<String>,
|
||||
/// The join rule for this room, if known.
|
||||
join_rule: Option<JoinRule>,
|
||||
}
|
||||
|
||||
impl RoomInfo {
|
||||
@@ -70,6 +74,11 @@ impl RoomInfo {
|
||||
let pinned_event_ids =
|
||||
room.pinned_event_ids().unwrap_or_default().iter().map(|id| id.to_string()).collect();
|
||||
|
||||
let join_rule = room.join_rule().try_into();
|
||||
if let Err(e) = &join_rule {
|
||||
warn!("Failed to parse join rule: {:?}", e);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
id: room.room_id().to_string(),
|
||||
creator: room.creator().as_ref().map(ToString::to_string),
|
||||
@@ -118,6 +127,7 @@ impl RoomInfo {
|
||||
num_unread_notifications: room.num_unread_notifications(),
|
||||
num_unread_mentions: room.num_unread_mentions(),
|
||||
pinned_event_ids,
|
||||
join_rule: join_rule.ok(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@ use ruma::{room::RoomType as RumaRoomType, space::SpaceRoomJoinRule};
|
||||
use tracing::warn;
|
||||
|
||||
use crate::{
|
||||
client::JoinRule, error::ClientError, room::Membership, room_member::RoomMember,
|
||||
client::JoinRule,
|
||||
error::ClientError,
|
||||
room::{Membership, RoomHero},
|
||||
room_member::RoomMember,
|
||||
utils::AsyncRuntimeDropped,
|
||||
};
|
||||
|
||||
@@ -38,6 +41,10 @@ impl RoomPreview {
|
||||
.try_into()
|
||||
.map_err(|_| anyhow::anyhow!("unhandled SpaceRoomJoinRule kind"))?,
|
||||
is_direct: info.is_direct,
|
||||
heroes: info
|
||||
.heroes
|
||||
.as_ref()
|
||||
.map(|heroes| heroes.iter().map(|h| h.to_owned().into()).collect()),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -85,13 +92,15 @@ pub struct RoomPreviewInfo {
|
||||
/// The room type (space, custom) or nothing, if it's a regular room.
|
||||
pub room_type: RoomType,
|
||||
/// Is the history world-readable for this room?
|
||||
pub is_history_world_readable: bool,
|
||||
pub is_history_world_readable: Option<bool>,
|
||||
/// The membership state for the current user, if known.
|
||||
pub membership: Option<Membership>,
|
||||
/// The join rule for this room (private, public, knock, etc.).
|
||||
pub join_rule: JoinRule,
|
||||
/// Whether the room is direct or not, if known.
|
||||
pub is_direct: Option<bool>,
|
||||
/// Room heroes.
|
||||
pub heroes: Option<Vec<RoomHero>>,
|
||||
}
|
||||
|
||||
impl TryFrom<SpaceRoomJoinRule> for JoinRule {
|
||||
|
||||
@@ -15,9 +15,7 @@
|
||||
use std::{collections::BTreeSet, sync::Arc, time::Duration};
|
||||
|
||||
use extension_trait::extension_trait;
|
||||
use matrix_sdk::attachment::{
|
||||
BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseThumbnailInfo, BaseVideoInfo,
|
||||
};
|
||||
use matrix_sdk::attachment::{BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseVideoInfo};
|
||||
use ruma::{
|
||||
assign,
|
||||
events::{
|
||||
@@ -42,7 +40,8 @@ use ruma::{
|
||||
VideoInfo as RumaVideoInfo,
|
||||
VideoMessageEventContent as RumaVideoMessageEventContent,
|
||||
},
|
||||
ImageInfo as RumaImageInfo, MediaSource, ThumbnailInfo as RumaThumbnailInfo,
|
||||
ImageInfo as RumaImageInfo, MediaSource as RumaMediaSource,
|
||||
ThumbnailInfo as RumaThumbnailInfo,
|
||||
},
|
||||
},
|
||||
matrix_uri::MatrixId as RumaMatrixId,
|
||||
@@ -154,11 +153,6 @@ impl From<&RumaMatrixId> for MatrixId {
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn media_source_from_url(url: String) -> Arc<MediaSource> {
|
||||
Arc::new(MediaSource::Plain(url.into()))
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn message_event_content_new(
|
||||
msgtype: MessageType,
|
||||
@@ -200,21 +194,84 @@ pub fn message_event_content_from_html_as_emote(
|
||||
)))
|
||||
}
|
||||
|
||||
#[extension_trait]
|
||||
pub impl MediaSourceExt for MediaSource {
|
||||
fn from_json(json: String) -> Result<MediaSource, ClientError> {
|
||||
let res = serde_json::from_str(&json)?;
|
||||
Ok(res)
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
pub struct MediaSource {
|
||||
pub(crate) media_source: RumaMediaSource,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl MediaSource {
|
||||
#[uniffi::constructor]
|
||||
pub fn from_url(url: String) -> Result<Arc<MediaSource>, ClientError> {
|
||||
let media_source = RumaMediaSource::Plain(url.into());
|
||||
media_source.verify()?;
|
||||
|
||||
Ok(Arc::new(MediaSource { media_source }))
|
||||
}
|
||||
|
||||
fn to_json(&self) -> String {
|
||||
serde_json::to_string(self).expect("Media source should always be serializable ")
|
||||
pub fn url(&self) -> String {
|
||||
self.media_source.url()
|
||||
}
|
||||
|
||||
// Used on Element X Android
|
||||
#[uniffi::constructor]
|
||||
pub fn from_json(json: String) -> Result<Arc<Self>, ClientError> {
|
||||
let media_source: RumaMediaSource = serde_json::from_str(&json)?;
|
||||
media_source.verify()?;
|
||||
|
||||
Ok(Arc::new(MediaSource { media_source }))
|
||||
}
|
||||
|
||||
// Used on Element X Android
|
||||
pub fn to_json(&self) -> String {
|
||||
serde_json::to_string(&self.media_source)
|
||||
.expect("Media source should always be serializable ")
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<RumaMediaSource> for MediaSource {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(value: RumaMediaSource) -> Result<Self, Self::Error> {
|
||||
value.verify()?;
|
||||
Ok(Self { media_source: value })
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&RumaMediaSource> for MediaSource {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(value: &RumaMediaSource) -> Result<Self, Self::Error> {
|
||||
value.verify()?;
|
||||
Ok(Self { media_source: value.clone() })
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MediaSource> for RumaMediaSource {
|
||||
fn from(value: MediaSource) -> Self {
|
||||
value.media_source
|
||||
}
|
||||
}
|
||||
|
||||
#[extension_trait]
|
||||
pub(crate) impl MediaSourceExt for RumaMediaSource {
|
||||
fn verify(&self) -> Result<(), ClientError> {
|
||||
match self {
|
||||
RumaMediaSource::Plain(url) => {
|
||||
url.validate().map_err(|e| ClientError::Generic { msg: e.to_string() })?;
|
||||
}
|
||||
RumaMediaSource::Encrypted(file) => {
|
||||
file.url.validate().map_err(|e| ClientError::Generic { msg: e.to_string() })?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn url(&self) -> String {
|
||||
match self {
|
||||
MediaSource::Plain(url) => url.to_string(),
|
||||
MediaSource::Encrypted(file) => file.url.to_string(),
|
||||
RumaMediaSource::Plain(url) => url.to_string(),
|
||||
RumaMediaSource::Encrypted(file) => file.url.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -280,7 +337,7 @@ fn get_body_and_filename(filename: String, caption: Option<String>) -> (String,
|
||||
}
|
||||
|
||||
impl TryFrom<MessageType> for RumaMessageType {
|
||||
type Error = serde_json::Error;
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(value: MessageType) -> Result<Self, Self::Error> {
|
||||
Ok(match value {
|
||||
@@ -292,7 +349,7 @@ impl TryFrom<MessageType> for RumaMessageType {
|
||||
MessageType::Image { content } => {
|
||||
let (body, filename) = get_body_and_filename(content.filename, content.caption);
|
||||
let mut event_content =
|
||||
RumaImageMessageEventContent::new(body, (*content.source).clone())
|
||||
RumaImageMessageEventContent::new(body, (*content.source).clone().into())
|
||||
.info(content.info.map(Into::into).map(Box::new));
|
||||
event_content.formatted = content.formatted_caption.map(Into::into);
|
||||
event_content.filename = filename;
|
||||
@@ -301,7 +358,7 @@ impl TryFrom<MessageType> for RumaMessageType {
|
||||
MessageType::Audio { content } => {
|
||||
let (body, filename) = get_body_and_filename(content.filename, content.caption);
|
||||
let mut event_content =
|
||||
RumaAudioMessageEventContent::new(body, (*content.source).clone())
|
||||
RumaAudioMessageEventContent::new(body, (*content.source).clone().into())
|
||||
.info(content.info.map(Into::into).map(Box::new));
|
||||
event_content.formatted = content.formatted_caption.map(Into::into);
|
||||
event_content.filename = filename;
|
||||
@@ -310,7 +367,7 @@ impl TryFrom<MessageType> for RumaMessageType {
|
||||
MessageType::Video { content } => {
|
||||
let (body, filename) = get_body_and_filename(content.filename, content.caption);
|
||||
let mut event_content =
|
||||
RumaVideoMessageEventContent::new(body, (*content.source).clone())
|
||||
RumaVideoMessageEventContent::new(body, (*content.source).clone().into())
|
||||
.info(content.info.map(Into::into).map(Box::new));
|
||||
event_content.formatted = content.formatted_caption.map(Into::into);
|
||||
event_content.filename = filename;
|
||||
@@ -319,7 +376,7 @@ impl TryFrom<MessageType> for RumaMessageType {
|
||||
MessageType::File { content } => {
|
||||
let (body, filename) = get_body_and_filename(content.filename, content.caption);
|
||||
let mut event_content =
|
||||
RumaFileMessageEventContent::new(body, (*content.source).clone())
|
||||
RumaFileMessageEventContent::new(body, (*content.source).clone().into())
|
||||
.info(content.info.map(Into::into).map(Box::new));
|
||||
event_content.formatted = content.formatted_caption.map(Into::into);
|
||||
event_content.filename = filename;
|
||||
@@ -345,9 +402,11 @@ impl TryFrom<MessageType> for RumaMessageType {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RumaMessageType> for MessageType {
|
||||
fn from(value: RumaMessageType) -> Self {
|
||||
match value {
|
||||
impl TryFrom<RumaMessageType> for MessageType {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(value: RumaMessageType) -> Result<Self, Self::Error> {
|
||||
Ok(match value {
|
||||
RumaMessageType::Emote(c) => MessageType::Emote {
|
||||
content: EmoteMessageContent {
|
||||
body: c.body.clone(),
|
||||
@@ -359,16 +418,17 @@ impl From<RumaMessageType> for MessageType {
|
||||
filename: c.filename().to_owned(),
|
||||
caption: c.caption().map(ToString::to_string),
|
||||
formatted_caption: c.formatted_caption().map(Into::into),
|
||||
source: Arc::new(c.source.clone()),
|
||||
info: c.info.as_deref().map(Into::into),
|
||||
source: Arc::new(c.source.try_into()?),
|
||||
info: c.info.as_deref().map(TryInto::try_into).transpose()?,
|
||||
},
|
||||
},
|
||||
|
||||
RumaMessageType::Audio(c) => MessageType::Audio {
|
||||
content: AudioMessageContent {
|
||||
filename: c.filename().to_owned(),
|
||||
caption: c.caption().map(ToString::to_string),
|
||||
formatted_caption: c.formatted_caption().map(Into::into),
|
||||
source: Arc::new(c.source.clone()),
|
||||
source: Arc::new(c.source.try_into()?),
|
||||
info: c.info.as_deref().map(Into::into),
|
||||
audio: c.audio.map(Into::into),
|
||||
voice: c.voice.map(Into::into),
|
||||
@@ -379,8 +439,8 @@ impl From<RumaMessageType> for MessageType {
|
||||
filename: c.filename().to_owned(),
|
||||
caption: c.caption().map(ToString::to_string),
|
||||
formatted_caption: c.formatted_caption().map(Into::into),
|
||||
source: Arc::new(c.source.clone()),
|
||||
info: c.info.as_deref().map(Into::into),
|
||||
source: Arc::new(c.source.try_into()?),
|
||||
info: c.info.as_deref().map(TryInto::try_into).transpose()?,
|
||||
},
|
||||
},
|
||||
RumaMessageType::File(c) => MessageType::File {
|
||||
@@ -388,8 +448,8 @@ impl From<RumaMessageType> for MessageType {
|
||||
filename: c.filename().to_owned(),
|
||||
caption: c.caption().map(ToString::to_string),
|
||||
formatted_caption: c.formatted_caption().map(Into::into),
|
||||
source: Arc::new(c.source.clone()),
|
||||
info: c.info.as_deref().map(Into::into),
|
||||
source: Arc::new(c.source.try_into()?),
|
||||
info: c.info.as_deref().map(TryInto::try_into).transpose()?,
|
||||
},
|
||||
},
|
||||
RumaMessageType::Notice(c) => MessageType::Notice {
|
||||
@@ -425,7 +485,7 @@ impl From<RumaMessageType> for MessageType {
|
||||
msgtype: value.msgtype().to_owned(),
|
||||
body: value.body().to_owned(),
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -520,7 +580,7 @@ impl From<ImageInfo> for RumaImageInfo {
|
||||
mimetype: value.mimetype,
|
||||
size: value.size.map(u64_to_uint),
|
||||
thumbnail_info: value.thumbnail_info.map(Into::into).map(Box::new),
|
||||
thumbnail_source: value.thumbnail_source.map(|source| (*source).clone()),
|
||||
thumbnail_source: value.thumbnail_source.map(|source| (*source).clone().into()),
|
||||
blurhash: value.blurhash,
|
||||
})
|
||||
}
|
||||
@@ -625,7 +685,7 @@ impl From<VideoInfo> for RumaVideoInfo {
|
||||
mimetype: value.mimetype,
|
||||
size: value.size.map(u64_to_uint),
|
||||
thumbnail_info: value.thumbnail_info.map(Into::into).map(Box::new),
|
||||
thumbnail_source: value.thumbnail_source.map(|source| (*source).clone()),
|
||||
thumbnail_source: value.thumbnail_source.map(|source| (*source).clone().into()),
|
||||
blurhash: value.blurhash,
|
||||
})
|
||||
}
|
||||
@@ -668,7 +728,7 @@ impl From<FileInfo> for RumaFileInfo {
|
||||
mimetype: value.mimetype,
|
||||
size: value.size.map(u64_to_uint),
|
||||
thumbnail_info: value.thumbnail_info.map(Into::into).map(Box::new),
|
||||
thumbnail_source: value.thumbnail_source.map(|source| (*source).clone()),
|
||||
thumbnail_source: value.thumbnail_source.map(|source| (*source).clone().into()),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -703,21 +763,6 @@ impl From<ThumbnailInfo> for RumaThumbnailInfo {
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&ThumbnailInfo> for BaseThumbnailInfo {
|
||||
type Error = MediaInfoError;
|
||||
|
||||
fn try_from(value: &ThumbnailInfo) -> Result<Self, MediaInfoError> {
|
||||
let height = UInt::try_from(value.height.ok_or(MediaInfoError::MissingField)?)
|
||||
.map_err(|_| MediaInfoError::InvalidField)?;
|
||||
let width = UInt::try_from(value.width.ok_or(MediaInfoError::MissingField)?)
|
||||
.map_err(|_| MediaInfoError::InvalidField)?;
|
||||
let size = UInt::try_from(value.size.ok_or(MediaInfoError::MissingField)?)
|
||||
.map_err(|_| MediaInfoError::InvalidField)?;
|
||||
|
||||
Ok(BaseThumbnailInfo { height: Some(height), width: Some(width), size: Some(size) })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct NoticeMessageContent {
|
||||
pub body: String,
|
||||
@@ -790,8 +835,10 @@ pub enum MessageFormat {
|
||||
Unknown { format: String },
|
||||
}
|
||||
|
||||
impl From<&matrix_sdk::ruma::events::room::ImageInfo> for ImageInfo {
|
||||
fn from(info: &matrix_sdk::ruma::events::room::ImageInfo) -> Self {
|
||||
impl TryFrom<&matrix_sdk::ruma::events::room::ImageInfo> for ImageInfo {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(info: &matrix_sdk::ruma::events::room::ImageInfo) -> Result<Self, Self::Error> {
|
||||
let thumbnail_info = info.thumbnail_info.as_ref().map(|info| ThumbnailInfo {
|
||||
height: info.height.map(Into::into),
|
||||
width: info.width.map(Into::into),
|
||||
@@ -799,15 +846,20 @@ impl From<&matrix_sdk::ruma::events::room::ImageInfo> for ImageInfo {
|
||||
size: info.size.map(Into::into),
|
||||
});
|
||||
|
||||
Self {
|
||||
Ok(Self {
|
||||
height: info.height.map(Into::into),
|
||||
width: info.width.map(Into::into),
|
||||
mimetype: info.mimetype.clone(),
|
||||
size: info.size.map(Into::into),
|
||||
thumbnail_info,
|
||||
thumbnail_source: info.thumbnail_source.clone().map(Arc::new),
|
||||
thumbnail_source: info
|
||||
.thumbnail_source
|
||||
.as_ref()
|
||||
.map(TryInto::try_into)
|
||||
.transpose()?
|
||||
.map(Arc::new),
|
||||
blurhash: info.blurhash.clone(),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -821,8 +873,10 @@ impl From<&RumaAudioInfo> for AudioInfo {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&RumaVideoInfo> for VideoInfo {
|
||||
fn from(info: &RumaVideoInfo) -> Self {
|
||||
impl TryFrom<&RumaVideoInfo> for VideoInfo {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(info: &RumaVideoInfo) -> Result<Self, Self::Error> {
|
||||
let thumbnail_info = info.thumbnail_info.as_ref().map(|info| ThumbnailInfo {
|
||||
height: info.height.map(Into::into),
|
||||
width: info.width.map(Into::into),
|
||||
@@ -830,21 +884,28 @@ impl From<&RumaVideoInfo> for VideoInfo {
|
||||
size: info.size.map(Into::into),
|
||||
});
|
||||
|
||||
Self {
|
||||
Ok(Self {
|
||||
duration: info.duration,
|
||||
height: info.height.map(Into::into),
|
||||
width: info.width.map(Into::into),
|
||||
mimetype: info.mimetype.clone(),
|
||||
size: info.size.map(Into::into),
|
||||
thumbnail_info,
|
||||
thumbnail_source: info.thumbnail_source.clone().map(Arc::new),
|
||||
thumbnail_source: info
|
||||
.thumbnail_source
|
||||
.as_ref()
|
||||
.map(TryInto::try_into)
|
||||
.transpose()?
|
||||
.map(Arc::new),
|
||||
blurhash: info.blurhash.clone(),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&RumaFileInfo> for FileInfo {
|
||||
fn from(info: &RumaFileInfo) -> Self {
|
||||
impl TryFrom<&RumaFileInfo> for FileInfo {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(info: &RumaFileInfo) -> Result<Self, Self::Error> {
|
||||
let thumbnail_info = info.thumbnail_info.as_ref().map(|info| ThumbnailInfo {
|
||||
height: info.height.map(Into::into),
|
||||
width: info.width.map(Into::into),
|
||||
@@ -852,12 +913,17 @@ impl From<&RumaFileInfo> for FileInfo {
|
||||
size: info.size.map(Into::into),
|
||||
});
|
||||
|
||||
Self {
|
||||
Ok(Self {
|
||||
mimetype: info.mimetype.clone(),
|
||||
size: info.size.map(Into::into),
|
||||
thumbnail_info,
|
||||
thumbnail_source: info.thumbnail_source.clone().map(Arc::new),
|
||||
}
|
||||
thumbnail_source: info
|
||||
.thumbnail_source
|
||||
.as_ref()
|
||||
.map(TryInto::try_into)
|
||||
.transpose()?
|
||||
.map(Arc::new),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,26 +16,55 @@ use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use matrix_sdk::{crypto::types::events::UtdCause, room::power_levels::power_level_user_changes};
|
||||
use matrix_sdk_ui::timeline::{PollResult, RoomPinnedEventsChange, TimelineDetails};
|
||||
use ruma::events::{room::MediaSource, FullStateEventContent};
|
||||
use ruma::events::{room::MediaSource as RumaMediaSource, EventContent, FullStateEventContent};
|
||||
|
||||
use super::ProfileDetails;
|
||||
use crate::ruma::{ImageInfo, Mentions, MessageType, PollKind};
|
||||
use crate::{
|
||||
error::ClientError,
|
||||
ruma::{ImageInfo, MediaSource, MediaSourceExt, Mentions, MessageType, PollKind},
|
||||
};
|
||||
|
||||
impl From<matrix_sdk_ui::timeline::TimelineItemContent> for TimelineItemContent {
|
||||
fn from(value: matrix_sdk_ui::timeline::TimelineItemContent) -> Self {
|
||||
use matrix_sdk_ui::timeline::TimelineItemContent as Content;
|
||||
|
||||
match value {
|
||||
Content::Message(message) => TimelineItemContent::Message { content: message.into() },
|
||||
Content::Message(message) => {
|
||||
let msgtype = message.msgtype().msgtype().to_owned();
|
||||
|
||||
match TryInto::<MessageContent>::try_into(message) {
|
||||
Ok(message) => TimelineItemContent::Message { content: message },
|
||||
Err(error) => TimelineItemContent::FailedToParseMessageLike {
|
||||
event_type: msgtype,
|
||||
error: error.to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Content::RedactedMessage => TimelineItemContent::RedactedMessage,
|
||||
|
||||
Content::Sticker(sticker) => {
|
||||
let content = sticker.content();
|
||||
TimelineItemContent::Sticker {
|
||||
body: content.body.clone(),
|
||||
info: (&content.info).into(),
|
||||
source: Arc::new(MediaSource::from(content.source.clone())),
|
||||
|
||||
let media_source = RumaMediaSource::from(content.source.clone());
|
||||
|
||||
if let Err(error) = media_source.verify() {
|
||||
return TimelineItemContent::FailedToParseMessageLike {
|
||||
event_type: sticker.content().event_type().to_string(),
|
||||
error: error.to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
match TryInto::<ImageInfo>::try_into(&content.info) {
|
||||
Ok(info) => TimelineItemContent::Sticker {
|
||||
body: content.body.clone(),
|
||||
info,
|
||||
source: Arc::new(MediaSource { media_source }),
|
||||
},
|
||||
Err(error) => TimelineItemContent::FailedToParseMessageLike {
|
||||
event_type: sticker.content().event_type().to_string(),
|
||||
error: error.to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,16 +146,18 @@ pub struct MessageContent {
|
||||
pub mentions: Option<Mentions>,
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_ui::timeline::Message> for MessageContent {
|
||||
fn from(value: matrix_sdk_ui::timeline::Message) -> Self {
|
||||
Self {
|
||||
msg_type: value.msgtype().clone().into(),
|
||||
impl TryFrom<matrix_sdk_ui::timeline::Message> for MessageContent {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(value: matrix_sdk_ui::timeline::Message) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
msg_type: value.msgtype().clone().try_into()?,
|
||||
body: value.body().to_owned(),
|
||||
in_reply_to: value.in_reply_to().map(|r| Arc::new(r.clone().into())),
|
||||
is_edited: value.is_edited(),
|
||||
thread_root: value.thread_root().map(|id| id.to_string()),
|
||||
mentions: value.mentions().cloned().map(|m| m.into()),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ use matrix_sdk::crypto::CollectStrategy;
|
||||
use matrix_sdk::{
|
||||
attachment::{
|
||||
AttachmentConfig, AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo,
|
||||
BaseThumbnailInfo, BaseVideoInfo, Thumbnail,
|
||||
BaseVideoInfo, Thumbnail,
|
||||
},
|
||||
deserialized_responses::{ShieldState as SdkShieldState, ShieldStateCode},
|
||||
room::edit::EditedContent as SdkEditedContent,
|
||||
@@ -53,7 +53,7 @@ use ruma::{
|
||||
},
|
||||
AnyMessageLikeEventContent,
|
||||
},
|
||||
EventId,
|
||||
EventId, UInt,
|
||||
};
|
||||
use tokio::{
|
||||
sync::Mutex,
|
||||
@@ -144,19 +144,26 @@ fn build_thumbnail_info(
|
||||
let thumbnail_data =
|
||||
fs::read(thumbnail_url).map_err(|_| RoomError::InvalidThumbnailData)?;
|
||||
|
||||
let base_thumbnail_info = BaseThumbnailInfo::try_from(&thumbnail_info)
|
||||
.map_err(|_| RoomError::InvalidAttachmentData)?;
|
||||
let height = thumbnail_info
|
||||
.height
|
||||
.and_then(|u| UInt::try_from(u).ok())
|
||||
.ok_or(RoomError::InvalidAttachmentData)?;
|
||||
let width = thumbnail_info
|
||||
.width
|
||||
.and_then(|u| UInt::try_from(u).ok())
|
||||
.ok_or(RoomError::InvalidAttachmentData)?;
|
||||
let size = thumbnail_info
|
||||
.size
|
||||
.and_then(|u| UInt::try_from(u).ok())
|
||||
.ok_or(RoomError::InvalidAttachmentData)?;
|
||||
|
||||
let mime_str =
|
||||
thumbnail_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
|
||||
let mime_type =
|
||||
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
|
||||
|
||||
let thumbnail = Thumbnail {
|
||||
data: thumbnail_data,
|
||||
content_type: mime_type,
|
||||
info: Some(base_thumbnail_info),
|
||||
};
|
||||
let thumbnail =
|
||||
Thumbnail { data: thumbnail_data, content_type: mime_type, height, width, size };
|
||||
|
||||
Ok(AttachmentConfig::with_thumbnail(thumbnail))
|
||||
}
|
||||
@@ -545,6 +552,7 @@ impl Timeline {
|
||||
.await
|
||||
{
|
||||
Ok(()) => Ok(()),
|
||||
|
||||
Err(timeline::Error::EventNotInTimeline(_)) => {
|
||||
// If we couldn't edit, assume it was an (remote) event that wasn't in the
|
||||
// timeline, and try to edit it via the room itself.
|
||||
@@ -560,7 +568,8 @@ impl Timeline {
|
||||
room.send_queue().send(edit_event).await?;
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => Err(err)?,
|
||||
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1278,6 +1287,7 @@ impl From<ReceiptType> for ruma::api::client::receipt::create_receipt::v3::Recei
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum EditedContent {
|
||||
RoomMessage { content: Arc<RoomMessageEventContentWithoutRelation> },
|
||||
MediaCaption { caption: Option<String>, formatted_caption: Option<FormattedBody> },
|
||||
PollStart { poll_data: PollData },
|
||||
}
|
||||
|
||||
@@ -1288,6 +1298,12 @@ impl TryFrom<EditedContent> for SdkEditedContent {
|
||||
EditedContent::RoomMessage { content } => {
|
||||
Ok(SdkEditedContent::RoomMessage((*content).clone()))
|
||||
}
|
||||
EditedContent::MediaCaption { caption, formatted_caption } => {
|
||||
Ok(SdkEditedContent::MediaCaption {
|
||||
caption,
|
||||
formatted_caption: formatted_caption.map(Into::into),
|
||||
})
|
||||
}
|
||||
EditedContent::PollStart { poll_data } => {
|
||||
let block: UnstablePollStartContentBlock = poll_data.clone().try_into()?;
|
||||
Ok(SdkEditedContent::PollStart {
|
||||
@@ -1299,6 +1315,23 @@ impl TryFrom<EditedContent> for SdkEditedContent {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a caption edit.
|
||||
///
|
||||
/// If no `formatted_caption` is provided, then it's assumed the `caption`
|
||||
/// represents valid Markdown that can be used as the formatted caption.
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
fn create_caption_edit(
|
||||
caption: Option<String>,
|
||||
formatted_caption: Option<FormattedBody>,
|
||||
) -> EditedContent {
|
||||
let formatted_caption =
|
||||
formatted_body_from(caption.as_deref(), formatted_caption.map(Into::into));
|
||||
EditedContent::MediaCaption {
|
||||
caption,
|
||||
formatted_caption: formatted_caption.as_ref().map(Into::into),
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper to retrieve some timeline item info lazily.
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
pub struct LazyTimelineItemProvider(Arc<matrix_sdk_ui::timeline::EventTimelineItem>);
|
||||
|
||||
@@ -30,7 +30,7 @@ uniffi = ["dep:uniffi", "matrix-sdk-crypto?/uniffi", "matrix-sdk-common/uniffi"]
|
||||
# Private feature, see
|
||||
# https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823 for the gory
|
||||
# details.
|
||||
test-send-sync = []
|
||||
test-send-sync = ["matrix-sdk-crypto?/test-send-sync"]
|
||||
|
||||
# "message-ids" feature doesn't do anything and is deprecated.
|
||||
message-ids = []
|
||||
|
||||
@@ -1459,10 +1459,14 @@ impl BaseClient {
|
||||
pub async fn share_room_key(&self, room_id: &RoomId) -> Result<Vec<Arc<ToDeviceRequest>>> {
|
||||
match self.olm_machine().await.as_ref() {
|
||||
Some(o) => {
|
||||
let (history_visibility, settings) = self
|
||||
.get_room(room_id)
|
||||
.map(|r| (r.history_visibility(), r.encryption_settings()))
|
||||
.unwrap_or((HistoryVisibility::Joined, None));
|
||||
let Some(room) = self.get_room(room_id) else {
|
||||
return Err(Error::InsufficientData);
|
||||
};
|
||||
|
||||
let history_visibility = room.history_visibility_or_default();
|
||||
let Some(room_encryption_event) = room.encryption_settings() else {
|
||||
return Err(Error::EncryptionNotEnabled);
|
||||
};
|
||||
|
||||
// Don't share the group session with members that are invited
|
||||
// if the history visibility is set to `Joined`
|
||||
@@ -1474,9 +1478,8 @@ impl BaseClient {
|
||||
|
||||
let members = self.store.get_user_ids(room_id, filter).await?;
|
||||
|
||||
let settings = settings.ok_or(Error::EncryptionNotEnabled)?;
|
||||
let settings = EncryptionSettings::new(
|
||||
settings,
|
||||
room_encryption_event,
|
||||
history_visibility,
|
||||
self.room_key_recipient_strategy.clone(),
|
||||
);
|
||||
|
||||
@@ -27,7 +27,7 @@ use ruma::{
|
||||
pub struct DebugListOfRawEventsNoId<'a, T>(pub &'a [Raw<T>]);
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl<'a, T> fmt::Debug for DebugListOfRawEventsNoId<'a, T> {
|
||||
impl<T> fmt::Debug for DebugListOfRawEventsNoId<'_, T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut list = f.debug_list();
|
||||
list.entries(self.0.iter().map(DebugRawEventNoId));
|
||||
@@ -41,7 +41,7 @@ impl<'a, T> fmt::Debug for DebugListOfRawEventsNoId<'a, T> {
|
||||
pub struct DebugInvitedRoom<'a>(pub &'a InvitedRoom);
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl<'a> fmt::Debug for DebugInvitedRoom<'a> {
|
||||
impl fmt::Debug for DebugInvitedRoom<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("InvitedRoom")
|
||||
.field("invite_state", &DebugListOfRawEvents(&self.0.invite_state.events))
|
||||
@@ -55,7 +55,7 @@ impl<'a> fmt::Debug for DebugInvitedRoom<'a> {
|
||||
pub struct DebugKnockedRoom<'a>(pub &'a KnockedRoom);
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl<'a> fmt::Debug for DebugKnockedRoom<'a> {
|
||||
impl fmt::Debug for DebugKnockedRoom<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("KnockedRoom")
|
||||
.field("knock_state", &DebugListOfRawEvents(&self.0.knock_state.events))
|
||||
@@ -66,7 +66,7 @@ impl<'a> fmt::Debug for DebugKnockedRoom<'a> {
|
||||
pub(crate) struct DebugListOfRawEvents<'a, T>(pub &'a [Raw<T>]);
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl<'a, T> fmt::Debug for DebugListOfRawEvents<'a, T> {
|
||||
impl<T> fmt::Debug for DebugListOfRawEvents<'_, T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut list = f.debug_list();
|
||||
list.entries(self.0.iter().map(DebugRawEvent));
|
||||
|
||||
@@ -16,12 +16,17 @@ use std::{collections::HashMap, num::NonZeroUsize, sync::RwLock as StdRwLock, ti
|
||||
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk_common::{
|
||||
ring_buffer::RingBuffer, store_locks::memory_store_helper::try_take_leased_lock,
|
||||
linked_chunk::{relational::RelationalLinkedChunk, Update},
|
||||
ring_buffer::RingBuffer,
|
||||
store_locks::memory_store_helper::try_take_leased_lock,
|
||||
};
|
||||
use ruma::{MxcUri, OwnedMxcUri};
|
||||
use ruma::{MxcUri, OwnedMxcUri, RoomId};
|
||||
|
||||
use super::{EventCacheStore, EventCacheStoreError, Result};
|
||||
use crate::media::{MediaRequestParameters, UniqueKey as _};
|
||||
use crate::{
|
||||
event_cache::{Event, Gap},
|
||||
media::{MediaRequestParameters, UniqueKey as _},
|
||||
};
|
||||
|
||||
/// In-memory, non-persistent implementation of the `EventCacheStore`.
|
||||
///
|
||||
@@ -29,8 +34,14 @@ use crate::media::{MediaRequestParameters, UniqueKey as _};
|
||||
#[allow(clippy::type_complexity)]
|
||||
#[derive(Debug)]
|
||||
pub struct MemoryStore {
|
||||
media: StdRwLock<RingBuffer<(OwnedMxcUri, String /* unique key */, Vec<u8>)>>,
|
||||
leases: StdRwLock<HashMap<String, (String, Instant)>>,
|
||||
inner: StdRwLock<MemoryStoreInner>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct MemoryStoreInner {
|
||||
media: RingBuffer<(OwnedMxcUri, String /* unique key */, Vec<u8>)>,
|
||||
leases: HashMap<String, (String, Instant)>,
|
||||
events: RelationalLinkedChunk<Event, Gap>,
|
||||
}
|
||||
|
||||
// SAFETY: `new_unchecked` is safe because 20 is not zero.
|
||||
@@ -39,8 +50,11 @@ const NUMBER_OF_MEDIAS: NonZeroUsize = unsafe { NonZeroUsize::new_unchecked(20)
|
||||
impl Default for MemoryStore {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
media: StdRwLock::new(RingBuffer::new(NUMBER_OF_MEDIAS)),
|
||||
leases: Default::default(),
|
||||
inner: StdRwLock::new(MemoryStoreInner {
|
||||
media: RingBuffer::new(NUMBER_OF_MEDIAS),
|
||||
leases: Default::default(),
|
||||
events: RelationalLinkedChunk::new(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,7 +77,20 @@ impl EventCacheStore for MemoryStore {
|
||||
key: &str,
|
||||
holder: &str,
|
||||
) -> Result<bool, Self::Error> {
|
||||
Ok(try_take_leased_lock(&self.leases, lease_duration_ms, key, holder))
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
|
||||
Ok(try_take_leased_lock(&mut inner.leases, lease_duration_ms, key, holder))
|
||||
}
|
||||
|
||||
async fn handle_linked_chunk_updates(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
updates: Vec<Update<Event, Gap>>,
|
||||
) -> Result<(), Self::Error> {
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
inner.events.apply_updates(room_id, updates);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn add_media_content(
|
||||
@@ -73,8 +100,10 @@ impl EventCacheStore for MemoryStore {
|
||||
) -> Result<()> {
|
||||
// Avoid duplication. Let's try to remove it first.
|
||||
self.remove_media_content(request).await?;
|
||||
|
||||
// Now, let's add it.
|
||||
self.media.write().unwrap().push((request.uri().to_owned(), request.unique_key(), data));
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
inner.media.push((request.uri().to_owned(), request.unique_key(), data));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -86,8 +115,10 @@ impl EventCacheStore for MemoryStore {
|
||||
) -> Result<(), Self::Error> {
|
||||
let expected_key = from.unique_key();
|
||||
|
||||
let mut medias = self.media.write().unwrap();
|
||||
if let Some((mxc, key, _)) = medias.iter_mut().find(|(_, key, _)| *key == expected_key) {
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
|
||||
if let Some((mxc, key, _)) = inner.media.iter_mut().find(|(_, key, _)| *key == expected_key)
|
||||
{
|
||||
*mxc = to.uri().to_owned();
|
||||
*key = to.unique_key();
|
||||
}
|
||||
@@ -98,8 +129,9 @@ impl EventCacheStore for MemoryStore {
|
||||
async fn get_media_content(&self, request: &MediaRequestParameters) -> Result<Option<Vec<u8>>> {
|
||||
let expected_key = request.unique_key();
|
||||
|
||||
let media = self.media.read().unwrap();
|
||||
Ok(media.iter().find_map(|(_media_uri, media_key, media_content)| {
|
||||
let inner = self.inner.read().unwrap();
|
||||
|
||||
Ok(inner.media.iter().find_map(|(_media_uri, media_key, media_content)| {
|
||||
(media_key == &expected_key).then(|| media_content.to_owned())
|
||||
}))
|
||||
}
|
||||
@@ -107,23 +139,27 @@ impl EventCacheStore for MemoryStore {
|
||||
async fn remove_media_content(&self, request: &MediaRequestParameters) -> Result<()> {
|
||||
let expected_key = request.unique_key();
|
||||
|
||||
let mut media = self.media.write().unwrap();
|
||||
let Some(index) = media
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
|
||||
let Some(index) = inner
|
||||
.media
|
||||
.iter()
|
||||
.position(|(_media_uri, media_key, _media_content)| media_key == &expected_key)
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
media.remove(index);
|
||||
inner.media.remove(index);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<()> {
|
||||
let mut media = self.media.write().unwrap();
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
|
||||
let expected_key = uri.to_owned();
|
||||
let positions = media
|
||||
let positions = inner
|
||||
.media
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(position, (media_uri, _media_key, _media_content))| {
|
||||
@@ -133,7 +169,7 @@ impl EventCacheStore for MemoryStore {
|
||||
|
||||
// Iterate in reverse-order so that positions stay valid after first removals.
|
||||
for position in positions.into_iter().rev() {
|
||||
media.remove(position);
|
||||
inner.media.remove(position);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -43,7 +43,7 @@ pub use self::{
|
||||
#[derive(Clone)]
|
||||
pub struct EventCacheStoreLock {
|
||||
/// The inner cross process lock that is used to lock the `EventCacheStore`.
|
||||
cross_process_lock: CrossProcessStoreLock<LockableEventCacheStore>,
|
||||
cross_process_lock: Arc<CrossProcessStoreLock<LockableEventCacheStore>>,
|
||||
|
||||
/// The store itself.
|
||||
///
|
||||
@@ -70,11 +70,11 @@ impl EventCacheStoreLock {
|
||||
let store = store.into_event_cache_store();
|
||||
|
||||
Self {
|
||||
cross_process_lock: CrossProcessStoreLock::new(
|
||||
cross_process_lock: Arc::new(CrossProcessStoreLock::new(
|
||||
LockableEventCacheStore(store.clone()),
|
||||
"default".to_owned(),
|
||||
holder,
|
||||
),
|
||||
)),
|
||||
store,
|
||||
}
|
||||
}
|
||||
@@ -100,13 +100,13 @@ pub struct EventCacheStoreLockGuard<'a> {
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl<'a> fmt::Debug for EventCacheStoreLockGuard<'a> {
|
||||
impl fmt::Debug for EventCacheStoreLockGuard<'_> {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.debug_struct("EventCacheStoreLockGuard").finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Deref for EventCacheStoreLockGuard<'a> {
|
||||
impl Deref for EventCacheStoreLockGuard<'_> {
|
||||
type Target = DynEventCacheStore;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
|
||||
@@ -15,11 +15,14 @@
|
||||
use std::{fmt, sync::Arc};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk_common::AsyncTraitDeps;
|
||||
use ruma::MxcUri;
|
||||
use matrix_sdk_common::{linked_chunk::Update, AsyncTraitDeps};
|
||||
use ruma::{MxcUri, RoomId};
|
||||
|
||||
use super::EventCacheStoreError;
|
||||
use crate::media::MediaRequestParameters;
|
||||
use crate::{
|
||||
event_cache::{Event, Gap},
|
||||
media::MediaRequestParameters,
|
||||
};
|
||||
|
||||
/// An abstract trait that can be used to implement different store backends
|
||||
/// for the event cache of the SDK.
|
||||
@@ -37,6 +40,15 @@ pub trait EventCacheStore: AsyncTraitDeps {
|
||||
holder: &str,
|
||||
) -> Result<bool, Self::Error>;
|
||||
|
||||
/// An [`Update`] reflects an operation that has happened inside a linked
|
||||
/// chunk. The linked chunk is used by the event cache to store the events
|
||||
/// in-memory. This method aims at forwarding this update inside this store.
|
||||
async fn handle_linked_chunk_updates(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
updates: Vec<Update<Event, Gap>>,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Add a media file's content in the media store.
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -131,6 +143,14 @@ impl<T: EventCacheStore> EventCacheStore for EraseEventCacheStoreError<T> {
|
||||
self.0.try_take_leased_lock(lease_duration_ms, key, holder).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn handle_linked_chunk_updates(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
updates: Vec<Update<Event, Gap>>,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.0.handle_linked_chunk_updates(room_id, updates).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn add_media_content(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
|
||||
@@ -74,7 +74,7 @@ pub fn is_suitable_for_latest_event<'a>(
|
||||
// Check if this is a replacement for another message. If it is, ignore it
|
||||
if let Some(original_message) = message.as_original() {
|
||||
let is_replacement =
|
||||
original_message.content.relates_to.as_ref().map_or(false, |relates_to| {
|
||||
original_message.content.relates_to.as_ref().is_some_and(|relates_to| {
|
||||
if let Some(relation_type) = relates_to.rel_type() {
|
||||
relation_type == RelationType::Replacement
|
||||
} else {
|
||||
@@ -83,12 +83,13 @@ pub fn is_suitable_for_latest_event<'a>(
|
||||
});
|
||||
|
||||
if is_replacement {
|
||||
return PossibleLatestEvent::NoUnsupportedMessageLikeType;
|
||||
PossibleLatestEvent::NoUnsupportedMessageLikeType
|
||||
} else {
|
||||
PossibleLatestEvent::YesRoomMessage(message)
|
||||
}
|
||||
return PossibleLatestEvent::YesRoomMessage(message);
|
||||
} else {
|
||||
PossibleLatestEvent::YesRoomMessage(message)
|
||||
}
|
||||
|
||||
return PossibleLatestEvent::YesRoomMessage(message);
|
||||
}
|
||||
|
||||
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::UnstablePollStart(poll)) => {
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![cfg_attr(target_arch = "wasm32", allow(clippy::arc_with_non_send_sync))]
|
||||
#![cfg_attr(test, allow(unexpected_cfgs))] // Triggered by the init_tracing_for_tests!() invocation.
|
||||
#![warn(missing_docs, missing_debug_implementations)]
|
||||
|
||||
pub use matrix_sdk_common::*;
|
||||
|
||||
@@ -438,7 +438,7 @@ fn events_intersects<'a>(
|
||||
let previous_events_ids = BTreeSet::from_iter(previous_events.filter_map(|ev| ev.event_id()));
|
||||
new_events
|
||||
.iter()
|
||||
.any(|ev| ev.event_id().map_or(false, |event_id| previous_events_ids.contains(&event_id)))
|
||||
.any(|ev| ev.event_id().is_some_and(|event_id| previous_events_ids.contains(&event_id)))
|
||||
}
|
||||
|
||||
/// Given a set of events coming from sync, for a room, update the
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#![allow(clippy::assign_op_pattern)] // triggered by bitflags! usage
|
||||
#![allow(clippy::assign_op_pattern)] // Triggered by bitflags! usage
|
||||
#![allow(unexpected_cfgs)] // Triggered by the `EventContent` macro usage
|
||||
|
||||
mod members;
|
||||
pub(crate) mod normal;
|
||||
|
||||
@@ -487,8 +487,14 @@ impl Room {
|
||||
}
|
||||
|
||||
/// Get the history visibility policy of this room.
|
||||
pub fn history_visibility(&self) -> HistoryVisibility {
|
||||
self.inner.read().history_visibility().clone()
|
||||
pub fn history_visibility(&self) -> Option<HistoryVisibility> {
|
||||
self.inner.read().history_visibility().cloned()
|
||||
}
|
||||
|
||||
/// Get the history visibility policy of this room, or a sensible default if
|
||||
/// the event is missing.
|
||||
pub fn history_visibility_or_default(&self) -> HistoryVisibility {
|
||||
self.inner.read().history_visibility_or_default().clone()
|
||||
}
|
||||
|
||||
/// Is the room considered to be public.
|
||||
@@ -573,49 +579,113 @@ impl Room {
|
||||
///
|
||||
/// [spec]: <https://matrix.org/docs/spec/client_server/latest#calculating-the-display-name-for-a-room>
|
||||
pub async fn compute_display_name(&self) -> StoreResult<RoomDisplayName> {
|
||||
let update_cache = |new_val: RoomDisplayName| {
|
||||
self.inner.update_if(|info| {
|
||||
if info.cached_display_name.as_ref() != Some(&new_val) {
|
||||
info.cached_display_name = Some(new_val.clone());
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
new_val
|
||||
};
|
||||
enum DisplayNameOrSummary {
|
||||
Summary(RoomSummary),
|
||||
DisplayName(RoomDisplayName),
|
||||
}
|
||||
|
||||
let summary = {
|
||||
let display_name_or_summary = {
|
||||
let inner = self.inner.read();
|
||||
|
||||
if let Some(name) = inner.name() {
|
||||
let name = name.trim().to_owned();
|
||||
drop(inner); // drop the lock on `self.inner` to avoid deadlocking in `update_cache`.
|
||||
return Ok(update_cache(RoomDisplayName::Named(name)));
|
||||
match (inner.name(), inner.canonical_alias()) {
|
||||
(Some(name), _) => {
|
||||
let name = RoomDisplayName::Named(name.trim().to_owned());
|
||||
DisplayNameOrSummary::DisplayName(name)
|
||||
}
|
||||
(None, Some(alias)) => {
|
||||
let name = RoomDisplayName::Aliased(alias.alias().trim().to_owned());
|
||||
DisplayNameOrSummary::DisplayName(name)
|
||||
}
|
||||
// We can't directly compute the display name from the summary here because Rust
|
||||
// thinks that the `inner` lock is still held even if we explicitly call `drop()`
|
||||
// on it. So we introduced the DisplayNameOrSummary type and do the computation in
|
||||
// two steps.
|
||||
(None, None) => DisplayNameOrSummary::Summary(inner.summary.clone()),
|
||||
}
|
||||
|
||||
if let Some(alias) = inner.canonical_alias() {
|
||||
let alias = alias.alias().trim().to_owned();
|
||||
drop(inner); // See above comment.
|
||||
return Ok(update_cache(RoomDisplayName::Aliased(alias)));
|
||||
}
|
||||
|
||||
inner.summary.clone()
|
||||
};
|
||||
|
||||
// From here, use some heroes to compute the room's name.
|
||||
let own_user_id = self.own_user_id().as_str();
|
||||
let display_name = match display_name_or_summary {
|
||||
DisplayNameOrSummary::Summary(summary) => {
|
||||
self.compute_display_name_from_summary(summary).await?
|
||||
}
|
||||
DisplayNameOrSummary::DisplayName(display_name) => display_name,
|
||||
};
|
||||
|
||||
// Update the cached display name before we return the newly computed value.
|
||||
self.inner.update_if(|info| {
|
||||
if info.cached_display_name.as_ref() != Some(&display_name) {
|
||||
info.cached_display_name = Some(display_name.clone());
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
Ok(display_name)
|
||||
}
|
||||
|
||||
/// Compute a [`RoomDisplayName`] from the given [`RoomSummary`].
|
||||
async fn compute_display_name_from_summary(
|
||||
&self,
|
||||
summary: RoomSummary,
|
||||
) -> StoreResult<RoomDisplayName> {
|
||||
let summary_member_count = summary.joined_member_count + summary.invited_member_count;
|
||||
|
||||
let (heroes, num_joined_invited_guess) = if !summary.room_heroes.is_empty() {
|
||||
let mut names = Vec::with_capacity(summary.room_heroes.len());
|
||||
for hero in &summary.room_heroes {
|
||||
if hero.user_id == own_user_id {
|
||||
continue;
|
||||
}
|
||||
if let Some(display_name) = &hero.display_name {
|
||||
names.push(display_name.clone());
|
||||
continue;
|
||||
}
|
||||
let heroes = self.extract_heroes(&summary.room_heroes).await?;
|
||||
(heroes, None)
|
||||
} else {
|
||||
let (heroes, num_joined_invited) = self.compute_summary().await?;
|
||||
(heroes, Some(num_joined_invited))
|
||||
};
|
||||
|
||||
let num_joined_invited = if self.state() == RoomState::Invited {
|
||||
// when we were invited we don't have a proper summary, we have to do best
|
||||
// guessing
|
||||
heroes.len() as u64 + 1
|
||||
} else if summary_member_count == 0 {
|
||||
if let Some(num_joined_invited) = num_joined_invited_guess {
|
||||
num_joined_invited
|
||||
} else {
|
||||
self.store
|
||||
.get_user_ids(self.room_id(), RoomMemberships::JOIN | RoomMemberships::INVITE)
|
||||
.await?
|
||||
.len() as u64
|
||||
}
|
||||
} else {
|
||||
summary_member_count
|
||||
};
|
||||
|
||||
debug!(
|
||||
room_id = ?self.room_id(),
|
||||
own_user = ?self.own_user_id,
|
||||
num_joined_invited,
|
||||
heroes = ?heroes,
|
||||
"Calculating name for a room based on heroes",
|
||||
);
|
||||
|
||||
let display_name = compute_display_name_from_heroes(
|
||||
num_joined_invited,
|
||||
heroes.iter().map(|hero| hero.as_str()).collect(),
|
||||
);
|
||||
|
||||
Ok(display_name)
|
||||
}
|
||||
|
||||
/// Extract and collect the display names of the room heroes from a
|
||||
/// [`RoomSummary`].
|
||||
///
|
||||
/// Returns the display names as a list of strings.
|
||||
async fn extract_heroes(&self, heroes: &[RoomHero]) -> StoreResult<Vec<String>> {
|
||||
let own_user_id = self.own_user_id().as_str();
|
||||
|
||||
let mut names = Vec::with_capacity(heroes.len());
|
||||
let heroes = heroes.iter().filter(|hero| hero.user_id != own_user_id);
|
||||
|
||||
for hero in heroes {
|
||||
if let Some(display_name) = &hero.display_name {
|
||||
names.push(display_name.clone());
|
||||
} else {
|
||||
match self.get_member(&hero.user_id).await {
|
||||
Ok(Some(member)) => {
|
||||
names.push(member.name().to_owned());
|
||||
@@ -628,42 +698,9 @@ impl Room {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(names, None)
|
||||
} else {
|
||||
let (heroes, num_joined_invited) = self.compute_summary().await?;
|
||||
(heroes, Some(num_joined_invited))
|
||||
};
|
||||
|
||||
let num_joined_invited = if self.state() == RoomState::Invited {
|
||||
// when we were invited we don't have a proper summary, we have to do best
|
||||
// guessing
|
||||
heroes.len() as u64 + 1
|
||||
} else if summary.joined_member_count == 0 && summary.invited_member_count == 0 {
|
||||
if let Some(num_joined_invited) = num_joined_invited_guess {
|
||||
num_joined_invited
|
||||
} else {
|
||||
self.store
|
||||
.get_user_ids(self.room_id(), RoomMemberships::JOIN | RoomMemberships::INVITE)
|
||||
.await?
|
||||
.len() as u64
|
||||
}
|
||||
} else {
|
||||
summary.joined_member_count + summary.invited_member_count
|
||||
};
|
||||
|
||||
debug!(
|
||||
room_id = ?self.room_id(),
|
||||
own_user = ?self.own_user_id,
|
||||
num_joined_invited,
|
||||
heroes = ?heroes,
|
||||
"Calculating name for a room based on heroes",
|
||||
);
|
||||
|
||||
Ok(update_cache(compute_display_name_from_heroes(
|
||||
num_joined_invited,
|
||||
heroes.iter().map(|hero| hero.as_str()).collect(),
|
||||
)))
|
||||
Ok(names)
|
||||
}
|
||||
|
||||
/// Compute the room summary with the data present in the store.
|
||||
@@ -1491,11 +1528,24 @@ impl RoomInfo {
|
||||
|
||||
/// Returns the history visibility for this room.
|
||||
///
|
||||
/// Defaults to `WorldReadable`, if missing.
|
||||
pub fn history_visibility(&self) -> &HistoryVisibility {
|
||||
/// Returns None if the event was never seen during sync.
|
||||
pub fn history_visibility(&self) -> Option<&HistoryVisibility> {
|
||||
match &self.base_info.history_visibility {
|
||||
Some(MinimalStateEvent::Original(ev)) => Some(&ev.content.history_visibility),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the history visibility for this room, or a sensible default.
|
||||
///
|
||||
/// Returns `Shared`, the default specified by the [spec], when the event is
|
||||
/// missing.
|
||||
///
|
||||
/// [spec]: https://spec.matrix.org/latest/client-server-api/#server-behaviour-7
|
||||
pub fn history_visibility_or_default(&self) -> &HistoryVisibility {
|
||||
match &self.base_info.history_visibility {
|
||||
Some(MinimalStateEvent::Original(ev)) => &ev.content.history_visibility,
|
||||
_ => &HistoryVisibility::WorldReadable,
|
||||
_ => &HistoryVisibility::Shared,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -90,6 +90,8 @@ pub trait StateStoreIntegrationTests {
|
||||
async fn test_send_queue_priority(&self);
|
||||
/// Test operations related to send queue dependents.
|
||||
async fn test_send_queue_dependents(&self);
|
||||
/// Test an update to a send queue dependent request.
|
||||
async fn test_update_send_queue_dependent(&self);
|
||||
/// Test saving/restoring server capabilities.
|
||||
async fn test_server_capabilities_saving(&self);
|
||||
}
|
||||
@@ -972,6 +974,24 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
|
||||
self.populate().await?;
|
||||
|
||||
{
|
||||
// Add a send queue request in that room.
|
||||
let txn = TransactionId::new();
|
||||
let ev =
|
||||
SerializableEventContent::new(&RoomMessageEventContent::text_plain("sup").into())
|
||||
.unwrap();
|
||||
self.save_send_queue_request(room_id, txn.clone(), ev.into(), 0).await?;
|
||||
|
||||
// Add a single dependent queue request.
|
||||
self.save_dependent_queued_request(
|
||||
room_id,
|
||||
&txn,
|
||||
ChildTransactionId::new(),
|
||||
DependentQueuedRequestKind::RedactEvent,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
self.remove_room(room_id).await?;
|
||||
|
||||
assert_eq!(self.get_room_infos().await?.len(), 1, "room is still there");
|
||||
@@ -1023,6 +1043,8 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
.is_empty(),
|
||||
"still event recepts in the store"
|
||||
);
|
||||
assert!(self.load_send_queue_requests(room_id).await?.is_empty());
|
||||
assert!(self.load_dependent_queued_requests(room_id).await?.is_empty());
|
||||
|
||||
self.remove_room(stripped_room_id).await?;
|
||||
|
||||
@@ -1458,7 +1480,7 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
// Update the event id.
|
||||
let event_id = owned_event_id!("$1");
|
||||
let num_updated = self
|
||||
.update_dependent_queued_request(
|
||||
.mark_dependent_queued_requests_as_ready(
|
||||
room_id,
|
||||
&txn0,
|
||||
SentRequestKey::Event(event_id.clone()),
|
||||
@@ -1528,6 +1550,54 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
let dependents = self.load_dependent_queued_requests(room_id).await.unwrap();
|
||||
assert_eq!(dependents.len(), 2);
|
||||
}
|
||||
|
||||
async fn test_update_send_queue_dependent(&self) {
|
||||
let room_id = room_id!("!test_send_queue_dependents:localhost");
|
||||
|
||||
let txn = TransactionId::new();
|
||||
|
||||
// Save a dependent redaction for an event.
|
||||
let child_txn = ChildTransactionId::new();
|
||||
|
||||
self.save_dependent_queued_request(
|
||||
room_id,
|
||||
&txn,
|
||||
child_txn.clone(),
|
||||
DependentQueuedRequestKind::RedactEvent,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// It worked.
|
||||
let dependents = self.load_dependent_queued_requests(room_id).await.unwrap();
|
||||
assert_eq!(dependents.len(), 1);
|
||||
assert_eq!(dependents[0].parent_transaction_id, txn);
|
||||
assert_eq!(dependents[0].own_transaction_id, child_txn);
|
||||
assert!(dependents[0].parent_key.is_none());
|
||||
assert_matches!(dependents[0].kind, DependentQueuedRequestKind::RedactEvent);
|
||||
|
||||
// Make it a reaction, instead of a redaction.
|
||||
self.update_dependent_queued_request(
|
||||
room_id,
|
||||
&child_txn,
|
||||
DependentQueuedRequestKind::ReactEvent { key: "👍".to_owned() },
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// It worked.
|
||||
let dependents = self.load_dependent_queued_requests(room_id).await.unwrap();
|
||||
assert_eq!(dependents.len(), 1);
|
||||
assert_eq!(dependents[0].parent_transaction_id, txn);
|
||||
assert_eq!(dependents[0].own_transaction_id, child_txn);
|
||||
assert!(dependents[0].parent_key.is_none());
|
||||
assert_matches!(
|
||||
&dependents[0].kind,
|
||||
DependentQueuedRequestKind::ReactEvent { key } => {
|
||||
assert_eq!(key, "👍");
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Macro building to allow your StateStore implementation to run the entire
|
||||
@@ -1686,6 +1756,12 @@ macro_rules! statestore_integration_tests {
|
||||
let store = get_store().await.expect("creating store failed").into_state_store();
|
||||
store.test_send_queue_dependents().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_update_send_queue_dependent() {
|
||||
let store = get_store().await.expect("creating store failed").into_state_store();
|
||||
store.test_update_send_queue_dependent().await;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -796,6 +796,8 @@ impl StateStore for MemoryStore {
|
||||
self.stripped_members.write().unwrap().remove(room_id);
|
||||
self.room_user_receipts.write().unwrap().remove(room_id);
|
||||
self.room_event_receipts.write().unwrap().remove(room_id);
|
||||
self.send_queue_events.write().unwrap().remove(room_id);
|
||||
self.dependent_send_queue_events.write().unwrap().remove(room_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -915,7 +917,7 @@ impl StateStore for MemoryStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_dependent_queued_request(
|
||||
async fn mark_dependent_queued_requests_as_ready(
|
||||
&self,
|
||||
room: &RoomId,
|
||||
parent_txn_id: &TransactionId,
|
||||
@@ -931,6 +933,23 @@ impl StateStore for MemoryStore {
|
||||
Ok(num_updated)
|
||||
}
|
||||
|
||||
async fn update_dependent_queued_request(
|
||||
&self,
|
||||
room: &RoomId,
|
||||
own_transaction_id: &ChildTransactionId,
|
||||
new_content: DependentQueuedRequestKind,
|
||||
) -> Result<bool, Self::Error> {
|
||||
let mut dependent_send_queue_events = self.dependent_send_queue_events.write().unwrap();
|
||||
let dependents = dependent_send_queue_events.entry(room.to_owned()).or_default();
|
||||
for d in dependents.iter_mut() {
|
||||
if d.own_transaction_id == *own_transaction_id {
|
||||
d.kind = new_content;
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
async fn remove_dependent_queued_request(
|
||||
&self,
|
||||
room: &RoomId,
|
||||
|
||||
@@ -367,6 +367,27 @@ pub struct DependentQueuedRequest {
|
||||
pub parent_key: Option<SentRequestKey>,
|
||||
}
|
||||
|
||||
impl DependentQueuedRequest {
|
||||
/// Does the dependent request represent a new event that is *not*
|
||||
/// aggregated, aka it is going to be its own item in a timeline?
|
||||
pub fn is_own_event(&self) -> bool {
|
||||
match self.kind {
|
||||
DependentQueuedRequestKind::EditEvent { .. }
|
||||
| DependentQueuedRequestKind::RedactEvent
|
||||
| DependentQueuedRequestKind::ReactEvent { .. }
|
||||
| DependentQueuedRequestKind::UploadFileWithThumbnail { .. } => {
|
||||
// These are all aggregated events, or non-visible items (file upload producing
|
||||
// a new MXC ID).
|
||||
false
|
||||
}
|
||||
DependentQueuedRequestKind::FinishUpload { .. } => {
|
||||
// This one graduates into a new media event.
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl fmt::Debug for QueuedRequest {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
|
||||
@@ -424,21 +424,31 @@ pub trait StateStore: AsyncTraitDeps {
|
||||
content: DependentQueuedRequestKind,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Update a set of dependent send queue requests with a key identifying the
|
||||
/// homeserver's response, effectively marking them as ready.
|
||||
/// Mark a set of dependent send queue requests as ready, using a key
|
||||
/// identifying the homeserver's response.
|
||||
///
|
||||
/// ⚠ Beware! There's no verification applied that the parent key type is
|
||||
/// compatible with the dependent event type. The invalid state may be
|
||||
/// lazily filtered out in `load_dependent_queued_requests`.
|
||||
///
|
||||
/// Returns the number of updated requests.
|
||||
async fn update_dependent_queued_request(
|
||||
async fn mark_dependent_queued_requests_as_ready(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
parent_txn_id: &TransactionId,
|
||||
sent_parent_key: SentRequestKey,
|
||||
) -> Result<usize, Self::Error>;
|
||||
|
||||
/// Update a dependent send queue request with the new content.
|
||||
///
|
||||
/// Returns true if the request was found and could be updated.
|
||||
async fn update_dependent_queued_request(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
own_transaction_id: &ChildTransactionId,
|
||||
new_content: DependentQueuedRequestKind,
|
||||
) -> Result<bool, Self::Error>;
|
||||
|
||||
/// Remove a specific dependent send queue request by id.
|
||||
///
|
||||
/// Returns true if the dependent send queue request has been indeed
|
||||
@@ -709,14 +719,14 @@ impl<T: StateStore> StateStore for EraseStateStoreError<T> {
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn update_dependent_queued_request(
|
||||
async fn mark_dependent_queued_requests_as_ready(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
parent_txn_id: &TransactionId,
|
||||
sent_parent_key: SentRequestKey,
|
||||
) -> Result<usize, Self::Error> {
|
||||
self.0
|
||||
.update_dependent_queued_request(room_id, parent_txn_id, sent_parent_key)
|
||||
.mark_dependent_queued_requests_as_ready(room_id, parent_txn_id, sent_parent_key)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
@@ -735,6 +745,18 @@ impl<T: StateStore> StateStore for EraseStateStoreError<T> {
|
||||
) -> Result<Vec<DependentQueuedRequest>, Self::Error> {
|
||||
self.0.load_dependent_queued_requests(room_id).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn update_dependent_queued_request(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
own_transaction_id: &ChildTransactionId,
|
||||
new_content: DependentQueuedRequestKind,
|
||||
) -> Result<bool, Self::Error> {
|
||||
self.0
|
||||
.update_dependent_queued_request(room_id, own_transaction_id, new_content)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience functionality for state stores.
|
||||
|
||||
@@ -248,7 +248,7 @@ impl Timeline {
|
||||
struct DebugInvitedRoomUpdates<'a>(&'a BTreeMap<OwnedRoomId, InvitedRoomUpdate>);
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl<'a> fmt::Debug for DebugInvitedRoomUpdates<'a> {
|
||||
impl fmt::Debug for DebugInvitedRoomUpdates<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_map().entries(self.0.iter().map(|(k, v)| (k, DebugInvitedRoom(v)))).finish()
|
||||
}
|
||||
@@ -257,7 +257,7 @@ impl<'a> fmt::Debug for DebugInvitedRoomUpdates<'a> {
|
||||
struct DebugKnockedRoomUpdates<'a>(&'a BTreeMap<OwnedRoomId, KnockedRoomUpdate>);
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl<'a> fmt::Debug for DebugKnockedRoomUpdates<'a> {
|
||||
impl fmt::Debug for DebugKnockedRoomUpdates<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_map().entries(self.0.iter().map(|(k, v)| (k, DebugKnockedRoom(v)))).finish()
|
||||
}
|
||||
|
||||
@@ -44,10 +44,16 @@ wasm-bindgen = "0.2.84"
|
||||
[dev-dependencies]
|
||||
assert_matches = { workspace = true }
|
||||
proptest = { version = "1.4.0", default-features = false, features = ["std"] }
|
||||
matrix-sdk-test = { workspace = true }
|
||||
matrix-sdk-test-macros = { path = "../../testing/matrix-sdk-test-macros" }
|
||||
wasm-bindgen-test = "0.3.33"
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
# Enable the test macro.
|
||||
tokio = { workspace = true, features = ["rt", "macros"] }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
|
||||
# Enable the JS feature for getrandom.
|
||||
getrandom = { version = "0.2.6", default-features = false, features = ["js"] }
|
||||
js-sys = "0.3.64"
|
||||
|
||||
[lints]
|
||||
|
||||
@@ -81,7 +81,7 @@ impl<T: 'static> Future for JoinHandle<T> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use assert_matches::assert_matches;
|
||||
use matrix_sdk_test::async_test;
|
||||
use matrix_sdk_test_macros::async_test;
|
||||
|
||||
use super::spawn;
|
||||
|
||||
|
||||
@@ -351,7 +351,7 @@ pub fn make_tracing_subscriber(logger: Option<JsLogger>) -> JsLoggingSubscriber
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use matrix_sdk_test::async_test;
|
||||
use matrix_sdk_test_macros::async_test;
|
||||
use tracing::{debug, subscriber::with_default};
|
||||
use wasm_bindgen::{JsCast, JsValue};
|
||||
|
||||
|
||||
@@ -302,6 +302,12 @@ impl UpdateToVectorDiff {
|
||||
self.chunks.insert(next_chunk_index, (*new, 0));
|
||||
}
|
||||
|
||||
// First chunk!
|
||||
(None, None) if self.chunks.is_empty() => {
|
||||
self.chunks.push_back((*new, 0));
|
||||
}
|
||||
|
||||
// Impossible state.
|
||||
(None, None) => {
|
||||
unreachable!(
|
||||
"Inserting new chunk with no previous nor next chunk identifiers \
|
||||
@@ -405,6 +411,14 @@ impl UpdateToVectorDiff {
|
||||
// Exiting the _detaching_ mode.
|
||||
detaching = false;
|
||||
}
|
||||
|
||||
Update::Clear => {
|
||||
// Clean `self.chunks`.
|
||||
self.chunks.clear();
|
||||
|
||||
// Let's straightforwardly emit a `VectorDiff::Clear`.
|
||||
diffs.push(VectorDiff::Clear);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -450,10 +464,11 @@ impl UpdateToVectorDiff {
|
||||
mod tests {
|
||||
use std::fmt::Debug;
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
use imbl::{vector, Vector};
|
||||
|
||||
use super::{
|
||||
super::{EmptyChunk, LinkedChunk},
|
||||
super::{ChunkIdentifierGenerator, EmptyChunk, LinkedChunk},
|
||||
VectorDiff,
|
||||
};
|
||||
|
||||
@@ -473,6 +488,7 @@ mod tests {
|
||||
VectorDiff::Remove { index } => {
|
||||
accumulator.remove(index);
|
||||
}
|
||||
VectorDiff::Clear => accumulator.clear(),
|
||||
diff => unimplemented!("{diff:?}"),
|
||||
}
|
||||
}
|
||||
@@ -686,14 +702,72 @@ mod tests {
|
||||
&[VectorDiff::Insert { index: 14, value: 'z' }],
|
||||
);
|
||||
|
||||
drop(linked_chunk);
|
||||
assert!(as_vector.take().is_empty());
|
||||
|
||||
// Finally, ensure the “reconstitued” vector is the one expected.
|
||||
// Ensure the “reconstitued” vector is the one expected.
|
||||
assert_eq!(
|
||||
accumulator,
|
||||
vector!['m', 'a', 'w', 'x', 'y', 'b', 'd', 'i', 'j', 'k', 'l', 'e', 'f', 'g', 'z', 'h']
|
||||
);
|
||||
|
||||
// Let's try to clear the linked chunk now.
|
||||
linked_chunk.clear();
|
||||
|
||||
apply_and_assert_eq(&mut accumulator, as_vector.take(), &[VectorDiff::Clear]);
|
||||
assert!(accumulator.is_empty());
|
||||
|
||||
drop(linked_chunk);
|
||||
assert!(as_vector.take().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_as_vector_with_update_clear() {
|
||||
let mut linked_chunk = LinkedChunk::<3, char, ()>::new_with_update_history();
|
||||
let mut as_vector = linked_chunk.as_vector().unwrap();
|
||||
|
||||
{
|
||||
// 1 initial chunk in the `UpdateToVectorDiff` mapper.
|
||||
let chunks = &as_vector.mapper.chunks;
|
||||
assert_eq!(chunks.len(), 1);
|
||||
assert_eq!(chunks[0].0, ChunkIdentifierGenerator::FIRST_IDENTIFIER);
|
||||
assert_eq!(chunks[0].1, 0);
|
||||
|
||||
assert!(as_vector.take().is_empty());
|
||||
}
|
||||
|
||||
linked_chunk.push_items_back(['a', 'b', 'c', 'd']);
|
||||
|
||||
{
|
||||
let diffs = as_vector.take();
|
||||
assert_eq!(diffs.len(), 2);
|
||||
assert_matches!(&diffs[0], VectorDiff::Append { .. });
|
||||
assert_matches!(&diffs[1], VectorDiff::Append { .. });
|
||||
|
||||
// 2 chunks in the `UpdateToVectorDiff` mapper.
|
||||
assert_eq!(as_vector.mapper.chunks.len(), 2);
|
||||
}
|
||||
|
||||
linked_chunk.clear();
|
||||
|
||||
{
|
||||
let diffs = as_vector.take();
|
||||
assert_eq!(diffs.len(), 1);
|
||||
assert_matches!(&diffs[0], VectorDiff::Clear);
|
||||
|
||||
// 1 chunk in the `UpdateToVectorDiff` mapper.
|
||||
let chunks = &as_vector.mapper.chunks;
|
||||
assert_eq!(chunks.len(), 1);
|
||||
assert_eq!(chunks[0].0, ChunkIdentifierGenerator::FIRST_IDENTIFIER);
|
||||
assert_eq!(chunks[0].1, 0);
|
||||
}
|
||||
|
||||
// And we can push again.
|
||||
linked_chunk.push_items_back(['a', 'b', 'c', 'd']);
|
||||
|
||||
{
|
||||
let diffs = as_vector.take();
|
||||
assert_eq!(diffs.len(), 2);
|
||||
assert_matches!(&diffs[0], VectorDiff::Append { .. });
|
||||
assert_matches!(&diffs[1], VectorDiff::Append { .. });
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -0,0 +1,465 @@
|
||||
// Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, HashSet},
|
||||
marker::PhantomData,
|
||||
};
|
||||
|
||||
use tracing::error;
|
||||
|
||||
use super::{
|
||||
Chunk, ChunkContent, ChunkIdentifier, ChunkIdentifierGenerator, Ends, LinkedChunk,
|
||||
ObservableUpdates,
|
||||
};
|
||||
|
||||
/// A temporary chunk representation in the [`LinkedChunkBuilder`].
|
||||
///
|
||||
/// Instead of using linking the chunks with pointers, this uses
|
||||
/// [`ChunkIdentifier`] as the temporary links to the previous and next chunks,
|
||||
/// which will get resolved later when re-building the full data structure. This
|
||||
/// allows using chunks that references other chunks that aren't known yet.
|
||||
struct TemporaryChunk<Item, Gap> {
|
||||
id: ChunkIdentifier,
|
||||
previous: Option<ChunkIdentifier>,
|
||||
next: Option<ChunkIdentifier>,
|
||||
content: ChunkContent<Item, Gap>,
|
||||
}
|
||||
|
||||
/// A data structure to rebuild a linked chunk from its raw representation.
|
||||
///
|
||||
/// A linked chunk can be rebuilt incrementally from its internal
|
||||
/// representation, with the chunks being added *in any order*, as long as they
|
||||
/// form a single connected component eventually (viz., there's no
|
||||
/// subgraphs/sublists isolated from the one final linked list). If they don't,
|
||||
/// then the final call to [`LinkedChunkBuilder::build()`] will result in an
|
||||
/// error).
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct LinkedChunkBuilder<const CAP: usize, Item, Gap> {
|
||||
/// Work-in-progress chunks.
|
||||
chunks: BTreeMap<ChunkIdentifier, TemporaryChunk<Item, Gap>>,
|
||||
|
||||
/// Is the final `LinkedChunk` expected to include an update history, as if
|
||||
/// it were created with [`LinkedChunk::new_with_update_history`]?
|
||||
build_with_update_history: bool,
|
||||
}
|
||||
|
||||
impl<const CAP: usize, Item, Gap> Default for LinkedChunkBuilder<CAP, Item, Gap> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<const CAP: usize, Item, Gap> LinkedChunkBuilder<CAP, Item, Gap> {
|
||||
/// Create an empty [`LinkedChunkBuilder`] with no update history.
|
||||
pub fn new() -> Self {
|
||||
Self { chunks: Default::default(), build_with_update_history: false }
|
||||
}
|
||||
|
||||
/// Stash a gap chunk with its content.
|
||||
///
|
||||
/// This can be called even if the previous and next chunks have not been
|
||||
/// added yet. Resolving these chunks will happen at the time of calling
|
||||
/// [`LinkedChunkBuilder::build()`].
|
||||
pub fn push_gap(
|
||||
&mut self,
|
||||
previous: Option<ChunkIdentifier>,
|
||||
id: ChunkIdentifier,
|
||||
next: Option<ChunkIdentifier>,
|
||||
content: Gap,
|
||||
) {
|
||||
let chunk = TemporaryChunk { id, previous, next, content: ChunkContent::Gap(content) };
|
||||
self.chunks.insert(id, chunk);
|
||||
}
|
||||
|
||||
/// Stash an item chunk with its contents.
|
||||
///
|
||||
/// This can be called even if the previous and next chunks have not been
|
||||
/// added yet. Resolving these chunks will happen at the time of calling
|
||||
/// [`LinkedChunkBuilder::build()`].
|
||||
pub fn push_items(
|
||||
&mut self,
|
||||
previous: Option<ChunkIdentifier>,
|
||||
id: ChunkIdentifier,
|
||||
next: Option<ChunkIdentifier>,
|
||||
items: impl IntoIterator<Item = Item>,
|
||||
) {
|
||||
let chunk = TemporaryChunk {
|
||||
id,
|
||||
previous,
|
||||
next,
|
||||
content: ChunkContent::Items(items.into_iter().collect()),
|
||||
};
|
||||
self.chunks.insert(id, chunk);
|
||||
}
|
||||
|
||||
/// Request that the resulting linked chunk will have an update history, as
|
||||
/// if it were created with [`LinkedChunk::new_with_update_history`].
|
||||
pub fn with_update_history(&mut self) {
|
||||
self.build_with_update_history = true;
|
||||
}
|
||||
|
||||
/// Run all error checks before reconstructing the full linked chunk.
|
||||
///
|
||||
/// Must be called after checking `self.chunks` isn't empty in
|
||||
/// [`Self::build`].
|
||||
///
|
||||
/// Returns the identifier of the first chunk.
|
||||
fn check_consistency(&mut self) -> Result<ChunkIdentifier, LinkedChunkBuilderError> {
|
||||
// Look for the first id.
|
||||
let first_id =
|
||||
self.chunks.iter().find_map(|(id, chunk)| chunk.previous.is_none().then_some(*id));
|
||||
|
||||
// There's no first chunk, but we've checked that `self.chunks` isn't empty:
|
||||
// it's a malformed list.
|
||||
let Some(first_id) = first_id else {
|
||||
return Err(LinkedChunkBuilderError::MissingFirstChunk);
|
||||
};
|
||||
|
||||
// We're going to iterate from the first to the last chunk.
|
||||
// Keep track of chunks we've already visited.
|
||||
let mut visited = HashSet::new();
|
||||
|
||||
// Start from the first chunk.
|
||||
let mut maybe_cur = Some(first_id);
|
||||
|
||||
while let Some(cur) = maybe_cur {
|
||||
// The chunk must be referenced in `self.chunks`.
|
||||
let Some(chunk) = self.chunks.get(&cur) else {
|
||||
return Err(LinkedChunkBuilderError::MissingChunk { id: cur });
|
||||
};
|
||||
|
||||
if let ChunkContent::Items(items) = &chunk.content {
|
||||
if items.len() > CAP {
|
||||
return Err(LinkedChunkBuilderError::ChunkTooLarge { id: cur });
|
||||
}
|
||||
}
|
||||
|
||||
// If it's not the first chunk,
|
||||
if cur != first_id {
|
||||
// It must have a previous link.
|
||||
let Some(prev) = chunk.previous else {
|
||||
return Err(LinkedChunkBuilderError::MultipleFirstChunks {
|
||||
first_candidate: first_id,
|
||||
second_candidate: cur,
|
||||
});
|
||||
};
|
||||
|
||||
// And we must have visited its predecessor at this point, since we've
|
||||
// iterated from the first chunk.
|
||||
if !visited.contains(&prev) {
|
||||
return Err(LinkedChunkBuilderError::MissingChunk { id: prev });
|
||||
}
|
||||
}
|
||||
|
||||
// Add the current chunk to the list of seen chunks.
|
||||
if !visited.insert(cur) {
|
||||
// If we didn't insert, then it was already visited: there's a cycle!
|
||||
return Err(LinkedChunkBuilderError::Cycle { repeated: cur });
|
||||
}
|
||||
|
||||
// Move on to the next chunk. If it's none, we'll quit the loop.
|
||||
maybe_cur = chunk.next;
|
||||
}
|
||||
|
||||
// If there are more chunks than those we've visited: some of them were not
|
||||
// linked to the "main" branch of the linked list, so we had multiple connected
|
||||
// components.
|
||||
if visited.len() != self.chunks.len() {
|
||||
return Err(LinkedChunkBuilderError::MultipleConnectedComponents);
|
||||
}
|
||||
|
||||
Ok(first_id)
|
||||
}
|
||||
|
||||
pub fn build(mut self) -> Result<Option<LinkedChunk<CAP, Item, Gap>>, LinkedChunkBuilderError> {
|
||||
if self.chunks.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Run checks.
|
||||
let first_id = self.check_consistency()?;
|
||||
|
||||
// We're now going to iterate from the first to the last chunk. As we're doing
|
||||
// this, we're also doing a few other things:
|
||||
//
|
||||
// - rebuilding the final `Chunk`s one by one, that will be linked using
|
||||
// pointers,
|
||||
// - counting items from the item chunks we'll encounter,
|
||||
// - finding the max `ChunkIdentifier` (`max_chunk_id`).
|
||||
|
||||
let mut max_chunk_id = first_id.index();
|
||||
|
||||
// Small helper to graduate a temporary chunk into a final one. As we're doing
|
||||
// this, we're also updating the maximum chunk id (that will be used to
|
||||
// set up the id generator), and the number of items in this chunk.
|
||||
|
||||
let mut graduate_chunk = |id: ChunkIdentifier| {
|
||||
let temp = self.chunks.remove(&id)?;
|
||||
|
||||
// Update the maximum chunk identifier, while we're around.
|
||||
max_chunk_id = max_chunk_id.max(id.index());
|
||||
|
||||
// Graduate the current temporary chunk into a final chunk.
|
||||
let chunk_ptr = Chunk::new_leaked(id, temp.content);
|
||||
|
||||
Some((temp.next, chunk_ptr))
|
||||
};
|
||||
|
||||
let Some((mut next_chunk_id, first_chunk_ptr)) = graduate_chunk(first_id) else {
|
||||
// Can't really happen, but oh well.
|
||||
return Err(LinkedChunkBuilderError::MissingFirstChunk);
|
||||
};
|
||||
|
||||
let mut prev_chunk_ptr = first_chunk_ptr;
|
||||
|
||||
while let Some(id) = next_chunk_id {
|
||||
let Some((new_next, mut chunk_ptr)) = graduate_chunk(id) else {
|
||||
// Can't really happen, but oh well.
|
||||
return Err(LinkedChunkBuilderError::MissingChunk { id });
|
||||
};
|
||||
|
||||
let chunk = unsafe { chunk_ptr.as_mut() };
|
||||
|
||||
// Link the current chunk to its previous one.
|
||||
let prev_chunk = unsafe { prev_chunk_ptr.as_mut() };
|
||||
prev_chunk.next = Some(chunk_ptr);
|
||||
chunk.previous = Some(prev_chunk_ptr);
|
||||
|
||||
// Prepare for the next iteration.
|
||||
prev_chunk_ptr = chunk_ptr;
|
||||
next_chunk_id = new_next;
|
||||
}
|
||||
|
||||
debug_assert!(self.chunks.is_empty());
|
||||
|
||||
// Maintain the convention that `Ends::last` may be unset.
|
||||
let last_chunk_ptr = prev_chunk_ptr;
|
||||
let last_chunk_ptr =
|
||||
if first_chunk_ptr == last_chunk_ptr { None } else { Some(last_chunk_ptr) };
|
||||
let links = Ends { first: first_chunk_ptr, last: last_chunk_ptr };
|
||||
|
||||
let chunk_identifier_generator =
|
||||
ChunkIdentifierGenerator::new_from_previous_chunk_identifier(ChunkIdentifier::new(
|
||||
max_chunk_id,
|
||||
));
|
||||
|
||||
let updates =
|
||||
if self.build_with_update_history { Some(ObservableUpdates::new()) } else { None };
|
||||
|
||||
Ok(Some(LinkedChunk { links, chunk_identifier_generator, updates, marker: PhantomData }))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum LinkedChunkBuilderError {
|
||||
#[error("chunk with id {} is too large", id.index())]
|
||||
ChunkTooLarge { id: ChunkIdentifier },
|
||||
|
||||
#[error("there's no first chunk")]
|
||||
MissingFirstChunk,
|
||||
|
||||
#[error("there are multiple first chunks")]
|
||||
MultipleFirstChunks { first_candidate: ChunkIdentifier, second_candidate: ChunkIdentifier },
|
||||
|
||||
#[error("unable to resolve chunk with id {}", id.index())]
|
||||
MissingChunk { id: ChunkIdentifier },
|
||||
|
||||
#[error("rebuilt chunks form a cycle: repeated identifier: {}", repeated.index())]
|
||||
Cycle { repeated: ChunkIdentifier },
|
||||
|
||||
#[error("multiple connected components")]
|
||||
MultipleConnectedComponents,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use assert_matches::assert_matches;
|
||||
|
||||
use super::LinkedChunkBuilder;
|
||||
use crate::linked_chunk::{ChunkIdentifier, LinkedChunkBuilderError};
|
||||
|
||||
#[test]
|
||||
fn test_empty() {
|
||||
let lcb = LinkedChunkBuilder::<3, char, char>::new();
|
||||
|
||||
// Building an empty linked chunk works, and returns `None`.
|
||||
let lc = lcb.build().unwrap();
|
||||
assert!(lc.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_success() {
|
||||
let mut lcb = LinkedChunkBuilder::<3, char, char>::new();
|
||||
|
||||
let cid0 = ChunkIdentifier::new(0);
|
||||
let cid1 = ChunkIdentifier::new(1);
|
||||
// Note: cid2 is missing on purpose, to confirm that it's fine to have holes in
|
||||
// the chunk id space.
|
||||
let cid3 = ChunkIdentifier::new(3);
|
||||
|
||||
// Check that we can successfully create a linked chunk, independently of the
|
||||
// order in which chunks are added.
|
||||
//
|
||||
// The final chunk will contain [cid0 <-> cid1 <-> cid3], in this order.
|
||||
|
||||
// Adding chunk cid0.
|
||||
lcb.push_items(None, cid0, Some(cid1), vec!['a', 'b', 'c']);
|
||||
// Adding chunk cid3.
|
||||
lcb.push_items(Some(cid1), cid3, None, vec!['d', 'e']);
|
||||
// Adding chunk cid1.
|
||||
lcb.push_gap(Some(cid0), cid1, Some(cid3), 'g');
|
||||
|
||||
let mut lc =
|
||||
lcb.build().expect("building works").expect("returns a non-empty linked chunk");
|
||||
|
||||
// Check the entire content first.
|
||||
assert_items_eq!(lc, ['a', 'b', 'c'] [-] ['d', 'e']);
|
||||
|
||||
// Run checks on the first chunk.
|
||||
let mut chunks = lc.chunks();
|
||||
let first_chunk = chunks.next().unwrap();
|
||||
{
|
||||
assert!(first_chunk.previous().is_none());
|
||||
assert_eq!(first_chunk.identifier(), cid0);
|
||||
}
|
||||
|
||||
// Run checks on the second chunk.
|
||||
let second_chunk = chunks.next().unwrap();
|
||||
{
|
||||
assert_eq!(second_chunk.identifier(), first_chunk.next().unwrap().identifier());
|
||||
assert_eq!(second_chunk.previous().unwrap().identifier(), first_chunk.identifier());
|
||||
assert_eq!(second_chunk.identifier(), cid1);
|
||||
}
|
||||
|
||||
// Run checks on the third chunk.
|
||||
let third_chunk = chunks.next().unwrap();
|
||||
{
|
||||
assert_eq!(third_chunk.identifier(), second_chunk.next().unwrap().identifier());
|
||||
assert_eq!(third_chunk.previous().unwrap().identifier(), second_chunk.identifier());
|
||||
assert!(third_chunk.next().is_none());
|
||||
assert_eq!(third_chunk.identifier(), cid3);
|
||||
}
|
||||
|
||||
// There's no more chunk.
|
||||
assert!(chunks.next().is_none());
|
||||
|
||||
// The linked chunk had 5 items.
|
||||
assert_eq!(lc.len(), 5);
|
||||
|
||||
// Now, if we add a new chunk, its identifier should be the previous one we used
|
||||
// + 1.
|
||||
lc.push_gap_back('h');
|
||||
|
||||
let last_chunk = lc.chunks().last().unwrap();
|
||||
assert_eq!(last_chunk.identifier(), ChunkIdentifier::new(cid3.index() + 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chunk_too_large() {
|
||||
let mut lcb = LinkedChunkBuilder::<3, char, char>::new();
|
||||
|
||||
let cid0 = ChunkIdentifier::new(0);
|
||||
|
||||
// Adding a chunk with 4 items will fail, because the max capacity specified in
|
||||
// the builder generics is 3.
|
||||
lcb.push_items(None, cid0, None, vec!['a', 'b', 'c', 'd']);
|
||||
|
||||
let res = lcb.build();
|
||||
assert_matches!(res, Err(LinkedChunkBuilderError::ChunkTooLarge { id }) => {
|
||||
assert_eq!(id, cid0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_missing_first_chunk() {
|
||||
let mut lcb = LinkedChunkBuilder::<3, char, char>::new();
|
||||
|
||||
let cid0 = ChunkIdentifier::new(0);
|
||||
let cid1 = ChunkIdentifier::new(1);
|
||||
let cid2 = ChunkIdentifier::new(2);
|
||||
|
||||
lcb.push_gap(Some(cid2), cid0, Some(cid1), 'g');
|
||||
lcb.push_items(Some(cid0), cid1, Some(cid2), ['a', 'b', 'c']);
|
||||
lcb.push_items(Some(cid1), cid2, Some(cid0), ['d', 'e', 'f']);
|
||||
|
||||
let res = lcb.build();
|
||||
assert_matches!(res, Err(LinkedChunkBuilderError::MissingFirstChunk));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_first_chunks() {
|
||||
let mut lcb = LinkedChunkBuilder::<3, char, char>::new();
|
||||
|
||||
let cid0 = ChunkIdentifier::new(0);
|
||||
let cid1 = ChunkIdentifier::new(1);
|
||||
|
||||
lcb.push_gap(None, cid0, Some(cid1), 'g');
|
||||
// Second chunk lies and pretends to be the first too.
|
||||
lcb.push_items(None, cid1, Some(cid0), ['a', 'b', 'c']);
|
||||
|
||||
let res = lcb.build();
|
||||
assert_matches!(res, Err(LinkedChunkBuilderError::MultipleFirstChunks { first_candidate, second_candidate }) => {
|
||||
assert_eq!(first_candidate, cid0);
|
||||
assert_eq!(second_candidate, cid1);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_missing_chunk() {
|
||||
let mut lcb = LinkedChunkBuilder::<3, char, char>::new();
|
||||
|
||||
let cid0 = ChunkIdentifier::new(0);
|
||||
let cid1 = ChunkIdentifier::new(1);
|
||||
lcb.push_gap(None, cid0, Some(cid1), 'g');
|
||||
|
||||
let res = lcb.build();
|
||||
assert_matches!(res, Err(LinkedChunkBuilderError::MissingChunk { id }) => {
|
||||
assert_eq!(id, cid1);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cycle() {
|
||||
let mut lcb = LinkedChunkBuilder::<3, char, char>::new();
|
||||
|
||||
let cid0 = ChunkIdentifier::new(0);
|
||||
let cid1 = ChunkIdentifier::new(1);
|
||||
lcb.push_gap(None, cid0, Some(cid1), 'g');
|
||||
lcb.push_gap(Some(cid0), cid1, Some(cid0), 'g');
|
||||
|
||||
let res = lcb.build();
|
||||
assert_matches!(res, Err(LinkedChunkBuilderError::Cycle { repeated }) => {
|
||||
assert_eq!(repeated, cid0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_connected_components() {
|
||||
let mut lcb = LinkedChunkBuilder::<3, char, char>::new();
|
||||
|
||||
let cid0 = ChunkIdentifier::new(0);
|
||||
let cid1 = ChunkIdentifier::new(1);
|
||||
let cid2 = ChunkIdentifier::new(2);
|
||||
|
||||
// cid0 and cid1 are linked to each other.
|
||||
lcb.push_gap(None, cid0, Some(cid1), 'g');
|
||||
lcb.push_items(Some(cid0), cid1, None, ['a', 'b', 'c']);
|
||||
// cid2 stands on its own.
|
||||
lcb.push_items(None, cid2, None, ['d', 'e', 'f']);
|
||||
|
||||
let res = lcb.build();
|
||||
assert_matches!(res, Err(LinkedChunkBuilderError::MultipleConnectedComponents));
|
||||
}
|
||||
}
|
||||
@@ -93,6 +93,8 @@ macro_rules! assert_items_eq {
|
||||
}
|
||||
|
||||
mod as_vector;
|
||||
mod builder;
|
||||
pub mod relational;
|
||||
mod updates;
|
||||
|
||||
use std::{
|
||||
@@ -103,8 +105,9 @@ use std::{
|
||||
sync::atomic::{AtomicU64, Ordering},
|
||||
};
|
||||
|
||||
use as_vector::*;
|
||||
use updates::*;
|
||||
pub use as_vector::*;
|
||||
pub use builder::*;
|
||||
pub use updates::*;
|
||||
|
||||
/// Errors of [`LinkedChunk`].
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
@@ -190,6 +193,36 @@ impl<const CAP: usize, Item, Gap> Ends<CAP, Item, Gap> {
|
||||
chunk = chunk.previous_mut()?;
|
||||
}
|
||||
}
|
||||
|
||||
/// Drop all chunks, and re-create the first one.
|
||||
fn clear(&mut self) {
|
||||
// Loop over all chunks, from the last to the first chunk, and drop them.
|
||||
{
|
||||
// Take the latest chunk.
|
||||
let mut current_chunk_ptr = self.last.or(Some(self.first));
|
||||
|
||||
// As long as we have another chunk…
|
||||
while let Some(chunk_ptr) = current_chunk_ptr {
|
||||
// Fetch the previous chunk pointer.
|
||||
let previous_ptr = unsafe { chunk_ptr.as_ref() }.previous;
|
||||
|
||||
// Re-box the chunk, and let Rust does its job.
|
||||
let _chunk_boxed = unsafe { Box::from_raw(chunk_ptr.as_ptr()) };
|
||||
|
||||
// Update the `current_chunk_ptr`.
|
||||
current_chunk_ptr = previous_ptr;
|
||||
}
|
||||
|
||||
// At this step, all chunks have been dropped, including
|
||||
// `self.first`.
|
||||
}
|
||||
|
||||
// Recreate the first chunk.
|
||||
self.first = Chunk::new_items_leaked(ChunkIdentifierGenerator::FIRST_IDENTIFIER);
|
||||
|
||||
// Reset the last chunk.
|
||||
self.last = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// The [`LinkedChunk`] structure.
|
||||
@@ -202,9 +235,6 @@ pub struct LinkedChunk<const CHUNK_CAPACITY: usize, Item, Gap> {
|
||||
/// The links to the chunks, i.e. the first and the last chunk.
|
||||
links: Ends<CHUNK_CAPACITY, Item, Gap>,
|
||||
|
||||
/// The number of items hold by this linked chunk.
|
||||
length: usize,
|
||||
|
||||
/// The generator of chunk identifiers.
|
||||
chunk_identifier_generator: ChunkIdentifierGenerator,
|
||||
|
||||
@@ -232,7 +262,6 @@ impl<const CAP: usize, Item, Gap> LinkedChunk<CAP, Item, Gap> {
|
||||
first: Chunk::new_items_leaked(ChunkIdentifierGenerator::FIRST_IDENTIFIER),
|
||||
last: None,
|
||||
},
|
||||
length: 0,
|
||||
chunk_identifier_generator: ChunkIdentifierGenerator::new_from_scratch(),
|
||||
updates: None,
|
||||
marker: PhantomData,
|
||||
@@ -251,17 +280,30 @@ impl<const CAP: usize, Item, Gap> LinkedChunk<CAP, Item, Gap> {
|
||||
first: Chunk::new_items_leaked(ChunkIdentifierGenerator::FIRST_IDENTIFIER),
|
||||
last: None,
|
||||
},
|
||||
length: 0,
|
||||
chunk_identifier_generator: ChunkIdentifierGenerator::new_from_scratch(),
|
||||
updates: Some(ObservableUpdates::new()),
|
||||
marker: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the number of items in this linked chunk.
|
||||
#[allow(clippy::len_without_is_empty)]
|
||||
pub fn len(&self) -> usize {
|
||||
self.length
|
||||
/// Clear all the chunks.
|
||||
pub fn clear(&mut self) {
|
||||
// Clear `self.links`.
|
||||
self.links.clear();
|
||||
|
||||
// Clear `self.chunk_identifier_generator`.
|
||||
self.chunk_identifier_generator = ChunkIdentifierGenerator::new_from_scratch();
|
||||
|
||||
// “Clear” `self.updates`.
|
||||
if let Some(updates) = self.updates.as_mut() {
|
||||
// TODO: Optimisation: Do we want to clear all pending `Update`s in `updates`?
|
||||
updates.push(Update::Clear);
|
||||
updates.push(Update::NewItemsChunk {
|
||||
previous: None,
|
||||
new: ChunkIdentifierGenerator::FIRST_IDENTIFIER,
|
||||
next: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Push items at the end of the [`LinkedChunk`], i.e. on the last
|
||||
@@ -277,7 +319,6 @@ impl<const CAP: usize, Item, Gap> LinkedChunk<CAP, Item, Gap> {
|
||||
I::IntoIter: ExactSizeIterator,
|
||||
{
|
||||
let items = items.into_iter();
|
||||
let number_of_items = items.len();
|
||||
|
||||
let last_chunk = self.links.latest_chunk_mut();
|
||||
|
||||
@@ -294,8 +335,6 @@ impl<const CAP: usize, Item, Gap> LinkedChunk<CAP, Item, Gap> {
|
||||
// OK.
|
||||
self.links.last = Some(last_chunk.as_ptr());
|
||||
}
|
||||
|
||||
self.length += number_of_items;
|
||||
}
|
||||
|
||||
/// Push a gap at the end of the [`LinkedChunk`], i.e. after the last
|
||||
@@ -333,7 +372,7 @@ impl<const CAP: usize, Item, Gap> LinkedChunk<CAP, Item, Gap> {
|
||||
.chunk_mut(chunk_identifier)
|
||||
.ok_or(Error::InvalidChunkIdentifier { identifier: chunk_identifier })?;
|
||||
|
||||
let (chunk, number_of_items) = match &mut chunk.content {
|
||||
let chunk = match &mut chunk.content {
|
||||
ChunkContent::Gap(..) => {
|
||||
return Err(Error::ChunkIsAGap { identifier: chunk_identifier })
|
||||
}
|
||||
@@ -347,50 +386,46 @@ impl<const CAP: usize, Item, Gap> LinkedChunk<CAP, Item, Gap> {
|
||||
|
||||
// Prepare the items to be pushed.
|
||||
let items = items.into_iter();
|
||||
let number_of_items = items.len();
|
||||
|
||||
(
|
||||
// Push at the end of the current items.
|
||||
if item_index == current_items_length {
|
||||
chunk
|
||||
// Push the new items.
|
||||
.push_items(items, &self.chunk_identifier_generator, &mut self.updates)
|
||||
// Push at the end of the current items.
|
||||
if item_index == current_items_length {
|
||||
chunk
|
||||
// Push the new items.
|
||||
.push_items(items, &self.chunk_identifier_generator, &mut self.updates)
|
||||
}
|
||||
// Insert inside the current items.
|
||||
else {
|
||||
if let Some(updates) = self.updates.as_mut() {
|
||||
updates.push(Update::DetachLastItems {
|
||||
at: Position(chunk_identifier, item_index),
|
||||
});
|
||||
}
|
||||
// Insert inside the current items.
|
||||
else {
|
||||
if let Some(updates) = self.updates.as_mut() {
|
||||
updates.push(Update::DetachLastItems {
|
||||
at: Position(chunk_identifier, item_index),
|
||||
});
|
||||
}
|
||||
|
||||
// Split the items.
|
||||
let detached_items = current_items.split_off(item_index);
|
||||
// Split the items.
|
||||
let detached_items = current_items.split_off(item_index);
|
||||
|
||||
let chunk = chunk
|
||||
// Push the new items.
|
||||
.push_items(items, &self.chunk_identifier_generator, &mut self.updates);
|
||||
let chunk = chunk
|
||||
// Push the new items.
|
||||
.push_items(items, &self.chunk_identifier_generator, &mut self.updates);
|
||||
|
||||
if let Some(updates) = self.updates.as_mut() {
|
||||
updates.push(Update::StartReattachItems);
|
||||
}
|
||||
if let Some(updates) = self.updates.as_mut() {
|
||||
updates.push(Update::StartReattachItems);
|
||||
}
|
||||
|
||||
let chunk = chunk
|
||||
// Finally, push the items that have been detached.
|
||||
.push_items(
|
||||
detached_items.into_iter(),
|
||||
&self.chunk_identifier_generator,
|
||||
&mut self.updates,
|
||||
);
|
||||
let chunk = chunk
|
||||
// Finally, push the items that have been detached.
|
||||
.push_items(
|
||||
detached_items.into_iter(),
|
||||
&self.chunk_identifier_generator,
|
||||
&mut self.updates,
|
||||
);
|
||||
|
||||
if let Some(updates) = self.updates.as_mut() {
|
||||
updates.push(Update::EndReattachItems);
|
||||
}
|
||||
if let Some(updates) = self.updates.as_mut() {
|
||||
updates.push(Update::EndReattachItems);
|
||||
}
|
||||
|
||||
chunk
|
||||
},
|
||||
number_of_items,
|
||||
)
|
||||
chunk
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -402,8 +437,6 @@ impl<const CAP: usize, Item, Gap> LinkedChunk<CAP, Item, Gap> {
|
||||
self.links.last = Some(chunk.as_ptr());
|
||||
}
|
||||
|
||||
self.length += number_of_items;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -471,8 +504,6 @@ impl<const CAP: usize, Item, Gap> LinkedChunk<CAP, Item, Gap> {
|
||||
}
|
||||
}
|
||||
|
||||
self.length -= 1;
|
||||
|
||||
// Stop borrowing `chunk`.
|
||||
}
|
||||
|
||||
@@ -623,10 +654,9 @@ impl<const CAP: usize, Item, Gap> LinkedChunk<CAP, Item, Gap> {
|
||||
|
||||
debug_assert!(chunk.is_first_chunk().not(), "A gap cannot be the first chunk");
|
||||
|
||||
let (maybe_last_chunk_ptr, number_of_items) = match &mut chunk.content {
|
||||
let maybe_last_chunk_ptr = match &mut chunk.content {
|
||||
ChunkContent::Gap(..) => {
|
||||
let items = items.into_iter();
|
||||
let number_of_items = items.len();
|
||||
|
||||
let last_inserted_chunk = chunk
|
||||
// Insert a new items chunk…
|
||||
@@ -637,11 +667,9 @@ impl<const CAP: usize, Item, Gap> LinkedChunk<CAP, Item, Gap> {
|
||||
// … and insert the items.
|
||||
.push_items(items, &self.chunk_identifier_generator, &mut self.updates);
|
||||
|
||||
(
|
||||
last_inserted_chunk.is_last_chunk().then(|| last_inserted_chunk.as_ptr()),
|
||||
number_of_items,
|
||||
)
|
||||
last_inserted_chunk.is_last_chunk().then(|| last_inserted_chunk.as_ptr())
|
||||
}
|
||||
|
||||
ChunkContent::Items(..) => {
|
||||
return Err(Error::ChunkIsItems { identifier: chunk_identifier })
|
||||
}
|
||||
@@ -663,8 +691,6 @@ impl<const CAP: usize, Item, Gap> LinkedChunk<CAP, Item, Gap> {
|
||||
self.links.last = Some(last_chunk_ptr);
|
||||
}
|
||||
|
||||
self.length += number_of_items;
|
||||
|
||||
// Stop borrowing `chunk`.
|
||||
}
|
||||
|
||||
@@ -842,41 +868,30 @@ impl<const CAP: usize, Item, Gap> LinkedChunk<CAP, Item, Gap> {
|
||||
|
||||
Some(AsVector::new(updates, token, chunk_iterator))
|
||||
}
|
||||
|
||||
/// Returns the number of items of the linked chunk.
|
||||
fn len(&self) -> usize {
|
||||
self.items().count()
|
||||
}
|
||||
}
|
||||
|
||||
impl<const CAP: usize, Item, Gap> Drop for LinkedChunk<CAP, Item, Gap> {
|
||||
fn drop(&mut self) {
|
||||
// Take the latest chunk.
|
||||
let mut current_chunk_ptr = self.links.last.or(Some(self.links.first));
|
||||
|
||||
// As long as we have another chunk…
|
||||
while let Some(chunk_ptr) = current_chunk_ptr {
|
||||
// Disconnect the chunk by updating `previous_chunk.next` pointer.
|
||||
let previous_ptr = unsafe { chunk_ptr.as_ref() }.previous;
|
||||
|
||||
if let Some(mut previous_ptr) = previous_ptr {
|
||||
unsafe { previous_ptr.as_mut() }.next = None;
|
||||
}
|
||||
|
||||
// Re-box the chunk, and let Rust does its job.
|
||||
let _chunk_boxed = unsafe { Box::from_raw(chunk_ptr.as_ptr()) };
|
||||
|
||||
// Update the `current_chunk_ptr`.
|
||||
current_chunk_ptr = previous_ptr;
|
||||
}
|
||||
|
||||
// At this step, all chunks have been dropped, including
|
||||
// `self.first`.
|
||||
// Only clear the links. Calling `Self::clear` would be an error as we don't
|
||||
// want to emit an `Update::Clear` when `self` is dropped. Instead, we only care
|
||||
// about freeing memory correctly. Rust can take care of everything except the
|
||||
// pointers in `self.links`, hence the specific call to `self.links.clear()`.
|
||||
self.links.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`LinkedChunk`] can be safely sent over thread boundaries if `Item: Send`
|
||||
/// and `Gap: Send`. The only unsafe part if around the `NonNull`, but the API
|
||||
/// and `Gap: Send`. The only unsafe part is around the `NonNull`, but the API
|
||||
/// and the lifetimes to deref them are designed safely.
|
||||
unsafe impl<const CAP: usize, Item: Send, Gap: Send> Send for LinkedChunk<CAP, Item, Gap> {}
|
||||
|
||||
/// A [`LinkedChunk`] can be safely share between threads if `Item: Sync` and
|
||||
/// `Gap: Sync`. The only unsafe part if around the `NonNull`, but the API and
|
||||
/// `Gap: Sync`. The only unsafe part is around the `NonNull`, but the API and
|
||||
/// the lifetimes to deref them are designed safely.
|
||||
unsafe impl<const CAP: usize, Item: Sync, Gap: Sync> Sync for LinkedChunk<CAP, Item, Gap> {}
|
||||
|
||||
@@ -931,10 +946,22 @@ impl ChunkIdentifierGenerator {
|
||||
/// It is not the position of the chunk, just its unique identifier.
|
||||
///
|
||||
/// Learn more with [`ChunkIdentifierGenerator`].
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
#[repr(transparent)]
|
||||
pub struct ChunkIdentifier(u64);
|
||||
|
||||
impl ChunkIdentifier {
|
||||
/// Create a new [`ChunkIdentifier`].
|
||||
pub(super) fn new(identifier: u64) -> Self {
|
||||
Self(identifier)
|
||||
}
|
||||
|
||||
/// Get the underlying identifier.
|
||||
fn index(&self) -> u64 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<u64> for ChunkIdentifier {
|
||||
fn eq(&self, other: &u64) -> bool {
|
||||
self.0 == *other
|
||||
@@ -948,6 +975,11 @@ impl PartialEq<u64> for ChunkIdentifier {
|
||||
pub struct Position(ChunkIdentifier, usize);
|
||||
|
||||
impl Position {
|
||||
/// Create a new [`Position`].
|
||||
pub(super) fn new(chunk_identifier: ChunkIdentifier, index: usize) -> Self {
|
||||
Self(chunk_identifier, index)
|
||||
}
|
||||
|
||||
/// Get the chunk identifier of the item.
|
||||
pub fn chunk_identifier(&self) -> ChunkIdentifier {
|
||||
self.0
|
||||
@@ -966,6 +998,16 @@ impl Position {
|
||||
pub fn decrement_index(&mut self) {
|
||||
self.1 = self.1.checked_sub(1).expect("Cannot decrement the index because it's already 0");
|
||||
}
|
||||
|
||||
/// Increment the index part (see [`Self::index`]), i.e. add 1.
|
||||
///
|
||||
/// # Panic
|
||||
///
|
||||
/// This method will panic if it will overflow, i.e. if the index is larger
|
||||
/// than `usize::MAX`.
|
||||
pub fn increment_index(&mut self) {
|
||||
self.1 = self.1.checked_add(1).expect("Cannot increment the index because it's too large");
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator over a [`LinkedChunk`] that traverses the chunk in backward
|
||||
@@ -1053,6 +1095,14 @@ impl<const CAPACITY: usize, Item, Gap> Chunk<CAPACITY, Item, Gap> {
|
||||
Self { previous: None, next: None, identifier, content }
|
||||
}
|
||||
|
||||
/// Create a new chunk given some content, but box it and leak it.
|
||||
fn new_leaked(identifier: ChunkIdentifier, content: ChunkContent<Item, Gap>) -> NonNull<Self> {
|
||||
let chunk = Self::new(identifier, content);
|
||||
let chunk_box = Box::new(chunk);
|
||||
|
||||
NonNull::from(Box::leak(chunk_box))
|
||||
}
|
||||
|
||||
/// Create a new gap chunk, but box it and leak it.
|
||||
fn new_gap_leaked(identifier: ChunkIdentifier, content: Gap) -> NonNull<Self> {
|
||||
let chunk = Self::new_gap(identifier, content);
|
||||
@@ -1322,7 +1372,6 @@ where
|
||||
.debug_struct("LinkedChunk")
|
||||
.field("first (deref)", unsafe { self.links.first.as_ref() })
|
||||
.field("last", &self.links.last)
|
||||
.field("length", &self.length)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
@@ -1363,7 +1412,10 @@ impl EmptyChunk {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::ops::Not;
|
||||
use std::{
|
||||
ops::Not,
|
||||
sync::{atomic::Ordering, Arc},
|
||||
};
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
|
||||
@@ -2607,4 +2659,59 @@ mod tests {
|
||||
assert!(chunks.next().unwrap().is_last_chunk());
|
||||
assert!(chunks.next().is_none());
|
||||
}
|
||||
|
||||
// Test `LinkedChunk::clear`. This test creates a `LinkedChunk` with `new` to
|
||||
// avoid creating too much confusion with `Update`s. The next test
|
||||
// `test_clear_emit_an_update_clear` uses `new_with_update_history` and only
|
||||
// test `Update::Clear`.
|
||||
#[test]
|
||||
fn test_clear() {
|
||||
let mut linked_chunk = LinkedChunk::<3, Arc<char>, Arc<()>>::new();
|
||||
|
||||
let item = Arc::new('a');
|
||||
let gap = Arc::new(());
|
||||
|
||||
linked_chunk.push_items_back([
|
||||
item.clone(),
|
||||
item.clone(),
|
||||
item.clone(),
|
||||
item.clone(),
|
||||
item.clone(),
|
||||
]);
|
||||
linked_chunk.push_gap_back(gap.clone());
|
||||
linked_chunk.push_items_back([item.clone()]);
|
||||
|
||||
assert_eq!(Arc::strong_count(&item), 7);
|
||||
assert_eq!(Arc::strong_count(&gap), 2);
|
||||
assert_eq!(linked_chunk.len(), 6);
|
||||
assert_eq!(linked_chunk.chunk_identifier_generator.next.load(Ordering::SeqCst), 3);
|
||||
|
||||
// Now, we can clear the linked chunk and see what happens.
|
||||
linked_chunk.clear();
|
||||
|
||||
assert_eq!(Arc::strong_count(&item), 1);
|
||||
assert_eq!(Arc::strong_count(&gap), 1);
|
||||
assert_eq!(linked_chunk.len(), 0);
|
||||
assert_eq!(linked_chunk.chunk_identifier_generator.next.load(Ordering::SeqCst), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clear_emit_an_update_clear() {
|
||||
use super::Update::*;
|
||||
|
||||
let mut linked_chunk = LinkedChunk::<3, char, ()>::new_with_update_history();
|
||||
linked_chunk.clear();
|
||||
|
||||
assert_eq!(
|
||||
linked_chunk.updates().unwrap().take(),
|
||||
&[
|
||||
Clear,
|
||||
NewItemsChunk {
|
||||
previous: None,
|
||||
new: ChunkIdentifierGenerator::FIRST_IDENTIFIER,
|
||||
next: None
|
||||
}
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,731 @@
|
||||
// Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Implementation for a _relational linked chunk_, see
|
||||
//! [`RelationalLinkedChunk`].
|
||||
|
||||
use ruma::{OwnedRoomId, RoomId};
|
||||
|
||||
use crate::linked_chunk::{ChunkIdentifier, Position, Update};
|
||||
|
||||
/// A row of the [`RelationalLinkedChunk::chunks`].
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct ChunkRow {
|
||||
room_id: OwnedRoomId,
|
||||
previous_chunk: Option<ChunkIdentifier>,
|
||||
chunk: ChunkIdentifier,
|
||||
next_chunk: Option<ChunkIdentifier>,
|
||||
}
|
||||
|
||||
/// A row of the [`RelationalLinkedChunk::items`].
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct ItemRow<Item, Gap> {
|
||||
room_id: OwnedRoomId,
|
||||
position: Position,
|
||||
item: Either<Item, Gap>,
|
||||
}
|
||||
|
||||
/// Kind of item.
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum Either<Item, Gap> {
|
||||
/// The content is an item.
|
||||
Item(Item),
|
||||
|
||||
/// The content is a gap.
|
||||
Gap(Gap),
|
||||
}
|
||||
|
||||
/// A [`LinkedChunk`] but with a relational layout, similar to what we
|
||||
/// would have in a database.
|
||||
///
|
||||
/// This is used by memory stores. The idea is to have a data layout that is
|
||||
/// similar for memory stores and for relational database stores, to represent a
|
||||
/// [`LinkedChunk`].
|
||||
///
|
||||
/// This type is also designed to receive [`Update`]. Applying `Update`s
|
||||
/// directly on a [`LinkedChunk`] is not ideal and particularly not trivial as
|
||||
/// the `Update`s do _not_ match the internal data layout of the `LinkedChunk`,
|
||||
/// they have been designed for storages, like a relational database for
|
||||
/// example.
|
||||
///
|
||||
/// This type is not as performant as [`LinkedChunk`] (in terms of memory
|
||||
/// layout, CPU caches etc.). It is only designed to be used in memory stores,
|
||||
/// which are mostly used for test purposes or light usage of the SDK.
|
||||
///
|
||||
/// [`LinkedChunk`]: super::LinkedChunk
|
||||
#[derive(Debug)]
|
||||
pub struct RelationalLinkedChunk<Item, Gap> {
|
||||
/// Chunks.
|
||||
chunks: Vec<ChunkRow>,
|
||||
|
||||
/// Items.
|
||||
items: Vec<ItemRow<Item, Gap>>,
|
||||
}
|
||||
|
||||
impl<Item, Gap> RelationalLinkedChunk<Item, Gap> {
|
||||
/// Create a new relational linked chunk.
|
||||
pub fn new() -> Self {
|
||||
Self { chunks: Vec::new(), items: Vec::new() }
|
||||
}
|
||||
|
||||
/// Apply [`Update`]s. That's the only way to write data inside this
|
||||
/// relational linked chunk.
|
||||
pub fn apply_updates(&mut self, room_id: &RoomId, updates: Vec<Update<Item, Gap>>) {
|
||||
for update in updates {
|
||||
match update {
|
||||
Update::NewItemsChunk { previous, new, next } => {
|
||||
insert_chunk(&mut self.chunks, room_id, previous, new, next);
|
||||
}
|
||||
|
||||
Update::NewGapChunk { previous, new, next, gap } => {
|
||||
insert_chunk(&mut self.chunks, room_id, previous, new, next);
|
||||
self.items.push(ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
position: Position::new(new, 0),
|
||||
item: Either::Gap(gap),
|
||||
});
|
||||
}
|
||||
|
||||
Update::RemoveChunk(chunk_identifier) => {
|
||||
remove_chunk(&mut self.chunks, room_id, chunk_identifier);
|
||||
|
||||
let indices_to_remove = self
|
||||
.items
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(
|
||||
|(nth, ItemRow { room_id: room_id_candidate, position, .. })| {
|
||||
(room_id == room_id_candidate
|
||||
&& position.chunk_identifier() == chunk_identifier)
|
||||
.then_some(nth)
|
||||
},
|
||||
)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for index_to_remove in indices_to_remove.into_iter().rev() {
|
||||
self.items.remove(index_to_remove);
|
||||
}
|
||||
}
|
||||
|
||||
Update::PushItems { mut at, items } => {
|
||||
for item in items {
|
||||
self.items.push(ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
position: at,
|
||||
item: Either::Item(item),
|
||||
});
|
||||
at.increment_index();
|
||||
}
|
||||
}
|
||||
|
||||
Update::RemoveItem { at } => {
|
||||
let mut entry_to_remove = None;
|
||||
|
||||
for (nth, ItemRow { room_id: room_id_candidate, position, .. }) in
|
||||
self.items.iter_mut().enumerate()
|
||||
{
|
||||
// Filter by room ID.
|
||||
if room_id != room_id_candidate {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find the item to remove.
|
||||
if *position == at {
|
||||
debug_assert!(entry_to_remove.is_none(), "Found the same entry twice");
|
||||
|
||||
entry_to_remove = Some(nth);
|
||||
}
|
||||
|
||||
// Update all items that come _after_ `at` to shift their index.
|
||||
if position.chunk_identifier() == at.chunk_identifier()
|
||||
&& position.index() > at.index()
|
||||
{
|
||||
position.decrement_index();
|
||||
}
|
||||
}
|
||||
|
||||
self.items.remove(entry_to_remove.expect("Remove an unknown item"));
|
||||
}
|
||||
|
||||
Update::DetachLastItems { at } => {
|
||||
let indices_to_remove = self
|
||||
.items
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(
|
||||
|(nth, ItemRow { room_id: room_id_candidate, position, .. })| {
|
||||
(room_id == room_id_candidate
|
||||
&& position.chunk_identifier() == at.chunk_identifier()
|
||||
&& position.index() >= at.index())
|
||||
.then_some(nth)
|
||||
},
|
||||
)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for index_to_remove in indices_to_remove.into_iter().rev() {
|
||||
self.items.remove(index_to_remove);
|
||||
}
|
||||
}
|
||||
|
||||
Update::StartReattachItems | Update::EndReattachItems => { /* nothing */ }
|
||||
|
||||
Update::Clear => {
|
||||
self.chunks.clear();
|
||||
self.items.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_chunk(
|
||||
chunks: &mut Vec<ChunkRow>,
|
||||
room_id: &RoomId,
|
||||
previous: Option<ChunkIdentifier>,
|
||||
new: ChunkIdentifier,
|
||||
next: Option<ChunkIdentifier>,
|
||||
) {
|
||||
// Find the previous chunk, and update its next chunk.
|
||||
if let Some(previous) = previous {
|
||||
let entry_for_previous_chunk = chunks
|
||||
.iter_mut()
|
||||
.find(|ChunkRow { room_id: room_id_candidate, chunk, .. }| {
|
||||
room_id == room_id_candidate && *chunk == previous
|
||||
})
|
||||
.expect("Previous chunk should be present");
|
||||
|
||||
// Link the chunk.
|
||||
entry_for_previous_chunk.next_chunk = Some(new);
|
||||
}
|
||||
|
||||
// Find the next chunk, and update its previous chunk.
|
||||
if let Some(next) = next {
|
||||
let entry_for_next_chunk = chunks
|
||||
.iter_mut()
|
||||
.find(|ChunkRow { room_id: room_id_candidate, chunk, .. }| {
|
||||
room_id == room_id_candidate && *chunk == next
|
||||
})
|
||||
.expect("Next chunk should be present");
|
||||
|
||||
// Link the chunk.
|
||||
entry_for_next_chunk.previous_chunk = Some(new);
|
||||
}
|
||||
|
||||
// Insert the chunk.
|
||||
chunks.push(ChunkRow {
|
||||
room_id: room_id.to_owned(),
|
||||
previous_chunk: previous,
|
||||
chunk: new,
|
||||
next_chunk: next,
|
||||
});
|
||||
}
|
||||
|
||||
fn remove_chunk(
|
||||
chunks: &mut Vec<ChunkRow>,
|
||||
room_id: &RoomId,
|
||||
chunk_to_remove: ChunkIdentifier,
|
||||
) {
|
||||
let entry_nth_to_remove = chunks
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find_map(|(nth, ChunkRow { room_id: room_id_candidate, chunk, .. })| {
|
||||
(room_id == room_id_candidate && *chunk == chunk_to_remove).then_some(nth)
|
||||
})
|
||||
.expect("Remove an unknown chunk");
|
||||
|
||||
let ChunkRow { room_id, previous_chunk: previous, next_chunk: next, .. } =
|
||||
chunks.remove(entry_nth_to_remove);
|
||||
|
||||
// Find the previous chunk, and update its next chunk.
|
||||
if let Some(previous) = previous {
|
||||
let entry_for_previous_chunk = chunks
|
||||
.iter_mut()
|
||||
.find(|ChunkRow { room_id: room_id_candidate, chunk, .. }| {
|
||||
&room_id == room_id_candidate && *chunk == previous
|
||||
})
|
||||
.expect("Previous chunk should be present");
|
||||
|
||||
// Insert the chunk.
|
||||
entry_for_previous_chunk.next_chunk = next;
|
||||
}
|
||||
|
||||
// Find the next chunk, and update its previous chunk.
|
||||
if let Some(next) = next {
|
||||
let entry_for_next_chunk = chunks
|
||||
.iter_mut()
|
||||
.find(|ChunkRow { room_id: room_id_candidate, chunk, .. }| {
|
||||
&room_id == room_id_candidate && *chunk == next
|
||||
})
|
||||
.expect("Next chunk should be present");
|
||||
|
||||
// Insert the chunk.
|
||||
entry_for_next_chunk.previous_chunk = previous;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Item, Gap> Default for RelationalLinkedChunk<Item, Gap> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruma::room_id;
|
||||
|
||||
use super::{ChunkIdentifier as CId, *};
|
||||
|
||||
#[test]
|
||||
fn test_new_items_chunk() {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
|
||||
|
||||
relational_linked_chunk.apply_updates(
|
||||
room_id,
|
||||
vec![
|
||||
// 0
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
// 1 after 0
|
||||
Update::NewItemsChunk { previous: Some(CId::new(0)), new: CId::new(1), next: None },
|
||||
// 2 before 0
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(2), next: Some(CId::new(0)) },
|
||||
// 3 between 2 and 0
|
||||
Update::NewItemsChunk {
|
||||
previous: Some(CId::new(2)),
|
||||
new: CId::new(3),
|
||||
next: Some(CId::new(0)),
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
// Chunks are correctly linked.
|
||||
assert_eq!(
|
||||
relational_linked_chunk.chunks,
|
||||
&[
|
||||
ChunkRow {
|
||||
room_id: room_id.to_owned(),
|
||||
previous_chunk: Some(CId::new(3)),
|
||||
chunk: CId::new(0),
|
||||
next_chunk: Some(CId::new(1))
|
||||
},
|
||||
ChunkRow {
|
||||
room_id: room_id.to_owned(),
|
||||
previous_chunk: Some(CId::new(0)),
|
||||
chunk: CId::new(1),
|
||||
next_chunk: None
|
||||
},
|
||||
ChunkRow {
|
||||
room_id: room_id.to_owned(),
|
||||
previous_chunk: None,
|
||||
chunk: CId::new(2),
|
||||
next_chunk: Some(CId::new(3))
|
||||
},
|
||||
ChunkRow {
|
||||
room_id: room_id.to_owned(),
|
||||
previous_chunk: Some(CId::new(2)),
|
||||
chunk: CId::new(3),
|
||||
next_chunk: Some(CId::new(0))
|
||||
},
|
||||
],
|
||||
);
|
||||
// Items have not been modified.
|
||||
assert!(relational_linked_chunk.items.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_new_gap_chunk() {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
|
||||
|
||||
relational_linked_chunk.apply_updates(
|
||||
room_id,
|
||||
vec![
|
||||
// 0
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
// 1 after 0
|
||||
Update::NewGapChunk {
|
||||
previous: Some(CId::new(0)),
|
||||
new: CId::new(1),
|
||||
next: None,
|
||||
gap: (),
|
||||
},
|
||||
// 2 after 1
|
||||
Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },
|
||||
],
|
||||
);
|
||||
|
||||
// Chunks are correctly linked.
|
||||
assert_eq!(
|
||||
relational_linked_chunk.chunks,
|
||||
&[
|
||||
ChunkRow {
|
||||
room_id: room_id.to_owned(),
|
||||
previous_chunk: None,
|
||||
chunk: CId::new(0),
|
||||
next_chunk: Some(CId::new(1))
|
||||
},
|
||||
ChunkRow {
|
||||
room_id: room_id.to_owned(),
|
||||
previous_chunk: Some(CId::new(0)),
|
||||
chunk: CId::new(1),
|
||||
next_chunk: Some(CId::new(2))
|
||||
},
|
||||
ChunkRow {
|
||||
room_id: room_id.to_owned(),
|
||||
previous_chunk: Some(CId::new(1)),
|
||||
chunk: CId::new(2),
|
||||
next_chunk: None
|
||||
},
|
||||
],
|
||||
);
|
||||
// Items contains the gap.
|
||||
assert_eq!(
|
||||
relational_linked_chunk.items,
|
||||
&[ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
position: Position::new(CId::new(1), 0),
|
||||
item: Either::Gap(())
|
||||
}],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_chunk() {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
|
||||
|
||||
relational_linked_chunk.apply_updates(
|
||||
room_id,
|
||||
vec![
|
||||
// 0
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
// 1 after 0
|
||||
Update::NewGapChunk {
|
||||
previous: Some(CId::new(0)),
|
||||
new: CId::new(1),
|
||||
next: None,
|
||||
gap: (),
|
||||
},
|
||||
// 2 after 1
|
||||
Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },
|
||||
// remove 1
|
||||
Update::RemoveChunk(CId::new(1)),
|
||||
],
|
||||
);
|
||||
|
||||
// Chunks are correctly linked.
|
||||
assert_eq!(
|
||||
relational_linked_chunk.chunks,
|
||||
&[
|
||||
ChunkRow {
|
||||
room_id: room_id.to_owned(),
|
||||
previous_chunk: None,
|
||||
chunk: CId::new(0),
|
||||
next_chunk: Some(CId::new(2))
|
||||
},
|
||||
ChunkRow {
|
||||
room_id: room_id.to_owned(),
|
||||
previous_chunk: Some(CId::new(0)),
|
||||
chunk: CId::new(2),
|
||||
next_chunk: None
|
||||
},
|
||||
],
|
||||
);
|
||||
// Items no longer contains the gap.
|
||||
assert!(relational_linked_chunk.items.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_push_items() {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
|
||||
|
||||
relational_linked_chunk.apply_updates(
|
||||
room_id,
|
||||
vec![
|
||||
// new chunk (this is not mandatory for this test, but let's try to be realistic)
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
// new items on 0
|
||||
Update::PushItems { at: Position::new(CId::new(0), 0), items: vec!['a', 'b', 'c'] },
|
||||
// new chunk (to test new items are pushed in the correct chunk)
|
||||
Update::NewItemsChunk { previous: Some(CId::new(0)), new: CId::new(1), next: None },
|
||||
// new items on 1
|
||||
Update::PushItems { at: Position::new(CId::new(1), 0), items: vec!['x', 'y', 'z'] },
|
||||
// new items on 0 again
|
||||
Update::PushItems { at: Position::new(CId::new(0), 3), items: vec!['d', 'e'] },
|
||||
],
|
||||
);
|
||||
|
||||
// Chunks are correctly linked.
|
||||
assert_eq!(
|
||||
relational_linked_chunk.chunks,
|
||||
&[
|
||||
ChunkRow {
|
||||
room_id: room_id.to_owned(),
|
||||
previous_chunk: None,
|
||||
chunk: CId::new(0),
|
||||
next_chunk: Some(CId::new(1))
|
||||
},
|
||||
ChunkRow {
|
||||
room_id: room_id.to_owned(),
|
||||
previous_chunk: Some(CId::new(0)),
|
||||
chunk: CId::new(1),
|
||||
next_chunk: None
|
||||
},
|
||||
],
|
||||
);
|
||||
// Items contains the pushed items.
|
||||
assert_eq!(
|
||||
relational_linked_chunk.items,
|
||||
&[
|
||||
ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
position: Position::new(CId::new(0), 0),
|
||||
item: Either::Item('a')
|
||||
},
|
||||
ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
position: Position::new(CId::new(0), 1),
|
||||
item: Either::Item('b')
|
||||
},
|
||||
ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
position: Position::new(CId::new(0), 2),
|
||||
item: Either::Item('c')
|
||||
},
|
||||
ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
position: Position::new(CId::new(1), 0),
|
||||
item: Either::Item('x')
|
||||
},
|
||||
ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
position: Position::new(CId::new(1), 1),
|
||||
item: Either::Item('y')
|
||||
},
|
||||
ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
position: Position::new(CId::new(1), 2),
|
||||
item: Either::Item('z')
|
||||
},
|
||||
ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
position: Position::new(CId::new(0), 3),
|
||||
item: Either::Item('d')
|
||||
},
|
||||
ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
position: Position::new(CId::new(0), 4),
|
||||
item: Either::Item('e')
|
||||
},
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_item() {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
|
||||
|
||||
relational_linked_chunk.apply_updates(
|
||||
room_id,
|
||||
vec![
|
||||
// new chunk (this is not mandatory for this test, but let's try to be realistic)
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
// new items on 0
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(0), 0),
|
||||
items: vec!['a', 'b', 'c', 'd', 'e'],
|
||||
},
|
||||
// remove an item: 'a'
|
||||
Update::RemoveItem { at: Position::new(CId::new(0), 0) },
|
||||
// remove an item: 'd'
|
||||
Update::RemoveItem { at: Position::new(CId::new(0), 2) },
|
||||
],
|
||||
);
|
||||
|
||||
// Chunks are correctly linked.
|
||||
assert_eq!(
|
||||
relational_linked_chunk.chunks,
|
||||
&[ChunkRow {
|
||||
room_id: room_id.to_owned(),
|
||||
previous_chunk: None,
|
||||
chunk: CId::new(0),
|
||||
next_chunk: None
|
||||
}],
|
||||
);
|
||||
// Items contains the pushed items.
|
||||
assert_eq!(
|
||||
relational_linked_chunk.items,
|
||||
&[
|
||||
ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
position: Position::new(CId::new(0), 0),
|
||||
item: Either::Item('b')
|
||||
},
|
||||
ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
position: Position::new(CId::new(0), 1),
|
||||
item: Either::Item('c')
|
||||
},
|
||||
ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
position: Position::new(CId::new(0), 2),
|
||||
item: Either::Item('e')
|
||||
},
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detach_last_items() {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
|
||||
|
||||
relational_linked_chunk.apply_updates(
|
||||
room_id,
|
||||
vec![
|
||||
// new chunk
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
// new chunk
|
||||
Update::NewItemsChunk { previous: Some(CId::new(0)), new: CId::new(1), next: None },
|
||||
// new items on 0
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(0), 0),
|
||||
items: vec!['a', 'b', 'c', 'd', 'e'],
|
||||
},
|
||||
// new items on 1
|
||||
Update::PushItems { at: Position::new(CId::new(1), 0), items: vec!['x', 'y', 'z'] },
|
||||
// detach last items on 0
|
||||
Update::DetachLastItems { at: Position::new(CId::new(0), 2) },
|
||||
],
|
||||
);
|
||||
|
||||
// Chunks are correctly linked.
|
||||
assert_eq!(
|
||||
relational_linked_chunk.chunks,
|
||||
&[
|
||||
ChunkRow {
|
||||
room_id: room_id.to_owned(),
|
||||
previous_chunk: None,
|
||||
chunk: CId::new(0),
|
||||
next_chunk: Some(CId::new(1))
|
||||
},
|
||||
ChunkRow {
|
||||
room_id: room_id.to_owned(),
|
||||
previous_chunk: Some(CId::new(0)),
|
||||
chunk: CId::new(1),
|
||||
next_chunk: None
|
||||
},
|
||||
],
|
||||
);
|
||||
// Items contains the pushed items.
|
||||
assert_eq!(
|
||||
relational_linked_chunk.items,
|
||||
&[
|
||||
ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
position: Position::new(CId::new(0), 0),
|
||||
item: Either::Item('a')
|
||||
},
|
||||
ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
position: Position::new(CId::new(0), 1),
|
||||
item: Either::Item('b')
|
||||
},
|
||||
ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
position: Position::new(CId::new(1), 0),
|
||||
item: Either::Item('x')
|
||||
},
|
||||
ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
position: Position::new(CId::new(1), 1),
|
||||
item: Either::Item('y')
|
||||
},
|
||||
ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
position: Position::new(CId::new(1), 2),
|
||||
item: Either::Item('z')
|
||||
},
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_start_and_end_reattach_items() {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
|
||||
|
||||
relational_linked_chunk
|
||||
.apply_updates(room_id, vec![Update::StartReattachItems, Update::EndReattachItems]);
|
||||
|
||||
// Nothing happened.
|
||||
assert!(relational_linked_chunk.chunks.is_empty());
|
||||
assert!(relational_linked_chunk.items.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clear() {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
|
||||
|
||||
relational_linked_chunk.apply_updates(
|
||||
room_id,
|
||||
vec![
|
||||
// new chunk (this is not mandatory for this test, but let's try to be realistic)
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
// new items on 0
|
||||
Update::PushItems { at: Position::new(CId::new(0), 0), items: vec!['a', 'b', 'c'] },
|
||||
],
|
||||
);
|
||||
|
||||
// Chunks are correctly linked.
|
||||
assert_eq!(
|
||||
relational_linked_chunk.chunks,
|
||||
&[ChunkRow {
|
||||
room_id: room_id.to_owned(),
|
||||
previous_chunk: None,
|
||||
chunk: CId::new(0),
|
||||
next_chunk: None,
|
||||
}],
|
||||
);
|
||||
// Items contains the pushed items.
|
||||
assert_eq!(
|
||||
relational_linked_chunk.items,
|
||||
&[
|
||||
ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
position: Position::new(CId::new(0), 0),
|
||||
item: Either::Item('a')
|
||||
},
|
||||
ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
position: Position::new(CId::new(0), 1),
|
||||
item: Either::Item('b')
|
||||
},
|
||||
ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
position: Position::new(CId::new(0), 2),
|
||||
item: Either::Item('c')
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
// Now, time for a clean up.
|
||||
relational_linked_chunk.apply_updates(room_id, vec![Update::Clear]);
|
||||
assert!(relational_linked_chunk.chunks.is_empty());
|
||||
assert!(relational_linked_chunk.items.is_empty());
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,9 @@ use super::{ChunkIdentifier, Position};
|
||||
///
|
||||
/// These updates are useful to store a `LinkedChunk` in another form of
|
||||
/// storage, like a database or something similar.
|
||||
///
|
||||
/// [`LinkedChunk`]: super::LinkedChunk
|
||||
/// [`LinkedChunk::updates`]: super::LinkedChunk::updates
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Update<Item, Gap> {
|
||||
/// A new chunk of kind Items has been created.
|
||||
@@ -96,11 +99,17 @@ pub enum Update<Item, Gap> {
|
||||
|
||||
/// Reattaching items (see [`Self::StartReattachItems`]) is finished.
|
||||
EndReattachItems,
|
||||
|
||||
/// All chunks have been cleared, i.e. all items and all gaps have been
|
||||
/// dropped.
|
||||
Clear,
|
||||
}
|
||||
|
||||
/// A collection of [`Update`]s that can be observed.
|
||||
///
|
||||
/// Get a value for this type with [`LinkedChunk::updates`].
|
||||
///
|
||||
/// [`LinkedChunk::updates`]: super::LinkedChunk::updates
|
||||
#[derive(Debug)]
|
||||
pub struct ObservableUpdates<Item, Gap> {
|
||||
pub(super) inner: Arc<RwLock<UpdatesInner<Item, Gap>>>,
|
||||
|
||||
@@ -343,7 +343,7 @@ mod tests {
|
||||
};
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
use matrix_sdk_test::async_test;
|
||||
use matrix_sdk_test_macros::async_test;
|
||||
use tokio::{
|
||||
spawn,
|
||||
time::{sleep, Duration},
|
||||
@@ -361,7 +361,7 @@ mod tests {
|
||||
|
||||
impl TestStore {
|
||||
fn try_take_leased_lock(&self, lease_duration_ms: u32, key: &str, holder: &str) -> bool {
|
||||
try_take_leased_lock(&self.leases, lease_duration_ms, key, holder)
|
||||
try_take_leased_lock(&mut self.leases.write().unwrap(), lease_duration_ms, key, holder)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -502,12 +502,11 @@ mod tests {
|
||||
pub mod memory_store_helper {
|
||||
use std::{
|
||||
collections::{hash_map::Entry, HashMap},
|
||||
sync::RwLock,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
pub fn try_take_leased_lock(
|
||||
leases: &RwLock<HashMap<String, (String, Instant)>>,
|
||||
leases: &mut HashMap<String, (String, Instant)>,
|
||||
lease_duration_ms: u32,
|
||||
key: &str,
|
||||
holder: &str,
|
||||
@@ -515,7 +514,7 @@ pub mod memory_store_helper {
|
||||
let now = Instant::now();
|
||||
let expiration = now + Duration::from_millis(lease_duration_ms.into());
|
||||
|
||||
match leases.write().unwrap().entry(key.to_owned()) {
|
||||
match leases.entry(key.to_owned()) {
|
||||
// There is an existing holder.
|
||||
Entry::Occupied(mut entry) => {
|
||||
let (current_holder, current_expiration) = entry.get_mut();
|
||||
|
||||
@@ -62,7 +62,7 @@ where
|
||||
pub(crate) mod tests {
|
||||
use std::{future, time::Duration};
|
||||
|
||||
use matrix_sdk_test::async_test;
|
||||
use matrix_sdk_test_macros::async_test;
|
||||
|
||||
use super::timeout;
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ macro_rules! timer {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[matrix_sdk_test::async_test]
|
||||
#[matrix_sdk_test_macros::async_test]
|
||||
async fn test_timer_name() {
|
||||
use tracing::{span, Level};
|
||||
|
||||
|
||||
@@ -23,6 +23,11 @@ experimental-algorithms = []
|
||||
uniffi = ["dep:uniffi"]
|
||||
_disable-minimum-rotation-period-ms = []
|
||||
|
||||
# Private feature, see
|
||||
# https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823 for the gory
|
||||
# details.
|
||||
test-send-sync = []
|
||||
|
||||
# "message-ids" feature doesn't do anything and is deprecated.
|
||||
message-ids = []
|
||||
|
||||
@@ -31,6 +36,7 @@ testing = ["matrix-sdk-test"]
|
||||
|
||||
[dependencies]
|
||||
aes = "0.8.1"
|
||||
aquamarine = { workspace = true }
|
||||
as_variant = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
bs58 = { version = "0.5.0" }
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
A no-network-IO implementation of a state machine that handles E2EE for
|
||||
[Matrix] clients.
|
||||
|
||||
# Usage
|
||||
A no-network-IO implementation of a state machine that handles end-to-end
|
||||
encryption for [Matrix] clients.
|
||||
|
||||
If you're just trying to write a Matrix client or bot in Rust, you're probably
|
||||
looking for [matrix-sdk] instead.
|
||||
|
||||
However, if you're looking to add E2EE to an existing Matrix client or library,
|
||||
read on.
|
||||
However, if you're looking to add end-to-end encryption to an existing Matrix
|
||||
client or library, read on.
|
||||
|
||||
The state machine works in a push/pull manner:
|
||||
|
||||
@@ -52,28 +50,42 @@ async fn main() -> Result<(), OlmError> {
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
It is recommended to use the [tutorial] to understand how end-to-end encryption
|
||||
works in Matrix and how to add end-to-end encryption support in your Matrix
|
||||
client library.
|
||||
|
||||
[Matrix]: https://matrix.org/
|
||||
[matrix-sdk]: https://github.com/matrix-org/matrix-rust-sdk/
|
||||
|
||||
# Room key forwarding algorithm
|
||||
|
||||
The decision tree below visualizes the way this crate decides whether a message
|
||||
key ("room key") will be [forwarded][forwarded_room_key] to a requester upon a
|
||||
key request, provided the `automatic-room-key-forwarding` feature is enabled.
|
||||
Key forwarding is sometimes also referred to as key *gossiping*.
|
||||
|
||||
[forwarded_room_key]: <https://spec.matrix.org/v1.10/client-server-api/#mforwarded_room_key>
|
||||
|
||||

|
||||
|
||||
|
||||
# Crate Feature Flags
|
||||
|
||||
The following crate feature flags are available:
|
||||
|
||||
* `qrcode`: Enbles QRcode generation and reading code
|
||||
| Feature | Default | Description |
|
||||
| ------------------- | :-----: | -------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `qrcode` | No | Enables QR code based interactive verification |
|
||||
| `js` | No | Enables JavaScript API usage for things like the current system time on WASM (does nothing on other targets) |
|
||||
| `testing` | No | Provides facilities and functions for tests, in particular for integration testing store implementations. ATTENTION: do not ever use outside of tests, we do not provide any stability warantees on these, these are merely helpers. If you find you _need_ any function provided here outside of tests, please open a Github Issue and inform us about your use case for us to consider. |
|
||||
|
||||
* `testing`: Provides facilities and functions for tests, in particular for integration testing store implementations. ATTENTION: do not ever use outside of tests, we do not provide any stability warantees on these, these are merely helpers. If you find you _need_ any function provided here outside of tests, please open a Github Issue and inform us about your use case for us to consider.
|
||||
|
||||
* `_disable-minimum-rotation-period-ms`: Do not use except for testing. Disables the floor on the rotation period of room keys.
|
||||
# Enabling logging
|
||||
|
||||
Users of the `matrix-sdk-crypto` crate can enable log output by depending on the
|
||||
`tracing-subscriber` crate and including the following line in their
|
||||
application (e.g. at the start of `main`):
|
||||
|
||||
```no_compile
|
||||
tracing_subscriber::fmt::init();
|
||||
```
|
||||
|
||||
The log output is controlled via the `RUST_LOG` environment variable by
|
||||
setting it to one of the `error`, `warn`, `info`, `debug` or `trace` levels.
|
||||
The output is printed to stdout.
|
||||
|
||||
The `RUST_LOG` variable also supports a more advanced syntax for filtering
|
||||
log output more precisely, for instance with crate-level granularity. For
|
||||
more information on this, check out the [tracing-subscriber documentation].
|
||||
|
||||
[tracing-subscriber documentation]: https://docs.rs/tracing-subscriber/latest/tracing_subscriber/
|
||||
|
||||
@@ -56,7 +56,7 @@ impl<'a, R: 'a + Read + std::fmt::Debug> std::fmt::Debug for AttachmentDecryptor
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, R: Read> Read for AttachmentDecryptor<'a, R> {
|
||||
impl<R: Read> Read for AttachmentDecryptor<'_, R> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
let read_bytes = self.inner.read(buf)?;
|
||||
|
||||
|
||||
@@ -616,7 +616,6 @@ impl GossipMachine {
|
||||
/// i.
|
||||
/// - `Err(x)`: Should *refuse* to share the session. `x` is the reason for
|
||||
/// the refusal.
|
||||
|
||||
#[cfg(feature = "automatic-room-key-forwarding")]
|
||||
async fn should_share_key(
|
||||
&self,
|
||||
|
||||
@@ -548,7 +548,7 @@ impl IdentityManager {
|
||||
// First time seen, create the identity. The current MSK will be pinned.
|
||||
let identity = OtherUserIdentityData::new(master_key, self_signing)?;
|
||||
let is_verified = maybe_verified_own_identity
|
||||
.map_or(false, |own_user_identity| own_user_identity.is_identity_signed(&identity));
|
||||
.is_some_and(|own_user_identity| own_user_identity.is_identity_signed(&identity));
|
||||
if is_verified {
|
||||
identity.mark_as_previously_verified();
|
||||
}
|
||||
|
||||
@@ -435,16 +435,20 @@ impl OtherUserIdentity {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Test helper
|
||||
/// Test helper that marks that an identity has been previously verified and
|
||||
/// persist the change in the store.
|
||||
#[cfg(test)]
|
||||
pub async fn mark_as_previously_verified(&self) -> Result<(), CryptoStoreError> {
|
||||
self.inner.mark_as_previously_verified();
|
||||
|
||||
let to_save = UserIdentityData::Other(self.inner.clone());
|
||||
let changes = Changes {
|
||||
identities: IdentityChanges { changed: vec![to_save], ..Default::default() },
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
self.verification_machine.store.inner().save_changes(changes).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -854,8 +858,8 @@ impl OtherUserIdentityData {
|
||||
|
||||
// Check if the new master_key is signed by our own **verified**
|
||||
// user_signing_key. If the identity was verified we remember it.
|
||||
let updated_is_verified = maybe_verified_own_user_signing_key
|
||||
.map_or(false, |own_user_signing_key| {
|
||||
let updated_is_verified =
|
||||
maybe_verified_own_user_signing_key.is_some_and(|own_user_signing_key| {
|
||||
own_user_signing_key.verify_master_key(&master_key).is_ok()
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
// Copyright 2024 Damir Jelić
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -16,6 +17,7 @@
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![warn(missing_docs, missing_debug_implementations)]
|
||||
#![cfg_attr(target_arch = "wasm32", allow(clippy::arc_with_non_send_sync))]
|
||||
#![cfg_attr(test, allow(unexpected_cfgs))] // Triggered by the init_tracing_for_tests!() invocation.
|
||||
|
||||
pub mod backups;
|
||||
mod ciphers;
|
||||
@@ -156,3 +158,944 @@ pub enum RoomEventDecryptionResult {
|
||||
/// We were unable to decrypt the event
|
||||
UnableToDecrypt(UnableToDecryptInfo),
|
||||
}
|
||||
|
||||
#[cfg_attr(doc, aquamarine::aquamarine)]
|
||||
/// A step by step guide that explains how to include [end-to-end-encryption]
|
||||
/// support in a [Matrix] client library.
|
||||
///
|
||||
/// This crate implements a [sans-network-io](https://sans-io.readthedocs.io/)
|
||||
/// state machine that allows you to add [end-to-end-encryption] support to a
|
||||
/// [Matrix] client library.
|
||||
///
|
||||
/// This guide aims to provide a comprehensive understanding of end-to-end
|
||||
/// encryption in Matrix without any prior knowledge requirements. However, it
|
||||
/// is recommended that the reader has a basic understanding of Matrix and its
|
||||
/// [client-server specification] for a more informed and efficient learning
|
||||
/// experience.
|
||||
///
|
||||
/// The [introductory](#introduction) section provides a simplified explanation
|
||||
/// of end-to-end encryption and its implementation in Matrix for those who may
|
||||
/// not have prior knowledge. If you already have a solid understanding of
|
||||
/// end-to-end encryption, including the [Olm] and [Megolm] protocols, you may
|
||||
/// choose to skip directly to the [Getting Started](#getting-started) section.
|
||||
///
|
||||
/// # Table of Contents
|
||||
/// 1. [Introduction](#introduction)
|
||||
/// 2. [Getting started](#getting-started)
|
||||
/// 3. [Decrypting room events](#decryption)
|
||||
/// 4. [Encrypting room events](#encryption)
|
||||
/// 5. [Interactively verifying devices and user identities](#verification)
|
||||
///
|
||||
/// # Introduction
|
||||
///
|
||||
/// Welcome to the first part of this guide, where we will introduce the
|
||||
/// fundamental concepts of end-to-end encryption and its implementation in
|
||||
/// Matrix.
|
||||
///
|
||||
/// This section will provide a clear and concise overview of what
|
||||
/// end-to-end encryption is and why it is important for secure communication.
|
||||
/// You will also learn about how Matrix uses end-to-end encryption to protect
|
||||
/// the privacy and security of its users' communications. Whether you are new
|
||||
/// to the topic or simply want to improve your understanding, this section will
|
||||
/// serve as a solid foundation for the rest of the guide.
|
||||
///
|
||||
/// Let's dive in!
|
||||
///
|
||||
/// ## Notation
|
||||
///
|
||||
/// ## End-to-end-encryption
|
||||
///
|
||||
/// End-to-end encryption (E2EE) is a method of secure communication where only
|
||||
/// the communicating devices, also known as "the ends," can read the data being
|
||||
/// transmitted. This means that the data is encrypted on one device, and can
|
||||
/// only be decrypted on the other device. The server is used only as a
|
||||
/// transport mechanism to deliver messages between devices.
|
||||
///
|
||||
/// The following chart displays how communication between two clients using a
|
||||
/// server in the middle usually works.
|
||||
///
|
||||
/// ```mermaid
|
||||
/// flowchart LR
|
||||
/// alice[Alice]
|
||||
/// bob[Bob]
|
||||
/// subgraph Homeserver
|
||||
/// direction LR
|
||||
/// outbox[Alice outbox]
|
||||
/// inbox[Bob inbox]
|
||||
/// outbox -. unencrypted .-> inbox
|
||||
/// end
|
||||
///
|
||||
/// alice -- encrypted --> outbox
|
||||
/// inbox -- encrypted --> bob
|
||||
/// ```
|
||||
///
|
||||
/// The next chart, instead, displays how the same flow is happening in a
|
||||
/// end-to-end-encrypted world.
|
||||
///
|
||||
/// ```mermaid
|
||||
/// flowchart LR
|
||||
/// alice[Alice]
|
||||
/// bob[Bob]
|
||||
/// subgraph Homeserver
|
||||
/// direction LR
|
||||
/// outbox[Alice outbox]
|
||||
/// inbox[Bob inbox]
|
||||
/// outbox == encrypted ==> inbox
|
||||
/// end
|
||||
///
|
||||
/// alice == encrypted ==> outbox
|
||||
/// inbox == encrypted ==> bob
|
||||
/// ```
|
||||
///
|
||||
/// Note that the path from the outbox to the inbox is now encrypted as well.
|
||||
///
|
||||
/// Alice and Bob have created a secure communication channel
|
||||
/// through which they can exchange messages confidentially, without the risk of
|
||||
/// the server accessing the contents of their messages.
|
||||
///
|
||||
/// ## Publishing cryptographic identities of devices
|
||||
///
|
||||
/// If Alice and Bob want to establish a secure channel over which they can
|
||||
/// exchange messages, they first need learn about each others cryptographic
|
||||
/// identities. This is achieved by using the homeserver as a public key
|
||||
/// directory.
|
||||
///
|
||||
/// A public key directory is used to store and distribute public keys of users
|
||||
/// in an end-to-end encrypted system. The basic idea behind a public key
|
||||
/// directory is that it allows users to easily discover and download the public
|
||||
/// keys of other users with whom they wish to establish an end-to-end encrypted
|
||||
/// communication.
|
||||
///
|
||||
/// Each user generates a pair of public and private keys. The user then uploads
|
||||
/// their public key to the public key directory. Other users can then search
|
||||
/// the directory to find the public key of the user they wish to communicate
|
||||
/// with, and download it to their own device.
|
||||
///
|
||||
/// ```mermaid
|
||||
/// flowchart LR
|
||||
/// alice[Alice]
|
||||
/// subgraph homeserver[Homeserver]
|
||||
/// direction LR
|
||||
/// directory[(Public key directory)]
|
||||
/// end
|
||||
/// bob[Bob]
|
||||
///
|
||||
/// alice -- upload keys --> directory
|
||||
/// directory -- download keys --> bob
|
||||
/// ```
|
||||
///
|
||||
/// Once a user has the other user's public key, they can use it to establish an
|
||||
/// end-to-end encrypted channel using a [key-agreement protocol].
|
||||
///
|
||||
/// ## Using the Triple Diffie-Hellman key-agreement protocol
|
||||
///
|
||||
/// In the triple Diffie-Hellman key agreement protocol (3DH in short), each
|
||||
/// user generates a long-term identity key pair and a set of one-time prekeys.
|
||||
/// When two users want to establish a shared secret key, they exchange their
|
||||
/// public identity keys and one of their prekeys. These public keys are then
|
||||
/// used in a [Diffie-Hellman] key exchange to compute a shared secret key.
|
||||
///
|
||||
/// The use of one-time prekeys ensures that the shared secret key is different
|
||||
/// for each session, even if the same identity keys are used.
|
||||
///
|
||||
/// ```mermaid
|
||||
/// flowchart LR
|
||||
/// subgraph alice_keys[Alice Keys]
|
||||
/// direction TB
|
||||
/// alice_key[Alice's identity key]
|
||||
/// alice_base_key[Alice's one-time key]
|
||||
/// end
|
||||
///
|
||||
/// subgraph bob_keys[Bob Keys]
|
||||
/// direction TB
|
||||
/// bob_key[Bob's identity key]
|
||||
/// bob_one_time[Bob's one-time key]
|
||||
/// end
|
||||
///
|
||||
/// alice_key <--> bob_one_time
|
||||
/// alice_base_key <--> bob_one_time
|
||||
/// alice_base_key <--> bob_key
|
||||
/// ```
|
||||
///
|
||||
/// Similar to [X3DH] (Extended Triple Diffie-Hellman) key agreement protocol
|
||||
///
|
||||
/// ## Speeding up encryption for large groups
|
||||
///
|
||||
/// In the previous section we learned how to utilize a key agreement protocol
|
||||
/// to establish secure 1-to-1 encrypted communication channels. These channels
|
||||
/// allow us to encrypt a message for each device separately.
|
||||
///
|
||||
/// One critical property of these channels is that, if you want to send a
|
||||
/// message to a group of devices, we'll need to encrypt the message for each
|
||||
/// device individually.
|
||||
///
|
||||
/// TODO Explain how megolm fits into this
|
||||
///
|
||||
/// # Getting started
|
||||
///
|
||||
/// Before we start writing any code, let us get familiar with the basic
|
||||
/// principle upon which this library is built.
|
||||
///
|
||||
/// The central piece of the library is the [`OlmMachine`] which acts as a state
|
||||
/// machine which consumes data that gets received from the homeserver and
|
||||
/// outputs data which should be sent to the homeserver.
|
||||
///
|
||||
/// ## Push/pull mechanism
|
||||
///
|
||||
/// The [`OlmMachine`] at the heart of it acts as a state machine that operates
|
||||
/// in a push/pull manner. HTTP responses which were received from the
|
||||
/// homeserver get forwarded into the [`OlmMachine`] and in turn the internal
|
||||
/// state gets updated which produces HTTP requests that need to be sent to the
|
||||
/// homeserver.
|
||||
///
|
||||
/// In a manner, we're pulling data from the server, we update our internal
|
||||
/// state based on the data and in turn push data back to the server.
|
||||
///
|
||||
/// ```mermaid
|
||||
/// flowchart LR
|
||||
/// homeserver[Homeserver]
|
||||
/// client[OlmMachine]
|
||||
///
|
||||
/// homeserver -- pull --> client
|
||||
/// client -- push --> homeserver
|
||||
/// ```
|
||||
///
|
||||
/// ## Initializing the state machine
|
||||
///
|
||||
/// ```
|
||||
/// use anyhow::Result;
|
||||
/// use matrix_sdk_crypto::OlmMachine;
|
||||
/// use ruma::user_id;
|
||||
///
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> Result<()> {
|
||||
/// let user_id = user_id!("@alice:localhost");
|
||||
/// let device_id = "DEVICEID".into();
|
||||
///
|
||||
/// let machine = OlmMachine::new(user_id, device_id).await;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// This will create a [`OlmMachine`] that does not persist any data TODO
|
||||
/// ```ignore
|
||||
/// use anyhow::Result;
|
||||
/// use matrix_sdk_crypto::OlmMachine;
|
||||
/// use matrix_sdk_sqlite::SqliteCryptoStore;
|
||||
/// use ruma::user_id;
|
||||
///
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> Result<()> {
|
||||
/// let user_id = user_id!("@alice:localhost");
|
||||
/// let device_id = "DEVICEID".into();
|
||||
///
|
||||
/// let store = SqliteCryptoStore::open("/home/example/matrix-client/", None).await?;
|
||||
///
|
||||
/// let machine = OlmMachine::with_store(user_id, device_id, store).await;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// # Decryption
|
||||
///
|
||||
/// In the world of encrypted communication, it is common to start with the
|
||||
/// encryption step when implementing a protocol. However, in the case of adding
|
||||
/// end-to-end encryption support to a Matrix client library, a simpler approach
|
||||
/// is to first focus on the decryption process. This is because there are
|
||||
/// already Matrix clients in existence that support encryption, which means
|
||||
/// that our client library can simply receive encrypted messages and then
|
||||
/// decrypt them.
|
||||
///
|
||||
/// In this section, we will guide you through the minimal steps
|
||||
/// necessary to get the decryption process up and running using the
|
||||
/// matrix-sdk-crypto Rust crate. By the end of this section you should have a
|
||||
/// Matrix client that is able to decrypt room events that other clients have
|
||||
/// sent.
|
||||
///
|
||||
/// To enable decryption the following three steps are needed:
|
||||
///
|
||||
/// 1. [The cryptographic identity of your device needs to be published to the
|
||||
/// homeserver](#uploading-identity-and-one-time-keys).
|
||||
/// 2. [Decryption keys coming in from other devices need to be processed and
|
||||
/// stored](#receiving-room-keys-and-related-changes).
|
||||
/// 3. [Individual messages need to be decrypted](#decrypting-room-events).
|
||||
///
|
||||
/// The simplified flowchart
|
||||
/// ```mermaid
|
||||
/// graph TD
|
||||
/// sync[Sync with the homeserver]
|
||||
/// receive_changes[Push E2EE related changes into the state machine]
|
||||
/// send_outgoing_requests[Send all outgoing requests to the homeserver]
|
||||
/// decrypt[Process the rest of the sync]
|
||||
///
|
||||
/// sync --> receive_changes;
|
||||
/// receive_changes --> send_outgoing_requests;
|
||||
/// send_outgoing_requests --> decrypt;
|
||||
/// decrypt -- repeat --> sync;
|
||||
/// ```
|
||||
///
|
||||
/// ## Uploading identity and one-time keys.
|
||||
///
|
||||
/// To enable end-to-end encryption in a Matrix client, the first step is to
|
||||
/// announce the support for it to other users in the network. This is done by
|
||||
/// publishing the client's long-term device keys and a set of one-time prekeys
|
||||
/// to the Matrix homeserver. The homeserver then makes this information
|
||||
/// available to other devices in the network.
|
||||
///
|
||||
/// The long-term device keys and one-time prekeys allow other devices to
|
||||
/// encrypt messages specifically for your device.
|
||||
///
|
||||
/// To achieve this, you will need to extract any requests that need to be sent
|
||||
/// to the homeserver from the [`OlmMachine`] and send them to the homeserver.
|
||||
/// The following snippet showcases how to achieve this using the
|
||||
/// [`OlmMachine::outgoing_requests()`] method:
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use std::collections::BTreeMap;
|
||||
/// # use ruma::api::client::keys::upload_keys::v3::Response;
|
||||
/// # use anyhow::Result;
|
||||
/// # use matrix_sdk_crypto::{OlmMachine, OutgoingRequest};
|
||||
/// # async fn send_request(request: OutgoingRequest) -> Result<Response> {
|
||||
/// # let response = unimplemented!();
|
||||
/// # Ok(response)
|
||||
/// # }
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> Result<()> {
|
||||
/// # let machine: OlmMachine = unimplemented!();
|
||||
/// // Get all the outgoing requests.
|
||||
/// let outgoing_requests = machine.outgoing_requests().await?;
|
||||
///
|
||||
/// // Send each request to the server and push the response into the state machine.
|
||||
/// // You can safely send these requests out in parallel.
|
||||
/// for request in outgoing_requests {
|
||||
/// let request_id = request.request_id();
|
||||
/// // Send the request to the server and await a response.
|
||||
/// let response = send_request(request).await?;
|
||||
/// // Push the response into the state machine.
|
||||
/// machine.mark_request_as_sent(&request_id, &response).await?;
|
||||
/// }
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// #### 🔒 Locking rule
|
||||
///
|
||||
/// It's important to note that the outgoing requests method in the
|
||||
/// [`OlmMachine`], while thread-safe, may return the same request multiple
|
||||
/// times if it is called multiple times before the request has been marked as
|
||||
/// sent. To prevent this issue, it is advisable to encapsulate the outgoing
|
||||
/// request handling logic into a separate helper method and protect it from
|
||||
/// being called multiple times concurrently using a lock.
|
||||
///
|
||||
/// This helps to ensure that the request is only handled once and prevents
|
||||
/// multiple identical requests from being sent.
|
||||
///
|
||||
/// Additionally, if an error occurs while sending a request using the
|
||||
/// [`OlmMachine::outgoing_requests()`] method, the request will be
|
||||
/// naturally retried the next time the method is called.
|
||||
///
|
||||
/// A more complete example, which uses a helper method, might look like this:
|
||||
/// ```no_run
|
||||
/// # use std::collections::BTreeMap;
|
||||
/// # use ruma::api::client::keys::upload_keys::v3::Response;
|
||||
/// # use anyhow::Result;
|
||||
/// # use matrix_sdk_crypto::{OlmMachine, OutgoingRequest};
|
||||
/// # async fn send_request(request: &OutgoingRequest) -> Result<Response> {
|
||||
/// # let response = unimplemented!();
|
||||
/// # Ok(response)
|
||||
/// # }
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> Result<()> {
|
||||
/// struct Client {
|
||||
/// outgoing_requests_lock: tokio::sync::Mutex<()>,
|
||||
/// olm_machine: OlmMachine,
|
||||
/// }
|
||||
///
|
||||
/// async fn process_outgoing_requests(client: &Client) -> Result<()> {
|
||||
/// // Let's acquire a lock so we know that we don't send out the same request out multiple
|
||||
/// // times.
|
||||
/// let guard = client.outgoing_requests_lock.lock().await;
|
||||
///
|
||||
/// for request in client.olm_machine.outgoing_requests().await? {
|
||||
/// let request_id = request.request_id();
|
||||
///
|
||||
/// match send_request(&request).await {
|
||||
/// Ok(response) => {
|
||||
/// client.olm_machine.mark_request_as_sent(&request_id, &response).await?;
|
||||
/// }
|
||||
/// Err(error) => {
|
||||
/// // It's OK to ignore transient HTTP errors since requests will be retried.
|
||||
/// eprintln!(
|
||||
/// "Error while sending out a end-to-end encryption \
|
||||
/// related request: {error:?}"
|
||||
/// );
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// Once we have the helper method that processes our outgoing requests we can
|
||||
/// structure our sync method as follows:
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use anyhow::Result;
|
||||
/// # use matrix_sdk_crypto::OlmMachine;
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> Result<()> {
|
||||
/// # struct Client {
|
||||
/// # outgoing_requests_lock: tokio::sync::Mutex<()>,
|
||||
/// # olm_machine: OlmMachine,
|
||||
/// # }
|
||||
/// # async fn process_outgoing_requests(client: &Client) -> Result<()> {
|
||||
/// # unimplemented!();
|
||||
/// # }
|
||||
/// # async fn send_out_sync_request(client: &Client) -> Result<()> {
|
||||
/// # unimplemented!();
|
||||
/// # }
|
||||
/// async fn sync(client: &Client) -> Result<()> {
|
||||
/// // This is happening at the top of the method so we advertise our
|
||||
/// // end-to-end encryption capabilities as soon as possible.
|
||||
/// process_outgoing_requests(client).await?;
|
||||
///
|
||||
/// // We can sync with the homeserver now.
|
||||
/// let response = send_out_sync_request(client).await?;
|
||||
///
|
||||
/// // Process the sync response here.
|
||||
///
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// ## Receiving room keys and related changes
|
||||
///
|
||||
/// The next step in our implementation is to forward messages that were sent
|
||||
/// directly to the client's device, and state updates about the one-time
|
||||
/// prekeys, to the [`OlmMachine`]. This is achieved using
|
||||
/// the [`OlmMachine::receive_sync_changes()`] method.
|
||||
///
|
||||
/// The method performs two tasks:
|
||||
///
|
||||
/// 1. It processes and, if necessary, decrypts each [to-device] event that was
|
||||
/// pushed into it, and returns the decrypted events. The original events are
|
||||
/// replaced with their decrypted versions.
|
||||
///
|
||||
/// 2. It produces internal state changes that may trigger the creation of new
|
||||
/// outgoing requests. For example, if the server informs the client that its
|
||||
/// one-time prekeys have been depleted, the OlmMachine will create an
|
||||
/// outgoing request to replenish them.
|
||||
///
|
||||
/// Our updated sync method now looks like this:
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use anyhow::Result;
|
||||
/// # use matrix_sdk_crypto::{EncryptionSyncChanges, OlmMachine};
|
||||
/// # use ruma::api::client::sync::sync_events::v3::Response;
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> Result<()> {
|
||||
/// # struct Client {
|
||||
/// # outgoing_requests_lock: tokio::sync::Mutex<()>,
|
||||
/// # olm_machine: OlmMachine,
|
||||
/// # }
|
||||
/// # async fn process_outgoing_requests(client: &Client) -> Result<()> {
|
||||
/// # unimplemented!();
|
||||
/// # }
|
||||
/// # async fn send_out_sync_request(client: &Client) -> Result<Response> {
|
||||
/// # unimplemented!();
|
||||
/// # }
|
||||
/// async fn sync(client: &Client) -> Result<()> {
|
||||
/// process_outgoing_requests(client).await?;
|
||||
///
|
||||
/// let response = send_out_sync_request(client).await?;
|
||||
///
|
||||
/// let sync_changes = EncryptionSyncChanges {
|
||||
/// to_device_events: response.to_device.events,
|
||||
/// changed_devices: &response.device_lists,
|
||||
/// one_time_keys_counts: &response.device_one_time_keys_count,
|
||||
/// unused_fallback_keys: response.device_unused_fallback_key_types.as_deref(),
|
||||
/// next_batch_token: Some(response.next_batch),
|
||||
/// };
|
||||
///
|
||||
/// // Push the sync changes into the OlmMachine, make sure that this is
|
||||
/// // happening before the `next_batch` token of the sync is persisted.
|
||||
/// let to_device_events = client
|
||||
/// .olm_machine
|
||||
/// .receive_sync_changes(sync_changes)
|
||||
/// .await?;
|
||||
///
|
||||
/// // Send the outgoing requests out that the sync changes produced.
|
||||
/// process_outgoing_requests(client).await?;
|
||||
///
|
||||
/// // Process the rest of the sync response here.
|
||||
///
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// It is important to note that the names of the fields in the response shown
|
||||
/// in the example match the names of the fields specified in the [sync]
|
||||
/// response specification.
|
||||
///
|
||||
/// It is critical to note that due to the ephemeral nature of to-device
|
||||
/// events[[1]], it is important to process these events before persisting the
|
||||
/// `next_batch` sync token. This is because if the `next_batch` sync token is
|
||||
/// persisted before processing the to-device events, some messages might be
|
||||
/// lost, leading to decryption failures.
|
||||
///
|
||||
/// ## Decrypting room events
|
||||
///
|
||||
/// The final step in the decryption process is to decrypt the room events that
|
||||
/// are received from the server. To do this, the encrypted events must be
|
||||
/// passed to the [`OlmMachine`], which will use the keys that were previously
|
||||
/// exchanged between devices to decrypt the events. The decrypted events can
|
||||
/// then be processed and displayed to the user in the Matrix client.
|
||||
///
|
||||
/// Room message [events] can be decrypted using the
|
||||
/// [`OlmMachine::decrypt_room_event()`] method:
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use std::collections::BTreeMap;
|
||||
/// # use anyhow::Result;
|
||||
/// # use matrix_sdk_crypto::{OlmMachine, DecryptionSettings, TrustRequirement};
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> Result<()> {
|
||||
/// # let encrypted = unimplemented!();
|
||||
/// # let room_id = unimplemented!();
|
||||
/// # let machine: OlmMachine = unimplemented!();
|
||||
/// # let settings = DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted };
|
||||
/// // Decrypt your room events now.
|
||||
/// let decrypted = machine
|
||||
/// .decrypt_room_event(encrypted, room_id, &settings)
|
||||
/// .await?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
/// It's worth mentioning that the [`OlmMachine::decrypt_room_event()`] method
|
||||
/// is designed to be thread-safe and can be safely called concurrently. This
|
||||
/// means that room message [events] can be processed in parallel, improving the
|
||||
/// overall efficiency of the end-to-end encryption implementation.
|
||||
///
|
||||
/// By allowing room message [events] to be processed concurrently, the client's
|
||||
/// implementation can take full advantage of the capabilities of modern
|
||||
/// hardware and achieve better performance, especially when dealing with a
|
||||
/// large number of messages at once.
|
||||
///
|
||||
/// # Encryption
|
||||
///
|
||||
/// In this section of the guide, we will focus on enabling the encryption of
|
||||
/// messages in our Matrix client library. Up until this point, we have been
|
||||
/// discussing the process of decrypting messages that have been encrypted by
|
||||
/// other devices. Now, we will shift our focus to the process of encrypting
|
||||
/// messages on the client side, so that they can be securely transmitted over
|
||||
/// the Matrix network to other devices.
|
||||
///
|
||||
/// This section will guide you through the steps required to set up the
|
||||
/// encryption process, including establishing the necessary sessions and
|
||||
/// encrypting messages using the Megolm group session. The specific steps are
|
||||
/// outlined bellow:
|
||||
///
|
||||
/// 1. [Cryptographic devices of other users need to be
|
||||
/// discovered](#tracking-users)
|
||||
///
|
||||
/// 2. [Secure channels between the devices need to be
|
||||
/// established](#establishing-end-to-end-encrypted-channels)
|
||||
///
|
||||
/// 3. [A room key needs to be exchanged with the group](#exchanging-room-keys)
|
||||
///
|
||||
/// 4. [Individual messages need to be encrypted using the room
|
||||
/// key](#encrypting-room-events)
|
||||
///
|
||||
/// The process for enabling encryption in a two-device scenario is also
|
||||
/// depicted in the following sequence diagram:
|
||||
///
|
||||
/// ```mermaid
|
||||
/// sequenceDiagram
|
||||
/// actor Alice
|
||||
/// participant Homeserver
|
||||
/// actor Bob
|
||||
///
|
||||
/// Alice->>Homeserver: Download Bob's one-time prekey
|
||||
/// Homeserver->>Alice: Bob's one-time prekey
|
||||
/// Alice->>Alice: Encrypt the room key
|
||||
/// Alice->>Homeserver: Send the room key to each of Bob's devices
|
||||
/// Homeserver->>Bob: Deliver the room key
|
||||
/// Alice->>Alice: Encrypt the message
|
||||
/// Alice->>Homeserver: Send the encrypted message
|
||||
/// Homeserver->>Bob: Deliver the encrypted message
|
||||
/// ```
|
||||
///
|
||||
/// In the following subsections, we will provide a step-by-step guide on how to
|
||||
/// enable the encryption of messages using the OlmMachine. We will outline the
|
||||
/// specific method calls and usage patterns that are required to establish the
|
||||
/// necessary sessions, encrypt messages, and send them over the Matrix network.
|
||||
///
|
||||
/// ## Tracking users
|
||||
///
|
||||
/// The first step in the process of encrypting a message and sending it to a
|
||||
/// device is to discover the devices that the recipient user has. This can be
|
||||
/// achieved by sending a request to the homeserver to retrieve a list of the
|
||||
/// recipient's device keys. The response to this request will include the
|
||||
/// device keys for all of the devices that belong to the recipient, as well as
|
||||
/// information about their current status and whether or not they support
|
||||
/// end-to-end encryption.
|
||||
///
|
||||
/// The process for discovering and keeping track of devices for a user is
|
||||
/// outlined in the Matrix specification in the "[Tracking the device list for a
|
||||
/// user]" section.
|
||||
///
|
||||
/// A simplified sequence diagram of the process can also be found bellow.
|
||||
///
|
||||
/// ```mermaid
|
||||
/// sequenceDiagram
|
||||
/// actor Alice
|
||||
/// participant Homeserver
|
||||
///
|
||||
/// Alice->>Homeserver: Sync with the homeserver
|
||||
/// Homeserver->>Alice: Users whose device list has changed
|
||||
/// Alice->>Alice: Mark user's devicel list as outdated
|
||||
/// Alice->>Homeserver: Ask the server for the new device list of all the outdated users
|
||||
/// Alice->>Alice: Update the local device list and mark the users as up-to-date
|
||||
/// ```
|
||||
///
|
||||
/// The OlmMachine refers to users whose devices we are tracking as "tracked
|
||||
/// users" and utilizes the [`OlmMachine::update_tracked_users()`] method to
|
||||
/// start considering users to be tracked. Keeping the above diagram in mind, we
|
||||
/// can now update our sync method as follows:
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use anyhow::Result;
|
||||
/// # use std::ops::Deref;
|
||||
/// # use matrix_sdk_crypto::{EncryptionSyncChanges, OlmMachine};
|
||||
/// # use ruma::api::client::sync::sync_events::v3::{Response, JoinedRoom};
|
||||
/// # use ruma::{OwnedUserId, serde::Raw, events::AnySyncStateEvent};
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> Result<()> {
|
||||
/// # struct Client {
|
||||
/// # outgoing_requests_lock: tokio::sync::Mutex<()>,
|
||||
/// # olm_machine: OlmMachine,
|
||||
/// # }
|
||||
/// # async fn process_outgoing_requests(client: &Client) -> Result<()> {
|
||||
/// # unimplemented!();
|
||||
/// # }
|
||||
/// # async fn send_out_sync_request(client: &Client) -> Result<Response> {
|
||||
/// # unimplemented!();
|
||||
/// # }
|
||||
/// # fn is_member_event_of_a_joined_user(event: &Raw<AnySyncStateEvent>) -> bool {
|
||||
/// # true
|
||||
/// # }
|
||||
/// # fn get_user_id(event: &Raw<AnySyncStateEvent>) -> OwnedUserId {
|
||||
/// # unimplemented!();
|
||||
/// # }
|
||||
/// # fn is_room_encrypted(room: &JoinedRoom) -> bool {
|
||||
/// # true
|
||||
/// # }
|
||||
/// async fn sync(client: &Client) -> Result<()> {
|
||||
/// process_outgoing_requests(client).await?;
|
||||
///
|
||||
/// let response = send_out_sync_request(client).await?;
|
||||
///
|
||||
/// let sync_changes = EncryptionSyncChanges {
|
||||
/// to_device_events: response.to_device.events,
|
||||
/// changed_devices: &response.device_lists,
|
||||
/// one_time_keys_counts: &response.device_one_time_keys_count,
|
||||
/// unused_fallback_keys: response.device_unused_fallback_key_types.as_deref(),
|
||||
/// next_batch_token: Some(response.next_batch),
|
||||
/// };
|
||||
///
|
||||
/// // Push the sync changes into the OlmMachine, make sure that this is
|
||||
/// // happening before the `next_batch` token of the sync is persisted.
|
||||
/// let to_device_events = client
|
||||
/// .olm_machine
|
||||
/// .receive_sync_changes(sync_changes)
|
||||
/// .await?;
|
||||
///
|
||||
/// // Send the outgoing requests out that the sync changes produced.
|
||||
/// process_outgoing_requests(client).await?;
|
||||
///
|
||||
/// // Collect all the joined and invited users of our end-to-end encrypted rooms here.
|
||||
/// let mut users = Vec::new();
|
||||
///
|
||||
/// for (_, room) in &response.rooms.join {
|
||||
/// // For simplicity reasons we're only looking at the state field of a joined room, but
|
||||
/// // the events in the timeline are important as well.
|
||||
/// for event in &room.state.events {
|
||||
/// if is_member_event_of_a_joined_user(event) && is_room_encrypted(room) {
|
||||
/// let user_id = get_user_id(event);
|
||||
/// users.push(user_id);
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// // Mark all the users that we consider to be in a end-to-end encrypted room with us to be
|
||||
/// // tracked. We need to know about all the devices each user has so we can later encrypt
|
||||
/// // messages for each of their devices.
|
||||
/// client.olm_machine.update_tracked_users(users.iter().map(Deref::deref)).await?;
|
||||
///
|
||||
/// // Process the rest of the sync response here.
|
||||
///
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// Now that we have discovered the devices of the users we'd like to
|
||||
/// communicate with in an end-to-end encrypted manner, we can start considering
|
||||
/// encrypting messages for those devices. This concludes the sync processing
|
||||
/// method, we are now ready to move on to the next section, which will explain
|
||||
/// how to begin the encryption process.
|
||||
///
|
||||
/// ## Establishing end-to-end encrypted channels
|
||||
///
|
||||
/// In the [Triple
|
||||
/// Diffie-Hellman](#using-the-triple-diffie-hellman-key-agreement-protocol)
|
||||
/// section, we described the need for two Curve25519 keys from the recipient
|
||||
/// device to establish a 1-to-1 secure channel: the long-term identity key of a
|
||||
/// device and a one-time prekey. In the previous section, we started tracking
|
||||
/// the device keys, including the long-term identity key that we need. The next
|
||||
/// step is to download the one-time prekey on an on-demand basis and establish
|
||||
/// the 1-to-1 secure channel.
|
||||
///
|
||||
/// To accomplish this, we can use the [`OlmMachine::get_missing_sessions()`]
|
||||
/// method in bulk, which will claim the one-time prekey for all the devices of
|
||||
/// a user that we're not already sharing a 1-to-1 encrypted channel with.
|
||||
///
|
||||
/// #### 🔒 Locking rule
|
||||
///
|
||||
/// As with the [`OlmMachine::outgoing_requests()`] method, it is necessary to
|
||||
/// protect this method with a lock, otherwise we will be creating more 1-to-1
|
||||
/// encrypted channels than necessary.
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use std::collections::{BTreeMap, HashSet};
|
||||
/// # use std::ops::Deref;
|
||||
/// # use anyhow::Result;
|
||||
/// # use ruma::UserId;
|
||||
/// # use ruma::api::client::keys::claim_keys::v3::{Response, Request};
|
||||
/// # use matrix_sdk_crypto::OlmMachine;
|
||||
/// # async fn send_request(request: &Request) -> Result<Response> {
|
||||
/// # let response = unimplemented!();
|
||||
/// # Ok(response)
|
||||
/// # }
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> Result<()> {
|
||||
/// # let users: HashSet<&UserId> = HashSet::new();
|
||||
/// # let machine: OlmMachine = unimplemented!();
|
||||
/// // Mark all the users that are part of an encrypted room as tracked
|
||||
/// if let Some((request_id, request)) =
|
||||
/// machine.get_missing_sessions(users.iter().map(Deref::deref)).await?
|
||||
/// {
|
||||
/// let response = send_request(&request).await?;
|
||||
/// machine.mark_request_as_sent(&request_id, &response).await?;
|
||||
/// }
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// With the ability to exchange messages directly with devices, we can now
|
||||
/// start sharing room keys over the 1-to-1 encrypted channel.
|
||||
///
|
||||
/// ## Exchanging room keys
|
||||
///
|
||||
/// To exchange a room key with our group, we will once again take a bulk
|
||||
/// approach. The [`OlmMachine::share_room_key()`] method is used to accomplish
|
||||
/// this step. This method will create a new room key, if necessary, and encrypt
|
||||
/// it for each device belonging to the users provided as an argument. It will
|
||||
/// then output an array of sendToDevice requests that we must send to the
|
||||
/// server, and mark the requests as sent.
|
||||
///
|
||||
/// #### 🔒 Locking rule
|
||||
///
|
||||
/// Like some of the previous methods, OlmMachine::share_room_key() needs to be
|
||||
/// protected by a lock to prevent the possibility of creating and sending
|
||||
/// multiple room keys simultaneously for the same group. The lock can be
|
||||
/// implemented on a per-room basis, which allows for parallel room key
|
||||
/// exchanges across different rooms.
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use std::collections::{BTreeMap, HashSet};
|
||||
/// # use std::ops::Deref;
|
||||
/// # use anyhow::Result;
|
||||
/// # use ruma::UserId;
|
||||
/// # use ruma::api::client::keys::claim_keys::v3::{Response, Request};
|
||||
/// # use matrix_sdk_crypto::{OlmMachine, requests::ToDeviceRequest, EncryptionSettings};
|
||||
/// # async fn send_request(request: &ToDeviceRequest) -> Result<Response> {
|
||||
/// # let response = unimplemented!();
|
||||
/// # Ok(response)
|
||||
/// # }
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> Result<()> {
|
||||
/// # let users: HashSet<&UserId> = HashSet::new();
|
||||
/// # let room_id = unimplemented!();
|
||||
/// # let settings = EncryptionSettings::default();
|
||||
/// # let machine: OlmMachine = unimplemented!();
|
||||
/// // Let's share a room key with our group.
|
||||
/// let requests = machine.share_room_key(
|
||||
/// room_id,
|
||||
/// users.iter().map(Deref::deref),
|
||||
/// EncryptionSettings::default(),
|
||||
/// ).await?;
|
||||
///
|
||||
/// // Make sure each request is sent out
|
||||
/// for request in requests {
|
||||
/// let request_id = &request.txn_id;
|
||||
/// let response = send_request(&request).await?;
|
||||
/// machine.mark_request_as_sent(&request_id, &response).await?;
|
||||
/// }
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// In order to ensure that room keys are rotated and exchanged when needed, the
|
||||
/// [`OlmMachine::share_room_key()`] method should be called before sending
|
||||
/// each room message in an end-to-end encrypted room. If a room key has
|
||||
/// already been exchanged, the method becomes a no-op.
|
||||
///
|
||||
/// ## Encrypting room events
|
||||
///
|
||||
/// After the room key has been successfully shared, a plaintext can be
|
||||
/// encrypted.
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use anyhow::Result;
|
||||
/// # use matrix_sdk_crypto::{DecryptionSettings, OlmMachine, TrustRequirement};
|
||||
/// # use ruma::events::{AnyMessageLikeEventContent, room::message::RoomMessageEventContent};
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> Result<()> {
|
||||
/// # let room_id = unimplemented!();
|
||||
/// # let event = unimplemented!();
|
||||
/// # let machine: OlmMachine = unimplemented!();
|
||||
/// # let settings = DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted };
|
||||
/// let content = AnyMessageLikeEventContent::RoomMessage(RoomMessageEventContent::text_plain("It's a secret to everybody."));
|
||||
/// let encrypted_content = machine.encrypt_room_event(room_id, content).await?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// ## Appendix: Combining the session creation and room key exchange
|
||||
///
|
||||
/// The steps from the previous three sections should combined into a single
|
||||
/// method that is used to send messages.
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use std::collections::{BTreeMap, HashSet};
|
||||
/// # use std::ops::Deref;
|
||||
/// # use anyhow::Result;
|
||||
/// # use serde_json::json;
|
||||
/// # use ruma::{UserId, RoomId, serde::Raw};
|
||||
/// # use ruma::api::client::keys::claim_keys::v3::{Response, Request};
|
||||
/// # use matrix_sdk_crypto::{EncryptionSettings, OlmMachine, ToDeviceRequest};
|
||||
/// # use tokio::sync::MutexGuard;
|
||||
/// # async fn send_request(request: &Request) -> Result<Response> {
|
||||
/// # let response = unimplemented!();
|
||||
/// # Ok(response)
|
||||
/// # }
|
||||
/// # async fn send_to_device_request(request: &ToDeviceRequest) -> Result<Response> {
|
||||
/// # let response = unimplemented!();
|
||||
/// # Ok(response)
|
||||
/// # }
|
||||
/// # async fn acquire_per_room_lock(room_id: &RoomId) -> MutexGuard<()> {
|
||||
/// # unimplemented!();
|
||||
/// # }
|
||||
/// # async fn get_joined_members(room_id: &RoomId) -> Vec<&UserId> {
|
||||
/// # unimplemented!();
|
||||
/// # }
|
||||
/// # fn is_room_encrypted(room_id: &RoomId) -> bool {
|
||||
/// # true
|
||||
/// # }
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> Result<()> {
|
||||
/// # let users: HashSet<&UserId> = HashSet::new();
|
||||
/// # let machine: OlmMachine = unimplemented!();
|
||||
/// struct Client {
|
||||
/// session_establishment_lock: tokio::sync::Mutex<()>,
|
||||
/// olm_machine: OlmMachine,
|
||||
/// }
|
||||
///
|
||||
/// async fn establish_sessions(client: &Client, users: &[&UserId]) -> Result<()> {
|
||||
/// if let Some((request_id, request)) =
|
||||
/// client.olm_machine.get_missing_sessions(users.iter().map(Deref::deref)).await?
|
||||
/// {
|
||||
/// let response = send_request(&request).await?;
|
||||
/// client.olm_machine.mark_request_as_sent(&request_id, &response).await?;
|
||||
/// }
|
||||
///
|
||||
/// Ok(())
|
||||
/// }
|
||||
///
|
||||
/// async fn share_room_key(machine: &OlmMachine, room_id: &RoomId, users: &[&UserId]) -> Result<()> {
|
||||
/// let _lock = acquire_per_room_lock(room_id).await;
|
||||
///
|
||||
/// let requests = machine.share_room_key(
|
||||
/// room_id,
|
||||
/// users.iter().map(Deref::deref),
|
||||
/// EncryptionSettings::default(),
|
||||
/// ).await?;
|
||||
///
|
||||
/// // Make sure each request is sent out
|
||||
/// for request in requests {
|
||||
/// let request_id = &request.txn_id;
|
||||
/// let response = send_to_device_request(&request).await?;
|
||||
/// machine.mark_request_as_sent(&request_id, &response).await?;
|
||||
/// }
|
||||
///
|
||||
/// Ok(())
|
||||
/// }
|
||||
///
|
||||
/// async fn send_message(client: &Client, room_id: &RoomId, message: &str) -> Result<()> {
|
||||
/// let mut content = json!({
|
||||
/// "body": message,
|
||||
/// "msgtype": "m.text",
|
||||
/// });
|
||||
///
|
||||
/// if is_room_encrypted(room_id) {
|
||||
/// let content = Raw::new(&json!({
|
||||
/// "body": message,
|
||||
/// "msgtype": "m.text",
|
||||
/// }))?.cast();
|
||||
///
|
||||
/// let users = get_joined_members(room_id).await;
|
||||
///
|
||||
/// establish_sessions(client, &users).await?;
|
||||
/// share_room_key(&client.olm_machine, room_id, &users).await?;
|
||||
///
|
||||
/// let encrypted = client
|
||||
/// .olm_machine
|
||||
/// .encrypt_room_event_raw(room_id, "m.room.message", &content)
|
||||
/// .await?;
|
||||
/// }
|
||||
///
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// TODO
|
||||
///
|
||||
/// [Matrix]: https://matrix.org/
|
||||
/// [Olm]: https://gitlab.matrix.org/matrix-org/olm/-/blob/master/docs/olm.md
|
||||
/// [Diffie-Hellman]: https://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange
|
||||
/// [Megolm]: https://gitlab.matrix.org/matrix-org/olm/blob/master/docs/megolm.md
|
||||
/// [end-to-end-encryption]: https://en.wikipedia.org/wiki/End-to-end_encryption
|
||||
/// [homeserver]: https://spec.matrix.org/unstable/#architecture
|
||||
/// [key-agreement protocol]: https://en.wikipedia.org/wiki/Key-agreement_protocol
|
||||
/// [client-server specification]: https://matrix.org/docs/spec/client_server/
|
||||
/// [forward secrecy]: https://en.wikipedia.org/wiki/Forward_secrecy
|
||||
/// [replay attacks]: https://en.wikipedia.org/wiki/Replay_attack
|
||||
/// [Tracking the device list for a user]: https://spec.matrix.org/unstable/client-server-api/#tracking-the-device-list-for-a-user
|
||||
/// [X3DH]: https://signal.org/docs/specifications/x3dh/
|
||||
/// [to-device]: https://spec.matrix.org/unstable/client-server-api/#send-to-device-messaging
|
||||
/// [sync]: https://spec.matrix.org/unstable/client-server-api/#get_matrixclientv3sync
|
||||
/// [events]: https://spec.matrix.org/unstable/client-server-api/#events
|
||||
///
|
||||
/// [1]: https://spec.matrix.org/unstable/client-server-api/#server-behaviour-4
|
||||
pub mod tutorial {}
|
||||
|
||||
@@ -2311,8 +2311,8 @@ impl OlmMachine {
|
||||
/// incremented and updated it in the database. Otherwise, `false`.
|
||||
///
|
||||
/// * The (possibly updated) generation counter.
|
||||
pub async fn maintain_crypto_store_generation<'a>(
|
||||
&'a self,
|
||||
pub async fn maintain_crypto_store_generation(
|
||||
&'_ self,
|
||||
generation: &Mutex<Option<u64>>,
|
||||
) -> StoreResult<(bool, u64)> {
|
||||
let mut gen_guard = generation.lock().await;
|
||||
|
||||
@@ -222,6 +222,8 @@ pub enum OutgoingRequests {
|
||||
|
||||
#[cfg(test)]
|
||||
impl OutgoingRequests {
|
||||
/// Test helper to destructure the [`OutgoingRequests`] as a
|
||||
/// [`ToDeviceRequest`].
|
||||
pub fn to_device(&self) -> Option<&ToDeviceRequest> {
|
||||
as_variant!(self, Self::ToDeviceRequest)
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ impl CryptoStoreWrapper {
|
||||
.await?
|
||||
.as_ref()
|
||||
.and_then(|i| i.own())
|
||||
.map_or(false, |own| own.is_verified());
|
||||
.is_some_and(|own| own.is_verified());
|
||||
|
||||
let secrets = changes.secrets.to_owned();
|
||||
let devices = changes.devices.to_owned();
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
/// cryptostore_integration_tests!();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
#[allow(unused_macros)]
|
||||
#[macro_export]
|
||||
macro_rules! cryptostore_integration_tests {
|
||||
|
||||
@@ -632,7 +632,7 @@ impl CryptoStore for MemoryStore {
|
||||
key: &str,
|
||||
holder: &str,
|
||||
) -> Result<bool> {
|
||||
Ok(try_take_leased_lock(&self.leases, lease_duration_ms, key, holder))
|
||||
Ok(try_take_leased_lock(&mut self.leases.write().unwrap(), lease_duration_ms, key, holder))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -231,7 +231,7 @@ pub(crate) struct SyncedKeyQueryManager<'a> {
|
||||
manager: &'a KeyQueryManager,
|
||||
}
|
||||
|
||||
impl<'a> SyncedKeyQueryManager<'a> {
|
||||
impl SyncedKeyQueryManager<'_> {
|
||||
/// Add entries to the list of users being tracked for device changes
|
||||
///
|
||||
/// Any users not already on the list are flagged as awaiting a key query.
|
||||
|
||||
@@ -138,7 +138,7 @@ pub(crate) enum CrossSigningSubKeys<'a> {
|
||||
UserSigning(&'a UserSigningPubkey),
|
||||
}
|
||||
|
||||
impl<'a> CrossSigningSubKeys<'a> {
|
||||
impl CrossSigningSubKeys<'_> {
|
||||
/// Get the id of the user that owns this cross signing subkey.
|
||||
pub fn user_id(&self) -> &UserId {
|
||||
match self {
|
||||
|
||||
@@ -184,6 +184,11 @@ where
|
||||
|
||||
impl<C: EventType + Debug + Sized + Serialize> DecryptedOlmV1Event<C> {
|
||||
#[cfg(test)]
|
||||
/// Test helper to create a new [`DecryptedOlmV1Event`] with the given
|
||||
/// content.
|
||||
///
|
||||
/// This should never be done in real code as we need to deserialize
|
||||
/// decrypted events.
|
||||
pub fn new(
|
||||
sender: &UserId,
|
||||
recipient: &UserId,
|
||||
|
||||
@@ -33,6 +33,19 @@ pub struct VerificationCache {
|
||||
inner: Arc<VerificationCacheInner>,
|
||||
}
|
||||
|
||||
// See https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823.
|
||||
#[cfg(not(feature = "test-send-sync"))]
|
||||
unsafe impl Sync for VerificationCache {}
|
||||
|
||||
#[cfg(feature = "test-send-sync")]
|
||||
#[test]
|
||||
// See https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823.
|
||||
fn test_send_sync_for_room() {
|
||||
fn assert_send_sync<T: Send + Sync>() {}
|
||||
|
||||
assert_send_sync::<VerificationCache>();
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct VerificationCacheInner {
|
||||
verification: StdRwLock<BTreeMap<OwnedUserId, BTreeMap<String, Verification>>>,
|
||||
|
||||
@@ -398,7 +398,7 @@ pub enum StartContent<'a> {
|
||||
Room(&'a KeyVerificationStartEventContent),
|
||||
}
|
||||
|
||||
impl<'a> StartContent<'a> {
|
||||
impl StartContent<'_> {
|
||||
#[allow(clippy::wrong_self_convention)]
|
||||
pub fn from_device(&self) -> &DeviceId {
|
||||
match self {
|
||||
@@ -458,7 +458,7 @@ impl<'a> From<&'a ToDeviceKeyVerificationDoneEventContent> for DoneContent<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> DoneContent<'a> {
|
||||
impl DoneContent<'_> {
|
||||
pub fn flow_id(&self) -> &str {
|
||||
match self {
|
||||
Self::ToDevice(c) => c.transaction_id.as_str(),
|
||||
|
||||
@@ -195,7 +195,7 @@ struct PendingStoreChanges<'a> {
|
||||
operations: &'a mut Vec<PendingOperation>,
|
||||
}
|
||||
|
||||
impl<'a> PendingStoreChanges<'a> {
|
||||
impl PendingStoreChanges<'_> {
|
||||
fn put(&mut self, key: JsValue, value: JsValue) {
|
||||
self.operations.push(PendingOperation::Put { key, value });
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//! Helpers for wasm32/browser environments
|
||||
|
||||
#![allow(dead_code)]
|
||||
use base64::{
|
||||
alphabet,
|
||||
@@ -15,8 +17,6 @@ use ruma::{
|
||||
use wasm_bindgen::JsValue;
|
||||
use web_sys::IdbKeyRange;
|
||||
|
||||
/// Helpers for wasm32/browser environments
|
||||
|
||||
/// ASCII Group Separator, for elements in the keys
|
||||
pub const KEY_SEPARATOR: &str = "\u{001D}";
|
||||
/// ASCII Record Separator is sure smaller than the Key Separator but smaller
|
||||
|
||||
@@ -1595,6 +1595,48 @@ impl_state_store!({
|
||||
}
|
||||
|
||||
async fn update_dependent_queued_request(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
own_transaction_id: &ChildTransactionId,
|
||||
new_content: DependentQueuedRequestKind,
|
||||
) -> Result<bool> {
|
||||
let encoded_key = self.encode_key(keys::DEPENDENT_SEND_QUEUE, room_id);
|
||||
|
||||
let tx = self.inner.transaction_on_one_with_mode(
|
||||
keys::DEPENDENT_SEND_QUEUE,
|
||||
IdbTransactionMode::Readwrite,
|
||||
)?;
|
||||
|
||||
let obj = tx.object_store(keys::DEPENDENT_SEND_QUEUE)?;
|
||||
|
||||
// We store an encoded vector of the dependent requests.
|
||||
// Reload the previous vector for this room, or create an empty one.
|
||||
let prev = obj.get(&encoded_key)?.await?;
|
||||
|
||||
let mut prev = prev.map_or_else(
|
||||
|| Ok(Vec::new()),
|
||||
|val| self.deserialize_value::<Vec<DependentQueuedRequest>>(&val),
|
||||
)?;
|
||||
|
||||
// Modify the dependent request, if found.
|
||||
let mut found = false;
|
||||
for entry in prev.iter_mut() {
|
||||
if entry.own_transaction_id == *own_transaction_id {
|
||||
found = true;
|
||||
entry.kind = new_content;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
obj.put_key_val(&encoded_key, &self.serialize_value(&prev)?)?;
|
||||
tx.await.into_result()?;
|
||||
}
|
||||
|
||||
Ok(found)
|
||||
}
|
||||
|
||||
async fn mark_dependent_queued_requests_as_ready(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
parent_txn_id: &TransactionId,
|
||||
|
||||
@@ -101,6 +101,9 @@ pub enum Error {
|
||||
|
||||
#[error("Redaction failed: {0}")]
|
||||
Redaction(#[source] ruma::canonical_json::RedactionError),
|
||||
|
||||
#[error("An update keyed by unique ID touched more than one entry")]
|
||||
InconsistentUpdate,
|
||||
}
|
||||
|
||||
macro_rules! impl_from {
|
||||
|
||||
@@ -3,11 +3,12 @@ use std::{borrow::Cow, fmt, path::Path, sync::Arc};
|
||||
use async_trait::async_trait;
|
||||
use deadpool_sqlite::{Object as SqliteAsyncConn, Pool as SqlitePool, Runtime};
|
||||
use matrix_sdk_base::{
|
||||
event_cache::store::EventCacheStore,
|
||||
event_cache::{store::EventCacheStore, Event, Gap},
|
||||
linked_chunk::Update,
|
||||
media::{MediaRequestParameters, UniqueKey},
|
||||
};
|
||||
use matrix_sdk_store_encryption::StoreCipher;
|
||||
use ruma::MilliSecondsSinceUnixEpoch;
|
||||
use ruma::{MilliSecondsSinceUnixEpoch, RoomId};
|
||||
use rusqlite::OptionalExtension;
|
||||
use tokio::fs;
|
||||
use tracing::debug;
|
||||
@@ -182,6 +183,14 @@ impl EventCacheStore for SqliteEventCacheStore {
|
||||
Ok(num_touched == 1)
|
||||
}
|
||||
|
||||
async fn handle_linked_chunk_updates(
|
||||
&self,
|
||||
_room_id: &RoomId,
|
||||
_updates: Vec<Update<Event, Gap>>,
|
||||
) -> Result<(), Self::Error> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn add_media_content(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
not(any(feature = "state-store", feature = "crypto-store", feature = "event-cache")),
|
||||
allow(dead_code, unused_imports)
|
||||
)]
|
||||
#![cfg_attr(test, allow(unexpected_cfgs))] // Triggered by the init_tracing_for_tests!() invocation.
|
||||
|
||||
#[cfg(feature = "crypto-store")]
|
||||
mod crypto_store;
|
||||
|
||||
@@ -509,6 +509,7 @@ trait SqliteConnectionStateStoreExt {
|
||||
fn remove_display_name(&self, room_id: &[u8], name: &[u8]) -> rusqlite::Result<()>;
|
||||
fn remove_room_display_names(&self, room_id: &[u8]) -> rusqlite::Result<()>;
|
||||
fn remove_room_send_queue(&self, room_id: &[u8]) -> rusqlite::Result<()>;
|
||||
fn remove_room_dependent_send_queue(&self, room_id: &[u8]) -> rusqlite::Result<()>;
|
||||
}
|
||||
|
||||
impl SqliteConnectionStateStoreExt for rusqlite::Connection {
|
||||
@@ -720,6 +721,12 @@ impl SqliteConnectionStateStoreExt for rusqlite::Connection {
|
||||
self.prepare("DELETE FROM send_queue_events WHERE room_id = ?")?.execute((room_id,))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_room_dependent_send_queue(&self, room_id: &[u8]) -> rusqlite::Result<()> {
|
||||
self.prepare("DELETE FROM dependent_send_queue_events WHERE room_id = ?")?
|
||||
.execute((room_id,))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -1726,6 +1733,10 @@ impl StateStore for SqliteStateStore {
|
||||
let send_queue_room_id = this.encode_key(keys::SEND_QUEUE, &room_id);
|
||||
txn.remove_room_send_queue(&send_queue_room_id)?;
|
||||
|
||||
let dependent_send_queue_room_id =
|
||||
this.encode_key(keys::DEPENDENTS_SEND_QUEUE, &room_id);
|
||||
txn.remove_room_dependent_send_queue(&dependent_send_queue_room_id)?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
@@ -1914,6 +1925,39 @@ impl StateStore for SqliteStateStore {
|
||||
}
|
||||
|
||||
async fn update_dependent_queued_request(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
own_transaction_id: &ChildTransactionId,
|
||||
new_content: DependentQueuedRequestKind,
|
||||
) -> Result<bool> {
|
||||
let room_id = self.encode_key(keys::DEPENDENTS_SEND_QUEUE, room_id);
|
||||
let content = self.serialize_json(&new_content)?;
|
||||
|
||||
// See comment in `save_send_queue_event`.
|
||||
let own_txn_id = own_transaction_id.to_string();
|
||||
|
||||
let num_updated = self
|
||||
.acquire()
|
||||
.await?
|
||||
.with_transaction(move |txn| {
|
||||
txn.prepare_cached(
|
||||
r#"UPDATE dependent_send_queue_events
|
||||
SET content = ?
|
||||
WHERE own_transaction_id = ?
|
||||
AND room_id = ?"#,
|
||||
)?
|
||||
.execute((content, own_txn_id, room_id))
|
||||
})
|
||||
.await?;
|
||||
|
||||
if num_updated > 1 {
|
||||
return Err(Error::InconsistentUpdate);
|
||||
}
|
||||
|
||||
Ok(num_updated == 1)
|
||||
}
|
||||
|
||||
async fn mark_dependent_queued_requests_as_ready(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
parent_txn_id: &TransactionId,
|
||||
|
||||
@@ -198,7 +198,7 @@ pub(crate) trait SqliteTransactionExt {
|
||||
Query: Fn(&Transaction<'_>, Vec<Key>) -> Result<Vec<Res>> + Send + 'static;
|
||||
}
|
||||
|
||||
impl<'a> SqliteTransactionExt for Transaction<'a> {
|
||||
impl SqliteTransactionExt for Transaction<'_> {
|
||||
fn chunk_large_query_over<Query, Res>(
|
||||
&self,
|
||||
mut keys_to_chunk: Vec<Key>,
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#![cfg_attr(test, allow(unexpected_cfgs))] // Triggered by the init_tracing_for_tests!() invocation.
|
||||
|
||||
use ruma::html::HtmlSanitizerMode;
|
||||
|
||||
mod events;
|
||||
|
||||
@@ -89,12 +89,15 @@ const DEFAULT_REQUIRED_STATE: &[(StateEventType, &str)] = &[
|
||||
(StateEventType::RoomPowerLevels, ""),
|
||||
(StateEventType::CallMember, "*"),
|
||||
(StateEventType::RoomJoinRules, ""),
|
||||
// Those two events are required to properly compute room previews.
|
||||
(StateEventType::RoomCreate, ""),
|
||||
(StateEventType::RoomHistoryVisibility, ""),
|
||||
];
|
||||
|
||||
/// The default `required_state` constant value for sliding sync room
|
||||
/// subscriptions that must be added to `DEFAULT_REQUIRED_STATE`.
|
||||
const DEFAULT_ROOM_SUBSCRIPTION_EXTRA_REQUIRED_STATE: &[(StateEventType, &str)] =
|
||||
&[(StateEventType::RoomCreate, ""), (StateEventType::RoomPinnedEvents, "")];
|
||||
&[(StateEventType::RoomPinnedEvents, "")];
|
||||
|
||||
/// The default `timeline_limit` value when used with room subscriptions.
|
||||
const DEFAULT_ROOM_SUBSCRIPTION_TIMELINE_LIMIT: u32 = 20;
|
||||
|
||||
@@ -31,7 +31,7 @@ use super::{
|
||||
Error, Timeline, TimelineDropHandle, TimelineFocus,
|
||||
};
|
||||
use crate::{
|
||||
timeline::{controller::TimelineEnd, event_item::RemoteEventOrigin},
|
||||
timeline::{controller::TimelineNewItemPosition, event_item::RemoteEventOrigin},
|
||||
unable_to_decrypt_hook::UtdHookManager,
|
||||
};
|
||||
|
||||
@@ -273,9 +273,9 @@ impl TimelineBuilder {
|
||||
|
||||
inner.add_events_at(
|
||||
events,
|
||||
TimelineEnd::Back,
|
||||
match origin {
|
||||
EventsOrigin::Sync => RemoteEventOrigin::Sync,
|
||||
TimelineNewItemPosition::End { origin: match origin {
|
||||
EventsOrigin::Sync => RemoteEventOrigin::Sync,
|
||||
}
|
||||
}
|
||||
).await;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ use imbl::Vector;
|
||||
#[cfg(test)]
|
||||
use matrix_sdk::crypto::OlmMachine;
|
||||
use matrix_sdk::{
|
||||
deserialized_responses::SyncTimelineEvent,
|
||||
deserialized_responses::{SyncTimelineEvent, TimelineEventKind as SdkTimelineEventKind},
|
||||
event_cache::{paginator::Paginator, RoomEventCache},
|
||||
send_queue::{
|
||||
LocalEcho, LocalEchoContent, RoomSendQueueUpdate, SendHandle, SendReactionHandle,
|
||||
@@ -52,8 +52,8 @@ use tracing::{
|
||||
};
|
||||
|
||||
pub(super) use self::state::{
|
||||
EventMeta, FullEventMeta, PendingEdit, PendingEditKind, TimelineEnd, TimelineMetadata,
|
||||
TimelineState, TimelineStateTransaction,
|
||||
EventMeta, FullEventMeta, PendingEdit, PendingEditKind, TimelineMetadata,
|
||||
TimelineNewItemPosition, TimelineState, TimelineStateTransaction,
|
||||
};
|
||||
use super::{
|
||||
event_handler::TimelineEventKind,
|
||||
@@ -404,8 +404,11 @@ impl<P: RoomDataProvider> TimelineController<P> {
|
||||
.map_err(PaginationError::Paginator)?,
|
||||
};
|
||||
|
||||
self.add_events_at(pagination.events, TimelineEnd::Front, RemoteEventOrigin::Pagination)
|
||||
.await;
|
||||
self.add_events_at(
|
||||
pagination.events,
|
||||
TimelineNewItemPosition::Start { origin: RemoteEventOrigin::Pagination },
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(pagination.hit_end_of_timeline)
|
||||
}
|
||||
@@ -428,8 +431,11 @@ impl<P: RoomDataProvider> TimelineController<P> {
|
||||
.map_err(PaginationError::Paginator)?,
|
||||
};
|
||||
|
||||
self.add_events_at(pagination.events, TimelineEnd::Back, RemoteEventOrigin::Pagination)
|
||||
.await;
|
||||
self.add_events_at(
|
||||
pagination.events,
|
||||
TimelineNewItemPosition::End { origin: RemoteEventOrigin::Pagination },
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(pagination.hit_end_of_timeline)
|
||||
}
|
||||
@@ -505,7 +511,7 @@ impl<P: RoomDataProvider> TimelineController<P> {
|
||||
let Some(prev_status) = prev_status else {
|
||||
match &item.kind {
|
||||
EventTimelineItemKind::Local(local) => {
|
||||
if let Some(send_handle) = local.send_handle.clone() {
|
||||
if let Some(send_handle) = &local.send_handle {
|
||||
if send_handle
|
||||
.react(key.to_owned())
|
||||
.await
|
||||
@@ -629,23 +635,14 @@ impl<P: RoomDataProvider> TimelineController<P> {
|
||||
pub(super) async fn add_events_at(
|
||||
&self,
|
||||
events: Vec<impl Into<SyncTimelineEvent>>,
|
||||
position: TimelineEnd,
|
||||
origin: RemoteEventOrigin,
|
||||
position: TimelineNewItemPosition,
|
||||
) -> HandleManyEventsResult {
|
||||
if events.is_empty() {
|
||||
return Default::default();
|
||||
}
|
||||
|
||||
let mut state = self.state.write().await;
|
||||
state
|
||||
.add_remote_events_at(
|
||||
events,
|
||||
position,
|
||||
origin,
|
||||
&self.room_data_provider,
|
||||
&self.settings,
|
||||
)
|
||||
.await
|
||||
state.add_remote_events_at(events, position, &self.room_data_provider, &self.settings).await
|
||||
}
|
||||
|
||||
pub(super) async fn clear(&self) {
|
||||
@@ -683,8 +680,7 @@ impl<P: RoomDataProvider> TimelineController<P> {
|
||||
state
|
||||
.replace_with_remote_events(
|
||||
events,
|
||||
TimelineEnd::Back,
|
||||
origin,
|
||||
TimelineNewItemPosition::End { origin },
|
||||
&self.room_data_provider,
|
||||
&self.settings,
|
||||
)
|
||||
@@ -1064,16 +1060,22 @@ impl<P: RoomDataProvider> TimelineController<P> {
|
||||
|
||||
match decryptor.decrypt_event_impl(original_json).await {
|
||||
Ok(event) => {
|
||||
trace!(
|
||||
"Successfully decrypted event that previously failed to decrypt"
|
||||
);
|
||||
if let SdkTimelineEventKind::UnableToDecrypt { utd_info, .. } =
|
||||
event.kind
|
||||
{
|
||||
info!(
|
||||
"Failed to decrypt event after receiving room key: {:?}",
|
||||
utd_info.reason
|
||||
);
|
||||
None
|
||||
} else {
|
||||
// Notify observers that we managed to eventually decrypt an event.
|
||||
if let Some(hook) = unable_to_decrypt_hook {
|
||||
hook.on_late_decrypt(&remote_event.event_id, *utd_cause).await;
|
||||
}
|
||||
|
||||
// Notify observers that we managed to eventually decrypt an event.
|
||||
if let Some(hook) = unable_to_decrypt_hook {
|
||||
hook.on_late_decrypt(&remote_event.event_id, *utd_cause).await;
|
||||
Some(event)
|
||||
}
|
||||
|
||||
Some(event)
|
||||
}
|
||||
Err(e) => {
|
||||
info!("Failed to decrypt event after receiving room key: {e}");
|
||||
|
||||
@@ -64,16 +64,27 @@ use crate::{
|
||||
unable_to_decrypt_hook::UtdHookManager,
|
||||
};
|
||||
|
||||
/// Which end of the timeline should an event be added to?
|
||||
///
|
||||
/// This is a simplification of `TimelineItemPosition` which doesn't contain the
|
||||
/// `Update` variant, when adding a bunch of events at the same time.
|
||||
/// This is a simplification of [`TimelineItemPosition`] which doesn't contain
|
||||
/// the [`TimelineItemPosition::UpdateDecrypted`] variant, because it is used
|
||||
/// only for **new** items.
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum TimelineEnd {
|
||||
/// Event should be prepended to the front of the timeline.
|
||||
Front,
|
||||
/// Event should be appended to the back of the timeline.
|
||||
Back,
|
||||
pub(crate) enum TimelineNewItemPosition {
|
||||
/// One or more items are prepended to the timeline (i.e. they're the
|
||||
/// oldest).
|
||||
Start { origin: RemoteEventOrigin },
|
||||
|
||||
/// One or more items are appended to the timeline (i.e. they're the most
|
||||
/// recent).
|
||||
End { origin: RemoteEventOrigin },
|
||||
}
|
||||
|
||||
impl From<TimelineNewItemPosition> for TimelineItemPosition {
|
||||
fn from(value: TimelineNewItemPosition) -> Self {
|
||||
match value {
|
||||
TimelineNewItemPosition::Start { origin } => Self::Start { origin },
|
||||
TimelineNewItemPosition::End { origin } => Self::End { origin },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -119,8 +130,7 @@ impl TimelineState {
|
||||
pub(super) async fn add_remote_events_at<P: RoomDataProvider>(
|
||||
&mut self,
|
||||
events: Vec<impl Into<SyncTimelineEvent>>,
|
||||
position: TimelineEnd,
|
||||
origin: RemoteEventOrigin,
|
||||
position: TimelineNewItemPosition,
|
||||
room_data_provider: &P,
|
||||
settings: &TimelineSettings,
|
||||
) -> HandleManyEventsResult {
|
||||
@@ -130,7 +140,7 @@ impl TimelineState {
|
||||
|
||||
let mut txn = self.transaction();
|
||||
let handle_many_res =
|
||||
txn.add_remote_events_at(events, position, origin, room_data_provider, settings).await;
|
||||
txn.add_remote_events_at(events, position, room_data_provider, settings).await;
|
||||
txn.commit();
|
||||
|
||||
handle_many_res
|
||||
@@ -241,7 +251,7 @@ impl TimelineState {
|
||||
let handle_one_res = txn
|
||||
.handle_remote_event(
|
||||
event.into(),
|
||||
TimelineItemPosition::UpdateDecrypted(idx),
|
||||
TimelineItemPosition::UpdateDecrypted { timeline_item_index: idx },
|
||||
room_data_provider,
|
||||
settings,
|
||||
&mut day_divider_adjuster,
|
||||
@@ -285,15 +295,13 @@ impl TimelineState {
|
||||
pub(super) async fn replace_with_remote_events<P: RoomDataProvider>(
|
||||
&mut self,
|
||||
events: Vec<SyncTimelineEvent>,
|
||||
position: TimelineEnd,
|
||||
origin: RemoteEventOrigin,
|
||||
position: TimelineNewItemPosition,
|
||||
room_data_provider: &P,
|
||||
settings: &TimelineSettings,
|
||||
) -> HandleManyEventsResult {
|
||||
let mut txn = self.transaction();
|
||||
txn.clear();
|
||||
let result =
|
||||
txn.add_remote_events_at(events, position, origin, room_data_provider, settings).await;
|
||||
let result = txn.add_remote_events_at(events, position, room_data_provider, settings).await;
|
||||
txn.commit();
|
||||
result
|
||||
}
|
||||
@@ -347,17 +355,13 @@ impl TimelineStateTransaction<'_> {
|
||||
pub(super) async fn add_remote_events_at<P: RoomDataProvider>(
|
||||
&mut self,
|
||||
events: Vec<impl Into<SyncTimelineEvent>>,
|
||||
position: TimelineEnd,
|
||||
origin: RemoteEventOrigin,
|
||||
position: TimelineNewItemPosition,
|
||||
room_data_provider: &P,
|
||||
settings: &TimelineSettings,
|
||||
) -> HandleManyEventsResult {
|
||||
let mut total = HandleManyEventsResult::default();
|
||||
|
||||
let position = match position {
|
||||
TimelineEnd::Front => TimelineItemPosition::Start { origin },
|
||||
TimelineEnd::Back => TimelineItemPosition::End { origin },
|
||||
};
|
||||
let position = position.into();
|
||||
|
||||
let mut day_divider_adjuster = DayDividerAdjuster::default();
|
||||
|
||||
@@ -447,7 +451,7 @@ impl TimelineStateTransaction<'_> {
|
||||
TimelineItemPosition::End { origin }
|
||||
| TimelineItemPosition::Start { origin } => origin,
|
||||
|
||||
TimelineItemPosition::UpdateDecrypted(idx) => self
|
||||
TimelineItemPosition::UpdateDecrypted { timeline_item_index: idx } => self
|
||||
.items
|
||||
.get(idx)
|
||||
.and_then(|item| item.as_event())
|
||||
@@ -703,7 +707,7 @@ impl TimelineStateTransaction<'_> {
|
||||
self.meta.all_events.push_back(event_meta.base_meta());
|
||||
}
|
||||
|
||||
TimelineItemPosition::UpdateDecrypted(_) => {
|
||||
TimelineItemPosition::UpdateDecrypted { .. } => {
|
||||
if let Some(event) =
|
||||
self.meta.all_events.iter_mut().find(|e| e.event_id == event_meta.event_id)
|
||||
{
|
||||
@@ -1027,7 +1031,7 @@ impl TimelineMetadata {
|
||||
.skip(*i + 1)
|
||||
// …that's not virtual and not sent by us…
|
||||
.find(|(_, item)| {
|
||||
item.as_event().map_or(false, |event| event.sender() != self.own_user_id)
|
||||
item.as_event().is_some_and(|event| event.sender() != self.own_user_id)
|
||||
})
|
||||
.map(|(i, _)| i);
|
||||
|
||||
@@ -1113,7 +1117,7 @@ pub(crate) struct FullEventMeta<'a> {
|
||||
pub timestamp: Option<MilliSecondsSinceUnixEpoch>,
|
||||
}
|
||||
|
||||
impl<'a> FullEventMeta<'a> {
|
||||
impl FullEventMeta<'_> {
|
||||
fn base_meta(&self) -> EventMeta {
|
||||
EventMeta { event_id: self.event_id.to_owned(), visible: self.visible }
|
||||
}
|
||||
|
||||
@@ -517,7 +517,7 @@ struct DayDividerInvariantsReport<'a, 'o> {
|
||||
errors: Vec<DayDividerInsertError>,
|
||||
}
|
||||
|
||||
impl<'a, 'o> Display for DayDividerInvariantsReport<'a, 'o> {
|
||||
impl Display for DayDividerInvariantsReport<'_, '_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
// Write all the items of a slice of timeline items.
|
||||
fn write_items(
|
||||
|
||||
@@ -54,15 +54,15 @@ pub enum Error {
|
||||
UnknownEncryptionState,
|
||||
|
||||
/// Something went wrong with the room event cache.
|
||||
#[error("Something went wrong with the room event cache.")]
|
||||
#[error(transparent)]
|
||||
EventCacheError(#[from] EventCacheError),
|
||||
|
||||
/// An error happened during pagination.
|
||||
#[error("An error happened during pagination.")]
|
||||
#[error(transparent)]
|
||||
PaginationError(#[from] PaginationError),
|
||||
|
||||
/// An error happened during pagination.
|
||||
#[error("An error happened when loading pinned events.")]
|
||||
#[error(transparent)]
|
||||
PinnedEventsError(#[from] PinnedEventsLoaderError),
|
||||
|
||||
/// An error happened while operating the room's send queue.
|
||||
|
||||
@@ -269,17 +269,26 @@ impl TimelineEventKind {
|
||||
pub(super) enum TimelineItemPosition {
|
||||
/// One or more items are prepended to the timeline (i.e. they're the
|
||||
/// oldest).
|
||||
Start { origin: RemoteEventOrigin },
|
||||
Start {
|
||||
/// The origin of the new item(s).
|
||||
origin: RemoteEventOrigin,
|
||||
},
|
||||
|
||||
/// One or more items are appended to the timeline (i.e. they're the most
|
||||
/// recent).
|
||||
End { origin: RemoteEventOrigin },
|
||||
End {
|
||||
/// The origin of the new item(s).
|
||||
origin: RemoteEventOrigin,
|
||||
},
|
||||
|
||||
/// A single item is updated, after it's been successfully decrypted.
|
||||
///
|
||||
/// This happens when an item that was a UTD must be replaced with the
|
||||
/// decrypted event.
|
||||
UpdateDecrypted(usize),
|
||||
UpdateDecrypted {
|
||||
/// The index of the **timeline item**.
|
||||
timeline_item_index: usize,
|
||||
},
|
||||
}
|
||||
|
||||
/// The outcome of handling a single event with
|
||||
@@ -481,8 +490,10 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> {
|
||||
if !self.result.item_added {
|
||||
trace!("No new item added");
|
||||
|
||||
if let Flow::Remote { position: TimelineItemPosition::UpdateDecrypted(idx), .. } =
|
||||
self.ctx.flow
|
||||
if let Flow::Remote {
|
||||
position: TimelineItemPosition::UpdateDecrypted { timeline_item_index: idx },
|
||||
..
|
||||
} = self.ctx.flow
|
||||
{
|
||||
// If add was not called, that means the UTD event is one that
|
||||
// wouldn't normally be visible. Remove it.
|
||||
@@ -576,7 +587,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> {
|
||||
replacement: PendingEdit,
|
||||
) {
|
||||
match position {
|
||||
TimelineItemPosition::Start { .. } | TimelineItemPosition::UpdateDecrypted(_) => {
|
||||
TimelineItemPosition::Start { .. } | TimelineItemPosition::UpdateDecrypted { .. } => {
|
||||
// Only insert the edit if there wasn't any other edit
|
||||
// before.
|
||||
//
|
||||
@@ -1012,7 +1023,8 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> {
|
||||
| TimelineItemPosition::End { origin } => origin,
|
||||
|
||||
// For updates, reuse the origin of the encrypted event.
|
||||
TimelineItemPosition::UpdateDecrypted(idx) => self.items[idx]
|
||||
TimelineItemPosition::UpdateDecrypted { timeline_item_index: idx } => self
|
||||
.items[idx]
|
||||
.as_event()
|
||||
.and_then(|ev| Some(ev.as_remote()?.origin))
|
||||
.unwrap_or_else(|| {
|
||||
@@ -1162,7 +1174,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> {
|
||||
|
||||
Flow::Remote {
|
||||
event_id: decrypted_event_id,
|
||||
position: TimelineItemPosition::UpdateDecrypted(idx),
|
||||
position: TimelineItemPosition::UpdateDecrypted { timeline_item_index: idx },
|
||||
..
|
||||
} => {
|
||||
trace!("Updating timeline item at position {idx}");
|
||||
|
||||
@@ -368,7 +368,15 @@ impl EventTimelineItem {
|
||||
|
||||
match self.content() {
|
||||
TimelineItemContent::Message(message) => {
|
||||
matches!(message.msgtype(), MessageType::Text(_) | MessageType::Emote(_))
|
||||
matches!(
|
||||
message.msgtype(),
|
||||
MessageType::Text(_)
|
||||
| MessageType::Emote(_)
|
||||
| MessageType::Audio(_)
|
||||
| MessageType::File(_)
|
||||
| MessageType::Image(_)
|
||||
| MessageType::Video(_)
|
||||
)
|
||||
}
|
||||
TimelineItemContent::Poll(poll) => {
|
||||
poll.response_data.is_empty() && poll.end_event_timestamp.is_none()
|
||||
@@ -726,12 +734,14 @@ impl ReactionsByKeyBySender {
|
||||
mod tests {
|
||||
use assert_matches::assert_matches;
|
||||
use assert_matches2::assert_let;
|
||||
use matrix_sdk::test_utils::{events::EventFactory, logged_in_client};
|
||||
use matrix_sdk::test_utils::logged_in_client;
|
||||
use matrix_sdk_base::{
|
||||
deserialized_responses::SyncTimelineEvent, latest_event::LatestEvent, sliding_sync::http,
|
||||
MinimalStateEvent, OriginalMinimalStateEvent,
|
||||
};
|
||||
use matrix_sdk_test::{async_test, sync_state_event, sync_timeline_event};
|
||||
use matrix_sdk_test::{
|
||||
async_test, event_factory::EventFactory, sync_state_event, sync_timeline_event,
|
||||
};
|
||||
use ruma::{
|
||||
event_id,
|
||||
events::{
|
||||
|
||||
@@ -461,6 +461,7 @@ impl Timeline {
|
||||
.into());
|
||||
}
|
||||
}
|
||||
|
||||
EditedContent::PollStart { new_content, .. } => {
|
||||
if matches!(item.content, TimelineItemContent::Poll(_)) {
|
||||
AnyMessageLikeEventContent::UnstablePollStart(
|
||||
@@ -476,6 +477,17 @@ impl Timeline {
|
||||
.into());
|
||||
}
|
||||
}
|
||||
|
||||
EditedContent::MediaCaption { caption, formatted_caption } => {
|
||||
if handle
|
||||
.edit_media_caption(caption, formatted_caption)
|
||||
.await
|
||||
.map_err(RoomSendQueueError::StorageError)?
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
return Err(EditError::InvalidLocalEchoState.into());
|
||||
}
|
||||
};
|
||||
|
||||
if !handle.edit(new_content).await.map_err(RoomSendQueueError::StorageError)? {
|
||||
|
||||
@@ -26,7 +26,7 @@ use matrix_sdk::event_cache::{
|
||||
use tracing::{instrument, trace, warn};
|
||||
|
||||
use super::Error;
|
||||
use crate::timeline::{controller::TimelineEnd, event_item::RemoteEventOrigin};
|
||||
use crate::timeline::{controller::TimelineNewItemPosition, event_item::RemoteEventOrigin};
|
||||
|
||||
impl super::Timeline {
|
||||
/// Add more events to the start of the timeline.
|
||||
@@ -81,7 +81,7 @@ impl super::Timeline {
|
||||
// `matrix_sdk::event_cache::RoomEventCacheUpdate` from
|
||||
// `matrix_sdk::event_cache::RoomPagination::run_backwards`.
|
||||
self.controller
|
||||
.add_events_at(events, TimelineEnd::Front, RemoteEventOrigin::Pagination)
|
||||
.add_events_at(events, TimelineNewItemPosition::Start { origin: RemoteEventOrigin::Pagination })
|
||||
.await;
|
||||
|
||||
if num_events == 0 && !reached_start {
|
||||
|
||||
@@ -35,7 +35,7 @@ use stream_assert::assert_next_matches;
|
||||
|
||||
use super::TestTimeline;
|
||||
use crate::timeline::{
|
||||
controller::{TimelineEnd, TimelineSettings},
|
||||
controller::{TimelineNewItemPosition, TimelineSettings},
|
||||
event_item::{AnyOtherFullStateEventContent, RemoteEventOrigin},
|
||||
tests::{ReadReceiptMap, TestRoomDataProvider},
|
||||
MembershipChange, TimelineDetails, TimelineItemContent, TimelineItemKind, VirtualTimelineItem,
|
||||
@@ -51,8 +51,7 @@ async fn test_initial_events() {
|
||||
.controller
|
||||
.add_events_at(
|
||||
vec![f.text_msg("A").sender(*ALICE), f.text_msg("B").sender(*BOB)],
|
||||
TimelineEnd::Back,
|
||||
RemoteEventOrigin::Sync,
|
||||
TimelineNewItemPosition::End { origin: RemoteEventOrigin::Sync },
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -91,7 +90,10 @@ async fn test_replace_with_initial_events_and_read_marker() {
|
||||
let f = &timeline.factory;
|
||||
let ev = f.text_msg("hey").sender(*ALICE).into_sync();
|
||||
|
||||
timeline.controller.add_events_at(vec![ev], TimelineEnd::Back, RemoteEventOrigin::Sync).await;
|
||||
timeline
|
||||
.controller
|
||||
.add_events_at(vec![ev], TimelineNewItemPosition::End { origin: RemoteEventOrigin::Sync })
|
||||
.await;
|
||||
|
||||
let items = timeline.controller.items().await;
|
||||
assert_eq!(items.len(), 2);
|
||||
@@ -317,8 +319,7 @@ async fn test_dedup_initial() {
|
||||
// … and a new event also came in
|
||||
event_c,
|
||||
],
|
||||
TimelineEnd::Back,
|
||||
RemoteEventOrigin::Sync,
|
||||
TimelineNewItemPosition::End { origin: RemoteEventOrigin::Sync },
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -354,7 +355,10 @@ async fn test_internal_id_prefix() {
|
||||
|
||||
timeline
|
||||
.controller
|
||||
.add_events_at(vec![ev_a, ev_b, ev_c], TimelineEnd::Back, RemoteEventOrigin::Sync)
|
||||
.add_events_at(
|
||||
vec![ev_a, ev_b, ev_c],
|
||||
TimelineNewItemPosition::End { origin: RemoteEventOrigin::Sync },
|
||||
)
|
||||
.await;
|
||||
|
||||
let timeline_items = timeline.controller.items().await;
|
||||
@@ -516,7 +520,10 @@ async fn test_replace_with_initial_events_when_batched() {
|
||||
let f = &timeline.factory;
|
||||
let ev = f.text_msg("hey").sender(*ALICE).into_sync();
|
||||
|
||||
timeline.controller.add_events_at(vec![ev], TimelineEnd::Back, RemoteEventOrigin::Sync).await;
|
||||
timeline
|
||||
.controller
|
||||
.add_events_at(vec![ev], TimelineNewItemPosition::End { origin: RemoteEventOrigin::Sync })
|
||||
.await;
|
||||
|
||||
let (items, mut stream) = timeline.controller.subscribe_batched().await;
|
||||
assert_eq!(items.len(), 2);
|
||||
|
||||
@@ -16,12 +16,9 @@ use std::sync::Arc;
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
use eyeball_im::VectorDiff;
|
||||
use matrix_sdk::{
|
||||
assert_next_matches_with_timeout, send_queue::RoomSendQueueUpdate,
|
||||
test_utils::events::EventFactory,
|
||||
};
|
||||
use matrix_sdk::{assert_next_matches_with_timeout, send_queue::RoomSendQueueUpdate};
|
||||
use matrix_sdk_base::store::QueueWedgeError;
|
||||
use matrix_sdk_test::{async_test, ALICE, BOB};
|
||||
use matrix_sdk_test::{async_test, event_factory::EventFactory, ALICE, BOB};
|
||||
use ruma::{
|
||||
event_id,
|
||||
events::{room::message::RoomMessageEventContent, AnyMessageLikeEventContent},
|
||||
|
||||
@@ -18,6 +18,7 @@ use std::{
|
||||
io::Cursor,
|
||||
iter,
|
||||
sync::{Arc, Mutex},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use as_variant::as_variant;
|
||||
@@ -43,6 +44,7 @@ use ruma::{
|
||||
};
|
||||
use serde_json::{json, value::to_raw_value};
|
||||
use stream_assert::assert_next_matches;
|
||||
use tokio::time::sleep;
|
||||
|
||||
use super::TestTimeline;
|
||||
use crate::{
|
||||
@@ -50,6 +52,17 @@ use crate::{
|
||||
unable_to_decrypt_hook::{UnableToDecryptHook, UnableToDecryptInfo, UtdHookManager},
|
||||
};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct DummyUtdHook {
|
||||
utds: Mutex<Vec<UnableToDecryptInfo>>,
|
||||
}
|
||||
|
||||
impl UnableToDecryptHook for DummyUtdHook {
|
||||
fn on_utd(&self, info: UnableToDecryptInfo) {
|
||||
self.utds.lock().unwrap().push(info);
|
||||
}
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_retry_message_decryption() {
|
||||
const SESSION_ID: &str = "gM8i47Xhu0q52xLfgUXzanCMpLinoyVyH7R58cBuVBU";
|
||||
@@ -67,17 +80,6 @@ async fn test_retry_message_decryption() {
|
||||
HztoSJUr/2Y\n\
|
||||
-----END MEGOLM SESSION DATA-----";
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct DummyUtdHook {
|
||||
utds: Mutex<Vec<UnableToDecryptInfo>>,
|
||||
}
|
||||
|
||||
impl UnableToDecryptHook for DummyUtdHook {
|
||||
fn on_utd(&self, info: UnableToDecryptInfo) {
|
||||
self.utds.lock().unwrap().push(info);
|
||||
}
|
||||
}
|
||||
|
||||
let hook = Arc::new(DummyUtdHook::default());
|
||||
let client = test_client_builder(None).build().await.unwrap();
|
||||
let utd_hook = Arc::new(UtdHookManager::new(hook.clone(), client));
|
||||
@@ -170,6 +172,73 @@ async fn test_retry_message_decryption() {
|
||||
}
|
||||
}
|
||||
|
||||
// There has been a regression when the `retry_event_decryption` function
|
||||
// changed from failing with an Error to instead return a new type of timeline
|
||||
// event in UTD. The regression caused the timeline to consider any
|
||||
// re-decryption attempt as successful.
|
||||
#[async_test]
|
||||
async fn test_false_positive_late_decryption_regression() {
|
||||
const SESSION_ID: &str = "gM8i47Xhu0q52xLfgUXzanCMpLinoyVyH7R58cBuVBU";
|
||||
|
||||
let hook = Arc::new(DummyUtdHook::default());
|
||||
let client = test_client_builder(None).build().await.unwrap();
|
||||
let utd_hook =
|
||||
Arc::new(UtdHookManager::new(hook.clone(), client).with_max_delay(Duration::from_secs(1)));
|
||||
|
||||
let timeline = TestTimeline::with_unable_to_decrypt_hook(utd_hook.clone());
|
||||
|
||||
let f = &timeline.factory;
|
||||
timeline
|
||||
.handle_live_event(
|
||||
f.event(RoomEncryptedEventContent::new(
|
||||
EncryptedEventScheme::MegolmV1AesSha2(
|
||||
MegolmV1AesSha2ContentInit {
|
||||
ciphertext: "\
|
||||
AwgAEtABPRMavuZMDJrPo6pGQP4qVmpcuapuXtzKXJyi3YpEsjSWdzuRKIgJzD4P\
|
||||
cSqJM1A8kzxecTQNJsC5q22+KSFEPxPnI4ltpm7GFowSoPSW9+bFdnlfUzEP1jPq\
|
||||
YevHAsMJp2fRKkzQQbPordrUk1gNqEpGl4BYFeRqKl9GPdKFwy45huvQCLNNueql\
|
||||
CFZVoYMuhxrfyMiJJAVNTofkr2um2mKjDTlajHtr39pTG8k0eOjSXkLOSdZvNOMz\
|
||||
hGhSaFNeERSA2G2YbeknOvU7MvjiO0AKuxaAe1CaVhAI14FCgzrJ8g0y5nly+n7x\
|
||||
QzL2G2Dn8EoXM5Iqj8W99iokQoVsSrUEnaQ1WnSIfewvDDt4LCaD/w7PGETMCQ"
|
||||
.to_owned(),
|
||||
sender_key: "DeHIg4gwhClxzFYcmNntPNF9YtsdZbmMy8+3kzCMXHA".to_owned(),
|
||||
device_id: "NLAZCWIOCO".into(),
|
||||
session_id: SESSION_ID.into(),
|
||||
}
|
||||
.into(),
|
||||
),
|
||||
None,
|
||||
))
|
||||
.sender(&BOB)
|
||||
.into_utd_sync_timeline_event(),
|
||||
)
|
||||
.await;
|
||||
|
||||
let own_user_id = user_id!("@example:morheus.localhost");
|
||||
let olm_machine = OlmMachine::new(own_user_id, "SomeDeviceId".into()).await;
|
||||
|
||||
timeline
|
||||
.controller
|
||||
.retry_event_decryption_test(
|
||||
room_id!("!DovneieKSTkdHKpIXy:morpheus.localhost"),
|
||||
olm_machine,
|
||||
Some(iter::once(SESSION_ID.to_owned()).collect()),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(timeline.controller.items().await.len(), 2);
|
||||
|
||||
// Wait past the max delay for utd late decryption detection
|
||||
sleep(Duration::from_secs(2)).await;
|
||||
|
||||
{
|
||||
let utds = hook.utds.lock().unwrap();
|
||||
assert_eq!(utds.len(), 1);
|
||||
// This is the main thing we're testing: if this wasn't identified as a definite
|
||||
// UTD, this would be `Some(..)`.
|
||||
assert!(utds[0].time_to_decrypt.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_retry_edit_decryption() {
|
||||
const SESSION1_KEY: &[u8] = b"\
|
||||
|
||||
@@ -31,11 +31,12 @@ use matrix_sdk::{
|
||||
event_cache::paginator::{PaginableRoom, PaginatorError},
|
||||
room::{EventWithContextResponse, Messages, MessagesOptions},
|
||||
send_queue::RoomSendQueueUpdate,
|
||||
test_utils::events::EventFactory,
|
||||
BoxFuture,
|
||||
};
|
||||
use matrix_sdk_base::{latest_event::LatestEvent, RoomInfo, RoomState};
|
||||
use matrix_sdk_test::{EventBuilder, ALICE, BOB, DEFAULT_TEST_ROOM_ID};
|
||||
use matrix_sdk_test::{
|
||||
event_factory::EventFactory, EventBuilder, ALICE, BOB, DEFAULT_TEST_ROOM_ID,
|
||||
};
|
||||
use ruma::{
|
||||
event_id,
|
||||
events::{
|
||||
@@ -56,7 +57,7 @@ use ruma::{
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use super::{
|
||||
controller::{TimelineEnd, TimelineSettings},
|
||||
controller::{TimelineNewItemPosition, TimelineSettings},
|
||||
event_handler::TimelineEventKind,
|
||||
event_item::RemoteEventOrigin,
|
||||
traits::RoomDataProvider,
|
||||
@@ -236,7 +237,10 @@ impl TestTimeline {
|
||||
async fn handle_live_event(&self, event: impl Into<SyncTimelineEvent>) {
|
||||
let event = event.into();
|
||||
self.controller
|
||||
.add_events_at(vec![event], TimelineEnd::Back, RemoteEventOrigin::Sync)
|
||||
.add_events_at(
|
||||
vec![event],
|
||||
TimelineNewItemPosition::End { origin: RemoteEventOrigin::Sync },
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -255,7 +259,10 @@ impl TestTimeline {
|
||||
async fn handle_back_paginated_event(&self, event: Raw<AnyTimelineEvent>) {
|
||||
let timeline_event = TimelineEvent::new(event.cast());
|
||||
self.controller
|
||||
.add_events_at(vec![timeline_event], TimelineEnd::Front, RemoteEventOrigin::Pagination)
|
||||
.add_events_at(
|
||||
vec![timeline_event],
|
||||
TimelineNewItemPosition::Start { origin: RemoteEventOrigin::Pagination },
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,8 +18,8 @@ use assert_matches2::{assert_let, assert_matches};
|
||||
use eyeball_im::VectorDiff;
|
||||
use futures_core::Stream;
|
||||
use futures_util::{FutureExt as _, StreamExt as _};
|
||||
use matrix_sdk::{deserialized_responses::SyncTimelineEvent, test_utils::events::EventFactory};
|
||||
use matrix_sdk_test::{async_test, sync_timeline_event, ALICE, BOB};
|
||||
use matrix_sdk::deserialized_responses::SyncTimelineEvent;
|
||||
use matrix_sdk_test::{async_test, event_factory::EventFactory, sync_timeline_event, ALICE, BOB};
|
||||
use ruma::{
|
||||
event_id, events::AnyMessageLikeEventContent, server_name, uint, EventId,
|
||||
MilliSecondsSinceUnixEpoch, OwnedEventId,
|
||||
@@ -28,8 +28,8 @@ use stream_assert::assert_next_matches;
|
||||
use tokio::time::timeout;
|
||||
|
||||
use crate::timeline::{
|
||||
controller::TimelineEnd, event_item::RemoteEventOrigin, tests::TestTimeline, ReactionStatus,
|
||||
TimelineEventItemId, TimelineItem,
|
||||
controller::TimelineNewItemPosition, event_item::RemoteEventOrigin, tests::TestTimeline,
|
||||
ReactionStatus, TimelineEventItemId, TimelineItem,
|
||||
};
|
||||
|
||||
const REACTION_KEY: &str = "👍";
|
||||
@@ -204,8 +204,7 @@ async fn test_initial_reaction_timestamp_is_stored() {
|
||||
// Event comes next.
|
||||
f.text_msg("A").event_id(&message_event_id).into_sync(),
|
||||
],
|
||||
TimelineEnd::Back,
|
||||
RemoteEventOrigin::Sync,
|
||||
TimelineNewItemPosition::End { origin: RemoteEventOrigin::Sync },
|
||||
)
|
||||
.await;
|
||||
|
||||
|
||||
@@ -15,8 +15,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use eyeball_im::VectorDiff;
|
||||
use matrix_sdk::test_utils::events::EventFactory;
|
||||
use matrix_sdk_test::{async_test, ALICE, BOB, CAROL};
|
||||
use matrix_sdk_test::{async_test, event_factory::EventFactory, ALICE, BOB, CAROL};
|
||||
use ruma::{
|
||||
event_id,
|
||||
events::{
|
||||
|
||||
@@ -29,8 +29,8 @@ use stream_assert::assert_next_matches;
|
||||
|
||||
use super::TestTimeline;
|
||||
use crate::timeline::{
|
||||
controller::TimelineEnd, event_item::RemoteEventOrigin, AnyOtherFullStateEventContent,
|
||||
TimelineDetails, TimelineItemContent,
|
||||
controller::TimelineNewItemPosition, event_item::RemoteEventOrigin,
|
||||
AnyOtherFullStateEventContent, TimelineDetails, TimelineItemContent,
|
||||
};
|
||||
|
||||
#[async_test]
|
||||
@@ -146,8 +146,7 @@ async fn test_reaction_redaction_timeline_filter() {
|
||||
.event_builder
|
||||
.make_sync_redacted_message_event(*ALICE, RedactedReactionEventContent::new()),
|
||||
)],
|
||||
TimelineEnd::Back,
|
||||
RemoteEventOrigin::Sync,
|
||||
TimelineNewItemPosition::End { origin: RemoteEventOrigin::Sync },
|
||||
)
|
||||
.await;
|
||||
// Timeline items are actually empty.
|
||||
|
||||
@@ -18,7 +18,7 @@ use eyeball::Subscriber;
|
||||
use futures_util::FutureExt as _;
|
||||
use indexmap::IndexMap;
|
||||
#[cfg(test)]
|
||||
use matrix_sdk::crypto::{DecryptionSettings, TrustRequirement};
|
||||
use matrix_sdk::crypto::{DecryptionSettings, RoomEventDecryptionResult, TrustRequirement};
|
||||
use matrix_sdk::{
|
||||
deserialized_responses::TimelineEvent, event_cache::paginator::PaginableRoom, BoxFuture,
|
||||
Result, Room,
|
||||
@@ -302,8 +302,14 @@ impl Decryptor for (matrix_sdk_base::crypto::OlmMachine, ruma::OwnedRoomId) {
|
||||
let (olm_machine, room_id) = self;
|
||||
let decryption_settings =
|
||||
DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted };
|
||||
let event =
|
||||
olm_machine.decrypt_room_event(raw.cast_ref(), room_id, &decryption_settings).await?;
|
||||
Ok(event.into())
|
||||
match olm_machine
|
||||
.try_decrypt_room_event(raw.cast_ref(), room_id, &decryption_settings)
|
||||
.await?
|
||||
{
|
||||
RoomEventDecryptionResult::Decrypted(decrypted) => Ok(decrypted.into()),
|
||||
RoomEventDecryptionResult::UnableToDecrypt(utd_info) => {
|
||||
Ok(TimelineEvent::new_utd_event(raw.clone(), utd_info))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ pub(super) struct EventTimelineItemWithId<'a> {
|
||||
pub internal_id: &'a TimelineUniqueId,
|
||||
}
|
||||
|
||||
impl<'a> EventTimelineItemWithId<'a> {
|
||||
impl EventTimelineItemWithId<'_> {
|
||||
/// Create a clone of the underlying [`TimelineItem`] with the given kind.
|
||||
pub fn with_inner_kind(&self, kind: impl Into<EventTimelineItemKind>) -> Arc<TimelineItem> {
|
||||
TimelineItem::new(self.inner.with_kind(kind), self.internal_id.clone())
|
||||
|
||||
@@ -290,11 +290,11 @@ impl UtdHookManager {
|
||||
///
|
||||
/// Must be called with the lock held on [`UtdHookManager::reported_utds`],
|
||||
/// and takes a `MutexGuard` to enforce that.
|
||||
async fn report_utd<'a>(
|
||||
async fn report_utd(
|
||||
info: UnableToDecryptInfo,
|
||||
parent_hook: &Arc<dyn UnableToDecryptHook>,
|
||||
client: &Client,
|
||||
reported_utds_lock: &mut MutexGuard<'a, GrowableBloom>,
|
||||
reported_utds_lock: &mut MutexGuard<'_, GrowableBloom>,
|
||||
) {
|
||||
let event_id = info.event_id.clone();
|
||||
parent_hook.on_utd(info);
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#![allow(unexpected_cfgs)] // Triggered by the init_tracing_for_tests!() invocation.
|
||||
|
||||
use itertools::Itertools as _;
|
||||
use matrix_sdk::deserialized_responses::TimelineEvent;
|
||||
use ruma::{events::AnyStateEvent, serde::Raw, EventId, RoomId};
|
||||
|
||||
@@ -358,6 +358,8 @@ async fn test_sync_all_states() -> Result<(), Error> {
|
||||
["m.room.power_levels", ""],
|
||||
["org.matrix.msc3401.call.member", "*"],
|
||||
["m.room.join_rules", ""],
|
||||
["m.room.create", ""],
|
||||
["m.room.history_visibility", ""],
|
||||
],
|
||||
"include_heroes": true,
|
||||
"filters": {
|
||||
@@ -2224,6 +2226,7 @@ async fn test_room_subscription() -> Result<(), Error> {
|
||||
["org.matrix.msc3401.call.member", "*"],
|
||||
["m.room.join_rules", ""],
|
||||
["m.room.create", ""],
|
||||
["m.room.history_visibility", ""],
|
||||
["m.room.pinned_events", ""],
|
||||
],
|
||||
"timeline_limit": 20,
|
||||
@@ -2263,6 +2266,7 @@ async fn test_room_subscription() -> Result<(), Error> {
|
||||
["org.matrix.msc3401.call.member", "*"],
|
||||
["m.room.join_rules", ""],
|
||||
["m.room.create", ""],
|
||||
["m.room.history_visibility", ""],
|
||||
["m.room.pinned_events", ""],
|
||||
],
|
||||
"timeline_limit": 20,
|
||||
|
||||
@@ -19,14 +19,12 @@ use assert_matches2::assert_let;
|
||||
use eyeball_im::VectorDiff;
|
||||
use futures_util::StreamExt;
|
||||
use matrix_sdk::{
|
||||
assert_next_matches_with_timeout,
|
||||
config::SyncSettings,
|
||||
executor::spawn,
|
||||
ruma::MilliSecondsSinceUnixEpoch,
|
||||
test_utils::{events::EventFactory, logged_in_client_with_server},
|
||||
assert_next_matches_with_timeout, config::SyncSettings, executor::spawn,
|
||||
ruma::MilliSecondsSinceUnixEpoch, test_utils::logged_in_client_with_server,
|
||||
};
|
||||
use matrix_sdk_test::{
|
||||
async_test, mocks::mock_encryption_state, JoinedRoomBuilder, SyncResponseBuilder,
|
||||
async_test, event_factory::EventFactory, mocks::mock_encryption_state, JoinedRoomBuilder,
|
||||
SyncResponseBuilder,
|
||||
};
|
||||
use matrix_sdk_ui::timeline::{EventSendState, RoomExt, TimelineItemContent};
|
||||
use ruma::{
|
||||
|
||||
@@ -20,13 +20,12 @@ use assert_matches2::assert_let;
|
||||
use eyeball_im::VectorDiff;
|
||||
use futures_util::{FutureExt, StreamExt};
|
||||
use matrix_sdk::{
|
||||
config::SyncSettings,
|
||||
room::edit::EditedContent,
|
||||
test_utils::{events::EventFactory, logged_in_client_with_server},
|
||||
config::SyncSettings, room::edit::EditedContent, test_utils::logged_in_client_with_server,
|
||||
Client,
|
||||
};
|
||||
use matrix_sdk_test::{
|
||||
async_test, mocks::mock_encryption_state, JoinedRoomBuilder, SyncResponseBuilder, ALICE, BOB,
|
||||
async_test, event_factory::EventFactory, mocks::mock_encryption_state, JoinedRoomBuilder,
|
||||
SyncResponseBuilder, ALICE, BOB,
|
||||
};
|
||||
use matrix_sdk_ui::{
|
||||
timeline::{
|
||||
|
||||
@@ -20,12 +20,12 @@ use assert_matches2::assert_let;
|
||||
use eyeball_im::VectorDiff;
|
||||
use futures_util::StreamExt;
|
||||
use matrix_sdk::{
|
||||
assert_next_matches_with_timeout,
|
||||
config::SyncSettings,
|
||||
test_utils::{events::EventFactory, logged_in_client_with_server},
|
||||
assert_next_matches_with_timeout, config::SyncSettings,
|
||||
test_utils::logged_in_client_with_server,
|
||||
};
|
||||
use matrix_sdk_test::{
|
||||
async_test, mocks::mock_encryption_state, JoinedRoomBuilder, SyncResponseBuilder, ALICE, BOB,
|
||||
async_test, event_factory::EventFactory, mocks::mock_encryption_state, JoinedRoomBuilder,
|
||||
SyncResponseBuilder, ALICE, BOB,
|
||||
};
|
||||
use matrix_sdk_ui::{timeline::TimelineFocus, Timeline};
|
||||
use ruma::{event_id, events::room::message::RoomMessageEventContent, room_id};
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
// Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{fs::File, io::Write as _, path::PathBuf, time::Duration};
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
use assert_matches2::assert_let;
|
||||
use eyeball_im::VectorDiff;
|
||||
use futures_util::{FutureExt, StreamExt};
|
||||
use matrix_sdk::{
|
||||
assert_let_timeout, attachment::AttachmentConfig, test_utils::mocks::MatrixMockServer,
|
||||
};
|
||||
use matrix_sdk_test::{async_test, event_factory::EventFactory, JoinedRoomBuilder, ALICE};
|
||||
use matrix_sdk_ui::timeline::{EventSendState, RoomExt, TimelineItemContent};
|
||||
use ruma::{
|
||||
event_id,
|
||||
events::room::{message::MessageType, MediaSource},
|
||||
room_id,
|
||||
};
|
||||
use serde_json::json;
|
||||
use tempfile::TempDir;
|
||||
use tokio::time::sleep;
|
||||
use wiremock::ResponseTemplate;
|
||||
|
||||
fn create_temporary_file(filename: &str) -> (TempDir, PathBuf) {
|
||||
let tmp_dir = TempDir::new().unwrap();
|
||||
let file_path = tmp_dir.path().join(filename);
|
||||
let mut file = File::create(&file_path).unwrap();
|
||||
file.write_all(b"hello world").unwrap();
|
||||
(tmp_dir, file_path)
|
||||
}
|
||||
|
||||
fn get_filename_and_caption(msg: &MessageType) -> (&str, Option<&str>) {
|
||||
match msg {
|
||||
MessageType::File(event) => (event.filename(), event.caption()),
|
||||
MessageType::Image(event) => (event.filename(), event.caption()),
|
||||
MessageType::Video(event) => (event.filename(), event.caption()),
|
||||
MessageType::Audio(event) => (event.filename(), event.caption()),
|
||||
_ => panic!("unexpected message type"),
|
||||
}
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_send_attachment() {
|
||||
let mock = MatrixMockServer::new().await;
|
||||
let client = mock.client_builder().build().await;
|
||||
|
||||
mock.mock_room_state_encryption().plain().mount().await;
|
||||
|
||||
let room_id = room_id!("!a98sd12bjh:example.org");
|
||||
let room = mock.sync_joined_room(&client, room_id).await;
|
||||
let timeline = room.timeline().await.unwrap();
|
||||
|
||||
let (items, mut timeline_stream) =
|
||||
timeline.subscribe_filter_map(|item| item.as_event().cloned()).await;
|
||||
|
||||
assert!(items.is_empty());
|
||||
|
||||
let f = EventFactory::new();
|
||||
mock.sync_room(
|
||||
&client,
|
||||
JoinedRoomBuilder::new(room_id).add_timeline_event(f.text_msg("hello").sender(&ALICE)),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Sanity check.
|
||||
assert_let_timeout!(Some(VectorDiff::PushBack { value: item }) = timeline_stream.next());
|
||||
assert_let!(TimelineItemContent::Message(msg) = item.content());
|
||||
assert_eq!(msg.body(), "hello");
|
||||
|
||||
// No other updates.
|
||||
assert!(timeline_stream.next().now_or_never().is_none());
|
||||
|
||||
// Store a file in a temporary directory.
|
||||
let (_tmp_dir, file_path) = create_temporary_file("test.bin");
|
||||
|
||||
// Set up mocks for the file upload.
|
||||
mock.mock_upload()
|
||||
.respond_with(ResponseTemplate::new(200).set_delay(Duration::from_secs(2)).set_body_json(
|
||||
json!({
|
||||
"content_uri": "mxc://sdk.rs/media"
|
||||
}),
|
||||
))
|
||||
.mock_once()
|
||||
.mount()
|
||||
.await;
|
||||
|
||||
mock.mock_room_send().ok(event_id!("$media")).mock_once().mount().await;
|
||||
|
||||
// Queue sending of an attachment.
|
||||
let config = AttachmentConfig::new().caption(Some("caption".to_owned()));
|
||||
timeline.send_attachment(&file_path, mime::TEXT_PLAIN, config).use_send_queue().await.unwrap();
|
||||
|
||||
{
|
||||
assert_let_timeout!(Some(VectorDiff::PushBack { value: item }) = timeline_stream.next());
|
||||
assert_matches!(item.send_state(), Some(EventSendState::NotSentYet));
|
||||
assert_let!(TimelineItemContent::Message(msg) = item.content());
|
||||
|
||||
// Body is the caption, because there's both a caption and filename.
|
||||
assert_eq!(msg.body(), "caption");
|
||||
assert_eq!(get_filename_and_caption(msg.msgtype()), ("test.bin", Some("caption")));
|
||||
|
||||
// The URI refers to the local cache.
|
||||
assert_let!(MessageType::File(file) = msg.msgtype());
|
||||
assert_let!(MediaSource::Plain(uri) = &file.source);
|
||||
assert!(uri.to_string().contains("localhost"));
|
||||
}
|
||||
|
||||
// Eventually, the media is updated with the final MXC IDs…
|
||||
sleep(Duration::from_secs(2)).await;
|
||||
|
||||
{
|
||||
assert_let_timeout!(
|
||||
Some(VectorDiff::Set { index: 1, value: item }) = timeline_stream.next()
|
||||
);
|
||||
assert_let!(TimelineItemContent::Message(msg) = item.content());
|
||||
assert_matches!(item.send_state(), Some(EventSendState::NotSentYet));
|
||||
assert_eq!(get_filename_and_caption(msg.msgtype()), ("test.bin", Some("caption")));
|
||||
|
||||
// The URI now refers to the final MXC URI.
|
||||
assert_let!(MessageType::File(file) = msg.msgtype());
|
||||
assert_let!(MediaSource::Plain(uri) = &file.source);
|
||||
assert_eq!(uri.to_string(), "mxc://sdk.rs/media");
|
||||
}
|
||||
|
||||
// And eventually the event itself is sent.
|
||||
{
|
||||
assert_let_timeout!(
|
||||
Some(VectorDiff::Set { index: 1, value: item }) = timeline_stream.next()
|
||||
);
|
||||
assert_matches!(item.send_state(), Some(EventSendState::Sent{ event_id }) => {
|
||||
assert_eq!(event_id, event_id!("$media"));
|
||||
});
|
||||
}
|
||||
|
||||
// That's all, folks!
|
||||
assert!(timeline_stream.next().now_or_never().is_none());
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_react_to_local_media() {
|
||||
let mock = MatrixMockServer::new().await;
|
||||
let client = mock.client_builder().build().await;
|
||||
|
||||
// Disable the sending queue, to simulate offline mode.
|
||||
client.send_queue().set_enabled(false).await;
|
||||
|
||||
mock.mock_room_state_encryption().plain().mount().await;
|
||||
|
||||
let room_id = room_id!("!a98sd12bjh:example.org");
|
||||
let room = mock.sync_joined_room(&client, room_id).await;
|
||||
let timeline = room.timeline().await.unwrap();
|
||||
|
||||
let (items, mut timeline_stream) =
|
||||
timeline.subscribe_filter_map(|item| item.as_event().cloned()).await;
|
||||
|
||||
assert!(items.is_empty());
|
||||
assert!(timeline_stream.next().now_or_never().is_none());
|
||||
|
||||
// Store a file in a temporary directory.
|
||||
let (_tmp_dir, file_path) = create_temporary_file("test.bin");
|
||||
|
||||
// Queue sending of an attachment (no captions).
|
||||
let config = AttachmentConfig::new();
|
||||
timeline.send_attachment(&file_path, mime::TEXT_PLAIN, config).use_send_queue().await.unwrap();
|
||||
|
||||
let item_id = {
|
||||
assert_let_timeout!(Some(VectorDiff::PushBack { value: item }) = timeline_stream.next());
|
||||
assert_let!(TimelineItemContent::Message(msg) = item.content());
|
||||
assert_eq!(get_filename_and_caption(msg.msgtype()), ("test.bin", None));
|
||||
|
||||
// The item starts with no reactions.
|
||||
assert!(item.reactions().is_empty());
|
||||
|
||||
item.identifier()
|
||||
};
|
||||
|
||||
// Add a reaction to the file media event.
|
||||
timeline.toggle_reaction(&item_id, "🤪").await.unwrap();
|
||||
|
||||
assert_let_timeout!(Some(VectorDiff::Set { index: 0, value: item }) = timeline_stream.next());
|
||||
assert_let!(TimelineItemContent::Message(msg) = item.content());
|
||||
assert_eq!(get_filename_and_caption(msg.msgtype()), ("test.bin", None));
|
||||
|
||||
// There's a reaction for the current user for the given emoji.
|
||||
let reactions = item.reactions();
|
||||
let own_user_id = client.user_id().unwrap();
|
||||
reactions.get("🤪").unwrap().get(own_user_id).unwrap();
|
||||
|
||||
// That's all, folks!
|
||||
assert!(timeline_stream.next().now_or_never().is_none());
|
||||
}
|
||||
@@ -19,12 +19,11 @@ use assert_matches2::assert_let;
|
||||
use eyeball_im::VectorDiff;
|
||||
use futures_util::StreamExt;
|
||||
use matrix_sdk::{
|
||||
assert_let_timeout,
|
||||
config::SyncSettings,
|
||||
test_utils::{events::EventFactory, logged_in_client_with_server},
|
||||
assert_let_timeout, config::SyncSettings, test_utils::logged_in_client_with_server,
|
||||
};
|
||||
use matrix_sdk_test::{
|
||||
async_test,
|
||||
event_factory::EventFactory,
|
||||
mocks::{mock_encryption_state, mock_redaction},
|
||||
sync_timeline_event, JoinedRoomBuilder, RoomAccountDataTestEvent, StateTestEvent,
|
||||
SyncResponseBuilder, BOB,
|
||||
@@ -53,6 +52,7 @@ use crate::mock_sync;
|
||||
mod echo;
|
||||
mod edit;
|
||||
mod focus_event;
|
||||
mod media;
|
||||
mod pagination;
|
||||
mod pinned_event;
|
||||
mod profiles;
|
||||
|
||||
@@ -3,14 +3,14 @@ use std::time::Duration;
|
||||
use assert_matches::assert_matches;
|
||||
use eyeball_im::VectorDiff;
|
||||
use matrix_sdk::{
|
||||
assert_next_matches_with_timeout,
|
||||
config::SyncSettings,
|
||||
sync::SyncResponse,
|
||||
test_utils::{events::EventFactory, logged_in_client_with_server},
|
||||
Client,
|
||||
assert_next_matches_with_timeout, config::SyncSettings, sync::SyncResponse,
|
||||
test_utils::logged_in_client_with_server, Client,
|
||||
};
|
||||
use matrix_sdk_base::deserialized_responses::TimelineEvent;
|
||||
use matrix_sdk_test::{async_test, JoinedRoomBuilder, StateTestEvent, SyncResponseBuilder, BOB};
|
||||
use matrix_sdk_test::{
|
||||
async_test, event_factory::EventFactory, JoinedRoomBuilder, StateTestEvent,
|
||||
SyncResponseBuilder, BOB,
|
||||
};
|
||||
use matrix_sdk_ui::{
|
||||
timeline::{RoomExt, TimelineFocus, TimelineItemContent},
|
||||
Timeline,
|
||||
|
||||
@@ -17,12 +17,10 @@ use std::{sync::Mutex, time::Duration};
|
||||
use assert_matches2::{assert_let, assert_matches};
|
||||
use eyeball_im::VectorDiff;
|
||||
use futures_util::{FutureExt as _, StreamExt as _};
|
||||
use matrix_sdk::{
|
||||
assert_next_matches_with_timeout,
|
||||
test_utils::{events::EventFactory, logged_in_client_with_server},
|
||||
};
|
||||
use matrix_sdk::{assert_next_matches_with_timeout, test_utils::logged_in_client_with_server};
|
||||
use matrix_sdk_test::{
|
||||
async_test,
|
||||
event_factory::EventFactory,
|
||||
mocks::{mock_encryption_state, mock_redaction},
|
||||
JoinedRoomBuilder, SyncResponseBuilder, ALICE,
|
||||
};
|
||||
|
||||
@@ -4,14 +4,11 @@ use assert_matches::assert_matches;
|
||||
use assert_matches2::assert_let;
|
||||
use eyeball_im::VectorDiff;
|
||||
use futures_util::StreamExt;
|
||||
use matrix_sdk::{
|
||||
config::SyncSettings,
|
||||
test_utils::{events::EventFactory, logged_in_client_with_server},
|
||||
};
|
||||
use matrix_sdk::{config::SyncSettings, test_utils::logged_in_client_with_server};
|
||||
use matrix_sdk_base::timeout::timeout;
|
||||
use matrix_sdk_test::{
|
||||
async_test, mocks::mock_encryption_state, EventBuilder, JoinedRoomBuilder, SyncResponseBuilder,
|
||||
ALICE, BOB, CAROL,
|
||||
async_test, event_factory::EventFactory, mocks::mock_encryption_state, EventBuilder,
|
||||
JoinedRoomBuilder, SyncResponseBuilder, ALICE, BOB, CAROL,
|
||||
};
|
||||
use matrix_sdk_ui::timeline::{
|
||||
Error as TimelineError, EventSendState, RoomExt, TimelineDetails, TimelineItemContent,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user