Start Matrix client authentication
This commit is contained in:
@@ -19,6 +19,8 @@ docker run --rm -it docker.io/pierre42100/matrix_gateway --help
|
|||||||
|
|
||||||
### Dependencies
|
### Dependencies
|
||||||
```
|
```
|
||||||
|
sudo apt install -y libsqlite3-dev
|
||||||
|
|
||||||
cd matrixgw_backend
|
cd matrixgw_backend
|
||||||
mkdir -p storage/maspostgres storage/synapse
|
mkdir -p storage/maspostgres storage/synapse
|
||||||
docker compose up
|
docker compose up
|
||||||
@@ -53,4 +55,4 @@ cargo fmt && cargo clippy && cargo run --
|
|||||||
cd matrixgw_frontend
|
cd matrixgw_frontend
|
||||||
npm install
|
npm install
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|||||||
2650
matrixgw_backend/Cargo.lock
generated
2650
matrixgw_backend/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -27,4 +27,7 @@ uuid = { version = "1.18.1", features = ["v4", "serde"] }
|
|||||||
ipnet = { version = "2.11.0", features = ["serde"] }
|
ipnet = { version = "2.11.0", features = ["serde"] }
|
||||||
rand = "0.9.2"
|
rand = "0.9.2"
|
||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
mailchecker = "6.0.19"
|
mailchecker = "6.0.19"
|
||||||
|
matrix-sdk = "0.14.0"
|
||||||
|
url = "2.5.7"
|
||||||
|
ractor = "0.15.9"
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
use crate::users::{APITokenID, UserEmail};
|
use crate::users::{APITokenID, UserEmail};
|
||||||
use crate::utils::crypt_utils::sha256str;
|
use crate::utils::crypt_utils::sha256str;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
use matrix_sdk::authentication::oauth::registration::{
|
||||||
|
ApplicationType, ClientMetadata, Localized, OAuthGrantType,
|
||||||
|
};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
/// Matrix gateway backend API
|
/// Matrix gateway backend API
|
||||||
#[derive(Parser, Debug, Clone)]
|
#[derive(Parser, Debug, Clone)]
|
||||||
@@ -76,6 +80,10 @@ pub struct AppConfig {
|
|||||||
#[arg(long, env, default_value = "APP_ORIGIN/oidc_cb")]
|
#[arg(long, env, default_value = "APP_ORIGIN/oidc_cb")]
|
||||||
oidc_redirect_url: String,
|
oidc_redirect_url: String,
|
||||||
|
|
||||||
|
/// Matrix oauth redirect URL
|
||||||
|
#[arg(long, env, default_value = "APP_ORIGIN/matrix_auth_cb")]
|
||||||
|
matrix_oauth_redirect_url: String,
|
||||||
|
|
||||||
/// Application storage path
|
/// Application storage path
|
||||||
#[arg(long, env, default_value = "app_storage")]
|
#[arg(long, env, default_value = "app_storage")]
|
||||||
storage_path: String,
|
storage_path: String,
|
||||||
@@ -146,6 +154,38 @@ impl AppConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Matrix OAuth redirect URL
|
||||||
|
pub fn matrix_oauth_redirect_url(&self) -> String {
|
||||||
|
self.matrix_oauth_redirect_url
|
||||||
|
.replace("APP_ORIGIN", &self.website_origin)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get Matrix client metadata information
|
||||||
|
pub fn matrix_client_metadata(&self) -> ClientMetadata {
|
||||||
|
let client_uri = Localized::new(
|
||||||
|
Url::parse(&self.website_origin).expect("Invalid website origin!"),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
ClientMetadata {
|
||||||
|
application_type: ApplicationType::Native,
|
||||||
|
grant_types: vec![OAuthGrantType::AuthorizationCode {
|
||||||
|
redirect_uris: vec![
|
||||||
|
Url::parse(&self.matrix_oauth_redirect_url())
|
||||||
|
.expect("Failed to parse matrix auth redirect URI!"),
|
||||||
|
],
|
||||||
|
}],
|
||||||
|
client_name: Some(Localized::new("MatrixGW".to_string(), [])),
|
||||||
|
logo_uri: Some(Localized::new(
|
||||||
|
Url::parse(&format!("{}/favicon.png", self.website_origin))
|
||||||
|
.expect("Invalid website origin!"),
|
||||||
|
[],
|
||||||
|
)),
|
||||||
|
policy_uri: Some(client_uri.clone()),
|
||||||
|
tos_uri: Some(client_uri.clone()),
|
||||||
|
client_uri,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Get storage path
|
/// Get storage path
|
||||||
pub fn storage_path(&self) -> &Path {
|
pub fn storage_path(&self) -> &Path {
|
||||||
Path::new(self.storage_path.as_str())
|
Path::new(self.storage_path.as_str())
|
||||||
@@ -170,6 +210,16 @@ impl AppConfig {
|
|||||||
pub fn user_api_token_metadata_file(&self, mail: &UserEmail, id: &APITokenID) -> PathBuf {
|
pub fn user_api_token_metadata_file(&self, mail: &UserEmail, id: &APITokenID) -> PathBuf {
|
||||||
self.user_api_token_directory(mail).join(id.0.to_string())
|
self.user_api_token_directory(mail).join(id.0.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get user Matrix database path
|
||||||
|
pub fn user_matrix_db_path(&self, mail: &UserEmail) -> PathBuf {
|
||||||
|
self.user_directory(mail).join("matrix-db")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get user Matrix database passphrase path
|
||||||
|
pub fn user_matrix_passphrase_path(&self, mail: &UserEmail) -> PathBuf {
|
||||||
|
self.user_directory(mail).join("matrix-db-passphrase")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, serde::Serialize)]
|
#[derive(Debug, Clone, serde::Serialize)]
|
||||||
|
|||||||
14
matrixgw_backend/src/controllers/matrix_link_controller.rs
Normal file
14
matrixgw_backend/src/controllers/matrix_link_controller.rs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
use crate::controllers::HttpResult;
|
||||||
|
use crate::extractors::matrix_client_extractor::MatrixClientExtractor;
|
||||||
|
use actix_web::HttpResponse;
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
struct StartAuthResponse {
|
||||||
|
url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start user authentication on Matrix server
|
||||||
|
pub async fn start_auth(client: MatrixClientExtractor) -> HttpResult {
|
||||||
|
let url = client.client.initiate_login().await?.to_string();
|
||||||
|
Ok(HttpResponse::Ok().json(StartAuthResponse { url }))
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ use actix_web::{HttpResponse, ResponseError};
|
|||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
|
||||||
pub mod auth_controller;
|
pub mod auth_controller;
|
||||||
|
pub mod matrix_link_controller;
|
||||||
pub mod server_controller;
|
pub mod server_controller;
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
|||||||
37
matrixgw_backend/src/extractors/matrix_client_extractor.rs
Normal file
37
matrixgw_backend/src/extractors/matrix_client_extractor.rs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
use crate::extractors::auth_extractor::AuthExtractor;
|
||||||
|
use crate::matrix_connection::matrix_client::MatrixClient;
|
||||||
|
use crate::matrix_connection::matrix_manager::MatrixManagerMsg;
|
||||||
|
use actix_web::dev::Payload;
|
||||||
|
use actix_web::{FromRequest, HttpRequest, web};
|
||||||
|
use ractor::ActorRef;
|
||||||
|
|
||||||
|
pub struct MatrixClientExtractor {
|
||||||
|
pub auth: AuthExtractor,
|
||||||
|
pub client: MatrixClient,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromRequest for MatrixClientExtractor {
|
||||||
|
type Error = actix_web::Error;
|
||||||
|
type Future = futures_util::future::LocalBoxFuture<'static, Result<Self, Self::Error>>;
|
||||||
|
|
||||||
|
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
|
||||||
|
let req = req.clone();
|
||||||
|
let mut payload = payload.take();
|
||||||
|
Box::pin(async move {
|
||||||
|
let auth = AuthExtractor::from_request(&req, &mut payload).await?;
|
||||||
|
|
||||||
|
let matrix_manager_actor =
|
||||||
|
web::Data::<ActorRef<MatrixManagerMsg>>::from_request(&req, &mut Payload::None)
|
||||||
|
.await?;
|
||||||
|
let client = ractor::call!(
|
||||||
|
matrix_manager_actor,
|
||||||
|
MatrixManagerMsg::GetClient,
|
||||||
|
auth.user.email.clone()
|
||||||
|
)
|
||||||
|
.expect("Failed to query manager actor!")
|
||||||
|
.expect("Failed to get client!");
|
||||||
|
|
||||||
|
Ok(Self { auth, client })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
pub mod auth_extractor;
|
pub mod auth_extractor;
|
||||||
|
pub mod matrix_client_extractor;
|
||||||
pub mod session_extractor;
|
pub mod session_extractor;
|
||||||
|
|||||||
@@ -2,5 +2,6 @@ pub mod app_config;
|
|||||||
pub mod constants;
|
pub mod constants;
|
||||||
pub mod controllers;
|
pub mod controllers;
|
||||||
pub mod extractors;
|
pub mod extractors;
|
||||||
|
pub mod matrix_connection;
|
||||||
pub mod users;
|
pub mod users;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ use actix_web::middleware::Logger;
|
|||||||
use actix_web::{App, HttpServer, web};
|
use actix_web::{App, HttpServer, web};
|
||||||
use matrixgw_backend::app_config::AppConfig;
|
use matrixgw_backend::app_config::AppConfig;
|
||||||
use matrixgw_backend::constants;
|
use matrixgw_backend::constants;
|
||||||
use matrixgw_backend::controllers::{auth_controller, server_controller};
|
use matrixgw_backend::controllers::{auth_controller, matrix_link_controller, server_controller};
|
||||||
|
use matrixgw_backend::matrix_connection::matrix_manager::MatrixManagerActor;
|
||||||
use matrixgw_backend::users::User;
|
use matrixgw_backend::users::User;
|
||||||
|
use ractor::Actor;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
@@ -29,12 +31,22 @@ async fn main() -> std::io::Result<()> {
|
|||||||
.expect("Failed to create auto-login account!");
|
.expect("Failed to create auto-login account!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create matrix clients manager actor
|
||||||
|
let (manager_actor, manager_actor_handle) = Actor::spawn(
|
||||||
|
Some("matrix-clients-manager".to_string()),
|
||||||
|
MatrixManagerActor,
|
||||||
|
(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("Failed to start Matrix manager actor!");
|
||||||
|
|
||||||
log::info!(
|
log::info!(
|
||||||
"Starting to listen on {} for {}",
|
"Starting to listen on {} for {}",
|
||||||
AppConfig::get().listen_address,
|
AppConfig::get().listen_address,
|
||||||
AppConfig::get().website_origin
|
AppConfig::get().website_origin
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let manager_actor_clone = manager_actor.clone();
|
||||||
HttpServer::new(move || {
|
HttpServer::new(move || {
|
||||||
let session_mw = SessionMiddleware::builder(redis_store.clone(), secret_key.clone())
|
let session_mw = SessionMiddleware::builder(redis_store.clone(), secret_key.clone())
|
||||||
.cookie_name("matrixgw-session".to_string())
|
.cookie_name("matrixgw-session".to_string())
|
||||||
@@ -53,6 +65,7 @@ async fn main() -> std::io::Result<()> {
|
|||||||
.wrap(Logger::default())
|
.wrap(Logger::default())
|
||||||
.wrap(session_mw)
|
.wrap(session_mw)
|
||||||
.wrap(cors)
|
.wrap(cors)
|
||||||
|
.app_data(web::Data::new(manager_actor_clone.clone()))
|
||||||
.app_data(web::Data::new(RemoteIPConfig {
|
.app_data(web::Data::new(RemoteIPConfig {
|
||||||
proxy: AppConfig::get().proxy_ip.clone(),
|
proxy: AppConfig::get().proxy_ip.clone(),
|
||||||
}))
|
}))
|
||||||
@@ -76,9 +89,20 @@ async fn main() -> std::io::Result<()> {
|
|||||||
"/api/auth/sign_out",
|
"/api/auth/sign_out",
|
||||||
web::get().to(auth_controller::sign_out),
|
web::get().to(auth_controller::sign_out),
|
||||||
)
|
)
|
||||||
|
// Matrix link controller
|
||||||
|
.route(
|
||||||
|
"/api/matrix_link/start_auth",
|
||||||
|
web::post().to(matrix_link_controller::start_auth),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.workers(4)
|
.workers(4)
|
||||||
.bind(&AppConfig::get().listen_address)?
|
.bind(&AppConfig::get().listen_address)?
|
||||||
.run()
|
.run()
|
||||||
.await
|
.await?;
|
||||||
|
|
||||||
|
// Terminate manager actor
|
||||||
|
manager_actor.stop(None);
|
||||||
|
manager_actor_handle.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
115
matrixgw_backend/src/matrix_connection/matrix_client.rs
Normal file
115
matrixgw_backend/src/matrix_connection/matrix_client.rs
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
use crate::app_config::AppConfig;
|
||||||
|
use crate::users::UserEmail;
|
||||||
|
use crate::utils::rand_utils::rand_string;
|
||||||
|
use matrix_sdk::authentication::oauth::OAuthError;
|
||||||
|
use matrix_sdk::authentication::oauth::error::OAuthDiscoveryError;
|
||||||
|
use matrix_sdk::ruma::serde::Raw;
|
||||||
|
use matrix_sdk::{Client, ClientBuildError};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
/// Matrix Gateway session errors
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
enum MatrixClientError {
|
||||||
|
#[error("Failed to create Matrix database storage directory! {0}")]
|
||||||
|
CreateMatrixDbDir(std::io::Error),
|
||||||
|
#[error("Failed to create database passphrase! {0}")]
|
||||||
|
CreateDbPassphrase(std::io::Error),
|
||||||
|
#[error("Failed to read database passphrase! {0}")]
|
||||||
|
ReadDbPassphrase(std::io::Error),
|
||||||
|
#[error("Failed to build Matrix client! {0}")]
|
||||||
|
BuildMatrixClient(ClientBuildError),
|
||||||
|
#[error("Failed to clear Matrix database storage directory! {0}")]
|
||||||
|
ClearMatrixDbDir(std::io::Error),
|
||||||
|
#[error("Failed to remove database passphrase! {0}")]
|
||||||
|
ClearDbPassphrase(std::io::Error),
|
||||||
|
#[error("Failed to fetch server metadata! {0}")]
|
||||||
|
FetchServerMetadata(OAuthDiscoveryError),
|
||||||
|
#[error("Failed to parse auth redirect URL! {0}")]
|
||||||
|
ParseAuthRedirectURL(url::ParseError),
|
||||||
|
#[error("Failed to build auth request! {0}")]
|
||||||
|
BuildAuthRequest(OAuthError),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct MatrixClient {
|
||||||
|
pub email: UserEmail,
|
||||||
|
pub client: Client,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MatrixClient {
|
||||||
|
/// Start to build Matrix client to initiate user authentication
|
||||||
|
pub async fn build_client(email: &UserEmail) -> anyhow::Result<Self> {
|
||||||
|
let db_path = AppConfig::get().user_matrix_db_path(email);
|
||||||
|
std::fs::create_dir_all(&db_path).map_err(MatrixClientError::CreateMatrixDbDir)?;
|
||||||
|
|
||||||
|
// Generate or load passphrase
|
||||||
|
let passphrase_path = AppConfig::get().user_matrix_passphrase_path(email);
|
||||||
|
if !passphrase_path.exists() {
|
||||||
|
std::fs::write(&passphrase_path, rand_string(32))
|
||||||
|
.map_err(MatrixClientError::CreateDbPassphrase)?;
|
||||||
|
}
|
||||||
|
let passphrase = std::fs::read_to_string(passphrase_path)
|
||||||
|
.map_err(MatrixClientError::ReadDbPassphrase)?;
|
||||||
|
|
||||||
|
let client = Client::builder()
|
||||||
|
.server_name_or_homeserver_url(&AppConfig::get().matrix_homeserver)
|
||||||
|
// Automatically refresh tokens if needed
|
||||||
|
.handle_refresh_tokens()
|
||||||
|
.sqlite_store(&db_path, Some(&passphrase))
|
||||||
|
.build()
|
||||||
|
.await
|
||||||
|
.map_err(MatrixClientError::BuildMatrixClient)?;
|
||||||
|
|
||||||
|
// Check metadata
|
||||||
|
let server_metadata = client
|
||||||
|
.oauth()
|
||||||
|
.server_metadata()
|
||||||
|
.await
|
||||||
|
.map_err(MatrixClientError::FetchServerMetadata)?;
|
||||||
|
log::info!("OAuth2 server issuer: {:?}", server_metadata.issuer);
|
||||||
|
|
||||||
|
// TODO : restore client if client already existed
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
email: email.clone(),
|
||||||
|
client,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Destroy this Matrix client instance
|
||||||
|
pub fn destroy(&self) -> anyhow::Result<()> {
|
||||||
|
let db_path = AppConfig::get().user_matrix_db_path(&self.email);
|
||||||
|
if db_path.is_file() {
|
||||||
|
std::fs::remove_dir_all(&db_path).map_err(MatrixClientError::ClearMatrixDbDir)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let passphrase_path = AppConfig::get().user_matrix_passphrase_path(&self.email);
|
||||||
|
if passphrase_path.is_file() {
|
||||||
|
std::fs::remove_file(passphrase_path).map_err(MatrixClientError::ClearDbPassphrase)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initiate oauth authentication
|
||||||
|
pub async fn initiate_login(&self) -> anyhow::Result<Url> {
|
||||||
|
let oauth = self.client.oauth();
|
||||||
|
|
||||||
|
let metadata = AppConfig::get().matrix_client_metadata();
|
||||||
|
let client_metadata = Raw::new(&metadata).expect("Couldn't serialize client metadata");
|
||||||
|
|
||||||
|
let auth = oauth
|
||||||
|
.login(
|
||||||
|
Url::parse(&AppConfig::get().matrix_oauth_redirect_url())
|
||||||
|
.map_err(MatrixClientError::ParseAuthRedirectURL)?,
|
||||||
|
None,
|
||||||
|
Some(client_metadata.into()),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
.await
|
||||||
|
.map_err(MatrixClientError::BuildAuthRequest)?;
|
||||||
|
|
||||||
|
Ok(auth.url)
|
||||||
|
}
|
||||||
|
}
|
||||||
62
matrixgw_backend/src/matrix_connection/matrix_manager.rs
Normal file
62
matrixgw_backend/src/matrix_connection/matrix_manager.rs
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
use crate::matrix_connection::matrix_client::MatrixClient;
|
||||||
|
use crate::users::UserEmail;
|
||||||
|
use ractor::{Actor, ActorProcessingErr, ActorRef, RpcReplyPort};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
pub struct MatrixManagerState {
|
||||||
|
pub clients: HashMap<UserEmail, MatrixClient>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum MatrixManagerMsg {
|
||||||
|
GetClient(UserEmail, RpcReplyPort<anyhow::Result<MatrixClient>>),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MatrixManagerActor;
|
||||||
|
|
||||||
|
impl Actor for MatrixManagerActor {
|
||||||
|
type Msg = MatrixManagerMsg;
|
||||||
|
type State = MatrixManagerState;
|
||||||
|
type Arguments = ();
|
||||||
|
|
||||||
|
async fn pre_start(
|
||||||
|
&self,
|
||||||
|
_myself: ActorRef<Self::Msg>,
|
||||||
|
_args: Self::Arguments,
|
||||||
|
) -> Result<Self::State, ActorProcessingErr> {
|
||||||
|
Ok(MatrixManagerState {
|
||||||
|
clients: HashMap::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle(
|
||||||
|
&self,
|
||||||
|
_myself: ActorRef<Self::Msg>,
|
||||||
|
message: Self::Msg,
|
||||||
|
state: &mut Self::State,
|
||||||
|
) -> Result<(), ActorProcessingErr> {
|
||||||
|
match message {
|
||||||
|
// Get client information
|
||||||
|
MatrixManagerMsg::GetClient(email, port) => {
|
||||||
|
let res = port.send(match state.clients.get(&email) {
|
||||||
|
None => {
|
||||||
|
// Generate client if required
|
||||||
|
log::info!("Building new client for {:?}", &email);
|
||||||
|
match MatrixClient::build_client(&email).await {
|
||||||
|
Ok(c) => {
|
||||||
|
state.clients.insert(email.clone(), c.clone());
|
||||||
|
Ok(c)
|
||||||
|
}
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(c) => Ok(c.clone()),
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Err(e) = res {
|
||||||
|
log::warn!("Failed to send client information: {e}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
2
matrixgw_backend/src/matrix_connection/mod.rs
Normal file
2
matrixgw_backend/src/matrix_connection/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod matrix_client;
|
||||||
|
pub mod matrix_manager;
|
||||||
Reference in New Issue
Block a user