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

feat: public room creation #17

Merged
merged 2 commits into from
Jan 28, 2024
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
33 changes: 29 additions & 4 deletions crates/core/src/account/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@ pub struct CreateAccountDto {
pub code: Secret,
}

#[derive(Debug, Validate)]
pub struct CreateUnverifiedAccountDto {
#[validate(custom = "CreateAccountDto::validate_username")]
pub username: String,
#[validate(custom = "CreateAccountDto::validate_password")]
pub password: Secret,
#[validate(email)]
pub email: String,
}

impl CreateAccountDto {
/// Validation logic for usernames enforced in user creation
fn validate_username(username: &str) -> std::result::Result<(), ValidationError> {
Expand Down Expand Up @@ -166,6 +176,25 @@ impl AccountService {
return Err(AccountErrorCode::InvalidVerificationCode.into());
}

let account = self
.register_unverified(CreateUnverifiedAccountDto {
username: dto.username,
password: dto.password,
email: dto.email.to_owned(),
})
.await?;

self.auth
.drop_verification_code(&dto.email, &dto.session)
.await?;

Ok(account)
}

/// Registers a new user account in Matrix Server without verifying the email ownership.
/// This shuld be used for testing purposes only.
#[instrument(skip(self, dto))]
pub async fn register_unverified(&self, dto: CreateUnverifiedAccountDto) -> Result<Account> {
dto.validate().map_err(|err| {
tracing::warn!(?err, "Failed to validate user creation dto");
AccountErrorCode::from(err)
Expand Down Expand Up @@ -208,10 +237,6 @@ impl AccountService {
Error::Unknown
})?;

self.auth
.drop_verification_code(&dto.email, &dto.session)
.await?;

let matrix_account = MatrixUser::query_user_account(&self.admin, user_id.clone())
.await
.map_err(|err| {
Expand Down
5 changes: 5 additions & 0 deletions crates/core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use thiserror::Error;
use crate::account::error::AccountErrorCode;
use crate::auth::error::AuthErrorCode;
use crate::mail::error::MailErrorCode;
use crate::room::error::RoomErrorCode;

pub type Result<T> = std::result::Result<T, Error>;

Expand All @@ -19,6 +20,8 @@ pub enum Error {
#[error("{0}")]
User(AccountErrorCode),
#[error("{0}")]
Room(RoomErrorCode),
#[error("{0}")]
Mail(#[from] MailErrorCode),
#[error("An error occured while starting up. {0}")]
Startup(String),
Expand All @@ -38,6 +41,7 @@ impl HttpStatusCode for Error {
Error::Auth(err) => err.status_code(),
Error::User(err) => err.status_code(),
Error::Mail(err) => err.status_code(),
Error::Room(err) => err.status_code(),
Error::Startup(_) | Error::Unknown => StatusCode::INTERNAL_SERVER_ERROR,
}
}
Expand All @@ -47,6 +51,7 @@ impl HttpStatusCode for Error {
Error::Auth(err) => err.error_code(),
Error::User(err) => err.error_code(),
Error::Mail(err) => err.error_code(),
Error::Room(err) => err.error_code(),
Error::Startup(_) => "SERVER_STARTUP_ERROR",
Error::Unknown => "UNKNOWN_ERROR",
}
Expand Down
5 changes: 5 additions & 0 deletions crates/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ pub mod account;
pub mod auth;
pub mod error;
pub mod mail;
pub mod room;
pub mod util;

pub use error::{Error, HttpStatusCode, Result};

use mail::service::MailService;
use room::service::RoomService;
use url::Url;

use std::fmt::Debug;
Expand Down Expand Up @@ -95,6 +97,7 @@ impl CommuneConfig {
pub struct Commune {
pub account: Arc<AccountService>,
pub auth: Arc<AuthService>,
pub room: Arc<RoomService>,
}

impl Commune {
Expand Down Expand Up @@ -144,10 +147,12 @@ impl Commune {
Arc::clone(&auth),
Arc::clone(&mail),
);
let room = RoomService::new(Arc::clone(&admin_client));

Ok(Self {
account: Arc::new(account),
auth,
room: Arc::new(room),
})
}
}
25 changes: 25 additions & 0 deletions crates/core/src/room/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
use http::StatusCode;
use thiserror::Error;
use validator::ValidationErrors;

use crate::error::HttpStatusCode;

#[derive(Debug, Error)]
pub enum RoomErrorCode {
#[error("Vaildation error. {0}")]
ValidationError(#[from] ValidationErrors),
}

impl HttpStatusCode for RoomErrorCode {
fn status_code(&self) -> StatusCode {
match self {
RoomErrorCode::ValidationError(_) => StatusCode::BAD_REQUEST,
}
}

fn error_code(&self) -> &'static str {
match self {
RoomErrorCode::ValidationError(_) => "CREATION_DETAIL_INVALID",
}
}
}
3 changes: 3 additions & 0 deletions crates/core/src/room/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub mod error;
pub mod model;
pub mod service;
4 changes: 4 additions & 0 deletions crates/core/src/room/model.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#[derive(Debug, Clone)]
pub struct Room {
pub room_id: String,
}
68 changes: 68 additions & 0 deletions crates/core/src/room/service.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
use std::sync::Arc;

use tracing::instrument;
use validator::Validate;

use matrix::client::resources::room::{
CreateRoomCreationContent, CreateRoomRequestBody, Room as MatrixRoom, RoomPreset,
};
use matrix::Client as MatrixAdminClient;

use crate::util::secret::Secret;
use crate::{Error, Result};

use super::error::RoomErrorCode;
use super::model::Room;

#[derive(Debug, Clone, Validate)]
pub struct CreateRoomDto {
#[validate(length(min = 3, max = 255))]
pub name: String,
#[validate(length(min = 3, max = 255))]
pub topic: String,
#[validate(length(min = 3, max = 255))]
pub alias: String,
}

pub struct RoomService {
admin: Arc<MatrixAdminClient>,
}

impl RoomService {
pub fn new(admin: Arc<MatrixAdminClient>) -> Self {
Self { admin }
}

/// Creates a Public Chat Room
#[instrument(skip(self, dto))]
pub async fn create_public_room(
&self,
access_token: &Secret,
dto: CreateRoomDto,
) -> Result<Room> {
dto.validate()
.map_err(|err| Error::Room(RoomErrorCode::from(err)))?;

match MatrixRoom::create(
&self.admin,
access_token.to_string(),
CreateRoomRequestBody {
creation_content: CreateRoomCreationContent { m_federate: false },
name: dto.name,
preset: RoomPreset::PublicChat,
room_alias_name: dto.alias,
topic: dto.topic,
},
)
.await
{
Ok(room) => Ok(Room {
room_id: room.room_id,
}),
Err(err) => {
tracing::error!("Failed to create room: {}", err);
Err(Error::Unknown)
}
}
}
}
1 change: 1 addition & 0 deletions crates/matrix/src/client/resources/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub mod login;
pub mod room;
pub mod session;
73 changes: 73 additions & 0 deletions crates/matrix/src/client/resources/room.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
use anyhow::Result;
use serde::{Deserialize, Serialize};
use tracing::instrument;

use crate::admin::resources::user_id::UserId;

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RoomPreset {
PrivateChat,
PublicChat,
TrustedPrivateChat,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CreateRoomCreationContent {
#[serde(rename = "m.federate")]
pub m_federate: bool,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CreateRoomRequestBody {
pub creation_content: CreateRoomCreationContent,
pub name: String,
pub preset: RoomPreset,
pub room_alias_name: String,
pub topic: String,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CreateRoomResponseBody {
pub room_id: String,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RoomApiError {
pub errcode: String,
pub error: String,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Room {
pub device_id: String,
pub is_guest: bool,
pub user_id: UserId,
}

impl Room {
/// Create a new room with various configuration options.
///
/// Refer: https://spec.matrix.org/v1.9/client-server-api/#creation
#[instrument(skip(client, access_token))]
pub async fn create(
client: &crate::http::Client,
access_token: impl Into<String>,
creation_content: CreateRoomRequestBody,
) -> Result<CreateRoomResponseBody> {
let mut tmp = (*client).clone();
tmp.set_token(access_token)?;

let resp = tmp
.post_json("/_matrix/client/v3/createRoom", &creation_content)
.await?;

if resp.status().is_success() {
return Ok(resp.json().await?);
}

let error = resp.json::<RoomApiError>().await?;

Err(anyhow::anyhow!(error.error))
}
}
1 change: 1 addition & 0 deletions crates/test/src/commune/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
mod room;
54 changes: 54 additions & 0 deletions crates/test/src/commune/room/create.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
use fake::faker::internet::en::{FreeEmail, Password, Username};
use fake::Fake;

use commune::account::service::CreateUnverifiedAccountDto;
use commune::auth::service::{LoginCredentials, LoginCredentialsResponse};
use commune::room::service::CreateRoomDto;
use commune::util::secret::Secret;

use crate::tools::environment::Environment;

#[tokio::test]
async fn creates_public_chat_room() {
let env = Environment::new().await;
let username: String = Username().fake();
let password: String = Password(10..20).fake();
let email: String = FreeEmail().fake();
let password = Secret::new(password);

env.commune
.account
.register_unverified(CreateUnverifiedAccountDto {
username: username.clone(),
password: password.clone(),
email,
})
.await
.expect("Failed to register account");

let LoginCredentialsResponse { access_token } = env
.commune
.auth
.login(LoginCredentials { username, password })
.await
.expect("Failed to login");

let room_name = String::from("MyVeryFirstPublicRoom");
let room_topic = String::from("MyVeryFirstPublicRoomTopic");
let room_alias = String::from("MyVeryFirstPublicRoomAlias");
let room = env
.commune
.room
.create_public_room(
&access_token,
CreateRoomDto {
name: room_name,
topic: room_topic,
alias: room_alias,
},
)
.await
.expect("Failed to create public room");

assert!(!room.room_id.is_empty(), "should return room_id");
}
1 change: 1 addition & 0 deletions crates/test/src/commune/room/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
mod create;
3 changes: 3 additions & 0 deletions crates/test/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
#[cfg(test)]
mod commune;

#[cfg(test)]
mod tools;

Expand Down
6 changes: 3 additions & 3 deletions crates/test/src/server/api/v1/account/session.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
use commune_server::router::api::v1::account::root::AccountRegisterPayload;
use commune_server::router::api::v1::account::session::AccountSessionResponse;
use commune_server::router::api::ApiError;
use fake::faker::internet::en::{FreeEmail, Password};
use fake::Fake;
use reqwest::StatusCode;
Expand All @@ -9,12 +6,15 @@ use uuid::Uuid;

use commune::util::secret::Secret;
use commune_server::router::api::v1::account::login::{AccountLoginPayload, AccountLoginResponse};
use commune_server::router::api::v1::account::root::AccountRegisterPayload;
use commune_server::router::api::v1::account::session::AccountSessionResponse;
use commune_server::router::api::v1::account::verify_code::{
AccountVerifyCodePayload, VerifyCodeResponse,
};
use commune_server::router::api::v1::account::verify_code_email::{
AccountVerifyCodeEmailPayload, VerifyCodeEmailResponse,
};
use commune_server::router::api::ApiError;

use crate::tools::http::HttpClient;
use crate::tools::maildev::MailDevClient;
Expand Down
Loading