mirror of
https://github.com/matrix-org/matrix-rust-sdk.git
synced 2026-05-18 13:40:55 -04:00
Merge pull request #3169 from matrix-org/mauroromito/directory_search
Room Directory Search
This commit is contained in:
@@ -58,6 +58,7 @@ use crate::{
|
||||
encryption::Encryption,
|
||||
notification::NotificationClientBuilder,
|
||||
notification_settings::NotificationSettings,
|
||||
room_directory_search::RoomDirectorySearch,
|
||||
sync_service::{SyncService, SyncServiceBuilder},
|
||||
task_handle::TaskHandle,
|
||||
ClientError,
|
||||
@@ -740,6 +741,12 @@ impl Client {
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
pub fn room_directory_search(&self) -> Arc<RoomDirectorySearch> {
|
||||
Arc::new(RoomDirectorySearch::new(
|
||||
matrix_sdk::room_directory_search::RoomDirectorySearch::new((*self.inner).clone()),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
|
||||
@@ -32,6 +32,7 @@ mod notification;
|
||||
mod notification_settings;
|
||||
mod platform;
|
||||
mod room;
|
||||
mod room_directory_search;
|
||||
mod room_info;
|
||||
mod room_list;
|
||||
mod room_member;
|
||||
|
||||
175
bindings/matrix-sdk-ffi/src/room_directory_search.rs
Normal file
175
bindings/matrix-sdk-ffi/src/room_directory_search.rs
Normal file
@@ -0,0 +1,175 @@
|
||||
// Copyright 2024 Mauro Romito
|
||||
// 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::{fmt::Debug, sync::Arc};
|
||||
|
||||
use eyeball_im::VectorDiff;
|
||||
use futures_util::StreamExt;
|
||||
use matrix_sdk::room_directory_search::RoomDirectorySearch as SdkRoomDirectorySearch;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use super::RUNTIME;
|
||||
use crate::{error::ClientError, task_handle::TaskHandle};
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum PublicRoomJoinRule {
|
||||
Public,
|
||||
Knock,
|
||||
}
|
||||
|
||||
impl TryFrom<ruma::directory::PublicRoomJoinRule> for PublicRoomJoinRule {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: ruma::directory::PublicRoomJoinRule) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
ruma::directory::PublicRoomJoinRule::Public => Ok(Self::Public),
|
||||
ruma::directory::PublicRoomJoinRule::Knock => Ok(Self::Knock),
|
||||
rule => Err(format!("unsupported join rule: {rule:?}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct RoomDescription {
|
||||
pub room_id: String,
|
||||
pub name: Option<String>,
|
||||
pub topic: Option<String>,
|
||||
pub alias: Option<String>,
|
||||
pub avatar_url: Option<String>,
|
||||
pub join_rule: Option<PublicRoomJoinRule>,
|
||||
pub is_world_readable: bool,
|
||||
pub joined_members: u64,
|
||||
}
|
||||
|
||||
impl From<matrix_sdk::room_directory_search::RoomDescription> for RoomDescription {
|
||||
fn from(value: matrix_sdk::room_directory_search::RoomDescription) -> Self {
|
||||
Self {
|
||||
room_id: value.room_id.to_string(),
|
||||
name: value.name,
|
||||
topic: value.topic,
|
||||
alias: value.alias.map(|alias| alias.to_string()),
|
||||
avatar_url: value.avatar_url.map(|url| url.to_string()),
|
||||
join_rule: value.join_rule.try_into().ok(),
|
||||
is_world_readable: value.is_world_readable,
|
||||
joined_members: value.joined_members,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct RoomDirectorySearch {
|
||||
pub(crate) inner: RwLock<SdkRoomDirectorySearch>,
|
||||
}
|
||||
|
||||
impl RoomDirectorySearch {
|
||||
pub fn new(inner: SdkRoomDirectorySearch) -> Self {
|
||||
Self { inner: RwLock::new(inner) }
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
impl RoomDirectorySearch {
|
||||
pub async fn next_page(&self) -> Result<(), ClientError> {
|
||||
let mut inner = self.inner.write().await;
|
||||
inner.next_page().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn search(&self, filter: Option<String>, batch_size: u32) -> Result<(), ClientError> {
|
||||
let mut inner = self.inner.write().await;
|
||||
inner.search(filter, batch_size).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn loaded_pages(&self) -> Result<u32, ClientError> {
|
||||
let inner = self.inner.read().await;
|
||||
Ok(inner.loaded_pages() as u32)
|
||||
}
|
||||
|
||||
pub async fn is_at_last_page(&self) -> Result<bool, ClientError> {
|
||||
let inner = self.inner.read().await;
|
||||
Ok(inner.is_at_last_page())
|
||||
}
|
||||
|
||||
pub async fn results(
|
||||
&self,
|
||||
listener: Box<dyn RoomDirectorySearchEntriesListener>,
|
||||
) -> TaskHandle {
|
||||
let (initial_values, mut stream) = self.inner.read().await.results();
|
||||
|
||||
TaskHandle::new(RUNTIME.spawn(async move {
|
||||
listener.on_update(vec![RoomDirectorySearchEntryUpdate::Reset {
|
||||
values: initial_values.into_iter().map(Into::into).collect(),
|
||||
}]);
|
||||
|
||||
while let Some(diffs) = stream.next().await {
|
||||
listener.on_update(diffs.into_iter().map(|diff| diff.into()).collect());
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct RoomDirectorySearchEntriesResult {
|
||||
pub entries_stream: Arc<TaskHandle>,
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum RoomDirectorySearchEntryUpdate {
|
||||
Append { values: Vec<RoomDescription> },
|
||||
Clear,
|
||||
PushFront { value: RoomDescription },
|
||||
PushBack { value: RoomDescription },
|
||||
PopFront,
|
||||
PopBack,
|
||||
Insert { index: u32, value: RoomDescription },
|
||||
Set { index: u32, value: RoomDescription },
|
||||
Remove { index: u32 },
|
||||
Truncate { length: u32 },
|
||||
Reset { values: Vec<RoomDescription> },
|
||||
}
|
||||
|
||||
impl From<VectorDiff<matrix_sdk::room_directory_search::RoomDescription>>
|
||||
for RoomDirectorySearchEntryUpdate
|
||||
{
|
||||
fn from(diff: VectorDiff<matrix_sdk::room_directory_search::RoomDescription>) -> Self {
|
||||
match diff {
|
||||
VectorDiff::Append { values } => {
|
||||
Self::Append { values: values.into_iter().map(|v| v.into()).collect() }
|
||||
}
|
||||
VectorDiff::Clear => Self::Clear,
|
||||
VectorDiff::PushFront { value } => Self::PushFront { value: value.into() },
|
||||
VectorDiff::PushBack { value } => Self::PushBack { value: value.into() },
|
||||
VectorDiff::PopFront => Self::PopFront,
|
||||
VectorDiff::PopBack => Self::PopBack,
|
||||
VectorDiff::Insert { index, value } => {
|
||||
Self::Insert { index: index as u32, value: value.into() }
|
||||
}
|
||||
VectorDiff::Set { index, value } => {
|
||||
Self::Set { index: index as u32, value: value.into() }
|
||||
}
|
||||
VectorDiff::Remove { index } => Self::Remove { index: index as u32 },
|
||||
VectorDiff::Truncate { length } => Self::Truncate { length: length as u32 },
|
||||
VectorDiff::Reset { values } => {
|
||||
Self::Reset { values: values.into_iter().map(|v| v.into()).collect() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
pub trait RoomDirectorySearchEntriesListener: Send + Sync + Debug {
|
||||
fn on_update(&self, room_entries_update: Vec<RoomDirectorySearchEntryUpdate>);
|
||||
}
|
||||
@@ -48,6 +48,7 @@ pub mod notification_settings;
|
||||
#[cfg(feature = "experimental-oidc")]
|
||||
pub mod oidc;
|
||||
pub mod room;
|
||||
pub mod room_directory_search;
|
||||
pub mod utils;
|
||||
pub mod futures {
|
||||
//! Named futures returned from methods on types in [the crate root][crate].
|
||||
|
||||
477
crates/matrix-sdk/src/room_directory_search.rs
Normal file
477
crates/matrix-sdk/src/room_directory_search.rs
Normal file
@@ -0,0 +1,477 @@
|
||||
// Copyright 2024 Mauro Romito
|
||||
// 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.
|
||||
|
||||
//! Types for searching the public room directory.
|
||||
|
||||
use eyeball_im::{ObservableVector, VectorDiff};
|
||||
use futures_core::Stream;
|
||||
use imbl::Vector;
|
||||
use ruma::{
|
||||
api::client::directory::get_public_rooms_filtered::v3::Request as PublicRoomsFilterRequest,
|
||||
directory::{Filter, PublicRoomJoinRule},
|
||||
OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId,
|
||||
};
|
||||
|
||||
use crate::{Client, Result};
|
||||
|
||||
/// This struct represents a single result of a room directory search.
|
||||
///
|
||||
/// It's produced by [`RoomDirectorySearch::results`].
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct RoomDescription {
|
||||
/// The room's ID.
|
||||
pub room_id: OwnedRoomId,
|
||||
/// The name of the room, if any.
|
||||
pub name: Option<String>,
|
||||
/// The topic of the room, if any.
|
||||
pub topic: Option<String>,
|
||||
/// The canonical alias of the room, if any.
|
||||
pub alias: Option<OwnedRoomAliasId>,
|
||||
/// The room's avatar URL, if any.
|
||||
pub avatar_url: Option<OwnedMxcUri>,
|
||||
/// The room's join rule.
|
||||
pub join_rule: PublicRoomJoinRule,
|
||||
/// Whether can be previewed
|
||||
pub is_world_readable: bool,
|
||||
/// The number of members that have joined the room.
|
||||
pub joined_members: u64,
|
||||
}
|
||||
|
||||
impl From<ruma::directory::PublicRoomsChunk> for RoomDescription {
|
||||
fn from(value: ruma::directory::PublicRoomsChunk) -> Self {
|
||||
Self {
|
||||
room_id: value.room_id,
|
||||
name: value.name,
|
||||
topic: value.topic,
|
||||
alias: value.canonical_alias,
|
||||
avatar_url: value.avatar_url,
|
||||
join_rule: value.join_rule,
|
||||
is_world_readable: value.world_readable,
|
||||
joined_members: value.num_joined_members.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
enum SearchState {
|
||||
/// The search has more pages and contains the next token to be used in the
|
||||
/// next page request.
|
||||
Next(String),
|
||||
/// The search has reached the end.
|
||||
End,
|
||||
/// The search is in a starting state, and has yet to fetch the first page.
|
||||
#[default]
|
||||
Start,
|
||||
}
|
||||
|
||||
impl SearchState {
|
||||
fn next_token(&self) -> Option<&str> {
|
||||
if let Self::Next(next_token) = &self {
|
||||
Some(next_token)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn is_at_end(&self) -> bool {
|
||||
matches!(self, Self::End)
|
||||
}
|
||||
}
|
||||
|
||||
/// `RoomDirectorySearch` allows searching the public room directory, with the
|
||||
/// capability of using a filter and a batch_size. This struct is also
|
||||
/// responsible for keeping the current state of the search, and exposing an
|
||||
/// update of stream of the results, reset the search, or ask for the next page.
|
||||
///
|
||||
/// ⚠️ Users must take great care when using the public room search since the
|
||||
/// results might contains NSFW content.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// use matrix_sdk::{room_directory_search::RoomDirectorySearch, Client};
|
||||
/// use url::Url;
|
||||
///
|
||||
/// async {
|
||||
/// let homeserver = Url::parse("http://localhost:8080")?;
|
||||
/// let client = Client::new(homeserver).await?;
|
||||
/// let mut room_directory_search = RoomDirectorySearch::new(client);
|
||||
/// room_directory_search.search(None, 10).await?;
|
||||
/// let (results, mut stream) = room_directory_search.results();
|
||||
/// room_directory_search.next_page().await?;
|
||||
/// anyhow::Ok(())
|
||||
/// };
|
||||
/// ```
|
||||
#[derive(Debug)]
|
||||
pub struct RoomDirectorySearch {
|
||||
batch_size: u32,
|
||||
filter: Option<String>,
|
||||
search_state: SearchState,
|
||||
client: Client,
|
||||
results: ObservableVector<RoomDescription>,
|
||||
}
|
||||
|
||||
impl RoomDirectorySearch {
|
||||
/// Constructor for the `RoomDirectorySearch`, requires a `Client`.
|
||||
pub fn new(client: Client) -> Self {
|
||||
Self {
|
||||
batch_size: 0,
|
||||
filter: None,
|
||||
search_state: Default::default(),
|
||||
client,
|
||||
results: ObservableVector::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Starts a filtered search for the server.
|
||||
///
|
||||
/// If the `filter` is not provided it will search for all the rooms.
|
||||
/// You can specify a `batch_size`` to control the number of rooms to fetch
|
||||
/// per request.
|
||||
///
|
||||
/// This method will clear the current search results and start a new one.
|
||||
// Should never be used concurrently with another `next_page` or a
|
||||
// `search`.
|
||||
pub async fn search(&mut self, filter: Option<String>, batch_size: u32) -> Result<()> {
|
||||
self.filter = filter;
|
||||
self.batch_size = batch_size;
|
||||
self.search_state = Default::default();
|
||||
self.results.clear();
|
||||
self.next_page().await
|
||||
}
|
||||
|
||||
/// Asks the server for the next page of the current search.
|
||||
// Should never be used concurrently with another `next_page` or a
|
||||
// `search`.
|
||||
pub async fn next_page(&mut self) -> Result<()> {
|
||||
if self.search_state.is_at_end() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut filter = Filter::new();
|
||||
filter.generic_search_term = self.filter.clone();
|
||||
|
||||
let mut request = PublicRoomsFilterRequest::new();
|
||||
request.filter = filter;
|
||||
request.limit = Some(self.batch_size.into());
|
||||
request.since = self.search_state.next_token().map(ToOwned::to_owned);
|
||||
|
||||
let response = self.client.public_rooms_filtered(request).await?;
|
||||
|
||||
if let Some(next_token) = response.next_batch {
|
||||
self.search_state = SearchState::Next(next_token);
|
||||
} else {
|
||||
self.search_state = SearchState::End;
|
||||
}
|
||||
|
||||
self.results.append(response.chunk.into_iter().map(Into::into).collect());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the initial values of the current stored room descriptions in the
|
||||
/// search, and a stream of updates for them.
|
||||
pub fn results(
|
||||
&self,
|
||||
) -> (Vector<RoomDescription>, impl Stream<Item = Vec<VectorDiff<RoomDescription>>>) {
|
||||
self.results.subscribe().into_values_and_batched_stream()
|
||||
}
|
||||
|
||||
/// Get the number of pages that have been loaded so far.
|
||||
pub fn loaded_pages(&self) -> usize {
|
||||
if self.batch_size == 0 {
|
||||
return 0;
|
||||
}
|
||||
(self.results.len() as f64 / self.batch_size as f64).ceil() as usize
|
||||
}
|
||||
|
||||
/// Get whether the search is at the last page
|
||||
pub fn is_at_last_page(&self) -> bool {
|
||||
self.search_state.is_at_end()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, not(target_arch = "wasm32")))]
|
||||
mod tests {
|
||||
use assert_matches::assert_matches;
|
||||
use eyeball_im::VectorDiff;
|
||||
use futures_util::StreamExt;
|
||||
use matrix_sdk_test::{async_test, test_json};
|
||||
use ruma::{directory::Filter, serde::Raw, RoomAliasId, RoomId};
|
||||
use serde_json::Value as JsonValue;
|
||||
use stream_assert::assert_pending;
|
||||
use wiremock::{
|
||||
matchers::{method, path_regex},
|
||||
Match, Mock, MockServer, Request, ResponseTemplate,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
room_directory_search::{RoomDescription, RoomDirectorySearch},
|
||||
test_utils::logged_in_client,
|
||||
Client,
|
||||
};
|
||||
|
||||
struct RoomDirectorySearchMatcher {
|
||||
next_token: Option<String>,
|
||||
filter_term: Option<String>,
|
||||
limit: u32,
|
||||
}
|
||||
|
||||
impl Match for RoomDirectorySearchMatcher {
|
||||
fn matches(&self, request: &Request) -> bool {
|
||||
let Ok(body) = request.body_json::<Raw<JsonValue>>() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
// The body's `since` field is set equal to the matcher's next_token.
|
||||
if !body.get_field::<String>("since").is_ok_and(|s| s == self.next_token) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if !body.get_field::<u32>("limit").is_ok_and(|s| s == Some(self.limit)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The body's `filter` field has `generic_search_term` equal to the matcher's
|
||||
// next_token.
|
||||
if !body.get_field::<Filter>("filter").is_ok_and(|s| {
|
||||
if self.filter_term.is_none() {
|
||||
s.is_none() || s.is_some_and(|s| s.generic_search_term.is_none())
|
||||
} else {
|
||||
s.is_some_and(|s| s.generic_search_term == self.filter_term)
|
||||
}
|
||||
}) {
|
||||
return false;
|
||||
}
|
||||
|
||||
method("POST").matches(request)
|
||||
&& path_regex("/_matrix/client/../publicRooms").matches(request)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_first_page_description() -> RoomDescription {
|
||||
RoomDescription {
|
||||
room_id: RoomId::parse("!ol19s:bleecker.street").unwrap(),
|
||||
name: Some("CHEESE".into()),
|
||||
topic: Some("Tasty tasty cheese".into()),
|
||||
alias: None,
|
||||
avatar_url: Some("mxc://bleeker.street/CHEDDARandBRIE".into()),
|
||||
join_rule: ruma::directory::PublicRoomJoinRule::Public,
|
||||
is_world_readable: true,
|
||||
joined_members: 37,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_second_page_description() -> RoomDescription {
|
||||
RoomDescription {
|
||||
room_id: RoomId::parse("!ca18r:bleecker.street").unwrap(),
|
||||
name: Some("PEAR".into()),
|
||||
topic: Some("Tasty tasty pear".into()),
|
||||
alias: RoomAliasId::parse("#murrays:pear.bar").ok(),
|
||||
avatar_url: Some("mxc://bleeker.street/pear".into()),
|
||||
join_rule: ruma::directory::PublicRoomJoinRule::Knock,
|
||||
is_world_readable: false,
|
||||
joined_members: 20,
|
||||
}
|
||||
}
|
||||
|
||||
async fn new_server_and_client() -> (MockServer, Client) {
|
||||
let server = MockServer::start().await;
|
||||
let client = logged_in_client(Some(server.uri())).await;
|
||||
(server, client)
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn search_success() {
|
||||
let (server, client) = new_server_and_client().await;
|
||||
|
||||
let mut room_directory_search = RoomDirectorySearch::new(client);
|
||||
Mock::given(RoomDirectorySearchMatcher { next_token: None, filter_term: None, limit: 1 })
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::PUBLIC_ROOMS))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
room_directory_search.search(None, 1).await.unwrap();
|
||||
let (results, mut stream) = room_directory_search.results();
|
||||
assert_pending!(stream);
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results[0], get_first_page_description());
|
||||
assert!(!room_directory_search.is_at_last_page());
|
||||
assert_eq!(room_directory_search.loaded_pages(), 1);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn search_success_paginated() {
|
||||
let (server, client) = new_server_and_client().await;
|
||||
|
||||
let mut room_directory_search = RoomDirectorySearch::new(client);
|
||||
Mock::given(RoomDirectorySearchMatcher { next_token: None, filter_term: None, limit: 1 })
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::PUBLIC_ROOMS))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
room_directory_search.search(None, 1).await.unwrap();
|
||||
let (initial_results, mut stream) = room_directory_search.results();
|
||||
assert_eq!(initial_results, vec![get_first_page_description()].into());
|
||||
assert!(!room_directory_search.is_at_last_page());
|
||||
assert_eq!(room_directory_search.loaded_pages(), 1);
|
||||
|
||||
Mock::given(RoomDirectorySearchMatcher {
|
||||
next_token: Some("p190q".into()),
|
||||
filter_term: None,
|
||||
limit: 1,
|
||||
})
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200).set_body_json(&*test_json::PUBLIC_ROOMS_FINAL_PAGE),
|
||||
)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
room_directory_search.next_page().await.unwrap();
|
||||
|
||||
let results_batch: Vec<VectorDiff<RoomDescription>> = stream.next().await.unwrap();
|
||||
assert_matches!(&results_batch[0], VectorDiff::Append { values } => { assert_eq!(values, &vec![get_second_page_description()].into()); });
|
||||
assert!(room_directory_search.is_at_last_page());
|
||||
assert_eq!(room_directory_search.loaded_pages(), 2);
|
||||
assert_pending!(stream);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn search_fails() {
|
||||
let (server, client) = new_server_and_client().await;
|
||||
|
||||
let mut room_directory_search = RoomDirectorySearch::new(client);
|
||||
Mock::given(RoomDirectorySearchMatcher { next_token: None, filter_term: None, limit: 1 })
|
||||
.respond_with(ResponseTemplate::new(404))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
assert!(room_directory_search.next_page().await.is_err());
|
||||
|
||||
let (results, mut stream) = room_directory_search.results();
|
||||
assert_eq!(results.len(), 0);
|
||||
assert!(!room_directory_search.is_at_last_page());
|
||||
assert_eq!(room_directory_search.loaded_pages(), 0);
|
||||
assert_pending!(stream);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn search_fails_when_paginating() {
|
||||
let (server, client) = new_server_and_client().await;
|
||||
|
||||
let mut room_directory_search = RoomDirectorySearch::new(client);
|
||||
Mock::given(RoomDirectorySearchMatcher { next_token: None, filter_term: None, limit: 1 })
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::PUBLIC_ROOMS))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
room_directory_search.search(None, 1).await.unwrap();
|
||||
|
||||
let (results, mut stream) = room_directory_search.results();
|
||||
assert_eq!(results, vec![get_first_page_description()].into());
|
||||
assert!(!room_directory_search.is_at_last_page());
|
||||
assert_eq!(room_directory_search.loaded_pages(), 1);
|
||||
assert_pending!(stream);
|
||||
|
||||
Mock::given(RoomDirectorySearchMatcher {
|
||||
next_token: Some("p190q".into()),
|
||||
filter_term: None,
|
||||
limit: 1,
|
||||
})
|
||||
.respond_with(ResponseTemplate::new(404))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
assert!(room_directory_search.next_page().await.is_err());
|
||||
assert_eq!(results, vec![get_first_page_description()].into());
|
||||
assert!(!room_directory_search.is_at_last_page());
|
||||
assert_eq!(room_directory_search.loaded_pages(), 1);
|
||||
assert_pending!(stream);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn search_success_paginated_with_filter() {
|
||||
let (server, client) = new_server_and_client().await;
|
||||
|
||||
let mut room_directory_search = RoomDirectorySearch::new(client);
|
||||
Mock::given(RoomDirectorySearchMatcher {
|
||||
next_token: None,
|
||||
filter_term: Some("bleecker.street".into()),
|
||||
limit: 1,
|
||||
})
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::PUBLIC_ROOMS))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
room_directory_search.search(Some("bleecker.street".into()), 1).await.unwrap();
|
||||
let (initial_results, mut stream) = room_directory_search.results();
|
||||
assert_eq!(initial_results, vec![get_first_page_description()].into());
|
||||
assert!(!room_directory_search.is_at_last_page());
|
||||
assert_eq!(room_directory_search.loaded_pages(), 1);
|
||||
|
||||
Mock::given(RoomDirectorySearchMatcher {
|
||||
next_token: Some("p190q".into()),
|
||||
filter_term: Some("bleecker.street".into()),
|
||||
limit: 1,
|
||||
})
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200).set_body_json(&*test_json::PUBLIC_ROOMS_FINAL_PAGE),
|
||||
)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
room_directory_search.next_page().await.unwrap();
|
||||
|
||||
let results_batch: Vec<VectorDiff<RoomDescription>> = stream.next().await.unwrap();
|
||||
assert_matches!(&results_batch[0], VectorDiff::Append { values } => { assert_eq!(values, &vec![get_second_page_description()].into()); });
|
||||
assert!(room_directory_search.is_at_last_page());
|
||||
assert_eq!(room_directory_search.loaded_pages(), 2);
|
||||
assert_pending!(stream);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn search_followed_by_another_search_with_filter() {
|
||||
let (server, client) = new_server_and_client().await;
|
||||
|
||||
let mut room_directory_search = RoomDirectorySearch::new(client);
|
||||
Mock::given(RoomDirectorySearchMatcher { next_token: None, filter_term: None, limit: 1 })
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::PUBLIC_ROOMS))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
room_directory_search.search(None, 1).await.unwrap();
|
||||
let (initial_results, mut stream) = room_directory_search.results();
|
||||
assert_eq!(initial_results, vec![get_first_page_description()].into());
|
||||
assert!(!room_directory_search.is_at_last_page());
|
||||
assert_eq!(room_directory_search.loaded_pages(), 1);
|
||||
|
||||
Mock::given(RoomDirectorySearchMatcher {
|
||||
next_token: None,
|
||||
filter_term: Some("bleecker.street".into()),
|
||||
limit: 1,
|
||||
})
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::PUBLIC_ROOMS))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
room_directory_search.search(Some("bleecker.street".into()), 1).await.unwrap();
|
||||
|
||||
let results_batch: Vec<VectorDiff<RoomDescription>> = stream.next().await.unwrap();
|
||||
assert_matches!(&results_batch[0], VectorDiff::Clear);
|
||||
assert_matches!(&results_batch[1], VectorDiff::Append { values } => { assert_eq!(values, &vec![get_first_page_description()].into()); });
|
||||
assert!(!room_directory_search.is_at_last_page());
|
||||
assert_eq!(room_directory_search.loaded_pages(), 1);
|
||||
assert_pending!(stream);
|
||||
}
|
||||
}
|
||||
@@ -4,4 +4,5 @@ mod invitations;
|
||||
mod reactions;
|
||||
mod redaction;
|
||||
mod repeated_join;
|
||||
mod room_directory_search;
|
||||
mod sliding_sync;
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
// Copyright 2024 Mauro Romito
|
||||
// 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::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use assert_matches::assert_matches;
|
||||
use eyeball_im::VectorDiff;
|
||||
use futures::StreamExt;
|
||||
use matrix_sdk::{
|
||||
room_directory_search::RoomDirectorySearch,
|
||||
ruma::api::client::room::{create_room::v3::Request as CreateRoomRequest, Visibility},
|
||||
};
|
||||
use rand::{thread_rng, Rng};
|
||||
use stream_assert::assert_pending;
|
||||
use tokio::time::sleep;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::helpers::TestClientBuilder;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_room_directory_search_filter() -> Result<()> {
|
||||
let alice = TestClientBuilder::new("alice".to_owned()).use_sqlite().build().await?;
|
||||
let search_string = random_string(32);
|
||||
for index in 0..25 {
|
||||
let mut request: CreateRoomRequest = CreateRoomRequest::new();
|
||||
request.visibility = Visibility::Public;
|
||||
let name = format!("{}_{}", search_string, index);
|
||||
warn!("room name: {}", name);
|
||||
request.name = Some(name);
|
||||
alice.create_room(request).await?;
|
||||
}
|
||||
sleep(Duration::from_secs(1)).await;
|
||||
let mut room_directory_search = RoomDirectorySearch::new(alice);
|
||||
let (values, mut stream) = room_directory_search.results();
|
||||
assert!(values.is_empty());
|
||||
room_directory_search.search(Some(search_string), 10).await?;
|
||||
let results_batch: Vec<VectorDiff<matrix_sdk::room_directory_search::RoomDescription>> =
|
||||
stream.next().await.unwrap();
|
||||
assert_matches!(&results_batch[0], VectorDiff::Clear);
|
||||
assert_matches!(&results_batch[1], VectorDiff::Append { values } => { assert_eq!(values.len(), 10); });
|
||||
|
||||
room_directory_search.next_page().await?;
|
||||
room_directory_search.next_page().await?;
|
||||
let results_batch = stream.next().await.unwrap();
|
||||
assert_eq!(results_batch.len(), 2);
|
||||
assert_matches!(&results_batch[0], VectorDiff::Append { values } => { assert_eq!(values.len(), 10); });
|
||||
assert_matches!(&results_batch[1], VectorDiff::Append { values } => { assert_eq!(values.len(), 5); });
|
||||
assert_pending!(stream);
|
||||
room_directory_search.next_page().await?;
|
||||
assert_pending!(stream);
|
||||
|
||||
// This should reset the state completely
|
||||
room_directory_search.search(None, 25).await?;
|
||||
let results_batch = stream.next().await.unwrap();
|
||||
assert_matches!(&results_batch[0], VectorDiff::Clear);
|
||||
assert_matches!(&results_batch[1], VectorDiff::Append { values } => { assert_eq!(values.len(), 25); });
|
||||
assert_pending!(stream);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn random_string(length: usize) -> String {
|
||||
thread_rng()
|
||||
.sample_iter(&rand::distributions::Alphanumeric)
|
||||
.take(length)
|
||||
.map(char::from)
|
||||
.collect()
|
||||
}
|
||||
@@ -239,6 +239,7 @@ pub static NOT_FOUND: Lazy<JsonValue> = Lazy::new(|| {
|
||||
});
|
||||
|
||||
/// `GET /_matrix/client/v3/publicRooms`
|
||||
/// `POST /_matrix/client/v3/publicRooms`
|
||||
pub static PUBLIC_ROOMS: Lazy<JsonValue> = Lazy::new(|| {
|
||||
json!({
|
||||
"chunk": [
|
||||
@@ -261,6 +262,28 @@ pub static PUBLIC_ROOMS: Lazy<JsonValue> = Lazy::new(|| {
|
||||
})
|
||||
});
|
||||
|
||||
/// `GET /_matrix/client/v3/publicRooms`
|
||||
/// `POST /_matrix/client/v3/publicRooms``
|
||||
pub static PUBLIC_ROOMS_FINAL_PAGE: Lazy<JsonValue> = Lazy::new(|| {
|
||||
json!({
|
||||
"chunk": [
|
||||
{
|
||||
"canonical_alias": "#murrays:pear.bar",
|
||||
"avatar_url": "mxc://bleeker.street/pear",
|
||||
"guest_can_join": false,
|
||||
"name": "PEAR",
|
||||
"num_joined_members": 20,
|
||||
"room_id": "!ca18r:bleecker.street",
|
||||
"topic": "Tasty tasty pear",
|
||||
"world_readable": false,
|
||||
"join_rule": "knock"
|
||||
}
|
||||
],
|
||||
"prev_batch": "p190q",
|
||||
"total_room_count_estimate": 115
|
||||
})
|
||||
});
|
||||
|
||||
/// `POST /_matrix/client/v3/refresh` without new refresh token.
|
||||
pub static REFRESH_TOKEN: Lazy<JsonValue> = Lazy::new(|| {
|
||||
json!({
|
||||
|
||||
@@ -18,8 +18,8 @@ pub mod sync_events;
|
||||
pub use api_responses::{
|
||||
DEVICES, GET_ALIAS, KEYS_QUERY, KEYS_QUERY_TWO_DEVICES_ONE_SIGNED, KEYS_UPLOAD, LOGIN,
|
||||
LOGIN_RESPONSE_ERR, LOGIN_TYPES, LOGIN_WITH_DISCOVERY, LOGIN_WITH_REFRESH_TOKEN, NOT_FOUND,
|
||||
PUBLIC_ROOMS, REFRESH_TOKEN, REFRESH_TOKEN_WITH_REFRESH_TOKEN, REGISTRATION_RESPONSE_ERR,
|
||||
UNKNOWN_TOKEN_SOFT_LOGOUT, VERSIONS, WELL_KNOWN, WHOAMI,
|
||||
PUBLIC_ROOMS, PUBLIC_ROOMS_FINAL_PAGE, REFRESH_TOKEN, REFRESH_TOKEN_WITH_REFRESH_TOKEN,
|
||||
REGISTRATION_RESPONSE_ERR, UNKNOWN_TOKEN_SOFT_LOGOUT, VERSIONS, WELL_KNOWN, WHOAMI,
|
||||
};
|
||||
pub use members::MEMBERS;
|
||||
pub use sync::{
|
||||
|
||||
Reference in New Issue
Block a user