appservice: Refactor API
parent
6b600d7e6d
commit
0bdcc0fbf9
|
@ -34,7 +34,9 @@ impl EventHandler for AppserviceEventHandler {
|
||||||
if let MembershipState::Invite = event.content.membership {
|
if let MembershipState::Invite = event.content.membership {
|
||||||
let user_id = UserId::try_from(event.state_key.clone()).unwrap();
|
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();
|
client.join_room_by_id(room.room_id()).await.unwrap();
|
||||||
}
|
}
|
||||||
|
@ -55,7 +57,7 @@ pub async fn main() -> std::io::Result<()> {
|
||||||
|
|
||||||
let event_handler = AppserviceEventHandler::new(appservice.clone());
|
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()))
|
HttpServer::new(move || App::new().service(appservice.actix_service()))
|
||||||
.bind(("0.0.0.0", 8090))?
|
.bind(("0.0.0.0", 8090))?
|
||||||
|
|
|
@ -61,11 +61,11 @@ async fn push_transactions(
|
||||||
request: IncomingRequest<api::event::push_events::v1::IncomingRequest>,
|
request: IncomingRequest<api::event::push_events::v1::IncomingRequest>,
|
||||||
appservice: Data<Appservice>,
|
appservice: Data<Appservice>,
|
||||||
) -> Result<HttpResponse, Error> {
|
) -> Result<HttpResponse, Error> {
|
||||||
if !appservice.hs_token_matches(request.access_token) {
|
if !appservice.compare_hs_token(request.access_token) {
|
||||||
return Ok(HttpResponse::Unauthorized().finish());
|
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("{}"))
|
Ok(HttpResponse::Ok().json("{}"))
|
||||||
}
|
}
|
||||||
|
@ -76,7 +76,7 @@ async fn query_user_id(
|
||||||
request: IncomingRequest<api::query::query_user_id::v1::IncomingRequest>,
|
request: IncomingRequest<api::query::query_user_id::v1::IncomingRequest>,
|
||||||
appservice: Data<Appservice>,
|
appservice: Data<Appservice>,
|
||||||
) -> Result<HttpResponse, Error> {
|
) -> Result<HttpResponse, Error> {
|
||||||
if !appservice.hs_token_matches(request.access_token) {
|
if !appservice.compare_hs_token(request.access_token) {
|
||||||
return Ok(HttpResponse::Unauthorized().finish());
|
return Ok(HttpResponse::Unauthorized().finish());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,7 +89,7 @@ async fn query_room_alias(
|
||||||
request: IncomingRequest<api::query::query_room_alias::v1::IncomingRequest>,
|
request: IncomingRequest<api::query::query_room_alias::v1::IncomingRequest>,
|
||||||
appservice: Data<Appservice>,
|
appservice: Data<Appservice>,
|
||||||
) -> Result<HttpResponse, Error> {
|
) -> Result<HttpResponse, Error> {
|
||||||
if !appservice.hs_token_matches(request.access_token) {
|
if !appservice.compare_hs_token(request.access_token) {
|
||||||
return Ok(HttpResponse::Unauthorized().finish());
|
return Ok(HttpResponse::Unauthorized().finish());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,13 +20,26 @@
|
||||||
//! the webserver for you
|
//! the webserver for you
|
||||||
//! * receive and validate requests from the homeserver correctly
|
//! * receive and validate requests from the homeserver correctly
|
||||||
//! * allow calling the homeserver with proper virtual user identity assertion
|
//! * allow calling the homeserver with proper virtual user identity assertion
|
||||||
//! * have the goal to have a consistent room state available by leveraging the
|
//! * have consistent room state by leveraging matrix-sdk's state store
|
||||||
//! stores that the matrix-sdk provides
|
//! * 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
|
//! # Quickstart
|
||||||
//!
|
//!
|
||||||
//! ```no_run
|
//! ```no_run
|
||||||
//! # async {
|
//! # async {
|
||||||
|
//! #
|
||||||
|
//! # use matrix_sdk::{async_trait, EventHandler};
|
||||||
|
//! #
|
||||||
|
//! # struct AppserviceEventHandler;
|
||||||
|
//! #
|
||||||
|
//! # #[async_trait]
|
||||||
|
//! # impl EventHandler for AppserviceEventHandler {}
|
||||||
|
//! #
|
||||||
//! use matrix_sdk_appservice::{Appservice, AppserviceRegistration};
|
//! use matrix_sdk_appservice::{Appservice, AppserviceRegistration};
|
||||||
//!
|
//!
|
||||||
//! let homeserver_url = "http://127.0.0.1:8008";
|
//! let homeserver_url = "http://127.0.0.1:8008";
|
||||||
|
@ -42,17 +55,23 @@
|
||||||
//! users:
|
//! users:
|
||||||
//! - exclusive: true
|
//! - exclusive: true
|
||||||
//! regex: '@_appservice_.*'
|
//! regex: '@_appservice_.*'
|
||||||
//! ")
|
//! ")?;
|
||||||
//! .unwrap();
|
|
||||||
//!
|
//!
|
||||||
//! let appservice = Appservice::new(homeserver_url, server_name, registration).await.unwrap();
|
//! let appservice = Appservice::new(homeserver_url, server_name, registration).await?;
|
||||||
//! // set event handler with `appservice.client().set_event_handler()` here
|
//! appservice.set_event_handler(Box::new(AppserviceEventHandler)).await?;
|
||||||
//! let (host, port) = appservice.get_host_and_port_from_registration().unwrap();
|
//!
|
||||||
//! appservice.run(host, port).await.unwrap();
|
//! 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
|
//! [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",)))]
|
#[cfg(not(any(feature = "actix",)))]
|
||||||
compile_error!("one webserver feature must be enabled. available ones: `actix`");
|
compile_error!("one webserver feature must be enabled. available ones: `actix`");
|
||||||
|
@ -79,7 +98,8 @@ use matrix_sdk::{
|
||||||
assign,
|
assign,
|
||||||
identifiers::{self, DeviceId, ServerNameBox, UserId},
|
identifiers::{self, DeviceId, ServerNameBox, UserId},
|
||||||
reqwest::Url,
|
reqwest::Url,
|
||||||
Client, ClientConfig, FromHttpResponseError, HttpError, RequestConfig, ServerError, Session,
|
Client, ClientConfig, EventHandler, FromHttpResponseError, HttpError, RequestConfig,
|
||||||
|
ServerError, Session,
|
||||||
};
|
};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
#[cfg(not(feature = "actix"))]
|
#[cfg(not(feature = "actix"))]
|
||||||
|
@ -96,6 +116,8 @@ pub type Host = String;
|
||||||
pub type Port = u16;
|
pub type Port = u16;
|
||||||
|
|
||||||
/// Appservice Registration
|
/// Appservice Registration
|
||||||
|
///
|
||||||
|
/// Wrapper around [`Registration`]
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct AppserviceRegistration {
|
pub struct AppserviceRegistration {
|
||||||
inner: Registration,
|
inner: Registration,
|
||||||
|
@ -117,6 +139,26 @@ impl AppserviceRegistration {
|
||||||
|
|
||||||
Ok(Self { inner: serde_yaml::from_reader(file)? })
|
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 {
|
impl From<Registration> for AppserviceRegistration {
|
||||||
|
@ -133,31 +175,20 @@ impl Deref for AppserviceRegistration {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_client(
|
async fn client_session_with_login_restore(
|
||||||
homeserver_url: &Url,
|
client: &Client,
|
||||||
server_name: &ServerNameBox,
|
|
||||||
registration: &AppserviceRegistration,
|
registration: &AppserviceRegistration,
|
||||||
localpart: Option<&str>,
|
localpart: impl AsRef<str> + Into<Box<str>>,
|
||||||
) -> Result<Client> {
|
server_name: &ServerNameBox,
|
||||||
let client = if localpart.is_some() {
|
) -> Result<()> {
|
||||||
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())?
|
|
||||||
};
|
|
||||||
|
|
||||||
let session = Session {
|
let session = Session {
|
||||||
access_token: registration.as_token.clone(),
|
access_token: registration.as_token.clone(),
|
||||||
user_id: UserId::parse_with_server_name(
|
user_id: UserId::parse_with_server_name(localpart, server_name)?,
|
||||||
localpart.unwrap_or(®istration.sender_localpart),
|
|
||||||
&server_name,
|
|
||||||
)?,
|
|
||||||
device_id: DeviceId::new(),
|
device_id: DeviceId::new(),
|
||||||
};
|
};
|
||||||
client.restore_login(session).await?;
|
client.restore_login(session).await?;
|
||||||
|
|
||||||
Ok(client)
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Appservice
|
/// Appservice
|
||||||
|
@ -189,60 +220,82 @@ impl Appservice {
|
||||||
let homeserver_url = homeserver_url.try_into()?;
|
let homeserver_url = homeserver_url.try_into()?;
|
||||||
let server_name = server_name.try_into()?;
|
let server_name = server_name.try_into()?;
|
||||||
|
|
||||||
let client = create_client(&homeserver_url, &server_name, ®istration, None).await?;
|
let client_sender_localpart = Client::new(homeserver_url.clone())?;
|
||||||
|
|
||||||
Ok(Appservice {
|
client_session_with_login_restore(
|
||||||
homeserver_url,
|
&client_sender_localpart,
|
||||||
server_name,
|
®istration,
|
||||||
registration,
|
registration.sender_localpart.as_ref(),
|
||||||
client_sender_localpart: client,
|
&server_name,
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get `Client` for the given `localpart`
|
|
||||||
///
|
|
||||||
/// 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.
|
|
||||||
///
|
|
||||||
/// [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?;
|
.await?;
|
||||||
|
|
||||||
self.ensure_registered(localpart).await?;
|
Ok(Appservice { homeserver_url, server_name, registration, client_sender_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
|
||||||
|
///
|
||||||
|
/// * `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(&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)
|
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(), {
|
let request = assign!(RegistrationRequest::new(), {
|
||||||
username: Some(localpart.as_ref()),
|
username: Some(localpart.as_ref()),
|
||||||
login_type: Some(&LoginType::ApplicationService),
|
login_type: Some(&LoginType::ApplicationService),
|
||||||
});
|
});
|
||||||
|
|
||||||
match self.client().register(request).await {
|
let client = self.client(None).await?;
|
||||||
|
match client.register(request).await {
|
||||||
Ok(_) => (),
|
Ok(_) => (),
|
||||||
Err(error) => match error {
|
Err(error) => match error {
|
||||||
matrix_sdk::Error::Http(HttpError::UiaaError(FromHttpResponseError::Http(
|
matrix_sdk::Error::Http(HttpError::UiaaError(FromHttpResponseError::Http(
|
||||||
|
@ -266,14 +319,14 @@ impl Appservice {
|
||||||
/// Get the Appservice [registration]
|
/// Get the Appservice [registration]
|
||||||
///
|
///
|
||||||
/// [registration]: https://matrix.org/docs/spec/application_service/r0.1.2#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
|
&self.registration
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compare the given `hs_token` against `registration.hs_token`
|
/// Compare the given `hs_token` against `registration.hs_token`
|
||||||
///
|
///
|
||||||
/// Returns `true` if the tokens match, `false` otherwise.
|
/// 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()
|
self.registration.hs_token == hs_token.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -290,26 +343,6 @@ impl Appservice {
|
||||||
Ok(false)
|
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`
|
/// Service to register on an Actix `App`
|
||||||
#[cfg(feature = "actix")]
|
#[cfg(feature = "actix")]
|
||||||
#[cfg_attr(docs, doc(cfg(feature = "actix")))]
|
#[cfg_attr(docs, doc(cfg(feature = "actix")))]
|
||||||
|
|
|
@ -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 = serde_json::from_value::<AnyStateEvent>(member_json()).unwrap();
|
||||||
let event: Raw<AnyRoomEvent> = AnyRoomEvent::State(event).into();
|
let event: Raw<AnyRoomEvent> = AnyRoomEvent::State(event).into();
|
||||||
|
@ -87,7 +87,7 @@ async fn test_event_handler() -> Result<()> {
|
||||||
events,
|
events,
|
||||||
);
|
);
|
||||||
|
|
||||||
appservice.client().receive_transaction(incoming).await?;
|
appservice.client(None).await?.receive_transaction(incoming).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -105,7 +105,7 @@ async fn test_transaction() -> Result<()> {
|
||||||
events,
|
events,
|
||||||
);
|
);
|
||||||
|
|
||||||
appservice.client().receive_transaction(incoming).await?;
|
appservice.client(None).await?.receive_transaction(incoming).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -116,7 +116,7 @@ async fn test_verify_hs_token() -> Result<()> {
|
||||||
|
|
||||||
let registration = appservice.registration();
|
let registration = appservice.registration();
|
||||||
|
|
||||||
assert!(appservice.hs_token_matches(®istration.hs_token));
|
assert!(appservice.compare_hs_token(®istration.hs_token));
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue