Skip to content

Commit

Permalink
feat(ui): Implement Notification API (matrix-org#2023)
Browse files Browse the repository at this point in the history
This is the first PR for splitting the sync loop into two. This offers a new high-level API, `NotificationApi`, that makes use of a separate `SlidingSync` instance which sole role is to listen to to-device events and e2ee; it's pre-configured to do so. That means we're not force-enabling e2ee and to-device by default for every sliding sync instance, and as such we won't either generate Olm requests to the home server in general.

In the future, this new high-level API will hide some low-level dirty details so that its can be instantiated in multiple processes at the same time (lock across process, invalidate and refill crypto caches, etc.).

An embedder who would want to make use of this would need the following:

- a main sliding sync instance, without e2ee and to-device. Using the `matrix_sdk_ui::RoomList` would be the best bet, at this time.
- an instance of this `matrix_sdk_ui::NotificationApi`, with a different identifier.

Note that this is not ready to be used in an external process; or it will cause the same kind of issues that we're seeing as of today: invalid crypto caches resulting in UTD, etc.

Fixes matrix-org#1961.
  • Loading branch information
bnjbvr authored Jun 19, 2023
1 parent 7c393d7 commit 4d3ca15
Show file tree
Hide file tree
Showing 14 changed files with 528 additions and 122 deletions.
2 changes: 1 addition & 1 deletion bindings/matrix-sdk-crypto-nodejs/src/machine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ impl OlmMachine {
#[napi(constructor)]
pub fn new() -> napi::Result<Self> {
Err(napi::Error::from_reason(
"To build an `OldMachine`, please use the `initialize` method",
"To build an `OlmMachine`, please use the `initialize` method",
))
}

Expand Down
2 changes: 1 addition & 1 deletion bindings/matrix-sdk-ffi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ eyeball-im = { workspace = true }
extension-trait = "1.0.1"
futures-core = { workspace = true }
futures-util = { workspace = true }
matrix-sdk-ui = { path = "../../crates/matrix-sdk-ui", default-features = false, features = ["e2e-encryption", "experimental-room-list"] }
matrix-sdk-ui = { path = "../../crates/matrix-sdk-ui", default-features = false, features = ["e2e-encryption", "experimental-room-list", "experimental-notification"] }
mime = "0.3.16"
# FIXME: we currently can't feature flag anything in the api.udl, therefore we must enforce experimental-sliding-sync being exposed here..
# see https://github.com/matrix-org/matrix-rust-sdk/issues/1014
Expand Down
7 changes: 7 additions & 0 deletions bindings/matrix-sdk-ffi/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::fmt::Display;

use matrix_sdk::{self, encryption::CryptoStoreError, HttpError, IdParseError, StoreError};
use matrix_sdk_ui::notifications;

#[derive(Debug, thiserror::Error)]
pub enum ClientError {
Expand Down Expand Up @@ -68,6 +69,12 @@ impl From<mime::FromStrError> for ClientError {
}
}

impl From<notifications::Error> for ClientError {
fn from(e: notifications::Error) -> Self {
Self::new(e)
}
}

#[derive(Debug, thiserror::Error, uniffi::Error)]
#[uniffi(flat_error)]
pub enum RoomError {
Expand Down
65 changes: 64 additions & 1 deletion bindings/matrix-sdk-ffi/src/sliding_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ use matrix_sdk::{
LoopCtrl, RoomListEntry as MatrixRoomEntry, SlidingSyncBuilder as MatrixSlidingSyncBuilder,
SlidingSyncListLoadingState, SlidingSyncMode,
};
use matrix_sdk_ui::timeline::SlidingSyncRoomExt;
use matrix_sdk_ui::{
notifications::NotificationSync as MatrixNotificationSync, timeline::SlidingSyncRoomExt,
};
use tracing::{error, warn};

use crate::{
Expand Down Expand Up @@ -869,6 +871,55 @@ impl SlidingSyncBuilder {
}
}

#[uniffi::export(callback_interface)]
pub trait NotificationSyncListener: Sync + Send {
/// Called whenever the notification sync loop terminates, and must be
/// restarted.
fn did_terminate(&self);
}

/// Full context for the notification sync loop.
#[derive(uniffi::Object)]
pub struct NotificationSync {
/// Unused field, kept for its `Drop` semantics.
_handle: TaskHandle,
}

impl NotificationSync {
fn start(
notification: MatrixNotificationSync,
listener: Box<dyn NotificationSyncListener>,
) -> TaskHandle {
TaskHandle::new(RUNTIME.spawn(async move {
let stream = notification.sync();
pin_mut!(stream);

loop {
match stream.next().await {
Some(Ok(())) => {
// Yay.
}

None => {
warn!("Notification sliding sync ended");
break;
}

Some(Err(err)) => {
// The internal sliding sync instance already handles retries for us, so if
// we get an error here, it means the maximum number of retries has been
// reached, and there's not much we can do anymore.
warn!("Error when handling notifications: {err}");
break;
}
}
}

listener.did_terminate();
}))
}
}

#[uniffi::export]
impl Client {
/// Creates a new Sliding Sync instance with the given identifier.
Expand All @@ -878,4 +929,16 @@ impl Client {
let inner = self.inner.sliding_sync(id)?;
Ok(Arc::new(SlidingSyncBuilder { inner, client: self.clone() }))
}

pub fn notification_sliding_sync(
&self,
id: String,
listener: Box<dyn NotificationSyncListener>,
) -> Result<Arc<NotificationSync>, ClientError> {
RUNTIME.block_on(async move {
let inner = MatrixNotificationSync::new(id, self.inner.clone()).await?;
let handle = NotificationSync::start(inner, listener);
Ok(Arc::new(NotificationSync { _handle: handle }))
})
}
}
3 changes: 2 additions & 1 deletion crates/matrix-sdk-ui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ version = "0.6.0"
edition = "2021"

[features]
default = ["e2e-encryption", "native-tls", "experimental-room-list"]
default = ["e2e-encryption", "native-tls", "experimental-room-list", "experimental-notification"]

e2e-encryption = ["matrix-sdk/e2e-encryption"]

native-tls = ["matrix-sdk/native-tls"]
rustls-tls = ["matrix-sdk/rustls-tls"]

experimental-room-list = ["experimental-sliding-sync", "dep:async-stream", "dep:eyeball-im-util"]
experimental-notification = ["experimental-sliding-sync", "dep:async-stream"]
experimental-sliding-sync = ["matrix-sdk/experimental-sliding-sync"]

testing = ["dep:eyeball-im-util"]
Expand Down
2 changes: 2 additions & 0 deletions crates/matrix-sdk-ui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

mod events;

#[cfg(feature = "experimental-notification")]
pub mod notifications;
#[cfg(feature = "experimental-room-list")]
pub mod room_list;
pub mod timeline;
Expand Down
117 changes: 117 additions & 0 deletions crates/matrix-sdk-ui/src/notifications/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// Copyright 2023 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 that specific language governing permissions and
// limitations under the License.

//! Notification API.
//!
//! The notification API is a high-level helper that is designed to take care of
//! handling the synchronization of notifications, be they received within the
//! app or within a dedicated notification process (e.g. the [NSE] process on
//! iOS devices).
//!
//! Under the hood, this uses a sliding sync instance configured with no lists,
//! but that enables the e2ee and to-device extensions, so that it can both
//! handle encryption and manage encryption keys; that's sufficient to decrypt
//! messages received in the notification processes.
//!
//! As this may be used across different processes, this also makes sure that
//! there's only one process writing to the databases holding encryption
//! information. TODO as of 2023-06-06, this hasn't been done yet.
//!
//! [NSE]: https://developer.apple.com/documentation/usernotifications/unnotificationserviceextension
use async_stream::stream;
use futures_core::stream::Stream;
use futures_util::{pin_mut, StreamExt};
use matrix_sdk::{Client, SlidingSync};
use ruma::{api::client::sync::sync_events::v4, assign};
use tracing::error;

/// High-level helper for synchronizing notifications using sliding sync.
///
/// See the module's documentation for more details.
#[derive(Clone)]
pub struct NotificationSync {
sliding_sync: SlidingSync,
}

impl NotificationSync {
/// Creates a new instance of a `NotificationSync`.
///
/// This will create and manage an instance of [`matrix_sdk::SlidingSync`].
/// The `id` is used as the identifier of that instance, as such make
/// sure to not reuse a name used by another Sliding Sync instance, at
/// the risk of causing problems.
pub async fn new(id: impl Into<String>, client: Client) -> Result<Self, Error> {
let sliding_sync = client
.sliding_sync(id)?
.enable_caching()?
.with_to_device_extension(
assign!(v4::ToDeviceConfig::default(), { enabled: Some(true)}),
)
.with_e2ee_extension(assign!(v4::E2EEConfig::default(), { enabled: Some(true)}))
.build()
.await?;

Ok(Self { sliding_sync })
}

/// Start synchronization of notifications.
///
/// This should be regularly polled, so as to ensure that the notifications
/// are sync'd.
pub fn sync(&self) -> impl Stream<Item = Result<(), Error>> + '_ {
stream!({
let sync = self.sliding_sync.sync();

pin_mut!(sync);

loop {
match sync.next().await {
Some(Ok(update_summary)) => {
// This API is only concerned with the e2ee and to-device extensions.
// Warn if anything weird has been received from the proxy.
if !update_summary.lists.is_empty() || !update_summary.rooms.is_empty() {
yield Err(Error::UnexpectedNonEmptyListsOrRooms);
} else {
// Cool cool, let's do it again.
yield Ok(());
}

continue;
}

Some(Err(err)) => {
yield Err(err.into());

break;
}

None => {
break;
}
}
}
})
}
}

/// Errors for the [`NotificationSync`].
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Unexpected rooms or lists in the sliding sync response.")]
UnexpectedNonEmptyListsOrRooms,

#[error("Something wrong happened in sliding sync: {0:#}")]
SlidingSyncError(#[from] matrix_sdk::Error),
}
5 changes: 4 additions & 1 deletion crates/matrix-sdk-ui/src/room_list/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ use matrix_sdk::{
};
pub use room::*;
use ruma::{
api::client::sync::sync_events::v4::SyncRequestListFilters,
api::client::sync::sync_events::v4::{E2EEConfig, SyncRequestListFilters, ToDeviceConfig},
assign,
events::{StateEventType, TimelineEventType},
OwnedRoomId, RoomId,
Expand All @@ -104,6 +104,9 @@ impl RoomList {
.enable_caching()
.map_err(Error::SlidingSync)?
.with_common_extensions()
// TODO different strategy when the encryption sync is in main by default
.with_e2ee_extension(assign!(E2EEConfig::default(), { enabled: Some(true) }))
.with_to_device_extension(assign!(ToDeviceConfig::default(), { enabled: Some(true) }))
// TODO revert to `add_cached_list` when reloading rooms from the cache is blazingly
// fast
.add_list(
Expand Down
3 changes: 3 additions & 0 deletions crates/matrix-sdk-ui/tests/integration/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,11 @@ use wiremock::{
Mock, MockServer, ResponseTemplate,
};

#[cfg(feature = "experimental-notification")]
mod notification;
#[cfg(feature = "experimental-room-list")]
mod room_list;
mod sliding_sync;
mod timeline;

#[cfg(all(test, not(target_arch = "wasm32")))]
Expand Down
Loading

0 comments on commit 4d3ca15

Please sign in to comment.