Merge pull request #3169 from matrix-org/mauroromito/directory_search

Room Directory Search
This commit is contained in:
Ivan Enderlin
2024-03-20 15:43:18 +01:00
committed by GitHub
9 changed files with 767 additions and 2 deletions

View File

@@ -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)]

View File

@@ -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;

View 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>);
}

View File

@@ -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].

View 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);
}
}

View File

@@ -4,4 +4,5 @@ mod invitations;
mod reactions;
mod redaction;
mod repeated_join;
mod room_directory_search;
mod sliding_sync;

View File

@@ -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()
}

View File

@@ -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!({

View File

@@ -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::{