Skip to main content

ruma_client/
client.rs

1use std::{
2    borrow::Cow,
3    sync::{Arc, Mutex},
4    time::Duration,
5};
6
7use assign::assign;
8use async_stream::try_stream;
9use futures_core::stream::Stream;
10use ruma::{
11    DeviceId,
12    api::{
13        AppserviceUserIdentity, OutgoingRequest, SupportedVersions,
14        auth_scheme::{AuthScheme, SendAccessToken},
15        client::{
16            account::register::{self, RegistrationKind},
17            session::login::{self, v3::LoginInfo},
18            sync::sync_events,
19            uiaa::{MatrixUserIdentifier, UserIdentifier},
20        },
21        path_builder::{PathBuilder, SinglePath, VersionHistory},
22    },
23    presence::PresenceState,
24};
25
26use crate::{Error, HttpClient, ResponseError, ResponseResult, send_customized_request};
27
28mod builder;
29
30pub use self::builder::ClientBuilder;
31
32/// How the client should handle access tokens when making requests.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
34#[non_exhaustive]
35pub enum TokenMode {
36    #[default]
37    /// Send access token only when the endpoint requires it.
38    SendIfRequired,
39
40    /// Always send access token to all endpoints.
41    SendAlways,
42
43    /// Use application service token handling.
44    AppService,
45}
46
47/// A client for the Matrix client-server API.
48#[derive(Clone, Debug)]
49pub struct Client<C>(Arc<ClientData<C>>);
50
51/// Data contained in Client's Rc
52#[derive(Debug)]
53struct ClientData<C> {
54    /// The URL of the homeserver to connect to.
55    homeserver_url: String,
56
57    /// The underlying HTTP client.
58    http_client: C,
59
60    /// The access token, if logged in.
61    access_token: Mutex<Option<String>>,
62
63    /// Mode for handling tokens when making requests.
64    token_mode: TokenMode,
65
66    /// The (known) Matrix versions the homeserver supports.
67    supported_matrix_versions: SupportedVersions,
68}
69
70impl Client<()> {
71    /// Creates a new client builder.
72    pub fn builder() -> ClientBuilder {
73        ClientBuilder::new()
74    }
75}
76
77impl<C> Client<C> {
78    /// Get a copy of the current `access_token`, if any.
79    ///
80    /// Useful for serializing and persisting the session to be restored later.
81    pub fn access_token(&self) -> Option<String> {
82        self.0.access_token.lock().expect("session mutex was poisoned").clone()
83    }
84}
85
86impl<C: HttpClient> Client<C> {
87    /// Makes a request to a Matrix API endpoint.
88    pub async fn send_request<R>(&self, request: R) -> ResponseResult<C, R>
89    where
90        R: OutgoingRequest,
91        for<'a> R::Authentication: AuthScheme<Input<'a> = SendAccessToken<'a>>,
92        R::PathBuilder: SupportedPathBuilder,
93    {
94        self.send_customized_request(request, |_| Ok(())).await
95    }
96
97    /// Makes a request to a Matrix API endpoint including additional URL parameters.
98    pub async fn send_customized_request<R, F>(
99        &self,
100        request: R,
101        customize: F,
102    ) -> ResponseResult<C, R>
103    where
104        R: OutgoingRequest,
105        for<'a> R::Authentication: AuthScheme<Input<'a> = SendAccessToken<'a>>,
106        R::PathBuilder: SupportedPathBuilder,
107        F: FnOnce(&mut http::Request<C::RequestBody>) -> Result<(), ResponseError<C, R>>,
108    {
109        let token_mode = self.0.token_mode;
110        let access_token = self.access_token();
111
112        let send_access_token = match (token_mode, access_token.as_deref()) {
113            (TokenMode::AppService, Some(at)) => SendAccessToken::Appservice(at),
114            (TokenMode::SendIfRequired, Some(at)) => SendAccessToken::IfRequired(at),
115            (TokenMode::SendAlways, Some(at)) => SendAccessToken::Always(at),
116            (_, None) => SendAccessToken::None,
117        };
118
119        send_customized_request(
120            &self.0.http_client,
121            &self.0.homeserver_url,
122            send_access_token,
123            R::PathBuilder::get_path_builder_input(self),
124            request,
125            customize,
126        )
127        .await
128    }
129
130    /// Makes a request to a Matrix API endpoint as a virtual user.
131    ///
132    /// This method is meant to be used by application services when interacting with the
133    /// client-server API.
134    pub async fn send_request_as<R>(
135        &self,
136        identity: AppserviceUserIdentity<'_>,
137        request: R,
138    ) -> ResponseResult<C, R>
139    where
140        R: OutgoingRequest,
141        for<'a> R::Authentication: AuthScheme<Input<'a> = SendAccessToken<'a>>,
142        R::PathBuilder: SupportedPathBuilder,
143    {
144        self.send_customized_request(request, |request| {
145            Ok(identity.maybe_add_to_uri(request.uri_mut())?)
146        })
147        .await
148    }
149
150    /// Log in with a username and password.
151    ///
152    /// In contrast to [`send_request`][Self::send_request], this method stores the access token
153    /// returned by the endpoint in this client, in addition to returning it.
154    pub async fn log_in(
155        &self,
156        user: &str,
157        password: &str,
158        device_id: Option<&DeviceId>,
159        initial_device_display_name: Option<&str>,
160    ) -> Result<login::v3::Response, Error<C::Error, ruma::api::error::Error>> {
161        let login_info = LoginInfo::Password(login::v3::Password::new(
162            UserIdentifier::Matrix(MatrixUserIdentifier::new(user.to_owned())),
163            password.to_owned(),
164        ));
165        let response = self
166            .send_request(assign!(login::v3::Request::new(login_info), {
167                device_id: device_id.map(ToOwned::to_owned),
168                initial_device_display_name: initial_device_display_name.map(ToOwned::to_owned),
169            }))
170            .await?;
171
172        *self.0.access_token.lock().unwrap() = Some(response.access_token.clone());
173
174        Ok(response)
175    }
176
177    /// Register as a guest.
178    ///
179    /// In contrast to [`send_request`][Self::send_request], this method stores the access token
180    /// returned by the endpoint in this client, in addition to returning it.
181    pub async fn register_guest(
182        &self,
183    ) -> Result<register::v3::Response, Error<C::Error, ruma::api::client::uiaa::UiaaResponse>>
184    {
185        let response = self
186            .send_request(assign!(register::v3::Request::new(), { kind: RegistrationKind::Guest }))
187            .await?;
188
189        self.0.access_token.lock().unwrap().clone_from(&response.access_token);
190
191        Ok(response)
192    }
193
194    /// Register as a new user on this server.
195    ///
196    /// In contrast to [`send_request`][Self::send_request], this method stores the access token
197    /// returned by the endpoint in this client, in addition to returning it.
198    ///
199    /// The username is the local part of the returned user_id. If it is omitted from this request,
200    /// the server will generate one.
201    pub async fn register_user(
202        &self,
203        username: Option<&str>,
204        password: &str,
205    ) -> Result<register::v3::Response, Error<C::Error, ruma::api::client::uiaa::UiaaResponse>>
206    {
207        let response = self
208            .send_request(assign!(register::v3::Request::new(), {
209                username: username.map(ToOwned::to_owned),
210                password: Some(password.to_owned())
211            }))
212            .await?;
213
214        self.0.access_token.lock().unwrap().clone_from(&response.access_token);
215
216        Ok(response)
217    }
218
219    /// Convenience method that represents repeated calls to the sync_events endpoint as a stream.
220    ///
221    /// # Example:
222    ///
223    /// ```no_run
224    /// use std::time::Duration;
225    ///
226    /// # use ruma::presence::PresenceState;
227    /// # use tokio_stream::{StreamExt as _};
228    /// # let homeserver_url = "https://example.com".to_owned();
229    /// # async {
230    /// # let client = ruma_client::Client::builder()
231    /// #     .homeserver_url(homeserver_url)
232    /// #     .build::<ruma_client::http_client::Dummy>()
233    /// #     .await?;
234    /// # let next_batch_token = String::new();
235    /// let mut sync_stream = Box::pin(client.sync(
236    ///     None,
237    ///     next_batch_token,
238    ///     PresenceState::Online,
239    ///     Some(Duration::from_secs(30)),
240    /// ));
241    /// while let Some(response) = sync_stream.try_next().await? {
242    ///     // Do something with the data in the response...
243    /// }
244    /// # Result::<(), ruma_client::Error<_, _>>::Ok(())
245    /// # };
246    /// ```
247    pub fn sync(
248        &self,
249        filter: Option<sync_events::v3::Filter>,
250        mut since: String,
251        set_presence: PresenceState,
252        timeout: Option<Duration>,
253    ) -> impl Stream<
254        Item = Result<sync_events::v3::Response, Error<C::Error, ruma::api::error::Error>>,
255    > + '_ {
256        try_stream! {
257            loop {
258                let response = self
259                    .send_request(assign!(sync_events::v3::Request::new(), {
260                        filter: filter.clone(),
261                        since: Some(since.clone()),
262                        set_presence: set_presence.clone(),
263                        timeout,
264                    }))
265                    .await?;
266
267                since.clone_from(&response.next_batch);
268                yield response;
269            }
270        }
271    }
272}
273
274/// Marker trait to identify [`PathBuilder`] implementors that the [`Client`] supports.
275///
276/// This trait can be implemented for custom `PathBuilder`s if necessary.
277pub trait SupportedPathBuilder: PathBuilder {
278    /// Get the [`PathBuilder::Input`] from the [`Client`].
279    fn get_path_builder_input<C>(client: &Client<C>) -> Self::Input<'_>;
280}
281
282impl SupportedPathBuilder for VersionHistory {
283    fn get_path_builder_input<C>(client: &Client<C>) -> Self::Input<'_> {
284        Cow::Borrowed(&client.0.supported_matrix_versions)
285    }
286}
287
288impl SupportedPathBuilder for SinglePath {
289    fn get_path_builder_input<C>(_client: &Client<C>) -> Self::Input<'_> {}
290}