Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

appservice: Refactor API #231

Merged
merged 1 commit into from
May 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions matrix_sdk_appservice/examples/actix_autojoin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ impl EventHandler for AppserviceEventHandler {
if let MembershipState::Invite = event.content.membership {
let user_id = UserId::try_from(event.state_key.clone()).unwrap();

let client = self.appservice.client_with_localpart(user_id.localpart()).await.unwrap();
self.appservice.register(user_id.localpart()).await.unwrap();

let client = self.appservice.client(Some(user_id.localpart())).await.unwrap();

client.join_room_by_id(room.room_id()).await.unwrap();
}
Expand All @@ -55,7 +57,7 @@ pub async fn main() -> std::io::Result<()> {

let event_handler = AppserviceEventHandler::new(appservice.clone());

appservice.client().set_event_handler(Box::new(event_handler)).await;
appservice.set_event_handler(Box::new(event_handler)).await.unwrap();

HttpServer::new(move || App::new().service(appservice.actix_service()))
.bind(("0.0.0.0", 8090))?
Expand Down
8 changes: 4 additions & 4 deletions matrix_sdk_appservice/src/actix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,11 @@ async fn push_transactions(
request: IncomingRequest<api::event::push_events::v1::IncomingRequest>,
appservice: Data<Appservice>,
) -> Result<HttpResponse, Error> {
if !appservice.hs_token_matches(request.access_token) {
if !appservice.compare_hs_token(request.access_token) {
return Ok(HttpResponse::Unauthorized().finish());
}

appservice.client().receive_transaction(request.incoming).await.unwrap();
appservice.client(None).await?.receive_transaction(request.incoming).await?;

Ok(HttpResponse::Ok().json("{}"))
}
Expand All @@ -76,7 +76,7 @@ async fn query_user_id(
request: IncomingRequest<api::query::query_user_id::v1::IncomingRequest>,
appservice: Data<Appservice>,
) -> Result<HttpResponse, Error> {
if !appservice.hs_token_matches(request.access_token) {
if !appservice.compare_hs_token(request.access_token) {
return Ok(HttpResponse::Unauthorized().finish());
}

Expand All @@ -89,7 +89,7 @@ async fn query_room_alias(
request: IncomingRequest<api::query::query_room_alias::v1::IncomingRequest>,
appservice: Data<Appservice>,
) -> Result<HttpResponse, Error> {
if !appservice.hs_token_matches(request.access_token) {
if !appservice.compare_hs_token(request.access_token) {
return Ok(HttpResponse::Unauthorized().finish());
}

Expand Down
207 changes: 120 additions & 87 deletions matrix_sdk_appservice/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,26 @@
//! the webserver for you
//! * receive and validate requests from the homeserver correctly
//! * allow calling the homeserver with proper virtual user identity assertion
//! * have the goal to have a consistent room state available by leveraging the
//! stores that the matrix-sdk provides
//! * have consistent room state by leveraging matrix-sdk's state store
//! * provide E2EE support by leveraging matrix-sdk's crypto store
//!
//! # Status
//!
//! The crate is in an experimental state. Follow
//! [matrix-org/matrix-rust-sdk#228] for progress.
//!
//! # Quickstart
//!
//! ```no_run
//! # async {
//! #
//! # use matrix_sdk::{async_trait, EventHandler};
//! #
//! # struct AppserviceEventHandler;
//! #
//! # #[async_trait]
//! # impl EventHandler for AppserviceEventHandler {}
//! #
//! use matrix_sdk_appservice::{Appservice, AppserviceRegistration};
//!
//! let homeserver_url = "http://127.0.0.1:8008";
Expand All @@ -42,17 +55,23 @@
//! users:
//! - exclusive: true
//! regex: '@_appservice_.*'
//! ")
//! .unwrap();
//! ")?;
//!
//! let appservice = Appservice::new(homeserver_url, server_name, registration).await.unwrap();
//! // set event handler with `appservice.client().set_event_handler()` here
//! let (host, port) = appservice.get_host_and_port_from_registration().unwrap();
//! appservice.run(host, port).await.unwrap();
//! let appservice = Appservice::new(homeserver_url, server_name, registration).await?;
//! appservice.set_event_handler(Box::new(AppserviceEventHandler)).await?;
//!
//! let (host, port) = appservice.registration().get_host_and_port()?;
//! appservice.run(host, port).await?;
//! #
//! # Ok::<(), Box<dyn std::error::Error + 'static>>(())
//! # };
//! ```
//!
//! Check the [examples directory] for fully working examples.
//!
//! [Application Service]: https://matrix.org/docs/spec/application_service/r0.1.2
//! [matrix-org/matrix-rust-sdk#228]: https://github.com/matrix-org/matrix-rust-sdk/issues/228
//! [examples directory]: https://github.com/matrix-org/matrix-rust-sdk/tree/master/matrix_sdk_appservice/examples

#[cfg(not(any(feature = "actix",)))]
compile_error!("one webserver feature must be enabled. available ones: `actix`");
Expand All @@ -79,7 +98,8 @@ use matrix_sdk::{
assign,
identifiers::{self, DeviceId, ServerNameBox, UserId},
reqwest::Url,
Client, ClientConfig, FromHttpResponseError, HttpError, RequestConfig, ServerError, Session,
Client, ClientConfig, EventHandler, FromHttpResponseError, HttpError, RequestConfig,
ServerError, Session,
};
use regex::Regex;
#[cfg(not(feature = "actix"))]
Expand All @@ -96,6 +116,8 @@ pub type Host = String;
pub type Port = u16;

/// Appservice Registration
///
/// Wrapper around [`Registration`]
#[derive(Debug, Clone)]
pub struct AppserviceRegistration {
inner: Registration,
Expand All @@ -117,6 +139,26 @@ impl AppserviceRegistration {

Ok(Self { inner: serde_yaml::from_reader(file)? })
}

/// Get the host and port from the registration URL
///
/// If no port is found it falls back to scheme defaults: 80 for http and
/// 443 for https
pub fn get_host_and_port(&self) -> Result<(Host, Port)> {
let uri = Uri::try_from(&self.inner.url)?;

let host = uri.host().ok_or(Error::MissingRegistrationHost)?.to_owned();
let port = match uri.port() {
Some(port) => Ok(port.as_u16()),
None => match uri.scheme_str() {
Some("http") => Ok(80),
Some("https") => Ok(443),
_ => Err(Error::MissingRegistrationPort),
},
}?;

Ok((host, port))
}
}

impl From<Registration> for AppserviceRegistration {
Expand All @@ -133,31 +175,20 @@ impl Deref for AppserviceRegistration {
}
}

async fn create_client(
homeserver_url: &Url,
server_name: &ServerNameBox,
async fn client_session_with_login_restore(
client: &Client,
registration: &AppserviceRegistration,
localpart: Option<&str>,
) -> Result<Client> {
let client = if localpart.is_some() {
let request_config = RequestConfig::default().assert_identity();
let config = ClientConfig::default().request_config(request_config);
Client::new_with_config(homeserver_url.clone(), config)?
} else {
Client::new(homeserver_url.clone())?
};

localpart: impl AsRef<str> + Into<Box<str>>,
server_name: &ServerNameBox,
) -> Result<()> {
let session = Session {
access_token: registration.as_token.clone(),
user_id: UserId::parse_with_server_name(
localpart.unwrap_or(&registration.sender_localpart),
&server_name,
)?,
user_id: UserId::parse_with_server_name(localpart, server_name)?,
device_id: DeviceId::new(),
};
client.restore_login(session).await?;

Ok(client)
Ok(())
}

/// Appservice
Expand Down Expand Up @@ -189,60 +220,82 @@ impl Appservice {
let homeserver_url = homeserver_url.try_into()?;
let server_name = server_name.try_into()?;

let client = create_client(&homeserver_url, &server_name, &registration, None).await?;
let client_sender_localpart = Client::new(homeserver_url.clone())?;

Ok(Appservice {
homeserver_url,
server_name,
registration,
client_sender_localpart: client,
})
}
client_session_with_login_restore(
&client_sender_localpart,
&registration,
registration.sender_localpart.as_ref(),
&server_name,
)
.await?;

/// Get `Client` for the user associated with the application service
/// (`sender_localpart` of the [registration])
///
/// [registration]: https://matrix.org/docs/spec/application_service/r0.1.2#registration
pub fn client(&self) -> Client {
self.client_sender_localpart.clone()
Ok(Appservice { homeserver_url, server_name, registration, client_sender_localpart })
}

/// Get `Client` for the given `localpart`
/// Get a [`Client`]
///
/// Will return a `Client` that's configured to [assert the identity] on all
/// outgoing homeserver requests if `localpart` is given. If not given
/// the `Client` will use the main user associated with this appservice,
/// that is the `sender_localpart` in the [`AppserviceRegistration`]
///
/// # Arguments
///
/// If the `localpart` is covered by the `namespaces` in the [registration]
/// all requests to the homeserver will [assert the identity] to the
/// according virtual user.
/// * `localpart` - The localpart of the user we want assert our identity to
///
/// [registration]: https://matrix.org/docs/spec/application_service/r0.1.2#registration
/// [assert the identity]:
/// https://matrix.org/docs/spec/application_service/r0.1.2#identity-assertion
pub async fn client_with_localpart(
&self,
localpart: impl AsRef<str> + Into<Box<str>>,
) -> Result<Client> {
let user_id = UserId::parse_with_server_name(localpart, &self.server_name)?;
let localpart = user_id.localpart().to_owned();

let client = create_client(
&self.homeserver_url,
&self.server_name,
&self.registration,
Some(&localpart),
)
.await?;

self.ensure_registered(localpart).await?;
/// [assert the identity]: https://matrix.org/docs/spec/application_service/r0.1.2#identity-assertion
pub async fn client(&self, localpart: Option<&str>) -> Result<Client> {
let localpart = localpart.unwrap_or_else(|| self.registration.sender_localpart.as_ref());

// The `as_token` in the `Session` maps to the main appservice user
// (`sender_localpart`) by default, so we don't need to assert identity
// in that case
let client = if localpart == self.registration.sender_localpart {
self.client_sender_localpart.clone()
} else {
let request_config = RequestConfig::default().assert_identity();
let config = ClientConfig::default().request_config(request_config);
let client = Client::new_with_config(self.homeserver_url.clone(), config)?;

client_session_with_login_restore(
&client,
&self.registration,
localpart,
&self.server_name,
)
.await?;

client
};

Ok(client)
}

async fn ensure_registered(&self, localpart: impl AsRef<str>) -> Result<()> {
/// Convenience wrapper around [`Client::set_event_handler()`]
pub async fn set_event_handler(&self, handler: Box<dyn EventHandler>) -> Result<()> {
let client = self.client(None).await?;
client.set_event_handler(handler).await;

Ok(())
}

/// Register a virtual user by sending a [`RegistrationRequest`] to the
/// homeserver
///
/// # Arguments
///
/// * `localpart` - The localpart of the user to register. Must be covered
/// by the namespaces in the [`Registration`] in order to succeed.
pub async fn register(&self, localpart: impl AsRef<str>) -> Result<()> {
let request = assign!(RegistrationRequest::new(), {
username: Some(localpart.as_ref()),
login_type: Some(&LoginType::ApplicationService),
});

match self.client().register(request).await {
let client = self.client(None).await?;
match client.register(request).await {
Ok(_) => (),
Err(error) => match error {
matrix_sdk::Error::Http(HttpError::UiaaError(FromHttpResponseError::Http(
Expand All @@ -266,14 +319,14 @@ impl Appservice {
/// Get the Appservice [registration]
///
/// [registration]: https://matrix.org/docs/spec/application_service/r0.1.2#registration
pub fn registration(&self) -> &Registration {
pub fn registration(&self) -> &AppserviceRegistration {
&self.registration
}

/// Compare the given `hs_token` against `registration.hs_token`
///
/// Returns `true` if the tokens match, `false` otherwise.
pub fn hs_token_matches(&self, hs_token: impl AsRef<str>) -> bool {
pub fn compare_hs_token(&self, hs_token: impl AsRef<str>) -> bool {
self.registration.hs_token == hs_token.as_ref()
}

Expand All @@ -290,26 +343,6 @@ impl Appservice {
Ok(false)
}

/// Get the host and port from the registration URL
///
/// If no port is found it falls back to scheme defaults: 80 for http and
/// 443 for https
pub fn get_host_and_port_from_registration(&self) -> Result<(Host, Port)> {
let uri = Uri::try_from(&self.registration.url)?;

let host = uri.host().ok_or(Error::MissingRegistrationHost)?.to_owned();
let port = match uri.port() {
Some(port) => Ok(port.as_u16()),
None => match uri.scheme_str() {
Some("http") => Ok(80),
Some("https") => Ok(443),
_ => Err(Error::MissingRegistrationPort),
},
}?;

Ok((host, port))
}

/// Service to register on an Actix `App`
#[cfg(feature = "actix")]
#[cfg_attr(docs, doc(cfg(feature = "actix")))]
Expand Down
8 changes: 4 additions & 4 deletions matrix_sdk_appservice/tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ async fn test_event_handler() -> Result<()> {
}
}

appservice.client().set_event_handler(Box::new(Example::new())).await;
appservice.set_event_handler(Box::new(Example::new())).await?;

let event = serde_json::from_value::<AnyStateEvent>(member_json()).unwrap();
let event: Raw<AnyRoomEvent> = AnyRoomEvent::State(event).into();
Expand All @@ -87,7 +87,7 @@ async fn test_event_handler() -> Result<()> {
events,
);

appservice.client().receive_transaction(incoming).await?;
appservice.client(None).await?.receive_transaction(incoming).await?;

Ok(())
}
Expand All @@ -105,7 +105,7 @@ async fn test_transaction() -> Result<()> {
events,
);

appservice.client().receive_transaction(incoming).await?;
appservice.client(None).await?.receive_transaction(incoming).await?;

Ok(())
}
Expand All @@ -116,7 +116,7 @@ async fn test_verify_hs_token() -> Result<()> {

let registration = appservice.registration();

assert!(appservice.hs_token_matches(&registration.hs_token));
assert!(appservice.compare_hs_token(&registration.hs_token));

Ok(())
}
Expand Down