2020-02-21 15:54:05 +00:00
|
|
|
// Copyright 2020 Damir Jelić
|
|
|
|
// Copyright 2020 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.
|
|
|
|
|
2020-04-23 08:52:47 +00:00
|
|
|
use std::collections::{BTreeMap, HashMap};
|
2019-10-23 20:47:00 +00:00
|
|
|
use std::convert::{TryFrom, TryInto};
|
2020-03-18 13:15:56 +00:00
|
|
|
use std::result::Result as StdResult;
|
2020-04-01 02:00:46 +00:00
|
|
|
use std::sync::Arc;
|
2020-02-28 09:33:17 +00:00
|
|
|
use std::time::{Duration, Instant};
|
2020-04-01 13:37:00 +00:00
|
|
|
|
2020-04-09 14:19:32 +00:00
|
|
|
use uuid::Uuid;
|
|
|
|
|
2020-04-02 22:22:52 +00:00
|
|
|
use futures::future::Future;
|
2020-03-11 10:42:59 +00:00
|
|
|
use tokio::sync::RwLock;
|
2020-03-16 12:31:03 +00:00
|
|
|
use tokio::time::delay_for as sleep;
|
2020-04-03 08:42:03 +00:00
|
|
|
#[cfg(feature = "encryption")]
|
|
|
|
use tracing::debug;
|
|
|
|
use tracing::{info, instrument, trace};
|
2019-10-23 20:47:00 +00:00
|
|
|
|
|
|
|
use http::Method as HttpMethod;
|
|
|
|
use http::Response as HttpResponse;
|
2019-10-24 20:34:58 +00:00
|
|
|
use reqwest::header::{HeaderValue, InvalidHeaderValue};
|
2019-10-23 20:47:00 +00:00
|
|
|
use url::Url;
|
|
|
|
|
2020-04-23 08:52:47 +00:00
|
|
|
use ruma_api::Endpoint;
|
2019-11-10 10:44:03 +00:00
|
|
|
use ruma_events::room::message::MessageEventContent;
|
2020-04-23 08:52:47 +00:00
|
|
|
use ruma_events::EventJson;
|
2019-10-23 20:47:00 +00:00
|
|
|
pub use ruma_events::EventType;
|
2020-04-10 20:32:28 +00:00
|
|
|
use ruma_identifiers::{RoomId, RoomIdOrAliasId, UserId};
|
2019-10-23 20:47:00 +00:00
|
|
|
|
2020-04-01 13:37:00 +00:00
|
|
|
#[cfg(feature = "encryption")]
|
2020-04-11 12:46:45 +00:00
|
|
|
use ruma_identifiers::DeviceId;
|
2020-04-01 13:37:00 +00:00
|
|
|
|
2019-10-23 20:47:00 +00:00
|
|
|
use crate::api;
|
|
|
|
use crate::base_client::Client as BaseClient;
|
2020-04-01 01:08:25 +00:00
|
|
|
use crate::models::Room;
|
2019-10-23 20:47:00 +00:00
|
|
|
use crate::session::Session;
|
2019-11-17 18:55:59 +00:00
|
|
|
use crate::VERSION;
|
2020-03-31 23:34:11 +00:00
|
|
|
use crate::{Error, EventEmitter, Result};
|
2020-03-29 12:05:40 +00:00
|
|
|
|
2020-03-24 15:18:56 +00:00
|
|
|
const DEFAULT_SYNC_TIMEOUT: Duration = Duration::from_secs(30);
|
2020-02-28 09:33:17 +00:00
|
|
|
|
2019-11-10 10:44:03 +00:00
|
|
|
#[derive(Clone)]
|
2020-02-21 13:29:25 +00:00
|
|
|
/// An async/await enabled Matrix client.
|
2020-04-15 11:52:29 +00:00
|
|
|
///
|
|
|
|
/// All of the state is held in an `Arc` so the `AsyncClient` can be cloned freely.
|
2019-10-23 20:47:00 +00:00
|
|
|
pub struct AsyncClient {
|
|
|
|
/// The URL of the homeserver to connect to.
|
|
|
|
homeserver: Url,
|
|
|
|
/// The underlying HTTP client.
|
|
|
|
http_client: reqwest::Client,
|
|
|
|
/// User session data.
|
2020-03-31 13:01:48 +00:00
|
|
|
pub(crate) base_client: Arc<RwLock<BaseClient>>,
|
2019-10-23 20:47:00 +00:00
|
|
|
}
|
|
|
|
|
2020-03-19 12:55:04 +00:00
|
|
|
impl std::fmt::Debug for AsyncClient {
|
|
|
|
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> StdResult<(), std::fmt::Error> {
|
|
|
|
write!(fmt, "AsyncClient {{ homeserver: {} }}", self.homeserver)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-23 20:47:00 +00:00
|
|
|
#[derive(Default, Debug)]
|
2020-02-21 13:29:25 +00:00
|
|
|
/// Configuration for the creation of the `AsyncClient`.
|
|
|
|
///
|
|
|
|
/// # Example
|
|
|
|
///
|
|
|
|
/// ```
|
2020-03-02 10:31:03 +00:00
|
|
|
/// # use matrix_sdk::AsyncClientConfig;
|
2020-02-21 13:29:25 +00:00
|
|
|
/// // To pass all the request through mitmproxy set the proxy and disable SSL
|
|
|
|
/// // verification
|
|
|
|
/// let client_config = AsyncClientConfig::new()
|
|
|
|
/// .proxy("http://localhost:8080")
|
|
|
|
/// .unwrap()
|
|
|
|
/// .disable_ssl_verification();
|
|
|
|
/// ```
|
2019-10-23 20:47:00 +00:00
|
|
|
pub struct AsyncClientConfig {
|
|
|
|
proxy: Option<reqwest::Proxy>,
|
2019-10-24 20:34:58 +00:00
|
|
|
user_agent: Option<HeaderValue>,
|
2019-10-23 20:47:00 +00:00
|
|
|
disable_ssl_verification: bool,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl AsyncClientConfig {
|
2020-02-21 13:29:25 +00:00
|
|
|
/// Create a new default `AsyncClientConfig`.
|
2019-10-23 20:47:00 +00:00
|
|
|
pub fn new() -> Self {
|
|
|
|
Default::default()
|
|
|
|
}
|
|
|
|
|
2020-02-21 13:29:25 +00:00
|
|
|
/// Set the proxy through which all the HTTP requests should go.
|
|
|
|
///
|
|
|
|
/// Note, only HTTP proxies are supported.
|
|
|
|
///
|
|
|
|
/// # Arguments
|
|
|
|
///
|
|
|
|
/// * `proxy` - The HTTP URL of the proxy.
|
|
|
|
///
|
|
|
|
/// # Example
|
|
|
|
///
|
|
|
|
/// ```
|
2020-02-24 11:51:42 +00:00
|
|
|
/// use matrix_sdk::AsyncClientConfig;
|
2020-02-21 13:29:25 +00:00
|
|
|
///
|
|
|
|
/// let client_config = AsyncClientConfig::new()
|
|
|
|
/// .proxy("http://localhost:8080")
|
|
|
|
/// .unwrap();
|
|
|
|
/// ```
|
2020-03-18 13:15:56 +00:00
|
|
|
pub fn proxy(mut self, proxy: &str) -> Result<Self> {
|
2019-10-23 20:47:00 +00:00
|
|
|
self.proxy = Some(reqwest::Proxy::all(proxy)?);
|
|
|
|
Ok(self)
|
|
|
|
}
|
|
|
|
|
2020-02-21 13:29:25 +00:00
|
|
|
/// Disable SSL verification for the HTTP requests.
|
2019-10-23 20:47:00 +00:00
|
|
|
pub fn disable_ssl_verification(mut self) -> Self {
|
|
|
|
self.disable_ssl_verification = true;
|
|
|
|
self
|
|
|
|
}
|
2019-10-24 20:34:58 +00:00
|
|
|
|
2020-02-21 13:29:25 +00:00
|
|
|
/// Set a custom HTTP user agent for the client.
|
2020-03-18 13:15:56 +00:00
|
|
|
pub fn user_agent(mut self, user_agent: &str) -> StdResult<Self, InvalidHeaderValue> {
|
2019-10-24 20:34:58 +00:00
|
|
|
self.user_agent = Some(HeaderValue::from_str(user_agent)?);
|
|
|
|
Ok(self)
|
|
|
|
}
|
2019-10-23 20:47:00 +00:00
|
|
|
}
|
|
|
|
|
2019-12-04 18:31:33 +00:00
|
|
|
#[derive(Debug, Default, Clone)]
|
2020-02-21 13:29:25 +00:00
|
|
|
/// Settings for a sync call.
|
2019-10-23 20:47:00 +00:00
|
|
|
pub struct SyncSettings {
|
2020-03-24 15:18:56 +00:00
|
|
|
pub(crate) timeout: Option<Duration>,
|
2019-10-23 20:47:00 +00:00
|
|
|
pub(crate) token: Option<String>,
|
2020-04-03 12:09:56 +00:00
|
|
|
pub(crate) full_state: bool,
|
2019-10-23 20:47:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl SyncSettings {
|
2020-02-21 13:29:25 +00:00
|
|
|
/// Create new default sync settings.
|
2019-10-23 20:47:00 +00:00
|
|
|
pub fn new() -> Self {
|
|
|
|
Default::default()
|
|
|
|
}
|
|
|
|
|
2020-02-21 13:29:25 +00:00
|
|
|
/// Set the sync token.
|
|
|
|
///
|
|
|
|
/// # Arguments
|
|
|
|
///
|
|
|
|
/// * `token` - The sync token that should be used for the sync call.
|
2019-10-23 20:47:00 +00:00
|
|
|
pub fn token<S: Into<String>>(mut self, token: S) -> Self {
|
|
|
|
self.token = Some(token.into());
|
|
|
|
self
|
|
|
|
}
|
|
|
|
|
2020-02-21 13:29:25 +00:00
|
|
|
/// Set the maximum time the server can wait, in milliseconds, before
|
|
|
|
/// responding to the sync request.
|
|
|
|
///
|
|
|
|
/// # Arguments
|
|
|
|
///
|
|
|
|
/// * `timeout` - The time the server is allowed to wait.
|
2020-03-24 15:18:56 +00:00
|
|
|
pub fn timeout(mut self, timeout: Duration) -> Self {
|
|
|
|
self.timeout = Some(timeout);
|
|
|
|
self
|
2019-10-23 20:47:00 +00:00
|
|
|
}
|
|
|
|
|
2020-02-21 13:29:25 +00:00
|
|
|
/// Should the server return the full state from the start of the timeline.
|
|
|
|
///
|
|
|
|
/// This does nothing if no sync token is set.
|
|
|
|
///
|
|
|
|
/// # Arguments
|
|
|
|
/// * `full_state` - A boolean deciding if the server should return the full
|
|
|
|
/// state or not.
|
2019-10-23 20:47:00 +00:00
|
|
|
pub fn full_state(mut self, full_state: bool) -> Self {
|
2020-04-03 12:09:56 +00:00
|
|
|
self.full_state = full_state;
|
2019-10-23 20:47:00 +00:00
|
|
|
self
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-09 14:26:00 +00:00
|
|
|
#[cfg(feature = "encryption")]
|
|
|
|
use api::r0::client_exchange::send_event_to_device;
|
2020-04-01 13:37:00 +00:00
|
|
|
#[cfg(feature = "encryption")]
|
2020-04-03 08:27:30 +00:00
|
|
|
use api::r0::keys::{claim_keys, get_keys, upload_keys, KeyAlgorithm};
|
2020-04-10 20:32:28 +00:00
|
|
|
use api::r0::membership::join_room_by_id;
|
|
|
|
use api::r0::membership::join_room_by_id_or_alias;
|
|
|
|
use api::r0::membership::kick_user;
|
|
|
|
use api::r0::membership::leave_room;
|
|
|
|
use api::r0::membership::{
|
|
|
|
invite_user::{self, InvitationRecipient},
|
|
|
|
Invite3pid,
|
|
|
|
};
|
2020-02-21 15:33:08 +00:00
|
|
|
use api::r0::message::create_message_event;
|
2020-04-11 12:46:45 +00:00
|
|
|
use api::r0::message::get_message_events;
|
2020-04-10 20:32:28 +00:00
|
|
|
use api::r0::room::create_room;
|
2019-10-23 20:47:00 +00:00
|
|
|
use api::r0::session::login;
|
|
|
|
use api::r0::sync::sync_events;
|
|
|
|
|
|
|
|
impl AsyncClient {
|
|
|
|
/// Creates a new client for making HTTP requests to the given homeserver.
|
2020-02-21 13:29:25 +00:00
|
|
|
///
|
|
|
|
/// # Arguments
|
|
|
|
///
|
|
|
|
/// * `homeserver_url` - The homeserver that the client should connect to.
|
|
|
|
/// * `session` - If a previous login exists, the access token can be
|
|
|
|
/// reused by giving a session object here.
|
2020-03-18 13:15:56 +00:00
|
|
|
pub fn new<U: TryInto<Url>>(homeserver_url: U, session: Option<Session>) -> Result<Self> {
|
2019-10-24 20:34:58 +00:00
|
|
|
let config = AsyncClientConfig::new();
|
|
|
|
AsyncClient::new_with_config(homeserver_url, session, config)
|
2019-10-23 20:47:00 +00:00
|
|
|
}
|
|
|
|
|
2020-02-21 13:29:25 +00:00
|
|
|
/// Create a new client with the given configuration.
|
|
|
|
///
|
|
|
|
/// # Arguments
|
|
|
|
///
|
|
|
|
/// * `homeserver_url` - The homeserver that the client should connect to.
|
|
|
|
/// * `session` - If a previous login exists, the access token can be
|
|
|
|
/// reused by giving a session object here.
|
|
|
|
/// * `config` - Configuration for the client.
|
2019-11-10 10:44:03 +00:00
|
|
|
pub fn new_with_config<U: TryInto<Url>>(
|
|
|
|
homeserver_url: U,
|
2019-10-23 20:47:00 +00:00
|
|
|
session: Option<Session>,
|
|
|
|
config: AsyncClientConfig,
|
2020-03-18 13:15:56 +00:00
|
|
|
) -> Result<Self> {
|
2020-03-31 23:34:11 +00:00
|
|
|
#[allow(clippy::match_wild_err_arm)]
|
2019-11-10 10:44:03 +00:00
|
|
|
let homeserver: Url = match homeserver_url.try_into() {
|
|
|
|
Ok(u) => u,
|
2020-02-21 13:29:46 +00:00
|
|
|
Err(_e) => panic!("Error parsing homeserver url"),
|
2019-11-10 10:44:03 +00:00
|
|
|
};
|
|
|
|
|
2019-10-23 20:47:00 +00:00
|
|
|
let http_client = reqwest::Client::builder();
|
|
|
|
|
|
|
|
let http_client = if config.disable_ssl_verification {
|
|
|
|
http_client.danger_accept_invalid_certs(true)
|
|
|
|
} else {
|
|
|
|
http_client
|
|
|
|
};
|
|
|
|
|
|
|
|
let http_client = match config.proxy {
|
|
|
|
Some(p) => http_client.proxy(p),
|
|
|
|
None => http_client,
|
|
|
|
};
|
|
|
|
|
|
|
|
let mut headers = reqwest::header::HeaderMap::new();
|
|
|
|
|
2019-10-24 20:34:58 +00:00
|
|
|
let user_agent = match config.user_agent {
|
|
|
|
Some(a) => a,
|
2020-03-11 10:43:31 +00:00
|
|
|
None => HeaderValue::from_str(&format!("matrix-rust-sdk {}", VERSION)).unwrap(),
|
2019-10-24 20:34:58 +00:00
|
|
|
};
|
|
|
|
|
2019-11-10 17:33:06 +00:00
|
|
|
headers.insert(reqwest::header::USER_AGENT, user_agent);
|
2019-10-23 20:47:00 +00:00
|
|
|
|
2020-02-21 11:13:38 +00:00
|
|
|
let http_client = http_client.default_headers(headers).build()?;
|
2019-10-23 20:47:00 +00:00
|
|
|
|
|
|
|
Ok(Self {
|
|
|
|
homeserver,
|
|
|
|
http_client,
|
2020-03-18 13:15:56 +00:00
|
|
|
base_client: Arc::new(RwLock::new(BaseClient::new(session)?)),
|
2019-10-23 20:47:00 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2020-01-11 21:24:32 +00:00
|
|
|
/// Is the client logged in.
|
2020-03-11 10:42:59 +00:00
|
|
|
pub async fn logged_in(&self) -> bool {
|
|
|
|
// TODO turn this into a atomic bool so this method doesn't need to be
|
|
|
|
// async.
|
|
|
|
self.base_client.read().await.logged_in()
|
2019-11-17 18:55:27 +00:00
|
|
|
}
|
|
|
|
|
2020-01-11 21:24:32 +00:00
|
|
|
/// The Homeserver of the client.
|
|
|
|
pub fn homeserver(&self) -> &Url {
|
|
|
|
&self.homeserver
|
|
|
|
}
|
|
|
|
|
2020-03-31 23:34:11 +00:00
|
|
|
/// Add `EventEmitter` to `AsyncClient`.
|
|
|
|
///
|
|
|
|
/// The methods of `EventEmitter` are called when the respective `RoomEvents` occur.
|
2020-04-14 22:10:10 +00:00
|
|
|
pub async fn add_event_emitter(&mut self, emitter: Box<dyn EventEmitter>) {
|
2020-03-31 23:34:11 +00:00
|
|
|
self.base_client.write().await.event_emitter = Some(emitter);
|
|
|
|
}
|
|
|
|
|
2020-04-02 19:59:13 +00:00
|
|
|
/// Returns an `Option` of the room name from a `RoomId`.
|
|
|
|
///
|
|
|
|
/// This is a human readable room name.
|
2020-04-03 13:07:40 +00:00
|
|
|
pub async fn get_room_name(&self, room_id: &RoomId) -> Option<String> {
|
2020-04-01 02:00:46 +00:00
|
|
|
self.base_client
|
|
|
|
.read()
|
|
|
|
.await
|
|
|
|
.calculate_room_name(room_id)
|
|
|
|
.await
|
2020-03-27 21:22:11 +00:00
|
|
|
}
|
|
|
|
|
2020-04-02 19:59:13 +00:00
|
|
|
/// Returns a `Vec` of the room names this client knows about.
|
|
|
|
///
|
|
|
|
/// This is a human readable list of room names.
|
2020-03-27 21:22:11 +00:00
|
|
|
pub async fn get_room_names(&self) -> Vec<String> {
|
2020-04-01 01:08:25 +00:00
|
|
|
self.base_client.read().await.calculate_room_names().await
|
|
|
|
}
|
|
|
|
|
2020-04-02 19:59:13 +00:00
|
|
|
/// Returns the rooms this client knows about.
|
|
|
|
///
|
|
|
|
/// A `HashMap` of room id to `matrix::models::Room`
|
2020-04-14 22:10:10 +00:00
|
|
|
pub async fn get_rooms(&self) -> HashMap<RoomId, Arc<tokio::sync::RwLock<Room>>> {
|
2020-04-01 01:08:25 +00:00
|
|
|
self.base_client.read().await.joined_rooms.clone()
|
2020-03-27 21:22:11 +00:00
|
|
|
}
|
|
|
|
|
2020-02-21 13:29:25 +00:00
|
|
|
/// Login to the server.
|
|
|
|
///
|
|
|
|
/// # Arguments
|
|
|
|
///
|
2020-03-02 10:31:03 +00:00
|
|
|
/// * `user` - The user that should be logged in to the homeserver.
|
|
|
|
///
|
|
|
|
/// * `password` - The password of the user.
|
|
|
|
///
|
|
|
|
/// * `device_id` - A unique id that will be associated with this session. If
|
2020-03-30 18:18:08 +00:00
|
|
|
/// not given the homeserver will create one. Can be an existing
|
2020-02-21 13:29:25 +00:00
|
|
|
/// device_id from a previous login call. Note that this should be done
|
2020-03-30 18:18:08 +00:00
|
|
|
/// only if the client also holds the encryption keys for this device.
|
2020-03-19 12:55:04 +00:00
|
|
|
#[instrument(skip(password))]
|
|
|
|
pub async fn login<S: Into<String> + std::fmt::Debug>(
|
2020-04-15 12:29:34 +00:00
|
|
|
&self,
|
2019-10-23 20:47:00 +00:00
|
|
|
user: S,
|
|
|
|
password: S,
|
|
|
|
device_id: Option<S>,
|
2020-03-27 08:22:29 +00:00
|
|
|
initial_device_display_name: Option<S>,
|
2020-03-18 13:15:56 +00:00
|
|
|
) -> Result<login::Response> {
|
2020-03-19 12:55:04 +00:00
|
|
|
info!("Logging in to {} as {:?}", self.homeserver, user);
|
|
|
|
|
2019-10-23 20:47:00 +00:00
|
|
|
let request = login::Request {
|
2020-02-21 15:33:08 +00:00
|
|
|
user: login::UserInfo::MatrixId(user.into()),
|
|
|
|
login_info: login::LoginInfo::Password {
|
|
|
|
password: password.into(),
|
|
|
|
},
|
2019-10-23 20:47:00 +00:00
|
|
|
device_id: device_id.map(|d| d.into()),
|
2020-03-27 08:22:29 +00:00
|
|
|
initial_device_display_name: initial_device_display_name.map(|d| d.into()),
|
2019-10-23 20:47:00 +00:00
|
|
|
};
|
|
|
|
|
2019-10-30 18:30:55 +00:00
|
|
|
let response = self.send(request).await?;
|
2020-03-11 10:42:59 +00:00
|
|
|
let mut client = self.base_client.write().await;
|
2020-03-18 13:15:56 +00:00
|
|
|
client.receive_login_response(&response).await?;
|
2019-10-23 20:47:00 +00:00
|
|
|
|
|
|
|
Ok(response)
|
|
|
|
}
|
|
|
|
|
2020-04-10 20:32:28 +00:00
|
|
|
/// Join a room by `RoomId`.
|
|
|
|
///
|
|
|
|
/// Returns a `join_room_by_id::Response` consisting of the
|
|
|
|
/// joined rooms `RoomId`.
|
|
|
|
///
|
|
|
|
/// # Arguments
|
|
|
|
///
|
2020-04-14 10:36:03 +00:00
|
|
|
/// * room_id - The `RoomId` of the room to be joined.
|
2020-04-10 20:32:28 +00:00
|
|
|
pub async fn join_room_by_id(&mut self, room_id: &RoomId) -> Result<join_room_by_id::Response> {
|
|
|
|
let request = join_room_by_id::Request {
|
|
|
|
room_id: room_id.clone(),
|
|
|
|
third_party_signed: None,
|
|
|
|
};
|
|
|
|
self.send(request).await
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Join a room by `RoomId`.
|
|
|
|
///
|
|
|
|
/// Returns a `join_room_by_id_or_alias::Response` consisting of the
|
|
|
|
/// joined rooms `RoomId`.
|
|
|
|
///
|
|
|
|
/// # Arguments
|
|
|
|
///
|
2020-04-14 10:36:03 +00:00
|
|
|
/// * alias - The `RoomId` or `RoomAliasId` of the room to be joined.
|
|
|
|
/// An alias looks like this `#name:example.com`
|
2020-04-10 20:32:28 +00:00
|
|
|
pub async fn join_room_by_id_or_alias(
|
2020-04-15 12:29:34 +00:00
|
|
|
&self,
|
2020-04-10 20:32:28 +00:00
|
|
|
alias: &RoomIdOrAliasId,
|
2020-04-23 08:52:47 +00:00
|
|
|
server_name: &str,
|
2020-04-10 20:32:28 +00:00
|
|
|
) -> Result<join_room_by_id_or_alias::Response> {
|
|
|
|
let request = join_room_by_id_or_alias::Request {
|
|
|
|
room_id_or_alias: alias.clone(),
|
2020-04-23 08:52:47 +00:00
|
|
|
server_name: server_name.to_owned(),
|
2020-04-10 20:32:28 +00:00
|
|
|
third_party_signed: None,
|
|
|
|
};
|
|
|
|
self.send(request).await
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Kick a user out of the specified room.
|
|
|
|
///
|
|
|
|
/// Returns a `kick_user::Response`, an empty response.
|
|
|
|
///
|
|
|
|
/// # Arguments
|
|
|
|
///
|
2020-04-14 10:36:03 +00:00
|
|
|
/// * room_id - The `RoomId` of the room the user should be kicked out of.
|
2020-04-10 20:32:28 +00:00
|
|
|
///
|
2020-04-14 10:36:03 +00:00
|
|
|
/// * user_id - The `UserId` of the user that should be kicked out of the room.
|
2020-04-10 20:32:28 +00:00
|
|
|
///
|
|
|
|
/// * reason - Optional reason why the room member is being kicked out.
|
|
|
|
pub async fn kick_user(
|
2020-04-15 12:29:34 +00:00
|
|
|
&self,
|
2020-04-10 20:32:28 +00:00
|
|
|
room_id: &RoomId,
|
|
|
|
user_id: &UserId,
|
|
|
|
reason: Option<String>,
|
|
|
|
) -> Result<kick_user::Response> {
|
|
|
|
let request = kick_user::Request {
|
|
|
|
reason,
|
|
|
|
room_id: room_id.clone(),
|
|
|
|
user_id: user_id.clone(),
|
|
|
|
};
|
|
|
|
self.send(request).await
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Leave the specified room.
|
|
|
|
///
|
|
|
|
/// Returns a `leave_room::Response`, an empty response.
|
|
|
|
///
|
|
|
|
/// # Arguments
|
|
|
|
///
|
2020-04-14 10:36:03 +00:00
|
|
|
/// * room_id - The `RoomId` of the room to leave.
|
2020-04-10 20:32:28 +00:00
|
|
|
///
|
2020-04-15 12:29:34 +00:00
|
|
|
pub async fn leave_room(&self, room_id: &RoomId) -> Result<leave_room::Response> {
|
2020-04-10 20:32:28 +00:00
|
|
|
let request = leave_room::Request {
|
|
|
|
room_id: room_id.clone(),
|
|
|
|
};
|
|
|
|
self.send(request).await
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Invite the specified user by `UserId` to the given room.
|
|
|
|
///
|
|
|
|
/// Returns a `invite_user::Response`, an empty response.
|
|
|
|
///
|
|
|
|
/// # Arguments
|
|
|
|
///
|
2020-04-14 10:36:03 +00:00
|
|
|
/// * room_id - The `RoomId` of the room to invite the specified user to.
|
2020-04-10 20:32:28 +00:00
|
|
|
///
|
2020-04-14 10:36:03 +00:00
|
|
|
/// * user_id - The `UserId` of the user to invite to the room.
|
2020-04-10 20:32:28 +00:00
|
|
|
pub async fn invite_user_by_id(
|
2020-04-15 12:29:34 +00:00
|
|
|
&self,
|
2020-04-10 20:32:28 +00:00
|
|
|
room_id: &RoomId,
|
|
|
|
user_id: &UserId,
|
|
|
|
) -> Result<invite_user::Response> {
|
|
|
|
let request = invite_user::Request {
|
|
|
|
room_id: room_id.clone(),
|
|
|
|
recipient: InvitationRecipient::UserId {
|
|
|
|
user_id: user_id.clone(),
|
|
|
|
},
|
|
|
|
};
|
|
|
|
self.send(request).await
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Invite the specified user by third party id to the given room.
|
|
|
|
///
|
|
|
|
/// Returns a `invite_user::Response`, an empty response.
|
|
|
|
///
|
|
|
|
/// # Arguments
|
|
|
|
///
|
2020-04-14 10:36:03 +00:00
|
|
|
/// * room_id - The `RoomId` of the room to invite the specified user to.
|
2020-04-10 20:32:28 +00:00
|
|
|
///
|
2020-04-14 10:36:03 +00:00
|
|
|
/// * invite_id - A third party id of a user to invite to the room.
|
2020-04-10 20:32:28 +00:00
|
|
|
pub async fn invite_user_by_3pid(
|
2020-04-15 12:29:34 +00:00
|
|
|
&self,
|
2020-04-10 20:32:28 +00:00
|
|
|
room_id: &RoomId,
|
|
|
|
invite_id: &Invite3pid,
|
|
|
|
) -> Result<invite_user::Response> {
|
|
|
|
let request = invite_user::Request {
|
|
|
|
room_id: room_id.clone(),
|
|
|
|
recipient: InvitationRecipient::ThirdPartyId(invite_id.clone()),
|
|
|
|
};
|
|
|
|
self.send(request).await
|
|
|
|
}
|
|
|
|
|
2020-04-14 10:36:03 +00:00
|
|
|
/// Create a room using the `RoomBuilder` and send the request.
|
2020-04-10 20:32:28 +00:00
|
|
|
///
|
2020-04-14 10:36:03 +00:00
|
|
|
/// Sends a request to `/_matrix/client/r0/createRoom`, returns a `create_room::Response`,
|
|
|
|
/// this is an empty response.
|
2020-04-10 20:32:28 +00:00
|
|
|
///
|
|
|
|
/// # Arguments
|
|
|
|
///
|
2020-04-14 10:36:03 +00:00
|
|
|
/// * room - The easiest way to create this request is using the `RoomBuilder`.
|
2020-04-10 20:32:28 +00:00
|
|
|
///
|
|
|
|
/// # Examples
|
2020-04-14 10:36:03 +00:00
|
|
|
/// ```no_run
|
2020-04-10 20:32:28 +00:00
|
|
|
/// use matrix_sdk::{AsyncClient, RoomBuilder};
|
2020-04-14 10:36:03 +00:00
|
|
|
/// # use matrix_sdk::api::r0::room::Visibility;
|
|
|
|
/// # use url::Url;
|
2020-04-14 10:42:42 +00:00
|
|
|
///
|
2020-04-14 10:36:03 +00:00
|
|
|
/// # let homeserver = Url::parse("http://example.com").unwrap();
|
2020-04-13 18:08:51 +00:00
|
|
|
/// let mut builder = RoomBuilder::default();
|
|
|
|
/// builder.creation_content(false)
|
2020-04-10 20:32:28 +00:00
|
|
|
/// .initial_state(vec![])
|
|
|
|
/// .visibility(Visibility::Public)
|
|
|
|
/// .name("name")
|
|
|
|
/// .room_version("v1.0");
|
|
|
|
///
|
2020-04-14 10:36:03 +00:00
|
|
|
/// let mut cli = AsyncClient::new(homeserver, None).unwrap();
|
|
|
|
/// # use futures::executor::block_on;
|
|
|
|
/// # block_on(async {
|
2020-04-13 18:08:51 +00:00
|
|
|
/// assert!(cli.create_room(builder).await.is_ok());
|
2020-04-14 10:36:03 +00:00
|
|
|
/// # });
|
2020-04-10 20:32:28 +00:00
|
|
|
/// ```
|
|
|
|
pub async fn create_room<R: Into<create_room::Request>>(
|
2020-04-15 12:29:34 +00:00
|
|
|
&self,
|
2020-04-10 20:32:28 +00:00
|
|
|
room: R,
|
|
|
|
) -> Result<create_room::Response> {
|
|
|
|
let request = room.into();
|
|
|
|
self.send(request).await
|
|
|
|
}
|
|
|
|
|
2020-04-14 10:36:03 +00:00
|
|
|
/// Get messages starting at a specific sync point using the
|
|
|
|
/// `MessagesRequestBuilder`s `from` field as a starting point.
|
2020-04-11 12:46:45 +00:00
|
|
|
///
|
2020-04-14 10:36:03 +00:00
|
|
|
/// Sends a request to `/_matrix/client/r0/rooms/{room_id}/messages` and
|
|
|
|
/// returns a `get_message_events::IncomingResponse` that contains chunks
|
|
|
|
/// of `RoomEvents`.
|
2020-04-11 12:46:45 +00:00
|
|
|
///
|
|
|
|
/// # Arguments
|
|
|
|
///
|
2020-04-14 10:42:42 +00:00
|
|
|
/// * request - The easiest way to create a `Request` is using the
|
2020-04-14 10:36:03 +00:00
|
|
|
/// `MessagesRequestBuilder`.
|
2020-04-13 18:46:54 +00:00
|
|
|
///
|
2020-04-13 18:08:51 +00:00
|
|
|
/// # Examples
|
2020-04-14 10:36:03 +00:00
|
|
|
/// ```no_run
|
|
|
|
/// # use std::convert::TryFrom;
|
|
|
|
/// use matrix_sdk::{AsyncClient, MessagesRequestBuilder};
|
|
|
|
/// # use matrix_sdk::identifiers::RoomId;
|
|
|
|
/// # use matrix_sdk::api::r0::filter::RoomEventFilter;
|
|
|
|
/// # use matrix_sdk::api::r0::message::get_message_events::Direction;
|
|
|
|
/// # use url::Url;
|
|
|
|
/// # use js_int::UInt;
|
2020-04-14 10:42:42 +00:00
|
|
|
///
|
2020-04-14 10:36:03 +00:00
|
|
|
/// # let homeserver = Url::parse("http://example.com").unwrap();
|
|
|
|
/// let mut builder = MessagesRequestBuilder::new();
|
2020-04-13 18:08:51 +00:00
|
|
|
/// builder.room_id(RoomId::try_from("!roomid:example.com").unwrap())
|
|
|
|
/// .from("t47429-4392820_219380_26003_2265".to_string())
|
|
|
|
/// .to("t4357353_219380_26003_2265".to_string())
|
|
|
|
/// .direction(Direction::Backward)
|
|
|
|
/// .limit(UInt::new(10).unwrap());
|
2020-04-13 18:46:54 +00:00
|
|
|
///
|
2020-04-14 10:36:03 +00:00
|
|
|
/// let mut cli = AsyncClient::new(homeserver, None).unwrap();
|
|
|
|
/// # use futures::executor::block_on;
|
|
|
|
/// # block_on(async {
|
|
|
|
/// assert!(cli.room_messages(builder).await.is_ok());
|
|
|
|
/// # });
|
2020-04-13 18:08:51 +00:00
|
|
|
/// ```
|
2020-04-12 12:21:23 +00:00
|
|
|
pub async fn room_messages<R: Into<get_message_events::Request>>(
|
2020-04-15 12:29:34 +00:00
|
|
|
&self,
|
2020-04-11 12:46:45 +00:00
|
|
|
request: R,
|
2020-04-23 08:52:47 +00:00
|
|
|
) -> Result<get_message_events::Response> {
|
2020-04-11 12:46:45 +00:00
|
|
|
let req = request.into();
|
2020-04-13 18:08:51 +00:00
|
|
|
self.send(req).await
|
2020-04-11 12:46:45 +00:00
|
|
|
}
|
|
|
|
|
2020-03-30 18:18:08 +00:00
|
|
|
/// Synchronize the client's state with the latest state on the server.
|
2020-02-21 13:29:25 +00:00
|
|
|
///
|
|
|
|
/// # Arguments
|
|
|
|
///
|
|
|
|
/// * `sync_settings` - Settings for the sync call.
|
2020-03-19 12:55:04 +00:00
|
|
|
#[instrument]
|
2020-04-23 08:52:47 +00:00
|
|
|
pub async fn sync(&self, sync_settings: SyncSettings) -> Result<sync_events::Response> {
|
2019-10-23 20:47:00 +00:00
|
|
|
let request = sync_events::Request {
|
|
|
|
filter: None,
|
|
|
|
since: sync_settings.token,
|
|
|
|
full_state: sync_settings.full_state,
|
2020-04-03 12:09:56 +00:00
|
|
|
set_presence: sync_events::SetPresence::Online,
|
2019-10-23 20:47:00 +00:00
|
|
|
timeout: sync_settings.timeout,
|
|
|
|
};
|
|
|
|
|
2020-03-12 14:41:11 +00:00
|
|
|
let mut response = self.send(request).await?;
|
2019-10-23 20:47:00 +00:00
|
|
|
|
2020-03-25 14:03:10 +00:00
|
|
|
for (room_id, room) in &mut response.rooms.join {
|
2020-04-17 01:00:50 +00:00
|
|
|
let matrix_room = {
|
2020-04-14 22:10:10 +00:00
|
|
|
let mut client = self.base_client.write().await;
|
2019-11-26 19:34:11 +00:00
|
|
|
for event in &room.state.events {
|
2020-04-23 08:52:47 +00:00
|
|
|
if let Ok(e) = event.deserialize() {
|
2020-04-03 13:07:40 +00:00
|
|
|
client.receive_joined_state_event(&room_id, &e).await;
|
2019-11-26 19:34:11 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-03 13:07:40 +00:00
|
|
|
client.get_or_create_room(&room_id).clone()
|
2019-11-26 19:34:11 +00:00
|
|
|
};
|
2019-10-23 21:36:57 +00:00
|
|
|
|
2020-04-17 01:00:50 +00:00
|
|
|
// RoomSummary contains information for calculating room name
|
|
|
|
matrix_room.write().await.set_room_summary(&room.summary);
|
|
|
|
|
2020-03-31 23:34:11 +00:00
|
|
|
// re looping is not ideal here
|
|
|
|
for event in &mut room.state.events {
|
2020-04-23 08:52:47 +00:00
|
|
|
if let Ok(e) = event.deserialize() {
|
2020-04-14 22:10:10 +00:00
|
|
|
let client = self.base_client.read().await;
|
2020-04-23 08:52:47 +00:00
|
|
|
client.emit_state_event(&room_id, &e).await;
|
2020-03-31 23:34:11 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-25 14:03:10 +00:00
|
|
|
for mut event in &mut room.timeline.events {
|
|
|
|
let decrypted_event = {
|
2020-04-14 22:10:10 +00:00
|
|
|
let mut client = self.base_client.write().await;
|
2020-03-25 14:03:10 +00:00
|
|
|
client
|
|
|
|
.receive_joined_timeline_event(room_id, &mut event)
|
|
|
|
.await
|
|
|
|
};
|
2019-10-30 18:30:55 +00:00
|
|
|
|
2020-03-26 10:23:39 +00:00
|
|
|
if let Some(e) = decrypted_event {
|
|
|
|
*event = e;
|
|
|
|
}
|
2020-03-28 12:58:02 +00:00
|
|
|
|
2020-04-23 08:52:47 +00:00
|
|
|
if let Ok(e) = event.deserialize() {
|
2020-04-14 22:10:10 +00:00
|
|
|
let client = self.base_client.read().await;
|
2020-04-23 08:52:47 +00:00
|
|
|
client.emit_timeline_event(&room_id, &e).await;
|
2019-11-26 18:06:29 +00:00
|
|
|
}
|
2019-10-23 20:47:00 +00:00
|
|
|
}
|
2020-03-29 12:05:40 +00:00
|
|
|
|
2020-03-29 20:24:31 +00:00
|
|
|
// look at AccountData to further cut down users by collecting ignored users
|
2020-03-31 23:34:11 +00:00
|
|
|
for account_data in &mut room.account_data.events {
|
|
|
|
{
|
2020-04-23 08:52:47 +00:00
|
|
|
if let Ok(e) = account_data.deserialize() {
|
2020-04-14 22:10:10 +00:00
|
|
|
let mut client = self.base_client.write().await;
|
2020-04-23 08:52:47 +00:00
|
|
|
client.receive_account_data_event(&room_id, &e).await;
|
|
|
|
client.emit_account_data_event(&room_id, &e).await;
|
2020-03-31 23:34:11 +00:00
|
|
|
}
|
2020-03-29 20:24:31 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-29 12:05:40 +00:00
|
|
|
// After the room has been created and state/timeline events accounted for we use the room_id of the newly created
|
2020-03-29 20:24:31 +00:00
|
|
|
// room to add any presence events that relate to a user in the current room. This is not super
|
2020-03-29 12:05:40 +00:00
|
|
|
// efficient but we need a room_id so we would loop through now or later.
|
2020-03-31 23:34:11 +00:00
|
|
|
for presence in &mut response.presence.events {
|
|
|
|
{
|
2020-04-23 08:52:47 +00:00
|
|
|
if let Ok(e) = presence.deserialize() {
|
2020-04-14 22:10:10 +00:00
|
|
|
let mut client = self.base_client.write().await;
|
2020-04-23 08:52:47 +00:00
|
|
|
client.receive_presence_event(&room_id, &e).await;
|
2020-03-29 12:05:40 +00:00
|
|
|
|
2020-04-23 08:52:47 +00:00
|
|
|
client.emit_presence_event(&room_id, &e).await;
|
2020-03-29 12:05:40 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-04-07 12:55:42 +00:00
|
|
|
|
|
|
|
for ephemeral in &mut room.ephemeral.events {
|
|
|
|
{
|
2020-04-23 08:52:47 +00:00
|
|
|
if let Ok(e) = ephemeral.deserialize() {
|
2020-04-14 22:10:10 +00:00
|
|
|
let mut client = self.base_client.write().await;
|
2020-04-23 08:52:47 +00:00
|
|
|
client.receive_ephemeral_event(&room_id, &e).await;
|
2020-04-07 12:55:42 +00:00
|
|
|
|
2020-04-23 08:52:47 +00:00
|
|
|
client.emit_ephemeral_event(&room_id, &e).await;
|
2020-04-07 12:55:42 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2019-10-23 20:47:00 +00:00
|
|
|
}
|
|
|
|
|
2020-03-11 10:42:59 +00:00
|
|
|
let mut client = self.base_client.write().await;
|
2020-03-12 14:41:11 +00:00
|
|
|
client.receive_sync_response(&mut response).await;
|
2020-03-11 10:42:59 +00:00
|
|
|
|
2019-10-23 20:47:00 +00:00
|
|
|
Ok(response)
|
|
|
|
}
|
|
|
|
|
2020-02-21 13:29:25 +00:00
|
|
|
/// Repeatedly call sync to synchronize the client state with the server.
|
|
|
|
///
|
|
|
|
/// # Arguments
|
|
|
|
///
|
|
|
|
/// * `sync_settings` - Settings for the sync call. Note that those settings
|
|
|
|
/// will be only used for the first sync call.
|
2020-03-02 10:31:03 +00:00
|
|
|
///
|
2020-02-21 13:29:25 +00:00
|
|
|
/// * `callback` - A callback that will be called every time a successful
|
|
|
|
/// response has been fetched from the server.
|
|
|
|
///
|
|
|
|
/// # Examples
|
|
|
|
///
|
|
|
|
/// The following example demonstrates how to sync forever while sending all
|
|
|
|
/// the interesting events through a mpsc channel to another thread e.g. a
|
|
|
|
/// UI thread.
|
|
|
|
///
|
2020-03-02 10:31:03 +00:00
|
|
|
/// ```compile_fail,E0658
|
|
|
|
/// # use matrix_sdk::events::{
|
|
|
|
/// # collections::all::RoomEvent,
|
|
|
|
/// # room::message::{MessageEvent, MessageEventContent, TextMessageEventContent},
|
|
|
|
/// # EventResult,
|
|
|
|
/// # };
|
|
|
|
/// # use matrix_sdk::Room;
|
|
|
|
/// # use std::sync::{Arc, RwLock};
|
|
|
|
/// # use matrix_sdk::{AsyncClient, SyncSettings};
|
|
|
|
/// # use url::Url;
|
|
|
|
/// # use futures::executor::block_on;
|
|
|
|
/// # block_on(async {
|
|
|
|
/// # let homeserver = Url::parse("http://localhost:8080").unwrap();
|
|
|
|
/// # let mut client = AsyncClient::new(homeserver, None).unwrap();
|
|
|
|
///
|
|
|
|
/// use async_std::sync::channel;
|
|
|
|
///
|
|
|
|
/// let (tx, rx) = channel(100);
|
|
|
|
///
|
|
|
|
/// let sync_channel = &tx;
|
|
|
|
/// let sync_settings = SyncSettings::new()
|
|
|
|
/// .timeout(30_000)
|
|
|
|
/// .unwrap();
|
|
|
|
///
|
2020-02-21 13:29:25 +00:00
|
|
|
/// client
|
|
|
|
/// .sync_forever(sync_settings, async move |response| {
|
|
|
|
/// let channel = sync_channel;
|
2020-03-02 10:31:03 +00:00
|
|
|
///
|
2020-02-21 13:29:25 +00:00
|
|
|
/// for (room_id, room) in response.rooms.join {
|
|
|
|
/// for event in room.timeline.events {
|
|
|
|
/// if let EventResult::Ok(e) = event {
|
|
|
|
/// channel.send(e).await;
|
|
|
|
/// }
|
|
|
|
/// }
|
|
|
|
/// }
|
|
|
|
/// })
|
|
|
|
/// .await;
|
2020-03-02 10:31:03 +00:00
|
|
|
/// })
|
2020-02-21 13:29:25 +00:00
|
|
|
/// ```
|
2020-03-19 12:55:04 +00:00
|
|
|
#[instrument(skip(callback))]
|
2019-12-04 21:33:26 +00:00
|
|
|
pub async fn sync_forever<C>(
|
2020-04-15 12:29:34 +00:00
|
|
|
&self,
|
2019-12-04 21:33:26 +00:00
|
|
|
sync_settings: SyncSettings,
|
2020-04-23 08:52:47 +00:00
|
|
|
callback: impl Fn(sync_events::Response) -> C + Send,
|
2019-12-04 21:33:26 +00:00
|
|
|
) where
|
|
|
|
C: Future<Output = ()>,
|
|
|
|
{
|
|
|
|
let mut sync_settings = sync_settings;
|
2020-02-28 09:33:17 +00:00
|
|
|
let mut last_sync_time: Option<Instant> = None;
|
2019-12-04 21:33:26 +00:00
|
|
|
|
|
|
|
loop {
|
|
|
|
let response = self.sync(sync_settings.clone()).await;
|
|
|
|
|
|
|
|
let response = if let Ok(r) = response {
|
|
|
|
r
|
|
|
|
} else {
|
|
|
|
sleep(Duration::from_secs(1)).await;
|
|
|
|
continue;
|
|
|
|
};
|
|
|
|
|
|
|
|
callback(response).await;
|
|
|
|
|
2020-03-11 10:43:58 +00:00
|
|
|
// TODO send out to-device messages here
|
|
|
|
|
|
|
|
#[cfg(feature = "encryption")]
|
|
|
|
{
|
|
|
|
if self.base_client.read().await.should_upload_keys().await {
|
|
|
|
let _ = self.keys_upload().await;
|
|
|
|
}
|
2020-04-01 13:37:00 +00:00
|
|
|
|
|
|
|
if self.base_client.read().await.should_query_keys().await {
|
2020-04-03 08:30:32 +00:00
|
|
|
let _ = self.keys_query().await;
|
2020-04-01 13:37:00 +00:00
|
|
|
}
|
2020-03-11 10:43:58 +00:00
|
|
|
}
|
|
|
|
|
2020-02-28 09:33:17 +00:00
|
|
|
let now = Instant::now();
|
|
|
|
|
|
|
|
// If the last sync happened less than a second ago, sleep for a
|
|
|
|
// while to not hammer out requests if the server doesn't respect
|
|
|
|
// the sync timeout.
|
|
|
|
if let Some(t) = last_sync_time {
|
|
|
|
if now - t <= Duration::from_secs(1) {
|
|
|
|
sleep(Duration::from_secs(1)).await;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
last_sync_time = Some(now);
|
|
|
|
|
2020-03-24 15:18:56 +00:00
|
|
|
sync_settings = SyncSettings::new().timeout(DEFAULT_SYNC_TIMEOUT).token(
|
|
|
|
self.sync_token()
|
|
|
|
.await
|
|
|
|
.expect("No sync token found after initial sync"),
|
|
|
|
);
|
2019-12-04 21:33:26 +00:00
|
|
|
}
|
|
|
|
}
|
2019-11-26 19:34:11 +00:00
|
|
|
|
2020-04-16 20:39:44 +00:00
|
|
|
async fn send<Request: Endpoint<ResponseError = ruma_client_api::Error> + std::fmt::Debug>(
|
2019-12-04 18:31:33 +00:00
|
|
|
&self,
|
|
|
|
request: Request,
|
2020-04-23 08:52:47 +00:00
|
|
|
) -> Result<Request::Response> {
|
2019-10-23 20:47:00 +00:00
|
|
|
let request: http::Request<Vec<u8>> = request.try_into()?;
|
|
|
|
let url = request.uri();
|
2020-04-09 14:27:43 +00:00
|
|
|
let path_and_query = url.path_and_query().unwrap();
|
|
|
|
let mut url = self.homeserver.clone();
|
|
|
|
|
|
|
|
url.set_path(path_and_query.path());
|
|
|
|
url.set_query(path_and_query.query());
|
2019-10-23 20:47:00 +00:00
|
|
|
|
2020-03-19 12:55:04 +00:00
|
|
|
trace!("Doing request {:?}", url);
|
|
|
|
|
2019-10-23 20:47:00 +00:00
|
|
|
let request_builder = match Request::METADATA.method {
|
|
|
|
HttpMethod::GET => self.http_client.get(url),
|
|
|
|
HttpMethod::POST => {
|
|
|
|
let body = request.body().clone();
|
2020-04-09 14:28:27 +00:00
|
|
|
self.http_client
|
|
|
|
.post(url)
|
|
|
|
.body(body)
|
|
|
|
.header(reqwest::header::CONTENT_TYPE, "application/json")
|
2019-10-23 20:47:00 +00:00
|
|
|
}
|
2019-11-10 10:44:03 +00:00
|
|
|
HttpMethod::PUT => {
|
|
|
|
let body = request.body().clone();
|
2020-04-09 14:28:27 +00:00
|
|
|
self.http_client
|
|
|
|
.put(url)
|
|
|
|
.body(body)
|
|
|
|
.header(reqwest::header::CONTENT_TYPE, "application/json")
|
2019-11-10 10:44:03 +00:00
|
|
|
}
|
2019-10-23 20:47:00 +00:00
|
|
|
HttpMethod::DELETE => unimplemented!(),
|
|
|
|
_ => panic!("Unsuported method"),
|
|
|
|
};
|
|
|
|
|
|
|
|
let request_builder = if Request::METADATA.requires_authentication {
|
2020-03-11 10:42:59 +00:00
|
|
|
let client = self.base_client.read().await;
|
2019-11-10 10:44:03 +00:00
|
|
|
|
|
|
|
if let Some(ref session) = client.session {
|
2019-10-23 20:47:00 +00:00
|
|
|
request_builder.bearer_auth(&session.access_token)
|
|
|
|
} else {
|
2020-03-18 13:15:56 +00:00
|
|
|
return Err(Error::AuthenticationRequired);
|
2019-10-23 20:47:00 +00:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
request_builder
|
|
|
|
};
|
2020-03-19 12:55:04 +00:00
|
|
|
let mut response = request_builder.send().await?;
|
|
|
|
|
|
|
|
trace!("Got response: {:?}", response);
|
2019-10-23 20:47:00 +00:00
|
|
|
|
|
|
|
let status = response.status();
|
2020-04-16 20:39:44 +00:00
|
|
|
let mut http_builder = HttpResponse::builder().status(status);
|
|
|
|
let headers = http_builder.headers_mut().unwrap();
|
2020-03-19 12:55:04 +00:00
|
|
|
|
|
|
|
for (k, v) in response.headers_mut().drain() {
|
|
|
|
if let Some(key) = k {
|
|
|
|
headers.insert(key, v);
|
|
|
|
}
|
|
|
|
}
|
2019-10-23 20:47:00 +00:00
|
|
|
let body = response.bytes().await?.as_ref().to_owned();
|
2020-04-16 20:39:44 +00:00
|
|
|
let http_response = http_builder.body(body).unwrap();
|
2019-10-23 20:47:00 +00:00
|
|
|
|
2020-04-23 08:52:47 +00:00
|
|
|
Ok(<Request::Response>::try_from(http_response)?)
|
2019-10-23 20:47:00 +00:00
|
|
|
}
|
2019-11-10 10:44:44 +00:00
|
|
|
|
2020-02-21 13:29:25 +00:00
|
|
|
/// Send a room message to the homeserver.
|
|
|
|
///
|
2020-03-02 10:31:03 +00:00
|
|
|
/// Returns the parsed response from the server.
|
|
|
|
///
|
2020-04-10 09:53:07 +00:00
|
|
|
/// If the encryption feature is enabled this method will transparently
|
|
|
|
/// encrypt the room message if the given room is encrypted.
|
|
|
|
///
|
2020-02-21 13:29:25 +00:00
|
|
|
/// # Arguments
|
|
|
|
///
|
2020-03-02 10:31:03 +00:00
|
|
|
/// * `room_id` - The id of the room that should receive the message.
|
2020-02-21 13:29:25 +00:00
|
|
|
///
|
2020-04-10 09:53:07 +00:00
|
|
|
/// * `content` - The content of the message event.
|
2020-04-13 18:46:54 +00:00
|
|
|
///
|
2020-04-13 18:08:51 +00:00
|
|
|
/// * `txn_id` - A unique `Uuid` that can be attached to a `MessageEvent` held
|
2020-04-13 18:46:54 +00:00
|
|
|
/// in it's unsigned field as `transaction_id`. If not given one is created for the
|
|
|
|
/// message.
|
2020-04-10 09:53:07 +00:00
|
|
|
///
|
|
|
|
/// # Example
|
|
|
|
/// ```no_run
|
|
|
|
/// # use matrix_sdk::Room;
|
|
|
|
/// # use std::sync::{Arc, RwLock};
|
|
|
|
/// # use matrix_sdk::{AsyncClient, SyncSettings};
|
|
|
|
/// # use url::Url;
|
|
|
|
/// # use futures::executor::block_on;
|
|
|
|
/// # use ruma_identifiers::RoomId;
|
|
|
|
/// # use std::convert::TryFrom;
|
|
|
|
/// use matrix_sdk::events::room::message::{MessageEventContent, TextMessageEventContent};
|
|
|
|
/// # block_on(async {
|
|
|
|
/// # let homeserver = Url::parse("http://localhost:8080").unwrap();
|
|
|
|
/// # let mut client = AsyncClient::new(homeserver, None).unwrap();
|
|
|
|
/// # let room_id = RoomId::try_from("!test:localhost").unwrap();
|
2020-04-13 18:08:51 +00:00
|
|
|
/// use uuid::Uuid;
|
2020-04-13 18:46:54 +00:00
|
|
|
///
|
2020-04-10 09:53:07 +00:00
|
|
|
/// let content = MessageEventContent::Text(TextMessageEventContent {
|
|
|
|
/// body: "Hello world".to_owned(),
|
|
|
|
/// format: None,
|
|
|
|
/// formatted_body: None,
|
|
|
|
/// relates_to: None,
|
|
|
|
/// });
|
2020-04-13 18:08:51 +00:00
|
|
|
/// let txn_id = Uuid::new_v4();
|
2020-04-13 18:46:54 +00:00
|
|
|
/// client.room_send(&room_id, content, Some(txn_id)).await.unwrap();
|
2020-04-10 09:53:07 +00:00
|
|
|
/// })
|
|
|
|
/// ```
|
2019-11-10 17:33:06 +00:00
|
|
|
pub async fn room_send(
|
2020-04-15 12:29:34 +00:00
|
|
|
&self,
|
2020-04-03 15:00:37 +00:00
|
|
|
room_id: &RoomId,
|
2020-04-10 09:53:07 +00:00
|
|
|
#[allow(unused_mut)] mut content: MessageEventContent,
|
2020-04-13 18:08:51 +00:00
|
|
|
txn_id: Option<Uuid>,
|
2020-03-18 13:15:56 +00:00
|
|
|
) -> Result<create_message_event::Response> {
|
2020-04-10 09:53:07 +00:00
|
|
|
#[allow(unused_mut)]
|
2020-04-09 14:26:00 +00:00
|
|
|
let mut event_type = EventType::RoomMessage;
|
|
|
|
|
2020-04-03 08:30:32 +00:00
|
|
|
#[cfg(feature = "encryption")]
|
|
|
|
{
|
|
|
|
let encrypted = {
|
|
|
|
let client = self.base_client.read().await;
|
|
|
|
let room = client.joined_rooms.get(room_id);
|
|
|
|
|
|
|
|
match room {
|
2020-04-15 12:29:34 +00:00
|
|
|
Some(r) => r.read().await.is_encrypted(),
|
2020-04-03 08:30:32 +00:00
|
|
|
None => false,
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
if encrypted {
|
|
|
|
let missing_sessions = {
|
|
|
|
let client = self.base_client.read().await;
|
|
|
|
let room = client.joined_rooms.get(room_id);
|
2020-04-15 12:29:34 +00:00
|
|
|
let room = room.as_ref().unwrap().read().await;
|
2020-04-03 08:30:32 +00:00
|
|
|
let users = room.members.keys();
|
|
|
|
self.base_client
|
|
|
|
.read()
|
|
|
|
.await
|
|
|
|
.get_missing_sessions(users)
|
|
|
|
.await
|
|
|
|
};
|
|
|
|
|
|
|
|
if !missing_sessions.is_empty() {
|
2020-04-10 09:50:18 +00:00
|
|
|
self.claim_one_time_keys(missing_sessions).await?;
|
2020-04-03 08:30:32 +00:00
|
|
|
}
|
2020-04-10 09:50:18 +00:00
|
|
|
|
|
|
|
if self
|
|
|
|
.base_client
|
|
|
|
.read()
|
|
|
|
.await
|
|
|
|
.should_share_group_session(room_id)
|
|
|
|
.await
|
|
|
|
{
|
|
|
|
// TODO we need to make sure that only one such request is
|
|
|
|
// in flight per room at a time.
|
|
|
|
self.share_group_session(room_id).await?;
|
|
|
|
}
|
|
|
|
|
|
|
|
content = self
|
|
|
|
.base_client
|
|
|
|
.read()
|
|
|
|
.await
|
|
|
|
.encrypt(room_id, content)
|
|
|
|
.await?;
|
|
|
|
event_type = EventType::RoomEncrypted;
|
2020-04-03 08:30:32 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-21 15:33:08 +00:00
|
|
|
let request = create_message_event::Request {
|
2020-04-03 15:00:37 +00:00
|
|
|
room_id: room_id.clone(),
|
2020-04-09 14:26:00 +00:00
|
|
|
event_type,
|
2020-04-14 10:42:42 +00:00
|
|
|
txn_id: txn_id.unwrap_or_else(Uuid::new_v4).to_string(),
|
2020-04-23 08:52:47 +00:00
|
|
|
data: EventJson::from(content),
|
2019-11-10 10:44:44 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
let response = self.send(request).await?;
|
|
|
|
Ok(response)
|
|
|
|
}
|
2019-11-24 21:40:52 +00:00
|
|
|
|
2020-04-03 08:27:30 +00:00
|
|
|
/// Claim one-time keys creating new Olm sessions.
|
|
|
|
///
|
|
|
|
/// # Arguments
|
|
|
|
///
|
|
|
|
/// * `users` - The list of user/device pairs that we should claim keys for.
|
|
|
|
///
|
|
|
|
/// # Panics
|
|
|
|
///
|
|
|
|
/// Panics if the client isn't logged in, or if no encryption keys need to
|
|
|
|
/// be uploaded.
|
|
|
|
#[cfg(feature = "encryption")]
|
|
|
|
#[cfg_attr(docsrs, doc(cfg(feature = "encryption")))]
|
|
|
|
#[instrument]
|
|
|
|
async fn claim_one_time_keys(
|
|
|
|
&self,
|
2020-04-23 08:52:47 +00:00
|
|
|
one_time_keys: BTreeMap<UserId, BTreeMap<DeviceId, KeyAlgorithm>>,
|
2020-04-03 08:27:30 +00:00
|
|
|
) -> Result<claim_keys::Response> {
|
|
|
|
let request = claim_keys::Request {
|
|
|
|
timeout: None,
|
|
|
|
one_time_keys,
|
|
|
|
};
|
|
|
|
|
|
|
|
let response = self.send(request).await?;
|
|
|
|
self.base_client
|
|
|
|
.write()
|
|
|
|
.await
|
|
|
|
.receive_keys_claim_response(&response)
|
|
|
|
.await?;
|
|
|
|
Ok(response)
|
|
|
|
}
|
|
|
|
|
2020-04-09 14:29:03 +00:00
|
|
|
/// Share a group session for a room.
|
|
|
|
///
|
|
|
|
/// # Arguments
|
|
|
|
///
|
|
|
|
/// * `room_id` - The ID of the room for which we want to share a group
|
|
|
|
/// session.
|
|
|
|
///
|
|
|
|
/// # Panics
|
|
|
|
///
|
|
|
|
/// Panics if the client isn't logged in.
|
|
|
|
#[cfg(feature = "encryption")]
|
|
|
|
#[cfg_attr(docsrs, doc(cfg(feature = "encryption")))]
|
|
|
|
#[instrument]
|
|
|
|
async fn share_group_session(&self, room_id: &RoomId) -> Result<()> {
|
|
|
|
let mut requests = self
|
|
|
|
.base_client
|
|
|
|
.read()
|
|
|
|
.await
|
|
|
|
.share_group_session(room_id)
|
|
|
|
.await
|
|
|
|
.expect("Keys don't need to be uploaded");
|
|
|
|
|
|
|
|
for request in requests.drain(..) {
|
2020-04-10 09:50:18 +00:00
|
|
|
let _response: send_event_to_device::Response = self.send(request).await?;
|
2020-04-09 14:29:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2020-03-11 10:42:59 +00:00
|
|
|
/// Upload the E2E encryption keys.
|
|
|
|
///
|
|
|
|
/// This uploads the long lived device keys as well as the required amount
|
|
|
|
/// of one-time keys.
|
|
|
|
///
|
|
|
|
/// # Panics
|
|
|
|
///
|
|
|
|
/// Panics if the client isn't logged in, or if no encryption keys need to
|
|
|
|
/// be uploaded.
|
|
|
|
#[cfg(feature = "encryption")]
|
2020-03-19 12:55:04 +00:00
|
|
|
#[cfg_attr(docsrs, doc(cfg(feature = "encryption")))]
|
|
|
|
#[instrument]
|
2020-03-18 13:15:56 +00:00
|
|
|
async fn keys_upload(&self) -> Result<upload_keys::Response> {
|
2020-03-11 10:42:59 +00:00
|
|
|
let (device_keys, one_time_keys) = self
|
|
|
|
.base_client
|
|
|
|
.read()
|
|
|
|
.await
|
|
|
|
.keys_for_upload()
|
|
|
|
.await
|
|
|
|
.expect("Keys don't need to be uploaded");
|
2020-03-19 12:55:04 +00:00
|
|
|
|
|
|
|
debug!(
|
|
|
|
"Uploading encryption keys device keys: {}, one-time-keys: {}",
|
|
|
|
device_keys.is_some(),
|
|
|
|
one_time_keys.as_ref().map_or(0, |k| k.len())
|
|
|
|
);
|
|
|
|
|
2020-03-11 10:42:59 +00:00
|
|
|
let request = upload_keys::Request {
|
|
|
|
device_keys,
|
|
|
|
one_time_keys,
|
|
|
|
};
|
|
|
|
|
|
|
|
let response = self.send(request).await?;
|
|
|
|
self.base_client
|
|
|
|
.write()
|
|
|
|
.await
|
|
|
|
.receive_keys_upload_response(&response)
|
2020-03-18 14:50:32 +00:00
|
|
|
.await?;
|
2020-03-11 10:42:59 +00:00
|
|
|
Ok(response)
|
|
|
|
}
|
|
|
|
|
2020-02-21 13:29:25 +00:00
|
|
|
/// Get the current, if any, sync token of the client.
|
|
|
|
/// This will be None if the client didn't sync at least once.
|
2020-03-11 10:42:59 +00:00
|
|
|
pub async fn sync_token(&self) -> Option<String> {
|
|
|
|
self.base_client.read().await.sync_token.clone()
|
2019-11-24 21:40:52 +00:00
|
|
|
}
|
2020-04-01 13:37:00 +00:00
|
|
|
|
|
|
|
/// Query the server for users device keys.
|
|
|
|
///
|
|
|
|
/// # Panics
|
|
|
|
///
|
|
|
|
/// Panics if no key query needs to be done.
|
2020-04-03 08:42:03 +00:00
|
|
|
#[cfg(feature = "encryption")]
|
|
|
|
#[cfg_attr(docsrs, doc(cfg(feature = "encryption")))]
|
|
|
|
#[instrument]
|
2020-04-01 13:37:00 +00:00
|
|
|
async fn keys_query(&self) -> Result<get_keys::Response> {
|
|
|
|
let mut users_for_query = self
|
|
|
|
.base_client
|
|
|
|
.read()
|
|
|
|
.await
|
|
|
|
.users_for_key_query()
|
|
|
|
.await
|
|
|
|
.expect("Keys don't need to be uploaded");
|
|
|
|
|
|
|
|
debug!(
|
|
|
|
"Querying device keys device for users: {:?}",
|
|
|
|
users_for_query
|
|
|
|
);
|
|
|
|
|
2020-04-23 08:52:47 +00:00
|
|
|
let mut device_keys: BTreeMap<UserId, Vec<DeviceId>> = BTreeMap::new();
|
2020-04-01 13:37:00 +00:00
|
|
|
|
|
|
|
for user in users_for_query.drain() {
|
2020-04-03 15:00:37 +00:00
|
|
|
device_keys.insert(user, Vec::new());
|
2020-04-01 13:37:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
let request = get_keys::Request {
|
|
|
|
timeout: None,
|
|
|
|
device_keys,
|
|
|
|
token: None,
|
|
|
|
};
|
|
|
|
|
|
|
|
let response = self.send(request).await?;
|
|
|
|
self.base_client
|
|
|
|
.write()
|
|
|
|
.await
|
|
|
|
.receive_keys_query_response(&response)
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
Ok(response)
|
|
|
|
}
|
2019-10-23 20:47:00 +00:00
|
|
|
}
|
2020-04-07 00:59:44 +00:00
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod test {
|
2020-04-07 12:55:42 +00:00
|
|
|
use super::{AsyncClient, Url};
|
2020-04-07 00:59:44 +00:00
|
|
|
use crate::events::collections::all::RoomEvent;
|
2020-04-07 12:55:42 +00:00
|
|
|
use crate::identifiers::{RoomId, UserId};
|
2020-04-07 00:59:44 +00:00
|
|
|
|
2020-04-07 12:55:42 +00:00
|
|
|
use crate::test_builder::EventBuilder;
|
2020-04-07 00:59:44 +00:00
|
|
|
|
2020-04-16 21:03:40 +00:00
|
|
|
use mockito::mock;
|
2020-04-07 00:59:44 +00:00
|
|
|
use std::convert::TryFrom;
|
2020-04-16 21:03:40 +00:00
|
|
|
use std::str::FromStr;
|
2020-04-07 00:59:44 +00:00
|
|
|
|
|
|
|
#[tokio::test]
|
2020-04-07 12:55:42 +00:00
|
|
|
async fn client_runner() {
|
|
|
|
let session = crate::Session {
|
|
|
|
access_token: "12345".to_owned(),
|
|
|
|
user_id: UserId::try_from("@example:localhost").unwrap(),
|
|
|
|
device_id: "DEVICEID".to_owned(),
|
|
|
|
};
|
|
|
|
let homeserver = url::Url::parse(&mockito::server_url()).unwrap();
|
|
|
|
let client = AsyncClient::new(homeserver, Some(session)).unwrap();
|
|
|
|
|
2020-04-07 00:59:44 +00:00
|
|
|
let rid = RoomId::try_from("!roomid:room.com").unwrap();
|
|
|
|
let uid = UserId::try_from("@example:localhost").unwrap();
|
|
|
|
|
2020-04-07 20:11:35 +00:00
|
|
|
let mut bld = EventBuilder::default()
|
2020-04-07 12:55:42 +00:00
|
|
|
.add_room_event_from_file("./tests/data/events/member.json", RoomEvent::RoomMember)
|
|
|
|
.add_room_event_from_file(
|
|
|
|
"./tests/data/events/power_levels.json",
|
|
|
|
RoomEvent::RoomPowerLevels,
|
|
|
|
)
|
2020-04-07 20:11:35 +00:00
|
|
|
.build_client_runner(rid, uid);
|
2020-04-07 12:55:42 +00:00
|
|
|
|
2020-04-07 20:11:35 +00:00
|
|
|
let cli = bld.set_client(client).to_client().await;
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
cli.homeserver(),
|
|
|
|
&Url::parse(&mockito::server_url()).unwrap()
|
|
|
|
);
|
2020-04-07 12:55:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
async fn mock_runner() {
|
|
|
|
use std::convert::TryFrom;
|
|
|
|
|
|
|
|
let session = crate::Session {
|
|
|
|
access_token: "12345".to_owned(),
|
|
|
|
user_id: UserId::try_from("@example:localhost").unwrap(),
|
|
|
|
device_id: "DEVICEID".to_owned(),
|
|
|
|
};
|
|
|
|
|
|
|
|
let homeserver = url::Url::parse(&mockito::server_url()).unwrap();
|
|
|
|
let client = AsyncClient::new(homeserver, Some(session)).unwrap();
|
|
|
|
|
2020-04-07 20:11:35 +00:00
|
|
|
let mut bld = EventBuilder::default()
|
2020-04-07 12:55:42 +00:00
|
|
|
.add_room_event_from_file("./tests/data/events/member.json", RoomEvent::RoomMember)
|
|
|
|
.add_room_event_from_file(
|
|
|
|
"./tests/data/events/power_levels.json",
|
|
|
|
RoomEvent::RoomPowerLevels,
|
|
|
|
)
|
|
|
|
.build_mock_runner(
|
|
|
|
"GET",
|
|
|
|
mockito::Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_string()),
|
2020-04-07 20:11:35 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
let cli = bld.set_client(client).to_client().await.unwrap();
|
2020-04-07 12:55:42 +00:00
|
|
|
|
2020-04-07 20:11:35 +00:00
|
|
|
assert_eq!(
|
|
|
|
cli.homeserver(),
|
|
|
|
&Url::parse(&mockito::server_url()).unwrap()
|
|
|
|
);
|
2020-04-07 00:59:44 +00:00
|
|
|
}
|
2020-04-16 21:03:40 +00:00
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
async fn login_error() {
|
|
|
|
let homeserver = Url::from_str(&mockito::server_url()).unwrap();
|
|
|
|
|
|
|
|
let _m = mock("POST", "/_matrix/client/r0/login")
|
|
|
|
.with_status(403)
|
|
|
|
.with_body_from_file("tests/data/login_response_error.json")
|
|
|
|
.create();
|
|
|
|
|
|
|
|
let client = AsyncClient::new(homeserver, None).unwrap();
|
|
|
|
|
|
|
|
if let Err(err) = client.login("example", "wordpass", None, None).await {
|
|
|
|
if let crate::Error::RumaResponse(ruma_api::error::FromHttpResponseError::Http(
|
|
|
|
ruma_api::error::ServerError::Known(ruma_client_api::error::Error {
|
|
|
|
kind,
|
|
|
|
message,
|
|
|
|
status_code,
|
|
|
|
}),
|
|
|
|
)) = err
|
|
|
|
{
|
|
|
|
if let ruma_client_api::error::ErrorKind::Forbidden = kind {
|
|
|
|
} else {
|
|
|
|
panic!(
|
|
|
|
"found the wrong `ErrorKind` {:?}, expected `Forbidden",
|
|
|
|
kind
|
|
|
|
);
|
|
|
|
}
|
|
|
|
assert_eq!(message, "Invalid password".to_string());
|
|
|
|
assert_eq!(status_code, http::StatusCode::from_u16(403).unwrap());
|
|
|
|
} else {
|
|
|
|
panic!(
|
|
|
|
"found the wrong `Error` type {:?}, expected `Error::RumaResponse",
|
|
|
|
err
|
|
|
|
);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
panic!("this request should return an `Err` variant")
|
|
|
|
}
|
|
|
|
}
|
2020-04-07 00:59:44 +00:00
|
|
|
}
|