client: Store supported Matrix versions in Client

… and refactor creation of Client.
This commit is contained in:
Jonas Platte 2022-02-19 00:05:38 +01:00
parent 3e0fb2f46d
commit 282abc9dc2
No known key found for this signature in database
GPG Key ID: 7D261D771D915378
7 changed files with 202 additions and 145 deletions

View File

@ -17,10 +17,13 @@ use ruma_common::presence::PresenceState;
use ruma_identifiers::{DeviceId, UserId}; use ruma_identifiers::{DeviceId, UserId};
use crate::{ use crate::{
add_user_id_to_query, send_customized_request, DefaultConstructibleHttpClient, Error, add_user_id_to_query, send_customized_request, Error, HttpClient, ResponseError, ResponseResult,
HttpClient, ResponseError, ResponseResult,
}; };
mod builder;
pub use self::builder::ClientBuilder;
/// A client for the Matrix client-server API. /// A client for the Matrix client-server API.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Client<C>(Arc<ClientData<C>>); pub struct Client<C>(Arc<ClientData<C>>);
@ -34,26 +37,21 @@ struct ClientData<C> {
/// The underlying HTTP client. /// The underlying HTTP client.
http_client: C, http_client: C,
/// User session data. /// The access token, if logged in.
access_token: Mutex<Option<String>>, access_token: Mutex<Option<String>>,
/// The (known) Matrix versions the homeserver supports.
supported_matrix_versions: Vec<MatrixVersion>,
}
impl Client<()> {
/// Creates a new client builder.
pub fn builder() -> ClientBuilder {
ClientBuilder::new()
}
} }
impl<C> Client<C> { impl<C> Client<C> {
/// Creates a new client using the given underlying HTTP client.
///
/// This allows the user to configure the details of HTTP as desired.
pub fn with_http_client(
http_client: C,
homeserver_url: String,
access_token: Option<String>,
) -> Self {
Self(Arc::new(ClientData {
homeserver_url,
http_client,
access_token: Mutex::new(access_token),
}))
}
/// Get a copy of the current `access_token`, if any. /// Get a copy of the current `access_token`, if any.
/// ///
/// Useful for serializing and persisting the session to be restored later. /// Useful for serializing and persisting the session to be restored later.
@ -62,32 +60,16 @@ impl<C> Client<C> {
} }
} }
impl<C: DefaultConstructibleHttpClient> Client<C> {
/// Creates a new client based on a default-constructed hyper HTTP client.
pub fn new(homeserver_url: String, access_token: Option<String>) -> Self {
Self(Arc::new(ClientData {
homeserver_url,
http_client: DefaultConstructibleHttpClient::default(),
access_token: Mutex::new(access_token),
}))
}
}
impl<C: HttpClient> Client<C> { impl<C: HttpClient> Client<C> {
/// Makes a request to a Matrix API endpoint. /// Makes a request to a Matrix API endpoint.
pub async fn send_request<R: OutgoingRequest>( pub async fn send_request<R: OutgoingRequest>(&self, request: R) -> ResponseResult<C, R> {
&self, self.send_customized_request(request, |_| Ok(())).await
request: R,
for_versions: &[MatrixVersion],
) -> ResponseResult<C, R> {
self.send_customized_request(request, for_versions, |_| Ok(())).await
} }
/// Makes a request to a Matrix API endpoint including additional URL parameters. /// Makes a request to a Matrix API endpoint including additional URL parameters.
pub async fn send_customized_request<R, F>( pub async fn send_customized_request<R, F>(
&self, &self,
request: R, request: R,
for_versions: &[MatrixVersion],
customize: F, customize: F,
) -> ResponseResult<C, R> ) -> ResponseResult<C, R>
where where
@ -104,7 +86,7 @@ impl<C: HttpClient> Client<C> {
&self.0.http_client, &self.0.http_client,
&self.0.homeserver_url, &self.0.homeserver_url,
send_access_token, send_access_token,
for_versions, &self.0.supported_matrix_versions,
request, request,
customize, customize,
) )
@ -119,10 +101,8 @@ impl<C: HttpClient> Client<C> {
&self, &self,
user_id: &UserId, user_id: &UserId,
request: R, request: R,
for_versions: &[MatrixVersion],
) -> ResponseResult<C, R> { ) -> ResponseResult<C, R> {
self.send_customized_request(request, for_versions, add_user_id_to_query::<C, R>(user_id)) self.send_customized_request(request, add_user_id_to_query::<C, R>(user_id)).await
.await
} }
/// Log in with a username and password. /// Log in with a username and password.
@ -142,7 +122,7 @@ impl<C: HttpClient> Client<C> {
device_id, device_id,
initial_device_display_name, initial_device_display_name,
} }
), &[MatrixVersion::V1_0]) ))
.await?; .await?;
*self.0.access_token.lock().unwrap() = Some(response.access_token.clone()); *self.0.access_token.lock().unwrap() = Some(response.access_token.clone());
@ -158,10 +138,7 @@ impl<C: HttpClient> Client<C> {
&self, &self,
) -> Result<register::v3::Response, Error<C::Error, ruma_client_api::uiaa::UiaaResponse>> { ) -> Result<register::v3::Response, Error<C::Error, ruma_client_api::uiaa::UiaaResponse>> {
let response = self let response = self
.send_request( .send_request(assign!(register::v3::Request::new(), { kind: RegistrationKind::Guest }))
assign!(register::v3::Request::new(), { kind: RegistrationKind::Guest }),
&[MatrixVersion::V1_0],
)
.await?; .await?;
*self.0.access_token.lock().unwrap() = response.access_token.clone(); *self.0.access_token.lock().unwrap() = response.access_token.clone();
@ -182,10 +159,9 @@ impl<C: HttpClient> Client<C> {
password: &str, password: &str,
) -> Result<register::v3::Response, Error<C::Error, ruma_client_api::uiaa::UiaaResponse>> { ) -> Result<register::v3::Response, Error<C::Error, ruma_client_api::uiaa::UiaaResponse>> {
let response = self let response = self
.send_request( .send_request(assign!(register::v3::Request::new(), {
assign!(register::v3::Request::new(), { username, password: Some(password)}), username, password: Some(password)
&[MatrixVersion::V1_0], }))
)
.await?; .await?;
*self.0.access_token.lock().unwrap() = response.access_token.clone(); *self.0.access_token.lock().unwrap() = response.access_token.clone();
@ -200,13 +176,15 @@ impl<C: HttpClient> Client<C> {
/// ```no_run /// ```no_run
/// use std::time::Duration; /// use std::time::Duration;
/// ///
/// # type MatrixClient = ruma_client::Client<ruma_client::http_client::Dummy>;
/// # use ruma_common::presence::PresenceState; /// # use ruma_common::presence::PresenceState;
/// # use tokio_stream::{StreamExt as _}; /// # use tokio_stream::{StreamExt as _};
/// # let homeserver_url = "https://example.com".parse().unwrap(); /// # let homeserver_url = "https://example.com".parse().unwrap();
/// # let client = MatrixClient::new(homeserver_url, None);
/// # let next_batch_token = String::new();
/// # async { /// # async {
/// # let client = ruma_client::Client::builder()
/// # .homeserver_url(homeserver_url)
/// # .build::<ruma_client::http_client::Dummy>()
/// # .await?;
/// # let next_batch_token = String::new();
/// let mut sync_stream = Box::pin(client.sync( /// let mut sync_stream = Box::pin(client.sync(
/// None, /// None,
/// next_batch_token, /// next_batch_token,
@ -235,7 +213,7 @@ impl<C: HttpClient> Client<C> {
since: Some(&since), since: Some(&since),
set_presence, set_presence,
timeout, timeout,
}), &[MatrixVersion::V1_0]) }))
.await?; .await?;
since = response.next_batch.clone(); since = response.next_batch.clone();

View File

@ -0,0 +1,95 @@
use std::sync::{Arc, Mutex};
use ruma_api::{MatrixVersion, SendAccessToken};
use ruma_client_api::discover::get_supported_versions;
use super::{Client, ClientData};
use crate::{DefaultConstructibleHttpClient, Error, HttpClient, HttpClientExt};
/// A [`Client`] builder.
///
/// This type can be used to construct a `Client` through a few method calls.
pub struct ClientBuilder {
homeserver_url: Option<String>,
access_token: Option<String>,
supported_matrix_versions: Option<Vec<MatrixVersion>>,
}
impl ClientBuilder {
pub(super) fn new() -> Self {
Self { homeserver_url: None, access_token: None, supported_matrix_versions: None }
}
/// Set the homeserver URL.
///
/// The homeserver URL must be set before calling [`build()`][Self::build] or
/// [`http_client()`][Self::http_client].
pub fn homeserver_url(self, url: String) -> Self {
Self { homeserver_url: Some(url), ..self }
}
/// Set the access token.
pub fn access_token(self, access_token: Option<String>) -> Self {
Self { access_token, ..self }
}
/// Set the supported Matrix versions.
///
/// This method generally *shouldn't* be called. The [`build()`][Self::build] or
/// [`http_client()`][Self::http_client] method will take care of doing a
/// [`get_supported_versions`] request to find out about the supported versions.
pub fn supported_matrix_versions(self, versions: Vec<MatrixVersion>) -> Self {
Self { supported_matrix_versions: Some(versions), ..self }
}
/// Finish building the [`Client`].
///
/// Uses [`DefaultConstructibleHttpClient::default()`] to create an HTTP client instance.
/// Unless the supported Matrix versions were manually set via
/// [`supported_matrix_versions`][Self::supported_matrix_versions], this will do a
/// [`get_supported_versions`] request to find out about the supported versions.
pub async fn build<C>(self) -> Result<Client<C>, Error<C::Error, ruma_client_api::Error>>
where
C: DefaultConstructibleHttpClient,
{
self.http_client(C::default()).await
}
/// Set the HTTP client to finish building the [`Client`].
///
/// Unless the supported Matrix versions were manually set via
/// [`supported_matrix_versions`][Self::supported_matrix_versions], this will do a
/// [`get_supported_versions`] request to find out about the supported versions.
pub async fn http_client<C>(
self,
http_client: C,
) -> Result<Client<C>, Error<C::Error, ruma_client_api::Error>>
where
C: HttpClient,
{
let homeserver_url = self
.homeserver_url
.expect("homeserver URL has to be set prior to calling .build() or .http_client()");
let supported_matrix_versions = match self.supported_matrix_versions {
Some(versions) => versions,
None => http_client
.send_matrix_request(
&homeserver_url,
SendAccessToken::None,
&[MatrixVersion::V1_0],
get_supported_versions::Request::new(),
)
.await?
.known_versions()
.collect(),
};
Ok(Client(Arc::new(ClientData {
homeserver_url,
http_client,
access_token: Mutex::new(self.access_token),
supported_matrix_versions,
})))
}
}

View File

@ -10,11 +10,14 @@
//! //!
//! ```ignore //! ```ignore
//! # // HACK: "ignore" the doctest here because client.log_in needs client-api feature. //! # // HACK: "ignore" the doctest here because client.log_in needs client-api feature.
//! // type MatrixClient = ruma_client::Client<ruma_client::http_client::_>; //! // type HttpClient = ruma_client::http_client::_;
//! # type MatrixClient = ruma_client::Client<ruma_client::http_client::Dummy>; //! # type HttpClient = ruma_client::http_client::Dummy;
//! # let work = async { //! # let work = async {
//! let homeserver_url = "https://example.com".parse().unwrap(); //! let homeserver_url = "https://example.com".parse().unwrap();
//! let client = MatrixClient::new(homeserver_url, None); //! let client = ruma::Client::builder()
//! .homeserver_url(homeserver_url)
//! .build::<ruma_client::http_client::Dummy>()
//! .await?;
//! //!
//! let session = client //! let session = client
//! .log_in("@alice:example.com", "secret", None, None) //! .log_in("@alice:example.com", "secret", None, None)
@ -31,14 +34,19 @@
//! application service that does not need to log in, but uses the access_token directly: //! application service that does not need to log in, but uses the access_token directly:
//! //!
//! ```no_run //! ```no_run
//! # type MatrixClient = ruma_client::Client<ruma_client::http_client::Dummy>; //! # type HttpClient = ruma_client::http_client::Dummy;
//! #
//! # async {
//! let homeserver_url = "https://example.com".parse().unwrap();
//! let client = ruma_client::Client::builder()
//! .homeserver_url(homeserver_url)
//! .access_token(Some("as_access_token".into()))
//! .build::<HttpClient>()
//! .await?;
//! //!
//! let work = async { //! // make calls to the API
//! let homeserver_url = "https://example.com".parse().unwrap(); //! # Result::<(), ruma_client::Error<_, _>>::Ok(())
//! let client = MatrixClient::new(homeserver_url, Some("as_access_token".into())); //! # };
//!
//! // make calls to the API
//! };
//! ``` //! ```
//! //!
//! The `Client` type also provides methods for registering a new account if you don't already have //! The `Client` type also provides methods for registering a new account if you don't already have
@ -51,27 +59,25 @@
//! For example: //! For example:
//! //!
//! ```no_run //! ```no_run
//! # type MatrixClient = ruma_client::Client<ruma_client::http_client::Dummy>;
//! # let homeserver_url = "https://example.com".parse().unwrap(); //! # let homeserver_url = "https://example.com".parse().unwrap();
//! # let client = MatrixClient::new(homeserver_url, None); //! # async {
//! # let client = ruma_client::Client::builder()
//! # .homeserver_url(homeserver_url)
//! # .build::<ruma_client::http_client::Dummy>()
//! # .await?;
//! use std::convert::TryFrom; //! use std::convert::TryFrom;
//! //!
//! use ruma_api::MatrixVersion; //! use ruma_api::MatrixVersion;
//! use ruma_client_api::alias::get_alias; //! use ruma_client_api::alias::get_alias;
//! use ruma_identifiers::{room_alias_id, room_id}; //! use ruma_identifiers::{room_alias_id, room_id};
//! //!
//! async { //! let response = client
//! let response = client //! .send_request(get_alias::v3::Request::new(room_alias_id!("#example_room:example.com")))
//! .send_request( //! .await?;
//! get_alias::v3::Request::new(room_alias_id!("#example_room:example.com")),
//! &[MatrixVersion::V1_0],
//! )
//! .await?;
//! //!
//! assert_eq!(response.room_id, room_id!("!n8f893n9:example.com")); //! assert_eq!(response.room_id, room_id!("!n8f893n9:example.com"));
//! # Result::<(), ruma_client::Error<_, _>>::Ok(()) //! # Result::<(), ruma_client::Error<_, _>>::Ok(())
//! } //! # };
//! # ;
//! ``` //! ```
//! //!
//! # Crate features //! # Crate features
@ -115,7 +121,7 @@ mod error;
pub mod http_client; pub mod http_client;
#[cfg(feature = "client-api")] #[cfg(feature = "client-api")]
pub use self::client::Client; pub use self::client::{Client, ClientBuilder};
pub use self::{ pub use self::{
error::Error, error::Error,
http_client::{DefaultConstructibleHttpClient, HttpClient, HttpClientExt}, http_client::{DefaultConstructibleHttpClient, HttpClient, HttpClientExt},

View File

@ -10,8 +10,6 @@ use ruma::{
}; };
use ruma_api::MatrixVersion; use ruma_api::MatrixVersion;
type MatrixClient = ruma::Client<ruma_client::http_client::Isahc>;
async fn hello_world( async fn hello_world(
homeserver_url: String, homeserver_url: String,
username: &str, username: &str,
@ -19,25 +17,18 @@ async fn hello_world(
room_alias: &RoomAliasId, room_alias: &RoomAliasId,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let http_client = isahc::HttpClient::new()?; let http_client = isahc::HttpClient::new()?;
let client = MatrixClient::with_http_client(http_client, homeserver_url, None); let client =
ruma::Client::builder().homeserver_url(homeserver_url).http_client(http_client).await?;
client.log_in(username, password, None, Some("ruma-example-client")).await?; client.log_in(username, password, None, Some("ruma-example-client")).await?;
let room_id = client let room_id = client.send_request(get_alias::v3::Request::new(room_alias)).await?.room_id;
.send_request(get_alias::v3::Request::new(room_alias), &[MatrixVersion::V1_0]) client.send_request(join_room_by_id::v3::Request::new(&room_id)).await?;
.await?
.room_id;
client client
.send_request(join_room_by_id::v3::Request::new(&room_id), &[MatrixVersion::V1_0]) .send_request(send_message_event::Request::new(
.await?; &room_id,
client "1",
.send_request( &RoomMessageEventContent::text_plain("Hello World!"),
send_message_event::Request::new( )?)
&room_id,
"1",
&RoomMessageEventContent::text_plain("Hello World!"),
)?,
&[MatrixVersion::V1_0],
)
.await?; .await?;
Ok(()) Ok(())

View File

@ -5,10 +5,9 @@ use ruma::{
events::room::message::RoomMessageEventContent, events::room::message::RoomMessageEventContent,
RoomAliasId, RoomAliasId,
}; };
use ruma_api::MatrixVersion;
use ruma_identifiers::TransactionId; use ruma_identifiers::TransactionId;
type MatrixClient = ruma::Client<ruma_client::http_client::HyperNativeTls>; type HttpClient = ruma::client::http_client::HyperNativeTls;
async fn hello_world( async fn hello_world(
homeserver_url: String, homeserver_url: String,
@ -16,25 +15,18 @@ async fn hello_world(
password: &str, password: &str,
room_alias: &RoomAliasId, room_alias: &RoomAliasId,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let client = MatrixClient::new(homeserver_url, None); let client =
ruma::Client::builder().homeserver_url(homeserver_url).build::<HttpClient>().await?;
client.log_in(username, password, None, Some("ruma-example-client")).await?; client.log_in(username, password, None, Some("ruma-example-client")).await?;
let room_id = client let room_id = client.send_request(get_alias::v3::Request::new(room_alias)).await?.room_id;
.send_request(get_alias::v3::Request::new(room_alias), &[MatrixVersion::V1_0]) client.send_request(join_room_by_id::v3::Request::new(&room_id)).await?;
.await?
.room_id;
client client
.send_request(join_room_by_id::v3::Request::new(&room_id), &[MatrixVersion::V1_0]) .send_request(send_message_event::v3::Request::new(
.await?; &room_id,
client &TransactionId::new(),
.send_request( &RoomMessageEventContent::text_plain("Hello World!"),
send_message_event::v3::Request::new( )?)
&room_id,
&TransactionId::new(),
&RoomMessageEventContent::text_plain("Hello World!"),
)?,
&[MatrixVersion::V1_0],
)
.await?; .await?;
Ok(()) Ok(())

View File

@ -11,25 +11,23 @@ use ruma::{
}; };
use tokio_stream::StreamExt as _; use tokio_stream::StreamExt as _;
type MatrixClient = ruma::Client<ruma_client::http_client::HyperNativeTls>; type HttpClient = ruma::client::http_client::HyperNativeTls;
async fn log_messages( async fn log_messages(
homeserver_url: String, homeserver_url: String,
username: &str, username: &str,
password: &str, password: &str,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let client = MatrixClient::new(homeserver_url, None); let client =
ruma::Client::builder().homeserver_url(homeserver_url).build::<HttpClient>().await?;
client.log_in(username, password, None, None).await?; client.log_in(username, password, None, None).await?;
let filter = FilterDefinition::ignore_all().into(); let filter = FilterDefinition::ignore_all().into();
let initial_sync_response = client let initial_sync_response = client
.send_request( .send_request(assign!(sync_events::v3::Request::new(), {
assign!(sync_events::v3::Request::new(), { filter: Some(&filter),
filter: Some(&filter), }))
}),
&[ruma_api::MatrixVersion::V1_0],
)
.await?; .await?;
let mut sync_stream = Box::pin(client.sync( let mut sync_stream = Box::pin(client.sync(

View File

@ -2,12 +2,9 @@ use std::{convert::TryInto, error::Error, io, process::exit, time::Duration};
use futures_util::future::{join, join_all}; use futures_util::future::{join, join_all};
use ruma::{ use ruma::{
api::{ api::client::{
client::{ filter::FilterDefinition, membership::join_room_by_id, message::send_message_event,
filter::FilterDefinition, membership::join_room_by_id, message::send_message_event, sync::sync_events,
sync::sync_events,
},
MatrixVersion,
}, },
assign, client, assign, client,
events::{ events::{
@ -41,11 +38,11 @@ async fn run() -> Result<(), Box<dyn Error>> {
let http_client = let http_client =
hyper::Client::builder().build::<_, hyper::Body>(hyper_tls::HttpsConnector::new()); hyper::Client::builder().build::<_, hyper::Body>(hyper_tls::HttpsConnector::new());
let matrix_client = if let Some(state) = read_state().await.ok().flatten() { let matrix_client = if let Some(state) = read_state().await.ok().flatten() {
MatrixClient::with_http_client( ruma::Client::builder()
http_client.clone(), .homeserver_url(config.homeserver.clone())
config.homeserver.to_owned(), .access_token(Some(state.access_token))
Some(state.access_token), .http_client(http_client.clone())
) .await?
} else if config.password.is_some() { } else if config.password.is_some() {
let client = create_matrix_session(http_client.clone(), &config).await?; let client = create_matrix_session(http_client.clone(), &config).await?;
@ -63,12 +60,9 @@ async fn run() -> Result<(), Box<dyn Error>> {
let filter = FilterDefinition::ignore_all().into(); let filter = FilterDefinition::ignore_all().into();
let initial_sync_response = matrix_client let initial_sync_response = matrix_client
.send_request( .send_request(assign!(sync_events::v3::Request::new(), {
assign!(sync_events::v3::Request::new(), { filter: Some(&filter),
filter: Some(&filter), }))
}),
&[MatrixVersion::V1_0],
)
.await?; .await?;
let user_id = &config.username; let user_id = &config.username;
let not_senders = &[user_id.clone()]; let not_senders = &[user_id.clone()];
@ -121,8 +115,11 @@ async fn create_matrix_session(
config: &Config, config: &Config,
) -> Result<MatrixClient, Box<dyn Error>> { ) -> Result<MatrixClient, Box<dyn Error>> {
if let Some(password) = &config.password { if let Some(password) = &config.password {
let client = let client = ruma::Client::builder()
MatrixClient::with_http_client(http_client, config.homeserver.to_owned(), None); .homeserver_url(config.homeserver.clone())
.http_client(http_client)
.await?;
if let Err(e) = client.log_in(config.username.as_ref(), password, None, None).await { if let Err(e) = client.log_in(config.username.as_ref(), password, None, None).await {
let reason = match e { let reason = match e {
client::Error::AuthenticationRequired => "invalid credentials specified".to_owned(), client::Error::AuthenticationRequired => "invalid credentials specified".to_owned(),
@ -136,6 +133,7 @@ async fn create_matrix_session(
}; };
return Err(format!("Failed to log in: {}", reason).into()); return Err(format!("Failed to log in: {}", reason).into());
} }
Ok(client) Ok(client)
} else { } else {
Err("Failed to create session: no password stored in config".to_owned().into()) Err("Failed to create session: no password stored in config".to_owned().into())
@ -167,10 +165,11 @@ async fn handle_message(
let txn_id = TransactionId::new(); let txn_id = TransactionId::new();
let req = send_message_event::v3::Request::new(room_id, &txn_id, &joke_content)?; let req = send_message_event::v3::Request::new(room_id, &txn_id, &joke_content)?;
// Do nothing if we can't send the message. // Do nothing if we can't send the message.
let _ = matrix_client.send_request(req, &[MatrixVersion::V1_0]).await; let _ = matrix_client.send_request(req).await;
} }
} }
} }
Ok(()) Ok(())
} }
@ -180,16 +179,14 @@ async fn handle_invitations(
room_id: &RoomId, room_id: &RoomId,
) -> Result<(), Box<dyn Error>> { ) -> Result<(), Box<dyn Error>> {
println!("invited to {}", &room_id); println!("invited to {}", &room_id);
matrix_client matrix_client.send_request(join_room_by_id::v3::Request::new(room_id)).await?;
.send_request(join_room_by_id::v3::Request::new(room_id), &[MatrixVersion::V1_0])
.await?;
let greeting = "Hello! My name is Mr. Bot! I like to tell jokes. Like this one: "; let greeting = "Hello! My name is Mr. Bot! I like to tell jokes. Like this one: ";
let joke = get_joke(http_client).await.unwrap_or_else(|_| "err... never mind.".to_owned()); let joke = get_joke(http_client).await.unwrap_or_else(|_| "err... never mind.".to_owned());
let content = RoomMessageEventContent::text_plain(format!("{}\n{}", greeting, joke)); let content = RoomMessageEventContent::text_plain(format!("{}\n{}", greeting, joke));
let txn_id = TransactionId::new(); let txn_id = TransactionId::new();
let message = send_message_event::v3::Request::new(room_id, &txn_id, &content)?; let message = send_message_event::v3::Request::new(room_id, &txn_id, &content)?;
matrix_client.send_request(message, &[MatrixVersion::V1_0]).await?; matrix_client.send_request(message).await?;
Ok(()) Ok(())
} }